845 lines
28 KiB
Vue
845 lines
28 KiB
Vue
<template>
|
||
<view class="member-page">
|
||
<GenealogyPageBackground />
|
||
<view class="page-header">
|
||
<PageHeader title="家谱成员" custom-back @back="requestBack" />
|
||
</view>
|
||
|
||
<view class="page-content">
|
||
<view v-if="!hasValidGenealogyId" class="state-card">
|
||
<text>暂时无法打开成员页面</text>
|
||
<text>没有找到家谱信息,请返回家谱总览后重新进入。</text>
|
||
<AppButton block label="返回家谱总览" @click="returnToOverview" />
|
||
</view>
|
||
<view v-else-if="memberListState === 'loading'" class="state-card">
|
||
<AppLoading text="正在读取家谱成员" description="请稍候,正在同步成员和管理权限。" />
|
||
</view>
|
||
<view v-else-if="memberListState === 'error'" class="state-card">
|
||
<text>暂时无法读取成员</text>
|
||
<text>{{ memberListError || "请检查网络后重试。" }}</text>
|
||
<AppButton block type="secondary" label="重新加载" @click="loadMembers" />
|
||
</view>
|
||
<view v-else-if="memberListState === 'empty'" class="state-card">
|
||
<text>{{ hasLeftGenealogy ? "已退出这部家谱" : "还没有成员记录" }}</text>
|
||
<text>{{ hasLeftGenealogy ? "你的退出操作已经完成。" : "家人加入后,会显示在这里。" }}</text>
|
||
<AppButton
|
||
block
|
||
:label="hasLeftGenealogy ? '返回我的家谱' : '返回家谱总览'"
|
||
@click="returnToOverview"
|
||
/>
|
||
</view>
|
||
<view v-else class="member-list">
|
||
<view class="member-intro">
|
||
<text>共 {{ members.length }} 位成员</text>
|
||
<text>这里管理的是已加入家谱的账号成员;家谱中的人物资料请到“世系树”维护。</text>
|
||
</view>
|
||
<text v-if="feedbackMessage" class="page-feedback" role="status">
|
||
{{ feedbackMessage }}
|
||
</text>
|
||
|
||
<view
|
||
v-for="member in members"
|
||
:key="member.memberId"
|
||
class="member-card"
|
||
>
|
||
<view class="member-card__heading">
|
||
<view class="member-card__identity">
|
||
<text>{{ member.memberName }}</text>
|
||
<text
|
||
v-if="member.appUserNickName && member.appUserNickName !== member.memberName"
|
||
>
|
||
账号昵称:{{ member.appUserNickName }}
|
||
</text>
|
||
</view>
|
||
<text class="role-badge">{{ roleLabel(member.roleType) }}</text>
|
||
</view>
|
||
<view class="member-card__details">
|
||
<text>与家谱关系:{{ member.relationName || "未填写" }}</text>
|
||
<text>世系人物:{{ lineageLabel(member) }}</text>
|
||
<text v-if="member.joinTime">
|
||
加入时间:{{ formatMinuteTimestamp(member.joinTime) }}
|
||
</text>
|
||
</view>
|
||
<view v-if="hasMemberActions(member)" class="member-card__actions">
|
||
<AppButton
|
||
v-if="member.capabilities.canEdit"
|
||
compact
|
||
type="secondary"
|
||
label="编辑资料"
|
||
@click="openMemberEditor(member)"
|
||
/>
|
||
<AppButton
|
||
v-if="member.capabilities.canEdit && member.lineagePersonId"
|
||
compact
|
||
type="secondary"
|
||
label="解除人物绑定"
|
||
@click="openConfirmation('unlink', member)"
|
||
/>
|
||
<AppButton
|
||
v-if="member.capabilities.canTransferOwner"
|
||
compact
|
||
type="secondary"
|
||
label="转让谱主"
|
||
@click="openConfirmation('transfer', member)"
|
||
/>
|
||
<AppButton
|
||
v-if="member.capabilities.canRemove"
|
||
compact
|
||
type="secondary"
|
||
label="移出家谱"
|
||
@click="openConfirmation('remove', member)"
|
||
/>
|
||
<AppButton
|
||
v-if="isOwnerViewer && member.roleType === GENEALOGY_MEMBER_ROLE.ADMIN"
|
||
compact
|
||
type="secondary"
|
||
label="权限设置"
|
||
@click="openPermissionEditor(member)"
|
||
/>
|
||
<AppButton
|
||
v-if="member.capabilities.canLeave"
|
||
compact
|
||
type="secondary"
|
||
label="退出家谱"
|
||
@click="openConfirmation('leave', member)"
|
||
/>
|
||
</view>
|
||
<text v-else class="member-card__readonly">当前账号只能查看这位成员。</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<AppDialog
|
||
:visible="Boolean(editTarget)"
|
||
eyebrow="成员资料"
|
||
:title="`编辑${editTarget?.memberName || '成员'}`"
|
||
message="称呼和关系需要填写;角色、世系人物请从已有选项中选择。"
|
||
:confirm-text="operationPending ? '正在保存…' : '保存修改'"
|
||
cancel-text="取消"
|
||
show-cancel
|
||
:close-on-mask="!operationPending"
|
||
@confirm="saveEdit"
|
||
@cancel="closeEdit"
|
||
>
|
||
<view class="edit-form">
|
||
<label class="edit-field">
|
||
<text>成员称呼</text>
|
||
<input v-model.trim="editForm.memberName" maxlength="50" placeholder="例如:王叔叔" />
|
||
</label>
|
||
<label class="edit-field">
|
||
<text>与家谱关系</text>
|
||
<input v-model.trim="editForm.relationName" maxlength="100" placeholder="例如:本族成员" />
|
||
</label>
|
||
<picker
|
||
:disabled="editTarget?.roleType === GENEALOGY_MEMBER_ROLE.OWNER"
|
||
:range="roleOptions.map((roleOption) => roleOption.label)"
|
||
:value="roleIndex"
|
||
@change="selectRole"
|
||
>
|
||
<view class="edit-field edit-field--picker">
|
||
<text>成员角色</text><text>{{ roleLabel(editForm.roleType) }}</text>
|
||
</view>
|
||
</picker>
|
||
<picker :disabled="personOptionsState === 'loading'" :range="personOptionLabels" :value="personOptionIndex" @change="selectPerson">
|
||
<view class="edit-field edit-field--picker">
|
||
<text>绑定世系人物</text>
|
||
<text>{{ selectedPersonLabel }}</text>
|
||
</view>
|
||
</picker>
|
||
<text v-if="personOptionsState === 'loading'" class="edit-hint">正在读取世系人物…</text>
|
||
<text v-else-if="personOptionsState === 'error'" class="edit-error">人物选项暂时无法读取,本次可先修改其他资料。</text>
|
||
<text v-if="editError" class="edit-error" role="alert">{{ editError }}</text>
|
||
</view>
|
||
</AppDialog>
|
||
|
||
<AppDialog
|
||
:visible="Boolean(confirmTarget)"
|
||
eyebrow="请确认"
|
||
:title="confirmationCopy.title"
|
||
:message="confirmationCopy.message"
|
||
:confirm-text="operationPending ? '正在处理…' : confirmationCopy.confirm"
|
||
cancel-text="取消"
|
||
show-cancel
|
||
:close-on-mask="!operationPending"
|
||
@confirm="runConfirmedOperation"
|
||
@cancel="closeConfirmation"
|
||
>
|
||
<text v-if="operationError" class="edit-error" role="alert">{{ operationError }}</text>
|
||
</AppDialog>
|
||
<AppDialog :visible="Boolean(permissionTarget)" eyebrow="管理员权限" :title="`设置${permissionTarget?.memberName || '成员'}的权限`" :confirm-text="permissionSaving ? '正在保存' : '保存权限'" cancel-text="取消" show-cancel :close-on-mask="!permissionSaving" @confirm="savePermissions" @cancel="closePermissionEditor">
|
||
<AppLoading v-if="permissionLoading" text="正在读取权限目录" />
|
||
<view v-else-if="permissionOptions.length" class="permission-list">
|
||
<label v-for="option in permissionOptions" :key="option.code" class="permission-item" :class="{ 'permission-item--disabled': !option.enabled }">
|
||
<checkbox :checked="selectedPermissionCodes.includes(option.code)" :disabled="!option.enabled || permissionSaving" @click="togglePermission(option)" />
|
||
<view><text>{{ option.label }}</text><text v-if="option.description">{{ option.description }}</text></view>
|
||
</label>
|
||
</view>
|
||
<text v-else-if="!permissionError" class="edit-hint">当前没有可授权的权限项。</text>
|
||
<textarea v-model.trim="permissionReason" maxlength="200" placeholder="授权原因(可选)" class="permission-reason" />
|
||
<text v-if="permissionError" class="edit-error">{{ permissionError }}</text>
|
||
</AppDialog>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, reactive, ref } from "vue";
|
||
import { onBackPress, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||
import AppButton from "@/components/AppButton.vue";
|
||
import AppDialog from "@/components/AppDialog.vue";
|
||
import AppLoading from "@/components/AppLoading.vue";
|
||
import GenealogyPageBackground from "@/components/genealogy/PageBackground.vue";
|
||
import PageHeader from "@/components/PageHeader.vue";
|
||
import {
|
||
GENEALOGY_MEMBER_ROLE,
|
||
GENEALOGY_MEMBER_ROLE_LABELS
|
||
} from "@/services/api/genealogy-member-contract.js";
|
||
import {
|
||
createRequestController,
|
||
isRequestCancelled
|
||
} from "@/services/api/request-controller.js";
|
||
import { genealogyMemberApi } from "@/services/api/genealogy-member-service.js";
|
||
import { genealogyCapabilityApi } from "@/services/api/genealogy-capability-service.js";
|
||
import { lineageApi } from "@/services/api/lineage-service.js";
|
||
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
|
||
import { formatMinuteTimestamp } from "@/utils/display-time.js";
|
||
import { handleBackPress, returnTo, runBackGuard } from "@/utils/navigation/gateway.js";
|
||
|
||
const genealogyId = ref("");
|
||
const members = ref([]);
|
||
const memberListState = ref("loading");
|
||
const memberListError = ref("");
|
||
const feedbackMessage = ref("");
|
||
const editTarget = ref(null);
|
||
const confirmTarget = ref(null);
|
||
const editError = ref("");
|
||
const operationError = ref("");
|
||
const operationPending = ref(false);
|
||
const hasLeftGenealogy = ref(false);
|
||
const personOptions = ref([]);
|
||
const personOptionsState = ref("idle");
|
||
const memberListController = createRequestController();
|
||
const permissionTarget = ref(null);
|
||
const selectedPermissionCodes = ref([]);
|
||
const permissionReason = ref("");
|
||
const permissionSaving = ref(false);
|
||
const permissionLoading = ref(false);
|
||
const permissionError = ref("");
|
||
const permissionOptions = ref([]);
|
||
const permissionCatalogController = createRequestController();
|
||
const memberPermissionController = createRequestController();
|
||
const memberPermissionSaveController = createRequestController();
|
||
let permissionLoadSequence = 0;
|
||
const personOptionsController = createRequestController();
|
||
const memberUpdateController = createRequestController();
|
||
const membershipOperationController = createRequestController();
|
||
let isPageActive = true;
|
||
|
||
const editForm = reactive({
|
||
memberName: "",
|
||
relationName: "",
|
||
roleType: GENEALOGY_MEMBER_ROLE.MEMBER,
|
||
lineagePersonId: "",
|
||
});
|
||
const hasValidGenealogyId = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
|
||
const isOwnerViewer = computed(() =>
|
||
members.value.some((member) => member.capabilities.canTransferOwner),
|
||
);
|
||
const roleOptions = computed(() => {
|
||
const options = [
|
||
...(isOwnerViewer.value
|
||
? [{ label: GENEALOGY_MEMBER_ROLE_LABELS[GENEALOGY_MEMBER_ROLE.ADMIN], value: GENEALOGY_MEMBER_ROLE.ADMIN }]
|
||
: []),
|
||
{ label: GENEALOGY_MEMBER_ROLE_LABELS[GENEALOGY_MEMBER_ROLE.EDITOR], value: GENEALOGY_MEMBER_ROLE.EDITOR },
|
||
{ label: GENEALOGY_MEMBER_ROLE_LABELS[GENEALOGY_MEMBER_ROLE.MEMBER], value: GENEALOGY_MEMBER_ROLE.MEMBER },
|
||
];
|
||
const currentRole = editTarget.value?.roleType;
|
||
if (currentRole && !options.some((option) => option.value === currentRole)) {
|
||
options.unshift({
|
||
label: GENEALOGY_MEMBER_ROLE_LABELS[currentRole] || "当前角色",
|
||
value: currentRole,
|
||
});
|
||
}
|
||
return options;
|
||
});
|
||
const roleIndex = computed(() =>
|
||
Math.max(
|
||
0,
|
||
roleOptions.value.findIndex(
|
||
(roleOption) => roleOption.value === editForm.roleType,
|
||
),
|
||
),
|
||
);
|
||
const personOptionLabels = computed(() => [
|
||
"保持当前绑定",
|
||
...personOptions.value.map(
|
||
(person) =>
|
||
`${person.name}${person.generation ? ` · 第${person.generation}世` : ""}`,
|
||
),
|
||
]);
|
||
const personOptionIndex = computed(() => {
|
||
if (!editForm.lineagePersonId || editForm.lineagePersonId === editTarget.value?.lineagePersonId) return 0;
|
||
const index = personOptions.value.findIndex(
|
||
(person) => person.id === editForm.lineagePersonId,
|
||
);
|
||
return index < 0 ? 0 : index + 1;
|
||
});
|
||
const selectedPersonLabel = computed(
|
||
() => personOptionLabels.value[personOptionIndex.value] || "保持当前绑定",
|
||
);
|
||
const confirmationCopy = computed(() => {
|
||
const memberName = confirmTarget.value?.member?.memberName || "这位成员";
|
||
const copyByOperation = {
|
||
unlink: {
|
||
title: "解除人物绑定?",
|
||
message: `解除后,“${memberName}”的账号仍在家谱中,但不再对应世系树人物。`,
|
||
confirm: "确认解除",
|
||
},
|
||
remove: {
|
||
title: "将成员移出家谱?",
|
||
message: `移出后,“${memberName}”将不能再以成员身份访问这部家谱。`,
|
||
confirm: "确认移出",
|
||
},
|
||
leave: {
|
||
title: "退出这部家谱?",
|
||
message: "退出后,你将不能再查看仅对成员开放的内容。",
|
||
confirm: "确认退出",
|
||
},
|
||
transfer: {
|
||
title: "转让谱主身份?",
|
||
message: `转让后,“${memberName}”将成为新谱主,你会变为管理员。`,
|
||
confirm: "确认转让",
|
||
},
|
||
};
|
||
return copyByOperation[confirmTarget.value?.kind] || {
|
||
title: "确认操作?",
|
||
message: "请确认是否继续。",
|
||
confirm: "确认",
|
||
};
|
||
});
|
||
|
||
const roleLabel = (role) => GENEALOGY_MEMBER_ROLE_LABELS[role] || "成员";
|
||
const lineageLabel = (member) =>
|
||
member.lineagePersonName || (member.lineagePersonId ? "已绑定" : "未绑定");
|
||
const hasMemberActions = (member) =>
|
||
Object.values(member.capabilities).some(Boolean);
|
||
|
||
const loadMembers = async () => {
|
||
if (!hasValidGenealogyId.value) return;
|
||
memberListController.abort();
|
||
memberListState.value = "loading";
|
||
memberListError.value = "";
|
||
try {
|
||
const loadedMembers = await genealogyMemberApi.getMembers(genealogyId.value, {
|
||
requestController: memberListController,
|
||
});
|
||
if (!isPageActive) return;
|
||
members.value = loadedMembers;
|
||
memberListState.value = members.value.length ? "ready" : "empty";
|
||
} catch (error) {
|
||
if (!isPageActive || isRequestCancelled(error)) return;
|
||
memberListError.value = getRequestErrorMessage(error, "成员加载失败,请稍后重试。");
|
||
memberListState.value = "error";
|
||
}
|
||
};
|
||
const loadPersonOptions = async () => {
|
||
if (personOptionsState.value === "loading" || personOptionsState.value === "ready") return;
|
||
personOptionsState.value = "loading";
|
||
try {
|
||
const loadedPersonOptions = await lineageApi.getLineagePersonOptions(genealogyId.value, {
|
||
requestController: personOptionsController,
|
||
});
|
||
if (!isPageActive) return;
|
||
personOptions.value = loadedPersonOptions;
|
||
personOptionsState.value = "ready";
|
||
} catch (error) {
|
||
if (!isPageActive || isRequestCancelled(error)) return;
|
||
personOptionsState.value = "error";
|
||
}
|
||
};
|
||
const openMemberEditor = (member) => {
|
||
if (!member.capabilities.canEdit || operationPending.value) return;
|
||
editTarget.value = member;
|
||
editForm.memberName = member.memberName;
|
||
editForm.relationName = member.relationName;
|
||
editForm.roleType = member.roleType;
|
||
editForm.lineagePersonId = member.lineagePersonId || "";
|
||
editError.value = "";
|
||
void loadPersonOptions();
|
||
};
|
||
const closeEdit = () => {
|
||
if (operationPending.value) return;
|
||
editTarget.value = null;
|
||
editError.value = "";
|
||
};
|
||
const selectRole = (event) => {
|
||
editForm.roleType = roleOptions.value[Number(event.detail.value)]?.value || editForm.roleType;
|
||
};
|
||
const selectPerson = (event) => {
|
||
const index = Number(event.detail.value);
|
||
editForm.lineagePersonId = index > 0 ? personOptions.value[index - 1]?.id || "" : editTarget.value?.lineagePersonId || "";
|
||
};
|
||
const saveEdit = async () => {
|
||
const target = editTarget.value;
|
||
if (!target || operationPending.value) return;
|
||
if (!editForm.memberName.trim()) {
|
||
editError.value = "请填写成员称呼。";
|
||
return;
|
||
}
|
||
operationPending.value = true;
|
||
editError.value = "";
|
||
try {
|
||
const updated = await genealogyMemberApi.updateMember(
|
||
genealogyId.value,
|
||
target.memberId,
|
||
{
|
||
memberName: editForm.memberName,
|
||
relationName: editForm.relationName,
|
||
...(editForm.roleType !== target.roleType
|
||
? { roleType: editForm.roleType }
|
||
: {}),
|
||
...(editForm.lineagePersonId &&
|
||
editForm.lineagePersonId !== target.lineagePersonId
|
||
? { lineagePersonId: editForm.lineagePersonId }
|
||
: {}),
|
||
},
|
||
{ requestController: memberUpdateController },
|
||
);
|
||
if (!isPageActive) return;
|
||
members.value = members.value.map((member) =>
|
||
member.memberId === updated.memberId ? updated : member,
|
||
);
|
||
feedbackMessage.value = `“${updated.memberName}”的成员资料已更新。`;
|
||
editTarget.value = null;
|
||
} catch (error) {
|
||
if (!isPageActive || isRequestCancelled(error)) return;
|
||
editError.value = getRequestErrorMessage(error, "保存失败,请稍后重试。");
|
||
} finally {
|
||
if (isPageActive) operationPending.value = false;
|
||
}
|
||
};
|
||
const openConfirmation = (kind, member) => {
|
||
if (operationPending.value) return;
|
||
confirmTarget.value = { kind, member };
|
||
operationError.value = "";
|
||
};
|
||
const closeConfirmation = () => {
|
||
if (operationPending.value) return;
|
||
confirmTarget.value = null;
|
||
operationError.value = "";
|
||
};
|
||
const runConfirmedOperation = async () => {
|
||
const target = confirmTarget.value;
|
||
if (!target || operationPending.value) return;
|
||
operationPending.value = true;
|
||
operationError.value = "";
|
||
let operationCommitted = false;
|
||
try {
|
||
const requestOptions = {
|
||
requestController: membershipOperationController,
|
||
};
|
||
switch (target.kind) {
|
||
case "unlink":
|
||
await genealogyMemberApi.unlinkMemberLineagePerson(
|
||
genealogyId.value,
|
||
target.member.memberId,
|
||
requestOptions,
|
||
);
|
||
break;
|
||
case "remove":
|
||
await genealogyMemberApi.removeMember(
|
||
genealogyId.value,
|
||
target.member.memberId,
|
||
requestOptions,
|
||
);
|
||
break;
|
||
case "leave":
|
||
await genealogyMemberApi.leaveGenealogy(genealogyId.value, requestOptions);
|
||
break;
|
||
case "transfer":
|
||
await genealogyMemberApi.transferGenealogyOwner(
|
||
genealogyId.value,
|
||
target.member.memberId,
|
||
requestOptions,
|
||
);
|
||
break;
|
||
default:
|
||
throw new Error(`不支持的成员操作:${target.kind}`);
|
||
}
|
||
operationCommitted = true;
|
||
if (!isPageActive) return;
|
||
confirmTarget.value = null;
|
||
if (target.kind === "leave") {
|
||
hasLeftGenealogy.value = true;
|
||
members.value = [];
|
||
memberListState.value = "empty";
|
||
await returnTo("G01");
|
||
return;
|
||
}
|
||
feedbackMessage.value =
|
||
({
|
||
unlink: "人物绑定已解除。",
|
||
remove: "成员已移出家谱。",
|
||
transfer: "谱主身份已转让。",
|
||
})[target.kind] || "操作已完成。";
|
||
await loadMembers();
|
||
} catch (error) {
|
||
if (!isPageActive) return;
|
||
if (operationCommitted && target.kind === "leave") {
|
||
hasLeftGenealogy.value = true;
|
||
members.value = [];
|
||
memberListState.value = "empty";
|
||
confirmTarget.value = null;
|
||
return;
|
||
}
|
||
if (isRequestCancelled(error)) return;
|
||
operationError.value = getRequestErrorMessage(error, "操作失败,请稍后重试。");
|
||
} finally {
|
||
if (isPageActive) operationPending.value = false;
|
||
}
|
||
};
|
||
const returnToOverview = () =>
|
||
hasLeftGenealogy.value
|
||
? returnTo("G01")
|
||
: returnTo("G05", { genealogyId: genealogyId.value });
|
||
const openPermissionEditor = async (member) => {
|
||
const activeLoad = ++permissionLoadSequence;
|
||
permissionCatalogController.abort();
|
||
memberPermissionController.abort();
|
||
permissionTarget.value = member;
|
||
permissionOptions.value = [];
|
||
selectedPermissionCodes.value = [];
|
||
permissionReason.value = "";
|
||
permissionError.value = "";
|
||
permissionLoading.value = true;
|
||
try {
|
||
const catalog = await genealogyCapabilityApi.getPermissionCatalog(
|
||
genealogyId.value,
|
||
{ requestController: permissionCatalogController },
|
||
);
|
||
const enabledCodes = catalog.filter((option) => option.enabled).map((option) => option.code);
|
||
const permissions = await genealogyCapabilityApi.getMemberPermissions(
|
||
genealogyId.value,
|
||
member.memberId,
|
||
enabledCodes,
|
||
{ requestController: memberPermissionController },
|
||
);
|
||
if (!isPageActive || activeLoad !== permissionLoadSequence) return;
|
||
permissionOptions.value = catalog;
|
||
selectedPermissionCodes.value = permissions.permissionCodes;
|
||
} catch (error) {
|
||
if (!isPageActive || activeLoad !== permissionLoadSequence || isRequestCancelled(error)) return;
|
||
permissionError.value = getRequestErrorMessage(error, "权限目录暂时无法读取,当前不会保存任何变更。");
|
||
} finally {
|
||
if (isPageActive && activeLoad === permissionLoadSequence) permissionLoading.value = false;
|
||
}
|
||
};
|
||
const closePermissionEditor = () => {
|
||
if (permissionSaving.value) return;
|
||
permissionLoadSequence += 1;
|
||
permissionCatalogController.abort();
|
||
memberPermissionController.abort();
|
||
permissionTarget.value = null;
|
||
permissionLoading.value = false;
|
||
};
|
||
const togglePermission = (option) => {
|
||
if (!option?.enabled || permissionSaving.value || permissionLoading.value) return;
|
||
selectedPermissionCodes.value = selectedPermissionCodes.value.includes(option.code)
|
||
? selectedPermissionCodes.value.filter((code) => code !== option.code)
|
||
: [...selectedPermissionCodes.value, option.code];
|
||
};
|
||
const savePermissions = async () => {
|
||
if (!permissionTarget.value || permissionSaving.value || permissionLoading.value || permissionError.value) return;
|
||
const enabledCodes = permissionOptions.value.filter((option) => option.enabled).map((option) => option.code);
|
||
permissionSaving.value = true;
|
||
permissionError.value = "";
|
||
try {
|
||
await genealogyCapabilityApi.saveMemberPermissions(
|
||
genealogyId.value,
|
||
permissionTarget.value.memberId,
|
||
selectedPermissionCodes.value,
|
||
permissionReason.value,
|
||
enabledCodes,
|
||
{ requestController: memberPermissionSaveController },
|
||
);
|
||
if (!isPageActive) return;
|
||
feedbackMessage.value = "管理员权限已保存";
|
||
permissionTarget.value = null;
|
||
} catch (error) {
|
||
if (!isPageActive || isRequestCancelled(error)) return;
|
||
permissionError.value = getRequestErrorMessage(error, "权限保存失败,请稍后重试。");
|
||
} finally {
|
||
if (isPageActive) permissionSaving.value = false;
|
||
}
|
||
};
|
||
const requestBack = () =>
|
||
runBackGuard({
|
||
transientOpen: Boolean(editTarget.value || confirmTarget.value || permissionTarget.value),
|
||
submitting: operationPending.value || permissionSaving.value,
|
||
"close-transient": () =>
|
||
editTarget.value
|
||
? closeEdit()
|
||
: confirmTarget.value
|
||
? closeConfirmation()
|
||
: closePermissionEditor(),
|
||
"block-submitting": () => true,
|
||
});
|
||
|
||
onLoad((query) => {
|
||
genealogyId.value = String(query?.genealogyId || "");
|
||
if (hasValidGenealogyId.value) void loadMembers();
|
||
});
|
||
onShow(() => {
|
||
if (
|
||
!hasLeftGenealogy.value &&
|
||
hasValidGenealogyId.value &&
|
||
memberListState.value !== "loading" &&
|
||
!operationPending.value
|
||
) {
|
||
void loadMembers();
|
||
}
|
||
});
|
||
onBackPress((event) => handleBackPress(event, requestBack));
|
||
onUnload(() => {
|
||
isPageActive = false;
|
||
memberListController.abort();
|
||
personOptionsController.abort();
|
||
memberUpdateController.abort();
|
||
membershipOperationController.abort();
|
||
permissionCatalogController.abort();
|
||
memberPermissionController.abort();
|
||
memberPermissionSaveController.abort();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||
.member-page {
|
||
display: flex;
|
||
min-height: 100vh;
|
||
flex-direction: column;
|
||
background: $paper;
|
||
}
|
||
|
||
.page-header,
|
||
.page-content {
|
||
z-index: 1;
|
||
}
|
||
|
||
.page-content {
|
||
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.state-card,
|
||
.member-intro,
|
||
.member-card {
|
||
box-sizing: border-box;
|
||
@include adaptive.adaptive-genealogy-state-panel;
|
||
}
|
||
|
||
.state-card {
|
||
min-height: 340rpx;
|
||
padding: 78rpx 52rpx 50rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.state-card > text {
|
||
display: block;
|
||
}
|
||
|
||
.state-card > text:first-child {
|
||
color: $ink;
|
||
font-family: STKaiti, KaiTi, serif;
|
||
font-size: clamp(19px, 35rpx, 24px);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.state-card > text:nth-child(2) {
|
||
margin-top: 18rpx;
|
||
color: $ink-muted;
|
||
font-size: clamp(15px, 24rpx, 18px);
|
||
line-height: 1.65;
|
||
}
|
||
|
||
.state-card .app-button {
|
||
margin-top: 28rpx;
|
||
}
|
||
|
||
.member-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.member-intro {
|
||
padding: 24rpx 28rpx;
|
||
}
|
||
|
||
.member-intro text {
|
||
display: block;
|
||
}
|
||
|
||
.member-intro text:first-child {
|
||
color: $ink;
|
||
font-family: STKaiti, KaiTi, serif;
|
||
font-size: clamp(18px, 31rpx, 22px);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.member-intro text:last-child {
|
||
margin-top: 10rpx;
|
||
color: $ink-muted;
|
||
font-size: clamp(14px, 22rpx, 17px);
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.page-feedback {
|
||
display: block;
|
||
padding: 18rpx 22rpx;
|
||
border: 1rpx solid rgba(66, 107, 88, 0.32);
|
||
border-radius: 12rpx;
|
||
background: rgba(66, 107, 88, 0.08);
|
||
color: #426b58;
|
||
font-size: clamp(14px, 23rpx, 17px);
|
||
}
|
||
|
||
.member-card {
|
||
padding: 26rpx 28rpx;
|
||
}
|
||
|
||
.member-card__heading {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 18rpx;
|
||
}
|
||
|
||
.member-card__identity {
|
||
min-width: 0;
|
||
}
|
||
|
||
.member-card__identity text {
|
||
display: block;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.member-card__identity text:first-child {
|
||
color: $ink;
|
||
font-family: STKaiti, KaiTi, serif;
|
||
font-size: clamp(18px, 32rpx, 23px);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.member-card__identity text:last-child {
|
||
margin-top: 6rpx;
|
||
color: $ink-muted;
|
||
font-size: clamp(13px, 21rpx, 16px);
|
||
}
|
||
|
||
.role-badge {
|
||
flex: 0 0 auto;
|
||
padding: 7rpx 14rpx;
|
||
border: 1rpx solid rgba(174, 113, 39, 0.38);
|
||
border-radius: 999rpx;
|
||
background: rgba(205, 161, 84, 0.12);
|
||
color: #8f160f;
|
||
font-size: clamp(13px, 21rpx, 16px);
|
||
}
|
||
|
||
.member-card__details {
|
||
margin-top: 18rpx;
|
||
}
|
||
|
||
.member-card__details text,
|
||
.member-card__readonly {
|
||
display: block;
|
||
margin-top: 7rpx;
|
||
overflow-wrap: anywhere;
|
||
color: $ink-muted;
|
||
font-size: clamp(14px, 23rpx, 17px);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.member-card__actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
gap: 12rpx;
|
||
margin-top: 22rpx;
|
||
}
|
||
|
||
.member-card__actions .app-button {
|
||
width: auto;
|
||
min-width: 138rpx;
|
||
}
|
||
|
||
.member-card__readonly {
|
||
margin-top: 18rpx;
|
||
}
|
||
|
||
.edit-form {
|
||
width: 100%;
|
||
margin-top: 22rpx;
|
||
text-align: left;
|
||
}
|
||
|
||
.edit-field {
|
||
@include adaptive.adaptive-genealogy-form-field;
|
||
display: flex;
|
||
width: 100%;
|
||
min-height: 86rpx;
|
||
box-sizing: border-box;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 18rpx;
|
||
margin-top: 12rpx;
|
||
padding: 14rpx 18rpx;
|
||
}
|
||
|
||
.edit-field > text:first-child {
|
||
flex: 0 0 auto;
|
||
color: $ink;
|
||
font-size: clamp(14px, 23rpx, 17px);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.edit-field input,
|
||
.edit-field > text:last-child {
|
||
min-width: 0;
|
||
flex: 1;
|
||
overflow-wrap: anywhere;
|
||
color: $ink;
|
||
font-size: clamp(14px, 23rpx, 17px);
|
||
text-align: right;
|
||
}
|
||
|
||
.edit-field--picker > text:last-child {
|
||
color: #8f160f;
|
||
}
|
||
|
||
.edit-hint,
|
||
.edit-error {
|
||
display: block;
|
||
width: 100%;
|
||
margin-top: 12rpx;
|
||
font-size: clamp(13px, 21rpx, 16px);
|
||
line-height: 1.5;
|
||
text-align: left;
|
||
}
|
||
|
||
.edit-hint {
|
||
color: $ink-muted;
|
||
}
|
||
|
||
.edit-error {
|
||
color: $brand-red;
|
||
}
|
||
|
||
.permission-list { margin-top: 18rpx; }
|
||
.permission-item { display: flex; min-height: 76rpx; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid rgba($gold, 0.38); color: $ink; font-size: 26rpx; }
|
||
.permission-item checkbox { margin-right: 16rpx; }
|
||
.permission-item view { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 6rpx; }
|
||
.permission-item view text:last-child:not(:first-child) { color: $ink-muted; font-size: 22rpx; line-height: 1.45; }
|
||
.permission-item--disabled { opacity: 0.54; }
|
||
.permission-reason { width: 100%; min-height: 100rpx; margin-top: 18rpx; padding: 16rpx; box-sizing: border-box; border: 1rpx solid rgba($gold, 0.38); border-radius: 10rpx; }
|
||
</style>
|