156 lines
11 KiB
Vue
156 lines
11 KiB
Vue
<!-- 页面编号: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>
|