修改部分样式

This commit is contained in:
rain
2026-07-27 17:29:58 +08:00
parent 04287e6777
commit 1e9e25fe67
53 changed files with 11562 additions and 2617 deletions
+138 -69
View File
@@ -85,48 +85,50 @@
/>
<template v-else-if="treeState === 'tree'">
<view
v-for="connector in lineageConnectors"
:key="connector.id"
class="lineage-connector"
:class="{ 'lineage-connector--spouse': connector.kind === 'spouse' }"
:style="connector.style"
/>
<view
v-for="connector in lineageConnectors"
:key="connector.id"
class="lineage-connector"
:class="{
'lineage-connector--spouse': connector.kind === 'spouse',
}"
:style="connector.style"
/>
<view
v-for="member in layoutMembers"
:key="member.id"
:id="`tree-member-${member.id}`"
class="member-node"
:class="{
'member-node--selected': selected?.id === member.id,
'member-node--spouse': Boolean(member.spouseOf),
}"
:style="nodeGridStyle(member)"
@click="openMemberPanel(member)"
>
<view class="member-node__surface">
<view class="member-node__portrait">
<view
v-for="member in layoutMembers"
:key="member.id"
:id="`tree-member-${member.id}`"
class="member-node"
:class="{
'member-node--selected': selected?.id === member.id,
'member-node--spouse': Boolean(member.spouseOf),
}"
:style="nodeGridStyle(member)"
@click="openMemberPanel(member)"
>
<view class="member-node__surface">
<view class="member-node__portrait">
<image
class="member-node__avatar"
src="/static/assets/foundation/transparent/tab-profile.png"
mode="aspectFit"
/>
</view>
<image
class="member-node__avatar"
src="/static/assets/foundation/transparent/tab-profile.png"
class="member-node__divider"
src="/static/assets/foundation/transparent/auth-divider-knot.png"
mode="aspectFit"
/>
</view>
<image
class="member-node__divider"
src="/static/assets/foundation/transparent/auth-divider-knot.png"
mode="aspectFit"
/>
<view class="member-node__copy">
<text class="node-name">{{ member.name }}</text>
<text class="node-relation"
>{{ nodeRelationText(member) }}</text
>
<text class="node-years">{{ member.years }}</text>
<view class="member-node__copy">
<text class="node-name">{{ member.name }}</text>
<text class="node-relation">{{
nodeRelationText(member)
}}</text>
<text class="node-years">{{ member.years }}</text>
</view>
</view>
</view>
</view>
</template>
<view v-else class="tree-state-card">
@@ -177,7 +179,10 @@
/>
<view class="member-action-profile__copy">
<text>{{ selected.name }}</text>
<text> {{ selected.generation }} · {{ selected.relation }} · {{ selected.branch }}</text>
<text
> {{ selected.generation }} · {{ selected.relation }} ·
{{ selected.branch }}</text
>
<text>点击头像查看资料</text>
</view>
</view>
@@ -197,7 +202,9 @@
:visible="unavailableActionVisible"
eyebrow="服务状态"
:title="unavailableAction?.label || '当前操作'"
:message="unavailableAction?.unavailableCopy || '该操作暂未接入可靠服务合同。'"
:message="
unavailableAction?.unavailableCopy || '该操作暂未接入可靠服务合同。'
"
confirm-text="我知道了"
@confirm="unavailableActionVisible = false"
@cancel="unavailableActionVisible = false"
@@ -247,17 +254,43 @@ const members = ref([]);
const memberActions = Object.freeze([
{ key: "VIEW_PROFILE", label: "查看资料", routeKey: "T03" },
{ key: "ADD_FATHER", label: "添加父亲", routeKey: "T04", relationType: "FATHER" },
{ key: "ADD_MOTHER", label: "添加母亲", routeKey: "T04", relationType: "MOTHER" },
{ key: "ADD_SPOUSE", label: "添加配偶", routeKey: "T04", relationType: "SPOUSE" },
{ key: "ADD_SIBLING", label: "添加兄弟姐妹", routeKey: "T04", relationType: "SIBLING" },
{
key: "ADD_FATHER",
label: "添加父亲",
routeKey: "T04",
relationType: "FATHER",
},
{
key: "ADD_MOTHER",
label: "添加母亲",
routeKey: "T04",
relationType: "MOTHER",
},
{
key: "ADD_SPOUSE",
label: "添加配偶",
routeKey: "T04",
relationType: "SPOUSE",
},
{
key: "ADD_SIBLING",
label: "添加兄弟姐妹",
routeKey: "T04",
relationType: "SIBLING",
},
{ key: "ADJUST_RANK", label: "调整排行", routeKey: "T06", mode: "rank" },
{ key: "ADD_SON", label: "添加儿子", routeKey: "T04", relationType: "SON" },
{ key: "ADD_DAUGHTER", label: "添加女儿", routeKey: "T04", relationType: "DAUGHTER" },
{
key: "ADD_DAUGHTER",
label: "添加女儿",
routeKey: "T04",
relationType: "DAUGHTER",
},
{
key: "BIND_INVITE",
label: "邀请绑定",
unavailableCopy: "邀请签发、目标身份查找、绑定 mutation 和结果查询尚无可靠合同;当前不会借邀请码或普通入谱申请替代。",
unavailableCopy:
"邀请签发、目标身份查找、绑定 mutation 和结果查询尚无可靠合同;当前不会借邀请码或普通入谱申请替代。",
},
{ key: "EDIT_PROFILE", label: "编辑信息", routeKey: "T05" },
]);
@@ -278,14 +311,21 @@ const layoutMembers = computed(() => {
.filter((member) => Number(member.generation) === generation)
.sort(
(left, right) =>
String(left.parentId || "").localeCompare(String(right.parentId || "")) ||
String(left.spouseOf || left.id).localeCompare(String(right.spouseOf || right.id)) ||
String(left.parentId || "").localeCompare(
String(right.parentId || ""),
) ||
String(left.spouseOf || left.id).localeCompare(
String(right.spouseOf || right.id),
) ||
Number(Boolean(left.spouseOf)) - Number(Boolean(right.spouseOf)) ||
String(left.id).localeCompare(String(right.id)),
),
]),
);
const maxCount = Math.max(1, ...Array.from(groups.values()).map((group) => group.length));
const maxCount = Math.max(
1,
...Array.from(groups.values()).map((group) => group.length),
);
const canvasWidth = Math.max(660, maxCount * MEMBER_GAP + 80);
return generations.flatMap((generation, generationIndex) => {
const group = groups.get(generation) || [];
@@ -365,7 +405,8 @@ const lineageConnectors = computed(() => {
});
const childrenByParent = new Map();
layoutMembers.value.forEach((member) => {
if (member.spouseOf || !member.parentId || !memberById.has(member.parentId)) return;
if (member.spouseOf || !member.parentId || !memberById.has(member.parentId))
return;
const children = childrenByParent.get(member.parentId) || [];
children.push(member);
childrenByParent.set(member.parentId, children);
@@ -382,7 +423,9 @@ const lineageConnectors = computed(() => {
const parentAnchorY = spouse
? snapToGrid(parentBottom + FAMILY_LINK_OFFSET)
: parentBottom;
const childTop = Math.min(...children.map((child) => child.y - NODE_HALF_HEIGHT));
const childTop = Math.min(
...children.map((child) => child.y - NODE_HALF_HEIGHT),
);
const branchY = snapToGrid((parentAnchorY + childTop) / 2);
const minX = Math.min(...children.map((child) => child.x));
const maxX = Math.max(...children.map((child) => child.x));
@@ -403,12 +446,20 @@ const lineageConnectors = computed(() => {
connectors.push({
id: `${parentId}-${spouse.id}-family-spouse`,
kind: "family",
style: verticalStyle(spouse.x, spouse.y + NODE_HALF_HEIGHT, parentAnchorY),
style: verticalStyle(
spouse.x,
spouse.y + NODE_HALF_HEIGHT,
parentAnchorY,
),
});
connectors.push({
id: `${parentId}-${spouse.id}-family-bridge`,
kind: "family",
style: horizontalStyle(Math.min(parent.x, spouse.x), Math.max(parent.x, spouse.x), parentAnchorY),
style: horizontalStyle(
Math.min(parent.x, spouse.x),
Math.max(parent.x, spouse.x),
parentAnchorY,
),
});
}
connectors.push({
@@ -422,10 +473,12 @@ const lineageConnectors = computed(() => {
gridRow: `${Math.round(branchY / GRID_UNIT) + 1} / span 1`,
},
});
children.forEach((child) => connectors.push({
id: `${parentId}-${child.id}`,
style: verticalStyle(child.x, branchY, child.y - NODE_HALF_HEIGHT),
}));
children.forEach((child) =>
connectors.push({
id: `${parentId}-${child.id}`,
style: verticalStyle(child.x, branchY, child.y - NODE_HALF_HEIGHT),
}),
);
});
layoutMembers.value.forEach((member) => {
if (!member.spouseOf) return;
@@ -505,7 +558,9 @@ const loadTree = async (query = {}) => {
selected.value =
layoutMembers.value.find(
(item) => String(item.id) === String(query.selectedId),
) || layoutMembers.value[0] || null;
) ||
layoutMembers.value[0] ||
null;
treeState.value = members.value.length ? "tree" : "empty";
if (treeState.value === "tree" && selected.value) {
await nextTick();
@@ -545,14 +600,17 @@ const handleTreeScroll = (event) => {
if (ignoreTreeScroll || treeState.value !== "tree") return;
const scrollLeft = Number(event?.detail?.scrollLeft || 0);
currentTreeScrollLeft.value = scrollLeft;
treeHasDrifted.value = Math.abs(scrollLeft - centeredTreeScrollLeft.value) > 80;
treeHasDrifted.value =
Math.abs(scrollLeft - centeredTreeScrollLeft.value) > 80;
};
const getFamilyPartner = (member) => {
if (!member) return null;
const partnerId = member.spouseOf || layoutMembers.value.find(
(item) => String(item.spouseOf || "") === String(member.id),
)?.id;
const partnerId =
member.spouseOf ||
layoutMembers.value.find(
(item) => String(item.spouseOf || "") === String(member.id),
)?.id;
const partner = layoutMembers.value.find(
(item) => String(item.id) === String(partnerId || ""),
);
@@ -566,9 +624,7 @@ const recenterSelectedMember = async () => {
const [scrollRect, nodeRect, spouseRect] = await new Promise((resolve) => {
const query = uni.createSelectorQuery();
query.select(".tree-scroll").boundingClientRect();
query
.select(`#tree-member-${selected.value.id}`)
.boundingClientRect();
query.select(`#tree-member-${selected.value.id}`).boundingClientRect();
if (spouse) {
query.select(`#tree-member-${spouse.id}`).boundingClientRect();
}
@@ -580,14 +636,15 @@ const recenterSelectedMember = async () => {
: nodeRect.left;
const targetRight = spouseRect
? Math.max(
nodeRect.left + nodeRect.width,
spouseRect.left + spouseRect.width,
)
nodeRect.left + nodeRect.width,
spouseRect.left + spouseRect.width,
)
: nodeRect.left + nodeRect.width;
const centeredScrollLeft = Math.max(
0,
currentTreeScrollLeft.value +
(targetLeft + targetRight) / 2 - (scrollRect.left + scrollRect.width / 2),
(targetLeft + targetRight) / 2 -
(scrollRect.left + scrollRect.width / 2),
);
treeScrollLeft.value = centeredScrollLeft;
currentTreeScrollLeft.value = centeredScrollLeft;
@@ -623,13 +680,25 @@ const handleStateAction = () => {
const toMember = () => {
if (!selected.value) return Promise.resolve(false);
memberActionPanelVisible.value = false;
return openPage("T03", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01");
return openPage(
"T03",
{ genealogyId: genealogyId.value, personId: String(selected.value.id) },
"T01",
);
};
const toDirectory = () =>
openPage("T07", { genealogyId: genealogyId.value }, "T01");
const toRank = () =>
selected.value
? openPage("T06", { genealogyId: genealogyId.value, personId: String(selected.value.id), mode: "rank" }, "T01")
? openPage(
"T06",
{
genealogyId: genealogyId.value,
personId: String(selected.value.id),
mode: "rank",
},
"T01",
)
: Promise.resolve(false);
const openMemberPanel = (member) => {
selected.value = member;
+403 -92
View File
@@ -13,7 +13,8 @@
>
<ModulePageBackground module="tree" />
<view class="member-page__header">
<PageHeader title="成员档案"
<PageHeader
title="成员档案"
:action="canPreviewEdit ? '编辑预览' : ''"
custom-back
@back="requestBack"
@@ -26,14 +27,26 @@
</view>
<view class="member-panel">
<AppLoading v-if="memberState === 'loading'" text="正在读取成员档案" description="请稍候,正在整理成员资料。" />
<AppLoading
v-if="memberState === 'loading'"
text="正在读取成员档案"
description="请稍候,正在整理成员资料。"
/>
<view v-else-if="memberState === 'detail' && member" class="member-detail">
<view
v-else-if="memberState === 'detail' && member"
class="member-detail"
>
<view class="member-heading">
<view class="member-heading__seal"><text>{{ member.name.slice(0, 1) }}</text></view>
<view class="member-heading__seal"
><text>{{ member.name.slice(0, 1) }}</text></view
>
<view>
<text>{{ member.name }}</text>
<text> {{ member.generation }} · {{ member.relation }} · {{ member.branch }}</text>
<text
> {{ member.generation }} · {{ member.relation }} ·
{{ member.branch }}</text
>
</view>
</view>
@@ -45,10 +58,13 @@
<text class="member-section-title">基本资料</text>
<view v-for="item in details" :key="item.label" class="member-info-row">
<text>{{ item.label }}</text><text>{{ item.value || "未填写" }}</text>
<text>{{ item.label }}</text
><text>{{ item.value || "未填写" }}</text>
</view>
<text class="member-section-title member-section-title--relation">亲属关系</text>
<text class="member-section-title member-section-title--relation"
>亲属关系</text
>
<view class="member-relatives">
<view
v-for="relative in member.relatives"
@@ -56,22 +72,40 @@
:data-person-id="relative.id"
@click="openRelative(relative.id)"
>
<text>{{ relative.relation }}</text><text>{{ relative.name }}</text>
<text>{{ relative.relation }}</text
><text>{{ relative.name }}</text>
</view>
<text v-if="!member.relatives.length">尚未记录可查看的亲属</text>
</view>
<view class="member-record-actions">
<view role="button" aria-label="查看成长日志" @click="toGrowthJournal"><text>成长日志</text></view>
<view role="button" aria-label="查看人生事服务状态" @click="toLifeEvents"><text>人生事待开放</text></view>
<view role="button" aria-label="查看成长日志" @click="toGrowthJournal"
><text>成长日志</text></view
>
<view
role="button"
aria-label="查看人生事服务状态"
@click="toLifeEvents"
><text>人生事待开放</text></view
>
</view>
<view v-if="canPreviewEdit" class="member-profile-action" @click="toEdit"><text>制作成员编辑预览</text></view>
<view
v-if="canPreviewEdit"
class="member-profile-action"
@click="toEdit"
><text>制作成员编辑预览</text></view
>
</view>
<view v-else-if="memberState === 'restricted' && member" class="member-restricted">
<view
v-else-if="memberState === 'restricted' && member"
class="member-restricted"
>
<view class="member-heading">
<view class="member-heading__seal"><text>{{ member.name.slice(0, 1) }}</text></view>
<view class="member-heading__seal"
><text>{{ member.name.slice(0, 1) }}</text></view
>
<view>
<text>{{ member.name }}</text>
<text> {{ member.generation }} · {{ member.relation }}</text>
@@ -79,13 +113,17 @@
</view>
<text>{{ restrictedCopy.title }}</text>
<text>{{ restrictedCopy.copy }}</text>
<view class="member-restricted-action" @click="toTree"><text>返回上一位成员</text></view>
<view class="member-restricted-action" @click="toTree"
><text>返回上一位成员</text></view
>
</view>
<view v-else class="member-error">
<text>成员档案暂不可用</text>
<text>{{ errorMessage }}</text>
<view class="member-profile-action" @click="toTree"><text>返回世系树</text></view>
<view class="member-profile-action" @click="toTree"
><text>返回世系树</text></view
>
</view>
</view>
</view>
@@ -97,7 +135,11 @@ import { onBackPress, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import {
appApi,
createRequestController,
isRequestCancelled,
} from "@/utils/api.js";
import {
consumeNavigationResult,
handleBackPress,
@@ -116,23 +158,69 @@ const trailIndex = ref(-1);
const memberRequestController = createRequestController();
let loadSequence = 0;
const details = computed(() => member.value ? [
{ label: "别名", value: member.value.aliasName },
{ label: "字辈", value: member.value.generationName },
{ label: "性别(字典值)", value: member.value.sex },
{ label: "人物状态(字典值)", value: member.value.personStatus },
{ label: "出生日期", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.birthDate },
{ label: "出生农历", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.birthLunar },
{ label: "生卒信息", value: member.value.years },
{ label: "出生地", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.birthplace },
{ label: "逝世农历", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.deathLunar },
{ label: "逝世地", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.deathPlace },
{ label: "安葬地", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.burialPlace },
{ label: "配偶", value: member.value.spouseNames },
{ label: "所属支系", value: member.value.branch },
{ label: "生平", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.biography },
{ label: "备注", value: member.value.remark },
] : []);
const details = computed(() =>
member.value
? [
{ label: "别名", value: member.value.aliasName },
{ label: "字辈", value: member.value.generationName },
{ label: "性别(字典值)", value: member.value.sex },
{ label: "人物状态(字典值)", value: member.value.personStatus },
{
label: "出生日期",
value:
member.value.status === "forbidden"
? "按权限隐藏"
: member.value.birthDate,
},
{
label: "出生农历",
value:
member.value.status === "forbidden"
? "按权限隐藏"
: member.value.birthLunar,
},
{ label: "生卒信息", value: member.value.years },
{
label: "出生地",
value:
member.value.status === "forbidden"
? "按权限隐藏"
: member.value.birthplace,
},
{
label: "逝世农历",
value:
member.value.status === "forbidden"
? "按权限隐藏"
: member.value.deathLunar,
},
{
label: "逝世地",
value:
member.value.status === "forbidden"
? "按权限隐藏"
: member.value.deathPlace,
},
{
label: "安葬地",
value:
member.value.status === "forbidden"
? "按权限隐藏"
: member.value.burialPlace,
},
{ label: "配偶", value: member.value.spouseNames },
{ label: "所属支系", value: member.value.branch },
{
label: "生平",
value:
member.value.status === "forbidden"
? "按权限隐藏"
: member.value.biography,
},
{ label: "备注", value: member.value.remark },
]
: [],
);
const canPreviewEdit = computed(() => memberState.value === "detail");
const restrictedCopy = computed(() =>
member.value?.status === "forbidden"
@@ -145,18 +233,29 @@ const restrictedCopy = computed(() =>
copy: "页面只保留姓名、关系与世代,不展示其他资料或操作入口。",
},
);
const statusLabel = computed(() => ({ privacy: "隐私资料", deceased: "离世纪念", forbidden: "访问受限" })[member.value?.status] || "成员状态");
const statusDescription = computed(() => ({
privacy: "部分资料仅向授权成员展示",
deceased: "查看生平保留与纪念资料说明",
forbidden: "当前账号只能查看有限身份信息",
})[member.value?.status] || "查看成员状态说明");
const memberContextDescription = computed(() => ({
loading: "正在读取成员资料",
detail: "成员身份与亲属关系",
restricted: "隐私成员展示基础身份",
error: "请重新选择成员",
}[memberState.value] || "请重新选择成员"));
const statusLabel = computed(
() =>
({ privacy: "隐私资料", deceased: "离世纪念", forbidden: "访问受限" })[
member.value?.status
] || "成员状态",
);
const statusDescription = computed(
() =>
({
privacy: "部分资料仅向授权成员展示",
deceased: "查看生平保留与纪念资料说明",
forbidden: "当前账号只能查看有限身份信息",
})[member.value?.status] || "查看成员状态说明",
);
const memberContextDescription = computed(
() =>
({
loading: "正在读取成员资料",
detail: "成员身份与亲属关系",
restricted: "隐私成员仅展示基础身份",
error: "请重新选择成员",
})[memberState.value] || "请重新选择成员",
);
const loadMember = async (nextPersonId, { preserveCurrent = false } = {}) => {
const normalizedPersonId = String(nextPersonId || "");
@@ -210,7 +309,9 @@ const initializeMemberTrail = async (initialPersonId) => {
memberTrail.splice(0, memberTrail.length);
trailIndex.value = -1;
const normalizedPersonId = String(initialPersonId || "");
const loaded = await loadMember(normalizedPersonId, { preserveCurrent: true });
const loaded = await loadMember(normalizedPersonId, {
preserveCurrent: true,
});
if (!loaded) return false;
memberTrail.splice(0, memberTrail.length, normalizedPersonId);
trailIndex.value = 0;
@@ -219,7 +320,8 @@ const initializeMemberTrail = async (initialPersonId) => {
const openRelative = async (nextPersonId) => {
const normalizedPersonId = String(nextPersonId || "");
if (!normalizedPersonId || normalizedPersonId === personId.value) return false;
if (!normalizedPersonId || normalizedPersonId === personId.value)
return false;
// 先读取目标成员,只有真实读取成功后才截断前进历史并推进轨迹;失败时
// 当前成员、当前索引和既有历史全部保持不变。
@@ -235,7 +337,9 @@ const popMemberTrail = async () => {
while (trailIndex.value > 0) {
const targetIndex = trailIndex.value - 1;
const historicalPersonId = memberTrail[targetIndex];
const loaded = await loadMember(historicalPersonId, { preserveCurrent: true });
const loaded = await loadMember(historicalPersonId, {
preserveCurrent: true,
});
if (loaded) {
trailIndex.value = targetIndex;
return true;
@@ -285,58 +389,265 @@ onUnload(() => {
const toEdit = () => {
if (!canPreviewEdit.value) return false;
return openPage("T05", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
return openPage(
"T05",
{ genealogyId: genealogyId.value, personId: personId.value },
"T03",
);
};
const openMemberState = () =>
openPage("T08", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
openPage(
"T08",
{ genealogyId: genealogyId.value, personId: personId.value },
"T03",
);
const toGrowthJournal = () =>
openPage("R08", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
openPage(
"R08",
{ genealogyId: genealogyId.value, personId: personId.value },
"T03",
);
const toLifeEvents = () =>
openPage("R09", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
openPage(
"R09",
{ genealogyId: genealogyId.value, personId: personId.value },
"T03",
);
const toTree = requestBack;
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.member-page { display: flex; min-height: 100vh; flex-direction: column; padding-bottom: 28rpx; box-sizing: border-box; background: $paper; }
.member-page__header, .member-context, .member-panel { z-index: 2; }
.member-context { @include adaptive.adaptive-tree-field; width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; }
.member-context text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-context text:first-child { color: $ink; font-size: 26rpx; font-weight: 700; }
.member-panel { @include adaptive.adaptive-tree-panel; width: calc(100% - 32rpx); margin: 16rpx auto 0; padding: 9% 8%; }
.member-panel > .app-loading { margin-top: 30%; }
.member-heading { display: flex; align-items: center; gap: 20rpx; }
.member-heading__seal { display: flex; width: 74rpx; height: 74rpx; flex: 0 0 74rpx; align-items: center; justify-content: center; background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / contain no-repeat; }
.member-heading__seal text { color: #fff7e7; font-size: 28rpx; }
.member-heading > view:last-child text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-heading > view:last-child text:last-child { font-size: 23rpx; }
.member-heading > view:last-child text:first-child { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 36rpx; font-weight: 700; }
.member-status-entry { @include adaptive.adaptive-genealogy-list-card; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14rpx; margin-top: 18rpx; padding: 18rpx 22rpx; }
.member-status-entry text { color: $ink-muted; font-size: 22rpx; line-height: 1.4; }
.member-status-entry text:first-child, .member-status-entry text:last-child { color: $brand-red; font-weight: 700; }
.member-section-title { display: block; margin-top: 24rpx; color: $brand-red; font-size: 24rpx; font-weight: 700; }
.member-info-row { @include adaptive.adaptive-tree-field; display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 64rpx; align-items: center; gap: 18rpx; margin-top: 8rpx; padding: 8rpx 18rpx; }
.member-info-row text { color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-info-row text:last-child { color: $ink; text-align: right; }
.member-relatives { margin-top: 8rpx; font-size: 23rpx; }
.member-relatives > view { display: flex; justify-content: space-between; gap: 20rpx; padding: 12rpx 18rpx; color: $ink; font-size: 23rpx; }
.member-relatives > view text:last-child { color: $brand-red; }
.member-relatives > text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.5; }
.member-record-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12rpx; margin-top: 20rpx; }
.member-record-actions > view { @include adaptive.adaptive-scroll-button(secondary); display: flex; min-height: 72rpx; align-items: center; justify-content: center; padding: 8rpx 14rpx; text-align: center; }
.member-record-actions text { color: $brand-red; font-size: 22rpx; font-weight: 700; line-height: 1.35; }
.member-profile-action { @include adaptive.adaptive-scroll-button(primary); display: flex; min-height: 76rpx; align-items: center; justify-content: center; margin-top: 22rpx; }
.member-profile-action text { color: #fff9ed; font-size: 24rpx; font-weight: 700; }
.member-restricted { padding-top: 14rpx; }
.member-restricted > text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.6; text-align: center; }
.member-restricted > text:first-of-type { margin-top: 28rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 31rpx; font-weight: 700; }
.member-restricted > text + text { margin-top: 12rpx; }
.member-restricted-action { @include adaptive.adaptive-scroll-button(secondary); display: flex; min-height: 76rpx; align-items: center; justify-content: center; margin-top: 22rpx; }
.member-restricted-action text { color: $brand-red; font-size: 24rpx; font-weight: 700; }
.member-error { margin-top: 30%; text-align: center; }
.member-error > text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.55; }
.member-error > text:nth-child(2) { font-size: 24rpx; }
.member-error > text:first-child { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; }
@media (min-width: 400px) { .member-context, .member-panel { width: calc(100% - 48rpx); } }
.member-page {
display: flex;
min-height: 100vh;
flex-direction: column;
padding-bottom: 28rpx;
box-sizing: border-box;
background: $paper;
}
.member-page__header,
.member-context,
.member-panel {
z-index: 2;
}
.member-context {
@include adaptive.adaptive-tree-field;
width: calc(100% - 32rpx);
margin: 18rpx auto 0;
padding: 16rpx 24rpx;
}
.member-context text {
display: block;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.45;
}
.member-context text:first-child {
color: $ink;
font-size: 26rpx;
font-weight: 700;
}
.member-panel {
@include adaptive.adaptive-tree-panel;
width: calc(100% - 32rpx);
margin: 16rpx auto 0;
padding: 9% 8%;
}
.member-panel > .app-loading {
margin-top: 30%;
}
.member-heading {
display: flex;
align-items: center;
gap: 20rpx;
}
.member-heading__seal {
display: flex;
width: 74rpx;
height: 74rpx;
flex: 0 0 74rpx;
align-items: center;
justify-content: center;
background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png")
center / contain no-repeat;
}
.member-heading__seal text {
color: #fff7e7;
font-size: 28rpx;
}
.member-heading > view:last-child text {
display: block;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.45;
}
.member-heading > view:last-child text:last-child {
font-size: 23rpx;
}
.member-heading > view:last-child text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 36rpx;
font-weight: 700;
}
.member-status-entry {
@include adaptive.adaptive-genealogy-list-card;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 14rpx;
margin-top: 18rpx;
padding: 18rpx 22rpx;
}
.member-status-entry text {
color: $ink-muted;
font-size: 22rpx;
line-height: 1.4;
}
.member-status-entry text:first-child,
.member-status-entry text:last-child {
color: $brand-red;
font-weight: 700;
}
.member-section-title {
display: block;
margin-top: 24rpx;
color: $brand-red;
font-size: 24rpx;
font-weight: 700;
}
.member-info-row {
@include adaptive.adaptive-tree-field;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
min-height: 64rpx;
align-items: center;
gap: 18rpx;
margin-top: 8rpx;
padding: 8rpx 18rpx;
}
.member-info-row text {
color: $ink-muted;
font-size: 23rpx;
line-height: 1.45;
}
.member-info-row text:last-child {
color: $ink;
text-align: right;
}
.member-relatives {
margin-top: 8rpx;
font-size: 23rpx;
}
.member-relatives > view {
display: flex;
justify-content: space-between;
gap: 20rpx;
padding: 12rpx 18rpx;
color: $ink;
font-size: 23rpx;
}
.member-relatives > view text:last-child {
color: $brand-red;
}
.member-relatives > text {
display: block;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.5;
}
.member-record-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12rpx;
margin-top: 20rpx;
}
.member-record-actions > view {
@include adaptive.adaptive-scroll-button(secondary);
display: flex;
min-height: 72rpx;
align-items: center;
justify-content: center;
padding: 8rpx 14rpx;
text-align: center;
}
.member-record-actions text {
color: $brand-red;
font-size: 22rpx;
font-weight: 700;
line-height: 1.35;
}
.member-profile-action {
@include adaptive.adaptive-scroll-button(primary);
display: flex;
min-height: 76rpx;
align-items: center;
justify-content: center;
margin-top: 22rpx;
}
.member-profile-action text {
color: #fff9ed;
font-size: 24rpx;
font-weight: 700;
}
.member-restricted {
padding-top: 14rpx;
}
.member-restricted > text {
display: block;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.6;
text-align: center;
}
.member-restricted > text:first-of-type {
margin-top: 28rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 31rpx;
font-weight: 700;
}
.member-restricted > text + text {
margin-top: 12rpx;
}
.member-restricted-action {
@include adaptive.adaptive-scroll-button(secondary);
display: flex;
min-height: 76rpx;
align-items: center;
justify-content: center;
margin-top: 22rpx;
}
.member-restricted-action text {
color: $brand-red;
font-size: 24rpx;
font-weight: 700;
}
.member-error {
margin-top: 30%;
text-align: center;
}
.member-error > text {
display: block;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.55;
}
.member-error > text:nth-child(2) {
font-size: 24rpx;
}
.member-error > text:first-child {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 34rpx;
font-weight: 700;
}
@media (min-width: 400px) {
.member-context,
.member-panel {
width: calc(100% - 48rpx);
}
}
</style>
+448 -111
View File
@@ -10,19 +10,33 @@
>
<ModulePageBackground module="tree" />
<view class="add-relative-page__header">
<PageHeader :title="isFirstMember ? '录入首位成员' : '新增亲属'" custom-back @back="requestBack" />
<PageHeader
:title="isFirstMember ? '录入首位成员' : '新增亲属'"
custom-back
@back="requestBack"
/>
</view>
<view class="add-relative-panel">
<AppLoading v-if="addState === 'loading'" text="正在读取成员资料" description="请稍候,正在确认当前人物。" />
<AppLoading
v-if="addState === 'loading'"
text="正在读取成员资料"
description="请稍候,正在确认当前人物。"
/>
<view v-else-if="addState === 'form'" class="add-relative-form">
<text class="form-eyebrow">{{ isFirstMember ? "建立世系起点" : "补全家族关系" }}</text>
<text class="form-eyebrow">{{
isFirstMember ? "建立世系起点" : "补全家族关系"
}}</text>
<text class="form-title">{{ formTitle }}</text>
<text class="form-copy">{{ formCopy }}</text>
<view v-if="!isFirstMember" class="member-context">
<text>当前成员</text><text>{{ currentMember.name }} · {{ currentMember.generation }} </text>
<text>当前成员</text
><text
>{{ currentMember.name }} ·
{{ currentMember.generation }} </text
>
</view>
<view class="form-field">
@@ -35,35 +49,66 @@
@input="clearError('name')"
/>
</view>
<text v-if="fieldErrors.name" class="field-error">{{ fieldErrors.name }}</text>
<text v-if="fieldErrors.name" class="field-error">{{
fieldErrors.name
}}</text>
<view class="form-field">
<text>人物编号</text>
<input v-model="addForm.personNo" maxlength="40" placeholder="不填则由服务端生成" placeholder-class="form-placeholder" />
<input
v-model="addForm.personNo"
maxlength="40"
placeholder="不填则由服务端生成"
placeholder-class="form-placeholder"
/>
</view>
<picker :range="sexOptions.map((item) => item.label)" :value="optionIndex(sexOptions, addForm.sex)" @change="selectOption('sex', sexOptions, $event)">
<picker
:range="sexOptions.map((item) => item.label)"
:value="optionIndex(sexOptions, addForm.sex)"
@change="selectOption('sex', sexOptions, $event)"
>
<view class="form-field form-field--picker">
<text>性别</text><text>{{ optionLabel(sexOptions, addForm.sex) || "请选择" }}</text>
<text>性别</text
><text>{{ optionLabel(sexOptions, addForm.sex) || "请选择" }}</text>
</view>
</picker>
<picker :range="bindingModeOptions.map((item) => item.label)" :value="optionIndex(bindingModeOptions, addForm.bindingMode)" @change="selectBindingMode">
<picker
:range="bindingModeOptions.map((item) => item.label)"
:value="optionIndex(bindingModeOptions, addForm.bindingMode)"
@change="selectBindingMode"
>
<view class="form-field form-field--picker">
<text>身份认领</text><text>{{ optionLabel(bindingModeOptions, addForm.bindingMode) }}</text>
<text>身份认领</text
><text>{{
optionLabel(bindingModeOptions, addForm.bindingMode)
}}</text>
</view>
</picker>
<view class="form-field">
<text>业务用户</text>
<text class="form-field__hint">{{ appUserBindingHint }}</text>
</view>
<text v-if="fieldErrors.bindingMode" class="field-error">{{ fieldErrors.bindingMode }}</text>
<text v-if="fieldErrors.bindingMode" class="field-error">{{
fieldErrors.bindingMode
}}</text>
<view class="form-field form-field--upload">
<text>头像</text>
<view>
<button class="upload-button" :disabled="isAvatarUploading || isSubmitting" @click="uploadAvatar">{{ isAvatarUploading ? "上传中" : "选择图片" }}</button>
<text v-if="avatarFileName" class="upload-receipt">已上传{{ avatarFileName }}</text>
<button
class="upload-button"
:disabled="isAvatarUploading || isSubmitting"
@click="uploadAvatar"
>
{{ isAvatarUploading ? "上传中…" : "选择图片" }}
</button>
<text v-if="avatarFileName" class="upload-receipt"
>已上传{{ avatarFileName }}</text
>
</view>
</view>
<text v-if="avatarUploadError" class="field-error">{{ avatarUploadError }}</text>
<text v-if="avatarUploadError" class="field-error">{{
avatarUploadError
}}</text>
<picker
v-if="!isFirstMember"
@@ -76,73 +121,162 @@
<text>{{ addForm.relation || "请选择亲属关系" }}</text>
</view>
</picker>
<text v-if="fieldErrors.relation" class="field-error">{{ fieldErrors.relation }}</text>
<text v-if="fieldErrors.relation" class="field-error">{{
fieldErrors.relation
}}</text>
<view class="form-field">
<text>别名</text>
<input v-model="addForm.aliasName" maxlength="40" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
<input
v-model="addForm.aliasName"
maxlength="40"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>字辈</text>
<input v-model="addForm.generationName" maxlength="12" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
<input
v-model="addForm.generationName"
maxlength="12"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>世代</text>
<input v-if="!isFirstMember" v-model="addForm.generation" type="number" placeholder="可选正整数" placeholder-class="form-placeholder" />
<input
v-if="!isFirstMember"
v-model="addForm.generation"
type="number"
placeholder="可选正整数"
placeholder-class="form-placeholder"
/>
<text v-else>首位成员固定为 1</text>
</view>
<picker :range="personOptionLabels" :value="personOptionIndex(addForm.fatherId)" @change="selectPerson('fatherId', $event)">
<view class="form-field form-field--picker"><text>父亲</text><text>{{ personOptionLabel(addForm.fatherId) }}</text></view>
<picker
:range="personOptionLabels"
:value="personOptionIndex(addForm.fatherId)"
@change="selectPerson('fatherId', $event)"
>
<view class="form-field form-field--picker"
><text>父亲</text
><text>{{ personOptionLabel(addForm.fatherId) }}</text></view
>
</picker>
<picker :range="personOptionLabels" :value="personOptionIndex(addForm.motherId)" @change="selectPerson('motherId', $event)">
<view class="form-field form-field--picker"><text>母亲</text><text>{{ personOptionLabel(addForm.motherId) }}</text></view>
<picker
:range="personOptionLabels"
:value="personOptionIndex(addForm.motherId)"
@change="selectPerson('motherId', $event)"
>
<view class="form-field form-field--picker"
><text>母亲</text
><text>{{ personOptionLabel(addForm.motherId) }}</text></view
>
</picker>
<picker mode="date" :value="addForm.birthDate" @change="selectBirthDate">
<picker
mode="date"
:value="addForm.birthDate"
@change="selectBirthDate"
>
<view class="form-field form-field--picker">
<text>出生日期</text><text>{{ addForm.birthDate || "请选择" }}</text>
<text>出生日期</text
><text>{{ addForm.birthDate || "请选择" }}</text>
</view>
</picker>
<picker :range="lunarOptions.map((item) => item.label)" :value="optionIndex(lunarOptions, addForm.birthLunar)" @change="selectOption('birthLunar', lunarOptions, $event)">
<picker
:range="lunarOptions.map((item) => item.label)"
:value="optionIndex(lunarOptions, addForm.birthLunar)"
@change="selectOption('birthLunar', lunarOptions, $event)"
>
<view class="form-field form-field--picker">
<text>出生农历</text><text>{{ optionLabel(lunarOptions, addForm.birthLunar) || "请选择" }}</text>
<text>出生农历</text
><text>{{
optionLabel(lunarOptions, addForm.birthLunar) || "请选择"
}}</text>
</view>
</picker>
<view class="form-field">
<text>出生地</text>
<input v-model="addForm.birthPlace" maxlength="60" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
<input
v-model="addForm.birthPlace"
maxlength="60"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<picker mode="date" :value="addForm.deathDate" @change="selectDeathDate">
<picker
mode="date"
:value="addForm.deathDate"
@change="selectDeathDate"
>
<view class="form-field form-field--picker">
<text>逝世日期</text><text>{{ addForm.deathDate || "请选择" }}</text>
<text>逝世日期</text
><text>{{ addForm.deathDate || "请选择" }}</text>
</view>
</picker>
<picker :range="lunarOptions.map((item) => item.label)" :value="optionIndex(lunarOptions, addForm.deathLunar)" @change="selectOption('deathLunar', lunarOptions, $event)">
<picker
:range="lunarOptions.map((item) => item.label)"
:value="optionIndex(lunarOptions, addForm.deathLunar)"
@change="selectOption('deathLunar', lunarOptions, $event)"
>
<view class="form-field form-field--picker">
<text>逝世农历</text><text>{{ optionLabel(lunarOptions, addForm.deathLunar) || "请选择" }}</text>
<text>逝世农历</text
><text>{{
optionLabel(lunarOptions, addForm.deathLunar) || "请选择"
}}</text>
</view>
</picker>
<view class="form-field">
<text>逝世地</text>
<input v-model="addForm.deathPlace" maxlength="60" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
<input
v-model="addForm.deathPlace"
maxlength="60"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>安葬地</text>
<input v-model="addForm.burialPlace" maxlength="60" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
<input
v-model="addForm.burialPlace"
maxlength="60"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<picker :range="personStatusOptions.map((item) => item.label)" :value="optionIndex(personStatusOptions, addForm.personStatus)" @change="selectOption('personStatus', personStatusOptions, $event)">
<picker
:range="personStatusOptions.map((item) => item.label)"
:value="optionIndex(personStatusOptions, addForm.personStatus)"
@change="selectOption('personStatus', personStatusOptions, $event)"
>
<view class="form-field form-field--picker">
<text>人物状态</text><text>{{ optionLabel(personStatusOptions, addForm.personStatus) || "请选择" }}</text>
<text>人物状态</text
><text>{{
optionLabel(personStatusOptions, addForm.personStatus) || "请选择"
}}</text>
</view>
</picker>
<view class="form-field">
<text>排序值</text>
<input v-model="addForm.sortOrder" type="number" placeholder="可选整数,值越小越靠前" placeholder-class="form-placeholder" />
<input
v-model="addForm.sortOrder"
type="number"
placeholder="可选整数,值越小越靠前"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>关系名称</text>
<input v-if="isFirstMember" v-model="addForm.relationName" maxlength="30" placeholder="可选关系显示名称" placeholder-class="form-placeholder" />
<input
v-if="isFirstMember"
v-model="addForm.relationName"
maxlength="30"
placeholder="可选关系显示名称"
placeholder-class="form-placeholder"
/>
<text v-else>{{ relationLabel }}</text>
</view>
@@ -169,7 +303,10 @@
<text class="form-note">{{ formNote }}</text>
<view class="form-action" @click="submitAdd">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="scaleToFill"
/>
<text>{{ isSubmitting ? "正在保存…" : "保存成员" }}</text>
</view>
</view>
@@ -179,7 +316,10 @@
<text class="form-title">{{ resultCopy.title }}</text>
<text class="form-copy">{{ resultCopy.copy }}</text>
<view class="form-action" @click="handleResultAction">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="scaleToFill"
/>
<text>{{ resultCopy.action }}</text>
</view>
</view>
@@ -207,14 +347,17 @@ import AppLoading from "@/components/AppLoading.vue";
import AppDialog from "@/components/AppDialog.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import { isImagePickCancelled, pickAndUploadImage } from "@/utils/resumable-image-upload.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import {
handleBackPress,
returnTo,
runBackGuard,
} from "@/utils/navigation.js";
appApi,
createRequestController,
isRequestCancelled,
} from "@/utils/api.js";
import {
isImagePickCancelled,
pickAndUploadImage,
} from "@/utils/resumable-image-upload.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { handleBackPress, returnTo, runBackGuard } from "@/utils/navigation.js";
const addState = ref("loading");
const genealogyId = ref("");
@@ -240,7 +383,14 @@ const relationIntents = Object.freeze({
SON: { label: "儿子" },
DAUGHTER: { label: "女儿" },
});
const genericRelationTypes = Object.freeze(["FATHER", "MOTHER", "SPOUSE", "SIBLING", "SON", "DAUGHTER"]);
const genericRelationTypes = Object.freeze([
"FATHER",
"MOTHER",
"SPOUSE",
"SIBLING",
"SON",
"DAUGHTER",
]);
const sexOptions = Object.freeze([
{ label: "男", value: "0" },
{ label: "女", value: "1" },
@@ -261,7 +411,9 @@ const bindingModeOptions = Object.freeze([
{ label: "绑定指定用户", value: "SPECIFIED" },
]);
const personOptions = ref([{ label: "不选择", value: "" }]);
const personOptionLabels = computed(() => personOptions.value.map((item) => item.label));
const personOptionLabels = computed(() =>
personOptions.value.map((item) => item.label),
);
const addForm = reactive({
bindingMode: "NONE",
personNo: "",
@@ -290,15 +442,22 @@ const addForm = reactive({
const fieldErrors = reactive({ name: "", relation: "", bindingMode: "" });
const isFirstMember = computed(() => mode.value === "first");
const activeRelationIntent = computed(() => relationIntents[relationType.value] || null);
const activeRelationIntent = computed(
() => relationIntents[relationType.value] || null,
);
const relationOptions = computed(() => {
const types = activeRelationIntent.value ? [relationType.value] : genericRelationTypes;
const types = activeRelationIntent.value
? [relationType.value]
: genericRelationTypes;
return types.map((type) => relationIntents[type].label);
});
const relationLabel = computed(() => activeRelationIntent.value?.label || addForm.relation || "亲属关系");
const relationLabel = computed(
() => activeRelationIntent.value?.label || addForm.relation || "亲属关系",
);
const appUserBindingHint = computed(() => {
if (addForm.bindingMode === "NONE") return "不绑定账号,不提交业务用户 ID";
if (addForm.bindingMode === "SELF") return "由服务端从 Token 绑定当前登录账号,不提交业务用户 ID";
if (addForm.bindingMode === "SELF")
return "由服务端从 Token 绑定当前登录账号,不提交业务用户 ID";
return "需先选择指定业务用户;当前没有可用候选接口";
});
const hasValidContext = computed(
@@ -306,18 +465,20 @@ const hasValidContext = computed(
Boolean(genealogyId.value) &&
(isFirstMember.value ? !personId.value : Boolean(currentMember.value)),
);
const relationIndex = computed(() => Math.max(0, relationOptions.value.indexOf(addForm.relation)));
const relationIndex = computed(() =>
Math.max(0, relationOptions.value.indexOf(addForm.relation)),
);
const formTitle = computed(() =>
isFirstMember.value ? "录入家谱中的第一位成员" : `${currentMember.value?.name || "当前成员"}添加${relationLabel.value}`,
isFirstMember.value
? "录入家谱中的第一位成员"
: `${currentMember.value?.name || "当前成员"}添加${relationLabel.value}`,
);
const formCopy = computed(() =>
isFirstMember.value
? "首位成员将成为世系起点,后续可从此人继续补充配偶、子女和后代。"
: "先确认新成员与当前成员的关系,再填写可核实的身份信息。",
);
const formNote = computed(() =>
"红色 * 为必填项,其余内容可按家谱记载补充。",
);
const formNote = computed(() => "红色 * 为必填项,其余内容可按家谱记载补充。");
const resultCopy = computed(() => {
if (hasValidContext.value) {
return {
@@ -335,7 +496,9 @@ const resultCopy = computed(() => {
};
});
const hasDraft = computed(() =>
Object.entries(addForm).some(([field, value]) => field !== "bindingMode" && String(value).trim()),
Object.entries(addForm).some(
([field, value]) => field !== "bindingMode" && String(value).trim(),
),
);
const discardConfirmation = createDiscardConfirmation((visible) => {
discardDialogVisible.value = visible;
@@ -363,18 +526,23 @@ const loadCurrentMember = async () => {
};
const loadPersonOptions = async () => {
try {
const rows = await appApi.getLineagePersonOptions(genealogyId.value, { requestController: personOptionsRequestController });
const rows = await appApi.getLineagePersonOptions(genealogyId.value, {
requestController: personOptionsRequestController,
});
const options = rows
.map((item) => {
const value = String(item?.personId ?? item?.id ?? "");
if (!/^[1-9]\d*$/.test(value)) return null;
const name = String(item?.name || item?.personName || item?.personNo || "未命名人物");
const name = String(
item?.name || item?.personName || item?.personNo || "未命名人物",
);
return { value, label: `${name}${value}` };
})
.filter(Boolean);
personOptions.value = [{ label: "不选择", value: "" }, ...options];
} catch (error) {
if (!isRequestCancelled(error)) personOptions.value = [{ label: "暂无可选人物", value: "" }];
if (!isRequestCancelled(error))
personOptions.value = [{ label: "暂无可选人物", value: "" }];
}
};
onLoad((query) => {
@@ -382,7 +550,11 @@ onLoad((query) => {
personId.value = String(query.personId || "");
mode.value = query.mode === "first" ? "first" : "relative";
relationType.value = String(query.relationType || "");
if (!genealogyId.value || query.state === "error" || (!isFirstMember.value && !personId.value)) {
if (
!genealogyId.value ||
query.state === "error" ||
(!isFirstMember.value && !personId.value)
) {
addState.value = "error";
errorMessage.value = "当前家谱或成员上下文无效,请从世系树重新进入。";
return;
@@ -421,22 +593,35 @@ const requestBack = () =>
onBackPress((event) => handleBackPress(event, requestBack));
const clearError = (field) => { fieldErrors[field] = ""; };
const clearError = (field) => {
fieldErrors[field] = "";
};
const selectRelation = (event) => {
addForm.relation = relationOptions.value[Number(event.detail.value)] || "";
clearError("relation");
};
const optionIndex = (options, value) => Math.max(0, options.findIndex((item) => item.value === value));
const optionLabel = (options, value) => options.find((item) => item.value === value)?.label || "";
const optionIndex = (options, value) =>
Math.max(
0,
options.findIndex((item) => item.value === value),
);
const optionLabel = (options, value) =>
options.find((item) => item.value === value)?.label || "";
const selectOption = (field, options, event) => {
addForm[field] = options[Number(event.detail.value)]?.value || "";
};
const selectBindingMode = (event) => {
addForm.bindingMode = bindingModeOptions[Number(event.detail.value)]?.value || "NONE";
addForm.bindingMode =
bindingModeOptions[Number(event.detail.value)]?.value || "NONE";
fieldErrors.bindingMode = "";
};
const personOptionIndex = (value) => Math.max(0, personOptions.value.findIndex((item) => item.value === value));
const personOptionLabel = (value) => personOptions.value.find((item) => item.value === value)?.label || "不选择";
const personOptionIndex = (value) =>
Math.max(
0,
personOptions.value.findIndex((item) => item.value === value),
);
const personOptionLabel = (value) =>
personOptions.value.find((item) => item.value === value)?.label || "不选择";
const selectPerson = (field, event) => {
addForm[field] = personOptions.value[Number(event.detail.value)]?.value || "";
};
@@ -445,27 +630,37 @@ const uploadAvatar = async () => {
isAvatarUploading.value = true;
avatarUploadError.value = "";
try {
const receipt = await pickAndUploadImage({ requestController: addRequestController });
const receipt = await pickAndUploadImage({
requestController: addRequestController,
});
addForm.avatarOssId = receipt.ossId;
avatarFileName.value = receipt.fileName || "头像图片";
} catch (error) {
if (!isImagePickCancelled(error) && !isRequestCancelled(error)) avatarUploadError.value = error?.message || "头像上传失败,请稍后重试";
if (!isImagePickCancelled(error) && !isRequestCancelled(error))
avatarUploadError.value = error?.message || "头像上传失败,请稍后重试";
} finally {
isAvatarUploading.value = false;
}
};
const selectBirthDate = (event) => { addForm.birthDate = event.detail.value || ""; };
const selectDeathDate = (event) => { addForm.deathDate = event.detail.value || ""; };
const selectBirthDate = (event) => {
addForm.birthDate = event.detail.value || "";
};
const selectDeathDate = (event) => {
addForm.deathDate = event.detail.value || "";
};
const validateAddForm = () => {
fieldErrors.name = addForm.name.trim() ? "" : "请填写成员姓名";
fieldErrors.relation = isFirstMember.value || addForm.relation ? "" : "请选择与当前成员的关系";
fieldErrors.bindingMode = addForm.bindingMode === "SPECIFIED"
? "当前未提供可选业务用户,不能指定认领"
: "";
fieldErrors.relation =
isFirstMember.value || addForm.relation ? "" : "请选择与当前成员的关系";
fieldErrors.bindingMode =
addForm.bindingMode === "SPECIFIED"
? "当前未提供可选业务用户,不能指定认领"
: "";
return !fieldErrors.name && !fieldErrors.relation && !fieldErrors.bindingMode;
};
const submitAdd = async () => {
if (isSubmitting.value || isAvatarUploading.value || !validateAddForm()) return;
if (isSubmitting.value || isAvatarUploading.value || !validateAddForm())
return;
isSubmitting.value = true;
try {
const payload = {
@@ -492,17 +687,27 @@ const submitAdd = async () => {
...(isFirstMember.value
? { generation: 1, relationName: addForm.relationName }
: {
...(addForm.generation ? { generation: Number(addForm.generation) } : {}),
relationName: relationLabel.value,
}),
...(addForm.generation
? { generation: Number(addForm.generation) }
: {}),
relationName: relationLabel.value,
}),
};
if (isFirstMember.value) {
await appApi.createPerson(genealogyId.value, payload, { requestController: addRequestController });
await appApi.createPerson(genealogyId.value, payload, {
requestController: addRequestController,
});
} else {
const type = activeRelationIntent.value
? relationType.value
: genericRelationTypes[relationOptions.value.indexOf(addForm.relation)];
await appApi.createRelatedPerson(genealogyId.value, personId.value, type, payload, { requestController: addRequestController });
await appApi.createRelatedPerson(
genealogyId.value,
personId.value,
type,
payload,
{ requestController: addRequestController },
);
}
Object.assign(addForm, {
bindingMode: "NONE",
@@ -539,11 +744,10 @@ const submitAdd = async () => {
isSubmitting.value = false;
}
};
const handleResultAction = () => hasValidContext.value ? (addState.value = "form") : returnToTree();
const handleResultAction = () =>
hasValidContext.value ? (addState.value = "form") : returnToTree();
const returnToTree = async () => {
const confirmed = hasDraft.value
? await requestDiscardConfirmation()
: true;
const confirmed = hasDraft.value ? await requestDiscardConfirmation() : true;
if (!confirmed) return false;
return returnTo("T01", { genealogyId: genealogyId.value });
};
@@ -552,29 +756,162 @@ const returnToTree = async () => {
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.add-relative-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.add-relative-page__header { z-index: 3; }
.add-relative-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
.member-context, .form-field { @include adaptive.adaptive-tree-field; display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; }
.member-context text:first-child, .form-field > text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
.member-context text:last-child, .form-field > text:last-child, .form-field input, .form-field textarea { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
.required-mark { display: inline-block; margin-right: 4rpx; color: $brand-red; font-size: 22rpx; line-height: 1; transform: translateY(-2rpx); }
.form-field textarea { width: auto; min-height: 54rpx; text-align: left; }
.form-field__hint, .upload-receipt { color: $ink-muted; font-size: 21rpx; line-height: 1.4; text-align: right; }
.form-field--upload > view { display: grid; justify-items: end; gap: 6rpx; }
.upload-button { min-height: 54rpx; margin: 0; padding: 0 16rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }
.form-field--summary { align-items: start; }
.form-placeholder { color: #a79884; }
.field-error { display: block; margin: 5rpx 18rpx 0; color: $brand-red; font-size: 22rpx; line-height: 32rpx; }
.form-note { text-align: center; }
.form-action { display: grid; width: 100%; min-height: 76rpx; margin-top: 18rpx; }
.form-action image, .form-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
.form-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
.add-result { margin-top: 30%; text-align: center; }
.add-result .form-eyebrow, .add-result .form-copy { text-align: center; }
.add-result .form-action { width: 420rpx; max-width: 100%; margin-right: auto; margin-left: auto; }
@media (min-width: 400px) { .add-relative-panel { width: calc(100% - 48rpx); } }
.add-relative-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.add-relative-page__header {
z-index: 3;
}
.add-relative-panel {
@include adaptive.adaptive-tree-panel;
z-index: 2;
width: calc(100% - 32rpx);
margin: 18rpx auto 28rpx;
padding: 7.5% 8%;
}
.form-eyebrow {
display: block;
color: $brand-red;
font-size: 22rpx;
letter-spacing: 3rpx;
}
.form-title {
display: block;
margin-top: 10rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 34rpx;
font-weight: 700;
line-height: 1.35;
}
.form-copy,
.form-note {
display: block;
margin-top: 10rpx;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.5;
}
.member-context,
.form-field {
@include adaptive.adaptive-tree-field;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
min-height: 78rpx;
align-items: center;
gap: 20rpx;
margin-top: 14rpx;
padding: 12rpx 22rpx;
}
.member-context text:first-child,
.form-field > text:first-child {
color: $ink;
font-size: 24rpx;
font-weight: 700;
}
.member-context text:last-child,
.form-field > text:last-child,
.form-field input,
.form-field textarea {
min-width: 0;
color: $ink;
font-size: 24rpx;
line-height: 1.45;
text-align: right;
}
.required-mark {
display: inline-block;
margin-right: 4rpx;
color: $brand-red;
font-size: 22rpx;
line-height: 1;
transform: translateY(-2rpx);
}
.form-field textarea {
width: auto;
min-height: 54rpx;
text-align: left;
}
.form-field__hint,
.upload-receipt {
color: $ink-muted;
font-size: 21rpx;
line-height: 1.4;
text-align: right;
}
.form-field--upload > view {
display: grid;
justify-items: end;
gap: 6rpx;
}
.upload-button {
min-height: 54rpx;
margin: 0;
padding: 0 16rpx;
border: 1rpx solid rgba(159, 23, 15, 0.42);
border-radius: 8rpx;
background: transparent;
color: $brand-red;
font-size: 22rpx;
}
.form-field--summary {
align-items: start;
min-height: 200rpx;
}
.form-placeholder {
color: #a79884;
}
.field-error {
display: block;
margin: 5rpx 18rpx 0;
color: $brand-red;
font-size: 22rpx;
line-height: 32rpx;
}
.form-note {
text-align: center;
}
.form-action {
display: grid;
width: 100%;
min-height: 76rpx;
margin-top: 18rpx;
}
.form-action image,
.form-action text {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.form-action text {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
color: #fff9ed;
font-size: 24rpx;
font-weight: 700;
}
.add-result {
margin-top: 30%;
text-align: center;
}
.add-result .form-eyebrow,
.add-result .form-copy {
text-align: center;
}
.add-result .form-action {
width: 420rpx;
max-width: 100%;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 400px) {
.add-relative-panel {
width: calc(100% - 48rpx);
}
}
</style>
+485 -133
View File
@@ -9,124 +9,279 @@
}"
>
<ModulePageBackground module="tree" />
<view class="edit-member-page__header"><PageHeader title="编辑成员" custom-back @back="requestBack" /></view>
<view class="edit-member-page__header"
><PageHeader title="编辑成员" custom-back @back="requestBack"
/></view>
<view class="edit-member-panel">
<AppLoading v-if="editState === 'loading'" text="正在读取成员资料" description="请稍候,正在确认可编辑字段。" />
<AppLoading
v-if="editState === 'loading'"
text="正在读取成员资料"
description="请稍候,正在确认可编辑字段。"
/>
<view v-else-if="editState === 'form'" class="edit-member-form">
<text class="form-eyebrow">成员档案维护</text>
<text class="form-title">完善{{ originalMember.name }}的生命记录</text>
<text class="form-copy">基础身份用于世系展示生平说明会显示在有权限查看的成员档案中</text>
<text class="form-copy"
>基础身份用于世系展示生平说明会显示在有权限查看的成员档案中</text
>
<view class="member-context">
<text>成员身份</text><text> {{ originalMember.generation }} · {{ originalMember.branch }}</text>
<text>成员身份</text
><text
> {{ originalMember.generation }} ·
{{ originalMember.branch }}</text
>
</view>
<view class="form-field">
<text>姓名</text>
<input v-model="editForm.name" maxlength="20" placeholder="请输入姓名" placeholder-class="form-placeholder" @input="clearError('name')" />
<input
v-model="editForm.name"
maxlength="20"
placeholder="请输入姓名"
placeholder-class="form-placeholder"
@input="clearError('name')"
/>
</view>
<text v-if="fieldErrors.name" class="field-error">{{ fieldErrors.name }}</text>
<text v-if="fieldErrors.name" class="field-error">{{
fieldErrors.name
}}</text>
<view class="form-field">
<text>人物编号</text>
<input v-model="editForm.personNo" maxlength="40" placeholder="不填则由服务端生成" placeholder-class="form-placeholder" />
<input
v-model="editForm.personNo"
maxlength="40"
placeholder="不填则由服务端生成"
placeholder-class="form-placeholder"
/>
</view>
<picker :range="sexOptions.map((item) => item.label)" :value="optionIndex(sexOptions, editForm.sex)" @change="selectOption('sex', sexOptions, $event)">
<picker
:range="sexOptions.map((item) => item.label)"
:value="optionIndex(sexOptions, editForm.sex)"
@change="selectOption('sex', sexOptions, $event)"
>
<view class="form-field form-field--picker">
<text>性别</text><text>{{ optionLabel(sexOptions, editForm.sex) || "请选择" }}</text>
<text>性别</text
><text>{{
optionLabel(sexOptions, editForm.sex) || "请选择"
}}</text>
</view>
</picker>
<picker :range="bindingModeOptions.map((item) => item.label)" :value="optionIndex(bindingModeOptions, editForm.bindingMode)" @change="selectBindingMode">
<picker
:range="bindingModeOptions.map((item) => item.label)"
:value="optionIndex(bindingModeOptions, editForm.bindingMode)"
@change="selectBindingMode"
>
<view class="form-field form-field--picker">
<text>身份认领</text><text>{{ optionLabel(bindingModeOptions, editForm.bindingMode) }}</text>
<text>身份认领</text
><text>{{
optionLabel(bindingModeOptions, editForm.bindingMode)
}}</text>
</view>
</picker>
<view class="form-field">
<text>业务用户</text>
<text class="form-field__hint">{{ appUserBindingHint }}</text>
</view>
<text v-if="fieldErrors.bindingMode" class="field-error">{{ fieldErrors.bindingMode }}</text>
<text v-if="fieldErrors.bindingMode" class="field-error">{{
fieldErrors.bindingMode
}}</text>
<view class="form-field form-field--upload">
<text>头像</text>
<view>
<button class="upload-button" :disabled="isAvatarUploading || isSubmitting" @click="uploadAvatar">{{ isAvatarUploading ? "上传中" : "选择图片" }}</button>
<text v-if="avatarFileName" class="upload-receipt">已上传{{ avatarFileName }}</text>
<button
class="upload-button"
:disabled="isAvatarUploading || isSubmitting"
@click="uploadAvatar"
>
{{ isAvatarUploading ? "上传中…" : "选择图片" }}
</button>
<text v-if="avatarFileName" class="upload-receipt"
>已上传{{ avatarFileName }}</text
>
</view>
</view>
<text v-if="avatarUploadError" class="field-error">{{ avatarUploadError }}</text>
<text v-if="avatarUploadError" class="field-error">{{
avatarUploadError
}}</text>
<view class="form-field">
<text>别名</text>
<input v-model="editForm.aliasName" placeholder="别名或曾用名" placeholder-class="form-placeholder" />
<input
v-model="editForm.aliasName"
placeholder="别名或曾用名"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>字辈</text>
<input v-model="editForm.generationName" maxlength="12" placeholder="例如:文字辈" placeholder-class="form-placeholder" />
<input
v-model="editForm.generationName"
maxlength="12"
placeholder="例如:文字辈"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>世代</text>
<input v-model="editForm.generation" type="number" placeholder="正整数" placeholder-class="form-placeholder" />
<input
v-model="editForm.generation"
type="number"
placeholder="正整数"
placeholder-class="form-placeholder"
/>
</view>
<picker :range="personOptionLabels" :value="personOptionIndex(editForm.fatherId)" @change="selectPerson('fatherId', $event)">
<view class="form-field form-field--picker"><text>父亲</text><text>{{ personOptionLabel(editForm.fatherId) }}</text></view>
<picker
:range="personOptionLabels"
:value="personOptionIndex(editForm.fatherId)"
@change="selectPerson('fatherId', $event)"
>
<view class="form-field form-field--picker"
><text>父亲</text
><text>{{ personOptionLabel(editForm.fatherId) }}</text></view
>
</picker>
<picker :range="personOptionLabels" :value="personOptionIndex(editForm.motherId)" @change="selectPerson('motherId', $event)">
<view class="form-field form-field--picker"><text>母亲</text><text>{{ personOptionLabel(editForm.motherId) }}</text></view>
<picker
:range="personOptionLabels"
:value="personOptionIndex(editForm.motherId)"
@change="selectPerson('motherId', $event)"
>
<view class="form-field form-field--picker"
><text>母亲</text
><text>{{ personOptionLabel(editForm.motherId) }}</text></view
>
</picker>
<picker mode="date" :value="editForm.birthDate" @change="selectDate('birthDate', $event)">
<view class="form-field form-field--picker"><text>出生日期</text><text>{{ editForm.birthDate || "未填写" }}</text></view>
<picker
mode="date"
:value="editForm.birthDate"
@change="selectDate('birthDate', $event)"
>
<view class="form-field form-field--picker"
><text>出生日期</text
><text>{{ editForm.birthDate || "未填写" }}</text></view
>
</picker>
<picker :range="lunarOptions.map((item) => item.label)" :value="optionIndex(lunarOptions, editForm.birthLunar)" @change="selectOption('birthLunar', lunarOptions, $event)">
<picker
:range="lunarOptions.map((item) => item.label)"
:value="optionIndex(lunarOptions, editForm.birthLunar)"
@change="selectOption('birthLunar', lunarOptions, $event)"
>
<view class="form-field form-field--picker">
<text>出生农历</text><text>{{ optionLabel(lunarOptions, editForm.birthLunar) || "请选择" }}</text>
<text>出生农历</text
><text>{{
optionLabel(lunarOptions, editForm.birthLunar) || "请选择"
}}</text>
</view>
</picker>
<view class="form-field">
<text>出生地</text>
<input v-model="editForm.birthPlace" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
<input
v-model="editForm.birthPlace"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<picker mode="date" :value="editForm.deathDate" @change="selectDate('deathDate', $event)">
<view class="form-field form-field--picker"><text>离世日期</text><text>{{ editForm.deathDate || "在世或未填写" }}</text></view>
<picker
mode="date"
:value="editForm.deathDate"
@change="selectDate('deathDate', $event)"
>
<view class="form-field form-field--picker"
><text>离世日期</text
><text>{{ editForm.deathDate || "在世或未填写" }}</text></view
>
</picker>
<picker :range="lunarOptions.map((item) => item.label)" :value="optionIndex(lunarOptions, editForm.deathLunar)" @change="selectOption('deathLunar', lunarOptions, $event)">
<picker
:range="lunarOptions.map((item) => item.label)"
:value="optionIndex(lunarOptions, editForm.deathLunar)"
@change="selectOption('deathLunar', lunarOptions, $event)"
>
<view class="form-field form-field--picker">
<text>逝世农历</text><text>{{ optionLabel(lunarOptions, editForm.deathLunar) || "请选择" }}</text>
<text>逝世农历</text
><text>{{
optionLabel(lunarOptions, editForm.deathLunar) || "请选择"
}}</text>
</view>
</picker>
<view class="form-field">
<text>逝世地</text>
<input v-model="editForm.deathPlace" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
<input
v-model="editForm.deathPlace"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>安葬地</text>
<input v-model="editForm.burialPlace" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
<input
v-model="editForm.burialPlace"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<picker :range="personStatusOptions.map((item) => item.label)" :value="optionIndex(personStatusOptions, editForm.personStatus)" @change="selectOption('personStatus', personStatusOptions, $event)">
<picker
:range="personStatusOptions.map((item) => item.label)"
:value="optionIndex(personStatusOptions, editForm.personStatus)"
@change="selectOption('personStatus', personStatusOptions, $event)"
>
<view class="form-field form-field--picker">
<text>人物状态</text><text>{{ optionLabel(personStatusOptions, editForm.personStatus) || "请选择" }}</text>
<text>人物状态</text
><text>{{
optionLabel(personStatusOptions, editForm.personStatus) ||
"请选择"
}}</text>
</view>
</picker>
<view class="form-field">
<text>排序值</text>
<input v-model="editForm.sortOrder" type="number" placeholder="可选整数,值越小越靠前" placeholder-class="form-placeholder" />
<input
v-model="editForm.sortOrder"
type="number"
placeholder="可选整数,值越小越靠前"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>关系名称</text>
<input v-model="editForm.relationName" maxlength="30" placeholder="可选关系显示名称" placeholder-class="form-placeholder" />
<input
v-model="editForm.relationName"
maxlength="30"
placeholder="可选关系显示名称"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field form-field--summary">
<text>人物简介</text>
<textarea v-model="editForm.summary" auto-height maxlength="500" placeholder="记录生平、迁徙或重要经历" placeholder-class="form-placeholder" />
<textarea
v-model="editForm.summary"
auto-height
maxlength="500"
placeholder="记录生平、迁徙或重要经历"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field form-field--summary">
<text>备注</text>
<textarea v-model="editForm.remark" auto-height placeholder="记录其他需要保留的信息" placeholder-class="form-placeholder" />
<textarea
v-model="editForm.remark"
auto-height
placeholder="记录其他需要保留的信息"
placeholder-class="form-placeholder"
/>
</view>
<text v-if="fieldErrors.dates" class="field-error">{{ fieldErrors.dates }}</text>
<text v-if="fieldErrors.dates" class="field-error">{{
fieldErrors.dates
}}</text>
<text class="form-note">隐私字段只向本人和具备维护权限的家谱管理员展示</text>
<text class="form-note"
>隐私字段只向本人和具备维护权限的家谱管理员展示</text
>
<view class="form-action" @click="saveMember">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="scaleToFill"
/>
<text>{{ isSubmitting ? "正在保存…" : "保存资料" }}</text>
</view>
</view>
@@ -136,7 +291,10 @@
<text class="form-title">{{ resultCopy.title }}</text>
<text class="form-copy">{{ resultCopy.copy }}</text>
<view class="form-action" @click="handleResultAction">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="scaleToFill"
/>
<text>{{ resultCopy.action }}</text>
</view>
</view>
@@ -164,14 +322,17 @@ import AppLoading from "@/components/AppLoading.vue";
import AppDialog from "@/components/AppDialog.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import { isImagePickCancelled, pickAndUploadImage } from "@/utils/resumable-image-upload.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import {
goBack,
handleBackPress,
runBackGuard,
} from "@/utils/navigation.js";
appApi,
createRequestController,
isRequestCancelled,
} from "@/utils/api.js";
import {
isImagePickCancelled,
pickAndUploadImage,
} from "@/utils/resumable-image-upload.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation.js";
const editState = ref("loading");
const genealogyId = ref("");
@@ -235,11 +396,17 @@ const bindingModeOptions = Object.freeze([
{ label: "绑定指定用户", value: "SPECIFIED" },
]);
const personOptions = ref([{ label: "不选择", value: "" }]);
const personOptionLabels = computed(() => personOptions.value.map((item) => item.label));
const personOptionLabels = computed(() =>
personOptions.value.map((item) => item.label),
);
const appUserBindingHint = computed(() => {
if (editForm.bindingMode === "NONE") return "不绑定账号,保存时不提交业务用户 ID";
if (editForm.bindingMode === "SELF") return "由服务端从 Token 绑定当前登录账号,保存时不提交业务用户 ID";
return editForm.appUserId ? "保留当前已认领的指定业务用户" : "需先选择指定业务用户;当前没有可用候选接口";
if (editForm.bindingMode === "NONE")
return "不绑定账号,保存时不提交业务用户 ID";
if (editForm.bindingMode === "SELF")
return "由服务端从 Token 绑定当前登录账号,保存时不提交业务用户 ID";
return editForm.appUserId
? "保留当前已认领的指定业务用户"
: "需先选择指定业务用户;当前没有可用候选接口";
});
const formSnapshot = computed(() => JSON.stringify(editForm));
@@ -252,13 +419,33 @@ const isDirty = computed(
Boolean(baseline.value) &&
formSnapshot.value !== baseline.value,
);
const resultCopy = computed(() => ({
error: failedAction.value === "save"
? { eyebrow: "保存失败", title: "成员档案尚未保存", copy: errorMessage.value || "服务未确认本次修改,请检查填写后重试。", action: "返回修改" }
: hasValidContext.value
? { eyebrow: "资料读取失败", title: "暂时无法读取成员档案", copy: errorMessage.value || "请返回后重试。", action: "重新读取" }
: { eyebrow: "成员入口无效", title: "没有找到要编辑的成员", copy: errorMessage.value || "请从成员档案重新进入。", action: "返回上一页" },
}[editState.value] || {}));
const resultCopy = computed(
() =>
({
error:
failedAction.value === "save"
? {
eyebrow: "保存失败",
title: "成员档案尚未保存",
copy:
errorMessage.value || "服务未确认本次修改,请检查填写后重试。",
action: "返回修改",
}
: hasValidContext.value
? {
eyebrow: "资料读取失败",
title: "暂时无法读取成员档案",
copy: errorMessage.value || "请返回后重试。",
action: "重新读取",
}
: {
eyebrow: "成员入口无效",
title: "没有找到要编辑的成员",
copy: errorMessage.value || "请从成员档案重新进入。",
action: "返回上一页",
},
})[editState.value] || {},
);
const discardConfirmation = createDiscardConfirmation((visible) => {
discardDialogVisible.value = visible;
});
@@ -278,15 +465,18 @@ const loadMember = async () => {
originalMember.value = member;
Object.assign(editForm, {
appUserId: member.appUserId || "",
bindingMode: member.bindingMode || (member.appUserId ? "SPECIFIED" : "NONE"),
bindingMode:
member.bindingMode || (member.appUserId ? "SPECIFIED" : "NONE"),
personNo: member.personNo || "",
name: member.name,
aliasName: member.aliasName || "",
sex: member.sex || "",
generation: member.generation || "",
generationName: member.generationName || "",
fatherId: member.relatives.find((item) => item.relation === "父亲")?.id || "",
motherId: member.relatives.find((item) => item.relation === "亲")?.id || "",
fatherId:
member.relatives.find((item) => item.relation === "亲")?.id || "",
motherId:
member.relatives.find((item) => item.relation === "母亲")?.id || "",
avatarOssId: member.avatarOssId || "",
birthDate: datePart(member.birthDate),
birthLunar: member.birthLunar || "",
@@ -315,18 +505,23 @@ const loadMember = async () => {
};
const loadPersonOptions = async () => {
try {
const rows = await appApi.getLineagePersonOptions(genealogyId.value, { requestController: personOptionsRequestController });
const rows = await appApi.getLineagePersonOptions(genealogyId.value, {
requestController: personOptionsRequestController,
});
const options = rows
.map((item) => {
const value = String(item?.personId ?? item?.id ?? "");
if (!/^[1-9]\d*$/.test(value) || value === personId.value) return null;
const name = String(item?.name || item?.personName || item?.personNo || "未命名人物");
const name = String(
item?.name || item?.personName || item?.personNo || "未命名人物",
);
return { value, label: `${name}${value}` };
})
.filter(Boolean);
personOptions.value = [{ label: "不选择", value: "" }, ...options];
} catch (error) {
if (!isRequestCancelled(error)) personOptions.value = [{ label: "暂无可选人物", value: "" }];
if (!isRequestCancelled(error))
personOptions.value = [{ label: "暂无可选人物", value: "" }];
}
};
@@ -360,31 +555,48 @@ const requestBack = () =>
onBackPress((event) => handleBackPress(event, requestBack));
const clearError = (field) => { fieldErrors[field] = ""; };
const optionIndex = (options, value) => Math.max(0, options.findIndex((item) => item.value === value));
const optionLabel = (options, value) => options.find((item) => item.value === value)?.label || "";
const clearError = (field) => {
fieldErrors[field] = "";
};
const optionIndex = (options, value) =>
Math.max(
0,
options.findIndex((item) => item.value === value),
);
const optionLabel = (options, value) =>
options.find((item) => item.value === value)?.label || "";
const selectOption = (field, options, event) => {
editForm[field] = options[Number(event.detail.value)]?.value || "";
};
const selectBindingMode = (event) => {
editForm.bindingMode = bindingModeOptions[Number(event.detail.value)]?.value || "NONE";
editForm.bindingMode =
bindingModeOptions[Number(event.detail.value)]?.value || "NONE";
fieldErrors.bindingMode = "";
};
const personOptionIndex = (value) => Math.max(0, personOptions.value.findIndex((item) => item.value === value));
const personOptionLabel = (value) => personOptions.value.find((item) => item.value === value)?.label || "不选择";
const personOptionIndex = (value) =>
Math.max(
0,
personOptions.value.findIndex((item) => item.value === value),
);
const personOptionLabel = (value) =>
personOptions.value.find((item) => item.value === value)?.label || "不选择";
const selectPerson = (field, event) => {
editForm[field] = personOptions.value[Number(event.detail.value)]?.value || "";
editForm[field] =
personOptions.value[Number(event.detail.value)]?.value || "";
};
const uploadAvatar = async () => {
if (isAvatarUploading.value || isSubmitting.value) return;
isAvatarUploading.value = true;
avatarUploadError.value = "";
try {
const receipt = await pickAndUploadImage({ requestController: editRequestController });
const receipt = await pickAndUploadImage({
requestController: editRequestController,
});
editForm.avatarOssId = receipt.ossId;
avatarFileName.value = receipt.fileName || "头像图片";
} catch (error) {
if (!isImagePickCancelled(error) && !isRequestCancelled(error)) avatarUploadError.value = error?.message || "头像上传失败,请稍后重试";
if (!isImagePickCancelled(error) && !isRequestCancelled(error))
avatarUploadError.value = error?.message || "头像上传失败,请稍后重试";
} finally {
isAvatarUploading.value = false;
}
@@ -395,41 +607,57 @@ const selectDate = (field, event) => {
};
const validateEditForm = () => {
fieldErrors.name = editForm.name.trim() ? "" : "请填写成员姓名";
fieldErrors.dates = editForm.birthDate && editForm.deathDate && editForm.deathDate < editForm.birthDate ? "离世日期不能早于出生日期" : "";
fieldErrors.bindingMode = editForm.bindingMode === "SPECIFIED" && !editForm.appUserId
? "当前未提供可选业务用户,不能指定认领"
: "";
fieldErrors.dates =
editForm.birthDate &&
editForm.deathDate &&
editForm.deathDate < editForm.birthDate
? "离世日期不能早于出生日期"
: "";
fieldErrors.bindingMode =
editForm.bindingMode === "SPECIFIED" && !editForm.appUserId
? "当前未提供可选业务用户,不能指定认领"
: "";
return !fieldErrors.name && !fieldErrors.dates && !fieldErrors.bindingMode;
};
const saveMember = async () => {
if (isSubmitting.value || isAvatarUploading.value || !validateEditForm()) return;
if (isSubmitting.value || isAvatarUploading.value || !validateEditForm())
return;
isSubmitting.value = true;
try {
await appApi.updatePerson(genealogyId.value, personId.value, {
bindingMode: editForm.bindingMode,
...(editForm.bindingMode === "SPECIFIED" ? { appUserId: editForm.appUserId } : {}),
personNo: editForm.personNo,
name: editForm.name,
aliasName: editForm.aliasName,
sex: editForm.sex,
generation: editForm.generation ? Number(editForm.generation) : undefined,
generationName: editForm.generationName,
fatherId: editForm.fatherId,
motherId: editForm.motherId,
avatarOssId: editForm.avatarOssId,
birthDate: editForm.birthDate,
birthLunar: editForm.birthLunar,
birthPlace: editForm.birthPlace,
deathDate: editForm.deathDate,
deathLunar: editForm.deathLunar,
deathPlace: editForm.deathPlace,
burialPlace: editForm.burialPlace,
personStatus: editForm.personStatus,
biography: editForm.summary,
remark: editForm.remark,
sortOrder: editForm.sortOrder,
relationName: editForm.relationName,
}, { requestController: editRequestController });
await appApi.updatePerson(
genealogyId.value,
personId.value,
{
bindingMode: editForm.bindingMode,
...(editForm.bindingMode === "SPECIFIED"
? { appUserId: editForm.appUserId }
: {}),
personNo: editForm.personNo,
name: editForm.name,
aliasName: editForm.aliasName,
sex: editForm.sex,
generation: editForm.generation
? Number(editForm.generation)
: undefined,
generationName: editForm.generationName,
fatherId: editForm.fatherId,
motherId: editForm.motherId,
avatarOssId: editForm.avatarOssId,
birthDate: editForm.birthDate,
birthLunar: editForm.birthLunar,
birthPlace: editForm.birthPlace,
deathDate: editForm.deathDate,
deathLunar: editForm.deathLunar,
deathPlace: editForm.deathPlace,
burialPlace: editForm.burialPlace,
personStatus: editForm.personStatus,
biography: editForm.summary,
remark: editForm.remark,
sortOrder: editForm.sortOrder,
relationName: editForm.relationName,
},
{ requestController: editRequestController },
);
baseline.value = formSnapshot.value;
await goBack();
} catch (error) {
@@ -442,9 +670,7 @@ const saveMember = async () => {
}
};
const returnToMember = async () => {
const confirmed = isDirty.value
? await requestDiscardConfirmation()
: true;
const confirmed = isDirty.value ? await requestDiscardConfirmation() : true;
if (!confirmed) return false;
return goBack();
};
@@ -464,28 +690,154 @@ const handleResultAction = () => {
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.edit-member-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.edit-member-page__header { z-index: 3; }
.edit-member-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
.member-context, .form-field { @include adaptive.adaptive-tree-field; display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; }
.member-context text:first-child, .form-field > text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
.member-context text:last-child, .form-field > text:last-child, .form-field input, .form-field textarea { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
.form-field textarea { width: auto; min-height: 54rpx; text-align: left; }
.form-field__hint, .upload-receipt { color: $ink-muted; font-size: 21rpx; line-height: 1.4; text-align: right; }
.form-field--upload > view { display: grid; justify-items: end; gap: 6rpx; }
.upload-button { min-height: 54rpx; margin: 0; padding: 0 16rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }
.form-field--summary { align-items: start; }
.form-placeholder { color: #a79884; }
.field-error { display: block; margin: 5rpx 18rpx 0; color: $brand-red; font-size: 22rpx; line-height: 32rpx; }
.form-note { text-align: center; }
.form-action { display: grid; width: 100%; min-height: 76rpx; margin-top: 18rpx; }
.form-action image, .form-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
.form-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
.edit-result { margin-top: 30%; text-align: center; }
.edit-result .form-eyebrow, .edit-result .form-copy { text-align: center; }
.edit-result .form-action { width: 420rpx; max-width: 100%; margin-right: auto; margin-left: auto; }
@media (min-width: 400px) { .edit-member-panel { width: calc(100% - 48rpx); } }
.edit-member-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.edit-member-page__header {
z-index: 3;
}
.edit-member-panel {
@include adaptive.adaptive-tree-panel;
z-index: 2;
width: calc(100% - 32rpx);
margin: 18rpx auto 28rpx;
padding: 7.5% 8%;
}
.form-eyebrow {
display: block;
color: $brand-red;
font-size: 22rpx;
letter-spacing: 3rpx;
}
.form-title {
display: block;
margin-top: 10rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 34rpx;
font-weight: 700;
line-height: 1.35;
}
.form-copy,
.form-note {
display: block;
margin-top: 10rpx;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.5;
}
.member-context,
.form-field {
@include adaptive.adaptive-tree-field;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
min-height: 78rpx;
align-items: center;
gap: 20rpx;
margin-top: 14rpx;
padding: 12rpx 22rpx;
}
.member-context text:first-child,
.form-field > text:first-child {
color: $ink;
font-size: 24rpx;
font-weight: 700;
}
.member-context text:last-child,
.form-field > text:last-child,
.form-field input,
.form-field textarea {
min-width: 0;
color: $ink;
font-size: 24rpx;
line-height: 1.45;
text-align: right;
}
.form-field textarea {
width: auto;
min-height: 54rpx;
text-align: left;
}
.form-field__hint,
.upload-receipt {
color: $ink-muted;
font-size: 21rpx;
line-height: 1.4;
text-align: right;
}
.form-field--upload > view {
display: grid;
justify-items: end;
gap: 6rpx;
}
.upload-button {
min-height: 54rpx;
margin: 0;
padding: 0 16rpx;
border: 1rpx solid rgba(159, 23, 15, 0.42);
border-radius: 8rpx;
background: transparent;
color: $brand-red;
font-size: 22rpx;
}
.form-field--summary {
align-items: start;
min-height: 200rpx;
}
.form-placeholder {
color: #a79884;
}
.field-error {
display: block;
margin: 5rpx 18rpx 0;
color: $brand-red;
font-size: 22rpx;
line-height: 32rpx;
}
.form-note {
text-align: center;
}
.form-action {
display: grid;
width: 100%;
min-height: 76rpx;
margin-top: 18rpx;
}
.form-action image,
.form-action text {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.form-action text {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
color: #fff9ed;
font-size: 24rpx;
font-weight: 700;
}
.edit-result {
margin-top: 30%;
text-align: center;
}
.edit-result .form-eyebrow,
.edit-result .form-copy {
text-align: center;
}
.edit-result .form-action {
width: 420rpx;
max-width: 100%;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 400px) {
.edit-member-panel {
width: calc(100% - 48rpx);
}
}
</style>
+151 -29
View File
@@ -10,35 +10,65 @@
}"
>
<ModulePageBackground module="tree" />
<view class="rank-page__header"><PageHeader title="调整排行" custom-back @back="goBack" /></view>
<view class="rank-page__header"
><PageHeader title="调整排行" custom-back @back="goBack"
/></view>
<view class="rank-panel">
<AppLoading v-if="rankState === 'loading'" text="正在读取成员资料" description="请稍候,正在确认待调整人物。" />
<AppLoading
v-if="rankState === 'loading'"
text="正在读取成员资料"
description="请稍候,正在确认待调整人物。"
/>
<view v-else-if="rankState === 'form' && member" class="rank-form">
<text class="form-eyebrow">同辈排行</text>
<text class="form-title">调整{{ member.name }}的排行</text>
<text class="form-copy">排行调整必须由服务端以同辈原子操作完成避免逐人保存造成部分成功</text>
<text class="form-copy"
>排行调整必须由服务端以同辈原子操作完成避免逐人保存造成部分成功</text
>
<view class="member-context">
<text>当前成员</text><text> {{ member.generation }} · {{ member.branch }}</text>
<text>当前成员</text
><text> {{ member.generation }} · {{ member.branch }}</text>
</view>
<view class="rank-notice">
<text>当前服务状态</text>
<text>尚未提供带版本冲突处理和完整结果的同辈原子重排合同</text>
</view>
<view class="form-action" @click="showRankUnavailable">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="scaleToFill"
/>
<text>查看服务状态</text>
</view>
</view>
<view v-else class="rank-result">
<text class="form-eyebrow">{{ rankState === "unavailable" ? "服务暂未开放" : "成员入口无效" }}</text>
<text class="form-title">{{ rankState === "unavailable" ? "暂不能提交排行调整" : "暂时无法读取成员资料" }}</text>
<text class="form-copy">{{ rankState === "unavailable" ? "当前人物更新接口只有单人物 sortOrder 候选,不能保证同辈排行原子一致。本页不会逐人写入,也不会显示本地预览成功。" : errorMessage }}</text>
<view class="form-action" @click="rankState === 'unavailable' ? (rankState = 'form') : goBack()">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
<text>{{ rankState === "unavailable" ? "返回查看" : "返回世系树" }}</text>
<text class="form-eyebrow">{{
rankState === "unavailable" ? "服务暂未开放" : "成员入口无效"
}}</text>
<text class="form-title">{{
rankState === "unavailable"
? "暂不能提交排行调整"
: "暂时无法读取成员资料"
}}</text>
<text class="form-copy">{{
rankState === "unavailable"
? "当前人物更新接口只有单人物 sortOrder 候选,不能保证同辈排行原子一致。本页不会逐人写入,也不会显示本地预览成功。"
: errorMessage
}}</text>
<view
class="form-action"
@click="rankState === 'unavailable' ? (rankState = 'form') : goBack()"
>
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="scaleToFill"
/>
<text>{{
rankState === "unavailable" ? "返回查看" : "返回世系树"
}}</text>
</view>
</view>
</view>
@@ -51,7 +81,11 @@ import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import {
appApi,
createRequestController,
isRequestCancelled,
} from "@/utils/api.js";
import { goBack, handleBackPress } from "@/utils/navigation.js";
const rankState = ref("loading");
@@ -76,7 +110,8 @@ const loadMember = async () => {
} catch (error) {
if (activeLoad !== loadSequence || isRequestCancelled(error)) return;
member.value = null;
errorMessage.value = error?.message || "当前成员资料暂不可用,请返回世系树后重试。";
errorMessage.value =
error?.message || "当前成员资料暂不可用,请返回世系树后重试。";
rankState.value = "error";
}
};
@@ -107,20 +142,107 @@ onBackPress((event) => handleBackPress(event, goBack));
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.rank-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.rank-page__header { z-index: 3; }
.rank-panel { @include adaptive.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
.form-copy { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
.member-context, .rank-notice { @include adaptive.adaptive-tree-field; display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 16rpx; padding: 12rpx 22rpx; }
.member-context text:first-child, .rank-notice text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
.member-context text:last-child, .rank-notice text:last-child { min-width: 0; color: $ink-muted; font-size: 23rpx; line-height: 1.45; text-align: right; }
.form-action { display: grid; width: 100%; min-height: 76rpx; margin-top: 22rpx; }
.form-action image, .form-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
.form-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
.rank-result { margin-top: 30%; text-align: center; }
.rank-result .form-eyebrow, .rank-result .form-copy { text-align: center; }
.rank-result .form-action { width: 420rpx; max-width: 100%; margin-right: auto; margin-left: auto; }
@media (min-width: 400px) { .rank-panel { width: calc(100% - 48rpx); } }
.rank-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.rank-page__header {
z-index: 3;
}
.rank-panel {
@include adaptive.adaptive-tree-panel;
z-index: 2;
width: calc(100% - 32rpx);
margin: 18rpx auto 28rpx;
padding: 7.5% 8%;
}
.form-eyebrow {
display: block;
color: $brand-red;
font-size: 22rpx;
letter-spacing: 3rpx;
}
.form-title {
display: block;
margin-top: 10rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 34rpx;
font-weight: 700;
line-height: 1.35;
}
.form-copy {
display: block;
margin-top: 10rpx;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.5;
}
.member-context,
.rank-notice {
@include adaptive.adaptive-tree-field;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
min-height: 78rpx;
align-items: center;
gap: 20rpx;
margin-top: 16rpx;
padding: 12rpx 22rpx;
}
.member-context text:first-child,
.rank-notice text:first-child {
color: $ink;
font-size: 24rpx;
font-weight: 700;
}
.member-context text:last-child,
.rank-notice text:last-child {
min-width: 0;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.45;
text-align: right;
}
.form-action {
display: grid;
width: 100%;
min-height: 76rpx;
margin-top: 22rpx;
}
.form-action image,
.form-action text {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.form-action text {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
color: #fff9ed;
font-size: 24rpx;
font-weight: 700;
}
.rank-result {
margin-top: 30%;
text-align: center;
}
.rank-result .form-eyebrow,
.rank-result .form-copy {
text-align: center;
}
.rank-result .form-action {
width: 420rpx;
max-width: 100%;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 400px) {
.rank-panel {
width: calc(100% - 48rpx);
}
}
</style>
+10 -2
View File
@@ -97,7 +97,11 @@ import AppButton from "@/components/AppButton.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import {
appApi,
createRequestController,
isRequestCancelled,
} from "@/utils/api.js";
import { goBack, openPage } from "@/utils/navigation.js";
const genealogyId = ref("");
const keyword = ref("");
@@ -162,7 +166,11 @@ const retryDirectory = () => {
};
const openMember = (item) =>
hasValidContext.value
? openPage("T03", { genealogyId: genealogyId.value, personId: String(item.id) }, "T07")
? openPage(
"T03",
{ genealogyId: genealogyId.value, personId: String(item.id) },
"T07",
)
: Promise.resolve(false);
onUnload(() => {
loadSequence += 1;
+137 -24
View File
@@ -8,7 +8,9 @@
}"
>
<ModulePageBackground module="tree" />
<view class="member-status-page__header"><PageHeader :title="pageTitle" /></view>
<view class="member-status-page__header"
><PageHeader :title="pageTitle"
/></view>
<view v-if="hasValidContext" class="member-status-context">
<text>{{ genealogyName }}</text>
@@ -36,8 +38,15 @@
</view>
</view>
<view v-if="statusState !== 'loading'" class="status-action" @click="handleAction">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
<view
v-if="statusState !== 'loading'"
class="status-action"
@click="handleAction"
>
<image
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
mode="scaleToFill"
/>
<text>{{ activeStatus.action }}</text>
</view>
</view>
@@ -49,7 +58,11 @@ import { onLoad, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import {
appApi,
createRequestController,
isRequestCancelled,
} from "@/utils/api.js";
import { goBack } from "@/utils/navigation.js";
const genealogyId = ref("");
@@ -74,7 +87,11 @@ const states = {
title: "暂无法展示状态说明",
copy: "服务端返回了状态字段,但未提供可读名称或说明;本页不会向家人展示内部状态值。",
guideTitle: "当前合同范围",
guides: ["状态由服务端维护", "状态展示需要后端提供可读字典", "当前页不执行停用或任何状态写入"],
guides: [
"状态由服务端维护",
"状态展示需要后端提供可读字典",
"当前页不执行停用或任何状态写入",
],
action: "返回成员档案",
},
error: {
@@ -82,13 +99,19 @@ const states = {
title: "没有找到要查看的成员",
copy: "请从成员档案或世系树重新选择成员。",
guideTitle: "重新进入方式",
guides: ["从世系树选择成员节点", "从成员目录打开成员档案", "确认当前家谱仍然有效"],
guides: [
"从世系树选择成员节点",
"从成员目录打开成员档案",
"确认当前家谱仍然有效",
],
action: "返回上一页",
},
};
const activeStatus = computed(() => states[statusState.value] || states.error);
const pageTitle = computed(() => "成员状态");
const hasValidContext = computed(() => Boolean(genealogyId.value && personId.value));
const hasValidContext = computed(() =>
Boolean(genealogyId.value && personId.value),
);
const memberIdentityCopy = computed(() => {
if (!member.value) return "";
return `${member.value.name} · 第 ${member.value.generation}`;
@@ -133,21 +156,111 @@ const handleAction = () => goBack();
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.member-status-page { display: flex; min-height: 100vh; flex-direction: column; padding-bottom: 34rpx; box-sizing: border-box; background: $paper; }
.member-status-page__header, .member-status-context, .status-card, .status-guidance, .status-action { z-index: 2; }
.member-status-context { @include adaptive.adaptive-tree-field; width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; }
.member-status-context text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
.member-status-context text:first-child { color: $ink; font-size: 26rpx; font-weight: 700; }
.status-card { @include adaptive.adaptive-tree-panel; width: calc(100% - 32rpx); min-height: 330rpx; margin: 18rpx auto 0; padding: 12% 10%; }
.status-card__copy text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.55; text-align: center; }
.status-card__copy text:first-child { color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
.status-card__copy text:nth-child(2) { margin-top: 12rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 32rpx; font-weight: 700; }
.status-card__copy text:last-child { margin-top: 14rpx; font-size: 24rpx; }
.status-guidance { @include adaptive.adaptive-genealogy-list-card; width: calc(100% - 48rpx); margin: 16rpx auto 0; padding: 24rpx 28rpx; }
.status-guidance text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.6; }
.status-guidance text:first-child { margin-bottom: 8rpx; color: $ink; font-size: 26rpx; font-weight: 700; }
.status-action { display: grid; width: calc(100% - 72rpx); min-height: 76rpx; margin: 22rpx auto 0; }
.status-action image, .status-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
.status-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
@media (min-width: 400px) { .member-status-context, .status-card { width: calc(100% - 48rpx); } }
.member-status-page {
display: flex;
min-height: 100vh;
flex-direction: column;
padding-bottom: 34rpx;
box-sizing: border-box;
background: $paper;
}
.member-status-page__header,
.member-status-context,
.status-card,
.status-guidance,
.status-action {
z-index: 2;
}
.member-status-context {
@include adaptive.adaptive-tree-field;
width: calc(100% - 32rpx);
margin: 18rpx auto 0;
padding: 16rpx 24rpx;
}
.member-status-context text {
display: block;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.45;
}
.member-status-context text:first-child {
color: $ink;
font-size: 26rpx;
font-weight: 700;
}
.status-card {
@include adaptive.adaptive-tree-panel;
width: calc(100% - 32rpx);
min-height: 330rpx;
margin: 18rpx auto 0;
padding: 12% 10%;
}
.status-card__copy text {
display: block;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.55;
text-align: center;
}
.status-card__copy text:first-child {
color: $brand-red;
font-size: 22rpx;
letter-spacing: 3rpx;
}
.status-card__copy text:nth-child(2) {
margin-top: 12rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 32rpx;
font-weight: 700;
}
.status-card__copy text:last-child {
margin-top: 14rpx;
font-size: 24rpx;
}
.status-guidance {
@include adaptive.adaptive-genealogy-list-card;
width: calc(100% - 48rpx);
margin: 16rpx auto 0;
padding: 24rpx 28rpx;
}
.status-guidance text {
display: block;
color: $ink-muted;
font-size: 24rpx;
line-height: 1.6;
}
.status-guidance text:first-child {
margin-bottom: 8rpx;
color: $ink;
font-size: 26rpx;
font-weight: 700;
}
.status-action {
display: grid;
width: calc(100% - 72rpx);
min-height: 76rpx;
margin: 22rpx auto 0;
}
.status-action image,
.status-action text {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.status-action text {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
color: #fff9ed;
font-size: 24rpx;
font-weight: 700;
}
@media (min-width: 400px) {
.member-status-context,
.status-card {
width: calc(100% - 48rpx);
}
}
</style>