Files
jiapuapp/pages/records/r02-person-detail.vue
T
2026-07-20 17:23:13 +08:00

156 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 页面编号R-02用途人物录详情同页编辑与受控状态 -->
<template>
<view class="person-detail-page" :class="`person-detail-state--${personState}`">
<ModulePageBackground module="records" />
<view class="person-detail-header"><PageHeader :title="personState === 'edit' ? '编辑人物' : '人物详情'" /></view>
<view v-if="personState === 'loading'" class="person-detail-loading">
<AppLoading text="正在读取人物档案" description="请稍候,正在整理人物资料。" />
</view>
<view v-else class="person-detail-content">
<template v-if="['detail', 'edit', 'privacy'].includes(personState)">
<view class="person-identity-card">
<image class="person-card-skin" src="/static/assets/modules/records/transparent/r01-person-name-card.png" mode="scaleToFill" />
<view class="person-identity-card__copy">
<text class="person-identity-card__name">{{ person.name }}</text>
<text class="person-identity-card__meta">{{ person.role }} · {{ person.generation }} </text>
<text class="person-identity-card__hint">人物录档案</text>
</view>
</view>
</template>
<template v-if="personState === 'detail'">
<view v-for="item in detailSections" :key="item.title" class="person-archive-card">
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
<view><text>{{ item.title }}</text><text>{{ item.copy }}</text></view>
</view>
<view class="person-edit-action" @click="enterEdit"><AppButton block label="编辑人物" /></view>
</template>
<template v-else-if="personState === 'edit'">
<view v-for="field in shortFields" :key="field.key" class="person-field">
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-field-frame.png" mode="scaleToFill" />
<text class="person-field__label">{{ field.label }}</text>
<input v-model="draft[field.key]" :type="field.key === 'generation' ? 'number' : 'text'" :placeholder="`请输入${field.label}`" />
<text v-if="errors[field.key]" class="person-field-error">{{ errors[field.key] }}</text>
</view>
<view v-for="field in longFields" :key="field.key" class="person-long-field">
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
<text class="person-long-field__label">{{ field.label }}</text>
<textarea v-model="draft[field.key]" :placeholder="`请输入${field.label}`" />
</view>
<view class="person-edit-actions">
<view class="person-save-action" @click="savePerson"><AppButton block label="保存人物" /></view>
<view class="person-cancel-action" @click="cancelEdit"><AppButton type="secondary" block label="取消编辑" /></view>
</view>
</template>
<view v-else-if="personState === 'privacy'" class="person-state-card">
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
<view><text>部分资料未公开</text><text>人物小传与家族印记受隐私设置保护当前只展示公开身份</text></view>
<view class="person-state-action" @click="returnToPeople"><AppButton block label="返回人物录" /></view>
</view>
<view v-else class="person-state-card">
<image class="person-card-skin" src="/static/assets/modules/records/transparent/module-content-frame.png" mode="scaleToFill" />
<view>
<text>{{ personState === 'expired' ? '人物档案已失效' : '人物档案暂不可用' }}</text>
<text>{{ personState === 'expired' ? '这份人物资料已无法查看,请返回人物录选择其他档案。' : '请稍后重新查看,已有资料不会受到影响。' }}</text>
</view>
<view class="person-state-action" @click="personState === 'expired' ? returnToPeople() : restoreDetail()">
<AppButton :type="personState === 'error' ? 'secondary' : 'primary'" block :label="personState === 'expired' ? '返回人物录' : '重新查看'" />
</view>
</view>
</view>
<AppToast :visible="toastVisible" message="人物资料已保存" />
</view>
</template>
<script setup>
import { computed, onUnmounted, reactive, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppLoading from "@/components/AppLoading.vue";
import AppToast from "@/components/AppToast.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
const people = [
{ id: "1", name: "汤文正", role: "家谱管理员", generation: "18", biography: "勤于修谱,常年整理族中旧照片与口述资料。", legacy: "参与维护汤氏家谱与家风家训。" },
{ id: "2", name: "汤淑华", role: "家族长辈", generation: "17", biography: "熟悉家族往事,愿意为后辈讲述旧时记忆。", legacy: "长期参与家族节庆与敬老活动。" },
{ id: "3", name: "汤文清", role: "青年代表", generation: "19", biography: "协助整理电子家谱和家族影像资料。", legacy: "推动年轻成员共同参与家谱维护。" },
];
const person = reactive({ ...people[0] });
const draft = reactive({ name: "", role: "", generation: "", biography: "", legacy: "" });
const errors = reactive({ name: "", role: "", generation: "" });
const personState = ref("loading");
const toastVisible = ref(false);
let toastTimer = null;
const shortFields = [{ key: "name", label: "姓名" }, { key: "role", label: "身份" }, { key: "generation", label: "世代" }];
const longFields = [{ key: "biography", label: "人物小传" }, { key: "legacy", label: "家族印记" }];
const detailSections = computed(() => [{ title: "人物小传", copy: person.biography }, { title: "家族印记", copy: person.legacy }]);
const copyToDraft = () => Object.assign(draft, { name: person.name, role: person.role, generation: person.generation, biography: person.biography, legacy: person.legacy });
const clearErrors = () => Object.assign(errors, { name: "", role: "", generation: "" });
const enterEdit = () => { copyToDraft(); clearErrors(); personState.value = "edit"; };
const cancelEdit = () => { copyToDraft(); clearErrors(); personState.value = "detail"; };
const savePerson = () => {
clearErrors();
if (!String(draft.name).trim()) errors.name = "请填写姓名";
if (!String(draft.role).trim()) errors.role = "请填写身份";
if (!String(draft.generation).trim()) errors.generation = "请填写世代";
if (errors.name || errors.role || errors.generation) return;
Object.assign(person, { ...draft, name: draft.name.trim(), role: draft.role.trim(), generation: String(draft.generation).trim() });
personState.value = "detail";
toastVisible.value = true;
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => { toastVisible.value = false; toastTimer = null; }, 1800);
};
const returnToPeople = () => uni.reLaunch({ url: "/pages/records/r01-people-list" });
const restoreDetail = () => { personState.value = "detail"; };
onLoad((query) => {
const selected = people.find((item) => item.id === String(query.personId || ""));
if (selected) Object.assign(person, selected);
copyToDraft();
const requested = ["loading", "privacy", "expired", "error", "edit"].includes(query.state) ? query.state : "detail";
personState.value = selected ? requested : "error";
});
onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer); });
</script>
<style scoped lang="scss">
.person-detail-page { min-height: 100vh; overflow-x: hidden; background: $paper; }
.person-detail-loading { min-height: calc(100vh - 100rpx); }
.person-detail-content { padding: 18rpx 24rpx 72rpx; }
.person-identity-card,.person-archive-card,.person-field,.person-long-field,.person-state-card { position: relative; }
.person-card-skin { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; }
.person-identity-card { display: flex; min-height: 190rpx; padding: 30rpx 10%; box-sizing: border-box; }
.person-identity-card__copy { position: relative; z-index: 1; display: flex; flex: 1; flex-direction: column; justify-content: center; }
.person-identity-card__copy text { display: block; }
.person-identity-card__name { color: $ink; font-family: STKaiti,KaiTi,serif; font-size: 38rpx; font-weight: 700; }
.person-identity-card__meta { margin-top: 8rpx; color: $ink-muted; font-size: 25rpx; font-weight: 600; }
.person-identity-card__hint { margin-top: 8rpx; color: #806a51; font-size: 21rpx; }
.person-archive-card { min-height: 154rpx; margin-top: 14rpx; padding: 32rpx 42rpx; box-sizing: border-box; }
.person-archive-card > view { position: relative; z-index: 1; }
.person-archive-card text { display: block; }
.person-archive-card text:first-child,.person-long-field__label { color: $brand-red; font-size: 24rpx; font-weight: 700; }
.person-archive-card text:last-child { margin-top: 11rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
.person-edit-action { margin-top: 20rpx; }
.person-field { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 8rpx 24rpx; min-height: 92rpx; margin-top: 12rpx; padding: 18rpx 28rpx; box-sizing: border-box; }
.person-field__label { position: relative; z-index: 1; color: $ink; font-size: 23rpx; font-weight: 700; }
.person-field input { position: relative; z-index: 1; width: 100%; min-width: 0; height: 56rpx; color: $ink; font-size: 23rpx; text-align: right; }
.person-field-error { position: relative; z-index: 1; grid-column: 1 / -1; display: block; color: $brand-red; font-size: 21rpx; text-align: right; }
.person-long-field { display: flex; flex-direction: column; min-height: 210rpx; margin-top: 14rpx; padding: 28rpx 38rpx; box-sizing: border-box; }
.person-long-field__label,.person-long-field textarea { position: relative; z-index: 1; }
.person-long-field textarea { width: 100%; height: 112rpx; min-height: 112rpx; margin-top: 14rpx; color: $ink; font-size: 23rpx; line-height: 1.5; }
.person-edit-actions { display: flex; flex-direction: column; gap: 14rpx; margin-top: 20rpx; }
.person-state-card { display: flex; flex-direction: column; min-height: 300rpx; margin-top: 26rpx; padding: 72rpx 54rpx 42rpx; box-sizing: border-box; text-align: center; }
.person-state-card > view { position: relative; z-index: 1; }
.person-state-card text { display: block; }
.person-state-card text:first-child { color: $ink; font-family: STKaiti,KaiTi,serif; font-size: 34rpx; font-weight: 700; }
.person-state-card text:last-child { margin-top: 16rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.6; }
.person-state-action { margin: 24rpx 48rpx 0; }
@media (min-width: 400px) { .person-detail-content { padding-right: 32rpx; padding-left: 32rpx; } }
@media (max-width: 340px) { .person-detail-content { padding-right: 18rpx; padding-left: 18rpx; } .person-identity-card__name { font-size: 35rpx; } }
</style>