ow
This commit is contained in:
1 parent
27284040cc
commit
9b01c4a57a
11 files changed
+1827
-77
No files matched your search
@@ -11,9 +11,12 @@
|
||||
import type { LoadedTreeNode } from "@/components/family-tree-chart/types";
|
||||
import type { FamilyPersonDisplayData } from "@/data/auto/familyPersonDisplayData";
|
||||
import type { AdminRow } from "@/admin/admin-types";
|
||||
import type { ExchangeScope } from "@/composables/useFamilyTreeExchange";
|
||||
import type { FamilyTreeExchangeFileInfo } from "@/data/auto/familyTreeExchangeFileInfo";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useThemeGradient, useThemeTokens } from "@/composables";
|
||||
import { formatFileSize, formatFileTime, useFamilyTreeExchange } from "@/composables/useFamilyTreeExchange";
|
||||
import { usePersonPickConfig } from "@/composables/usePickerConfigs";
|
||||
import { usePolicyOptions } from "@/composables/usePolicyOptions";
|
||||
import { FamilyPersonEntity } from "@/data/auto/familyPersonEntity";
|
||||
@@ -1035,6 +1038,298 @@ async function rejectPermission(record: { PersonId: string; UserName: string }):
|
||||
load();
|
||||
}
|
||||
|
||||
// ===== 交换文件(.fm 导入导出,加密信封见 utils/familyTreeExchange)=====
|
||||
const { exchangeBusy, exportTree, listFiles, decryptPayload, readAndDecrypt, importTree, importSubTree, listBackups, restoreBackup } = useFamilyTreeExchange();
|
||||
const exportVisible = ref(false);
|
||||
const importVisible = ref(false);
|
||||
const exScope = ref<ExchangeScope>("ALL");
|
||||
const exPassword = ref("");
|
||||
const exSaveLocal = ref(true);
|
||||
const exUploadServer = ref(true);
|
||||
const importFiles = ref<FamilyTreeExchangeFileInfo[]>([]);
|
||||
const importFilesLoading = ref(false);
|
||||
const pickedFileName = ref("");
|
||||
const imPassword = ref("");
|
||||
// 二期:导入模式(ALL=整谱替换 / SUB=支系替换,与所选文件范围必须同构)
|
||||
const imScope = ref<ExchangeScope>("ALL");
|
||||
/** 小系导出闸:管理员任意锚点(D8),成员只限可编支系内的当前族员 */
|
||||
const canSubExport = computed(() => data.value.IsManager || (data.value.IsCanAmend && !!currentPerson.value?.PersonId));
|
||||
const canExport = computed(() => data.value.IsManager || data.value.IsMember);
|
||||
|
||||
function openExchangeExport(): void {
|
||||
exScope.value = "ALL";
|
||||
exPassword.value = "";
|
||||
exSaveLocal.value = true;
|
||||
exUploadServer.value = true;
|
||||
exportVisible.value = true;
|
||||
}
|
||||
|
||||
function onExSaveChange(value: boolean): void {
|
||||
exSaveLocal.value = value;
|
||||
}
|
||||
|
||||
function onExUploadChange(value: boolean): void {
|
||||
exUploadServer.value = value;
|
||||
}
|
||||
|
||||
/** 导出全链:拉明文→加密→落盘/上传(composable 内部完成),成功报文件名 */
|
||||
async function runExchangeExport(): Promise<void> {
|
||||
if (exPassword.value.length < 4) {
|
||||
toast(t("FamilyTreePage.exchangePwdShort"));
|
||||
return;
|
||||
}
|
||||
if (!exSaveLocal.value && !exUploadServer.value) {
|
||||
toast(t("FamilyTreePage.exchangeTargetTip"));
|
||||
return;
|
||||
}
|
||||
if (exScope.value === "SUB" && !canSubExport.value) {
|
||||
toast(t("FamilyTreePage.exchangeSubDenied"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const fileName = await exportTree({
|
||||
treeId: treeId.value,
|
||||
scope: exScope.value,
|
||||
anchorPersonId: currentPerson.value?.PersonId,
|
||||
password: exPassword.value,
|
||||
saveLocal: exSaveLocal.value,
|
||||
uploadServer: exUploadServer.value,
|
||||
});
|
||||
exportVisible.value = false;
|
||||
toast(t("FamilyTreePage.exchangeExportDone").replace("{name}", fileName));
|
||||
}
|
||||
catch (error: any) {
|
||||
toast(error?.message === "exchange-export-empty"
|
||||
? t("FamilyTreePage.exchangeExportEmpty")
|
||||
: error?.message || t("FamilyTreePage.exchangeFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function openExchangeImport(): Promise<void> {
|
||||
// 非管理员看不到整谱替换 chip(D4 管理员限),默认落在小系替换
|
||||
imScope.value = data.value.IsManager ? "ALL" : "SUB";
|
||||
pickedFileName.value = "";
|
||||
imPassword.value = "";
|
||||
bkMode.value = false;
|
||||
pickedBackup.value = "";
|
||||
clearLocalFile();
|
||||
importVisible.value = true;
|
||||
await loadImportFiles();
|
||||
}
|
||||
|
||||
/** 模式切换:换范围重拉列表,已选文件清空防串模式 */
|
||||
async function onImScopeChange(scope: ExchangeScope): Promise<void> {
|
||||
if (imScope.value === scope) {
|
||||
return;
|
||||
}
|
||||
imScope.value = scope;
|
||||
pickedFileName.value = "";
|
||||
await loadImportFiles();
|
||||
}
|
||||
|
||||
/** 交换目录里本谱对应模式的文件(后端已降序,composable 再按 Scope 过滤) */
|
||||
async function loadImportFiles(): Promise<void> {
|
||||
importFilesLoading.value = true;
|
||||
try {
|
||||
importFiles.value = await listFiles(treeId.value, imScope.value);
|
||||
}
|
||||
catch {
|
||||
importFiles.value = [];
|
||||
}
|
||||
finally {
|
||||
importFilesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 三期备份还原(R4):导入弹窗第三个 chips;BK 明文列表不需密码,选中即待还原回滚点
|
||||
const bkMode = ref(false);
|
||||
const backupFiles = ref<FamilyTreeExchangeFileInfo[]>([]);
|
||||
const backupLoading = ref(false);
|
||||
const pickedBackup = ref("");
|
||||
|
||||
/** 回文件模式(ALL/SUB):退 bkMode,选中文件保留(同模式切回不丢进度) */
|
||||
function pickImScope(scope: ExchangeScope): void {
|
||||
bkMode.value = false;
|
||||
void onImScopeChange(scope);
|
||||
}
|
||||
|
||||
async function enterBkMode(): Promise<void> {
|
||||
if (bkMode.value) {
|
||||
return;
|
||||
}
|
||||
bkMode.value = true;
|
||||
pickedFileName.value = "";
|
||||
clearLocalFile();
|
||||
if (!backupFiles.value.length) {
|
||||
await fetchBackups();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBackups(): Promise<void> {
|
||||
backupLoading.value = true;
|
||||
try {
|
||||
backupFiles.value = await listBackups(treeId.value);
|
||||
}
|
||||
catch {
|
||||
backupFiles.value = [];
|
||||
}
|
||||
finally {
|
||||
backupLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 还原执行:二次确认(整谱回滚+重编号+清绑定说清)→ 后端回放 BK(还原前自动再备份现谱) */
|
||||
async function runRestoreBackup(): Promise<void> {
|
||||
if (!pickedBackup.value) {
|
||||
toast(t("FamilyTreePage.exchangePickBackupTip"));
|
||||
return;
|
||||
}
|
||||
const bk = backupFiles.value.find(x => x.FileName === pickedBackup.value);
|
||||
const confirmResult = await new Promise<boolean>((resolve) => {
|
||||
uni.showModal({
|
||||
title: t("FamilyTreePage.exchangeRestoreConfirmTitle"),
|
||||
content: t("FamilyTreePage.exchangeRestoreConfirm").replace("{time}", formatFileTime(bk?.LastModified ?? "")),
|
||||
success: res => resolve(!!res.confirm),
|
||||
fail: () => resolve(false),
|
||||
});
|
||||
});
|
||||
if (!confirmResult) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const count = await restoreBackup(treeId.value, pickedBackup.value);
|
||||
importVisible.value = false;
|
||||
toast(t("FamilyTreePage.exchangeRestoreDone").replace("{count}", String(count)));
|
||||
data.value = new FamilyTreeData(); // 还原后全员重编号,清旧上下文再重拉
|
||||
await load();
|
||||
}
|
||||
catch (error: any) {
|
||||
toast(error?.message || t("FamilyTreePage.exchangeFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
// 本地上传通道:A 谱导出的 .fm 在任意谱界面直接选中导入(跨谱嫁接,不经服务器交换目录)
|
||||
const localFileText = ref("");
|
||||
const localFileName = ref("");
|
||||
|
||||
function clearLocalFile(): void {
|
||||
localFileText.value = "";
|
||||
localFileName.value = "";
|
||||
}
|
||||
|
||||
/** uni.chooseFile 选本地 .fm 并读文本(H5 原生 File.text / App FileSystemManager),与服务器列表通道互斥 */
|
||||
async function pickLocalFile(): Promise<void> {
|
||||
try {
|
||||
const picked: any = await new Promise((resolve, reject) => {
|
||||
uni.chooseFile({ count: 1, extension: [".fm"], success: resolve, fail: reject });
|
||||
});
|
||||
const file = picked.tempFiles?.[0];
|
||||
const name = String(file?.name || picked.tempFilePaths?.[0]?.split("/").pop() || "exchange.fm");
|
||||
let text = "";
|
||||
// #ifdef H5
|
||||
text = await file.text();
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
const res: any = await new Promise((resolve, reject) => {
|
||||
uni.getFileSystemManager().readFile({
|
||||
filePath: picked.tempFilePaths[0],
|
||||
encoding: "utf8",
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
text = String(res.data ?? "");
|
||||
// #endif
|
||||
if (!text.trim()) {
|
||||
toast(t("FamilyTreePage.exchangeLocalBad"));
|
||||
return;
|
||||
}
|
||||
localFileText.value = text;
|
||||
localFileName.value = name;
|
||||
pickedFileName.value = "";
|
||||
}
|
||||
catch (e: any) {
|
||||
// 用户取消不算错(errMsg 含 cancel),其余给统一读失败提示
|
||||
if (!String(e?.errMsg ?? e ?? "").includes("cancel")) {
|
||||
toast(t("FamilyTreePage.exchangeLocalBad"));
|
||||
console.warn("[familyTreeExchange] choose local file failed:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 导入三步合一:验文件(服务器/本地两路)+验密码 → 破坏性行为二次确认 → 按模式执行(整谱替换/支系替换) */
|
||||
async function runExchangeImport(): Promise<void> {
|
||||
if (!pickedFileName.value && !localFileName.value) {
|
||||
toast(t("FamilyTreePage.exchangePickFileTip"));
|
||||
return;
|
||||
}
|
||||
if (imPassword.value.length < 4) {
|
||||
toast(t("FamilyTreePage.exchangePwdShort"));
|
||||
return;
|
||||
}
|
||||
let payload;
|
||||
let preview;
|
||||
let scope;
|
||||
try {
|
||||
if (localFileName.value) {
|
||||
// 本地文件:模式自动识别(payload.Scope 说了算,支持跨谱嫁接),chips 跟随文件
|
||||
({ payload, preview, scope } = await decryptPayload(localFileText.value, imPassword.value, null));
|
||||
imScope.value = scope;
|
||||
}
|
||||
else {
|
||||
({ payload, preview } = await readAndDecrypt(pickedFileName.value, imPassword.value, imScope.value));
|
||||
scope = imScope.value;
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
toast(error?.message === "exchange-scope-mismatch"
|
||||
? t("FamilyTreePage.exchangeScopeMismatch")
|
||||
: error?.message === "exchange-legacy-sub"
|
||||
? t("FamilyTreePage.exchangeLegacySub")
|
||||
: t("FamilyTreePage.exchangeDecryptFailed"));
|
||||
return;
|
||||
}
|
||||
// 支系替换的锚点口径(P2):与当前族员对比,姓+谱名同一人才同意;本地 SUB 文件解密后才知道要锚点
|
||||
if (scope === "SUB" && !currentPerson.value?.PersonId) {
|
||||
toast(t("FamilyTreePage.exchangeSubAnchorMissing"));
|
||||
return;
|
||||
}
|
||||
// 二次确认:ALL 按同谱/跨谱区分——同谱(含本谱服务器文件)编号与绑定原样保留,跨谱嫁接才清绑定重编号;SUB 说清只动哪根枝(备份都在后端自动落盘可回滚)
|
||||
const sameTree = String(payload.FamilyTree?.TreeId ?? "").toUpperCase() === String(treeId.value).toUpperCase();
|
||||
const confirmResult = await new Promise<boolean>((resolve) => {
|
||||
uni.showModal({
|
||||
title: t(scope === "SUB" ? "FamilyTreePage.exchangeImportConfirmSubTitle" : "FamilyTreePage.exchangeImportConfirmTitle"),
|
||||
content: scope === "SUB"
|
||||
? t("FamilyTreePage.exchangeImportConfirmSub")
|
||||
.replace("{tree}", preview.TreeName)
|
||||
.replace("{count}", String(preview.PersonCount))
|
||||
.replace("{anchor}", currentPersonName.value || "-")
|
||||
: t(sameTree ? "FamilyTreePage.exchangeImportConfirmSame" : "FamilyTreePage.exchangeImportConfirm")
|
||||
.replace("{tree}", preview.TreeName)
|
||||
.replace("{count}", String(preview.PersonCount))
|
||||
.replace("{time}", preview.ExportedAt),
|
||||
success: res => resolve(!!res.confirm),
|
||||
fail: () => resolve(false),
|
||||
});
|
||||
});
|
||||
if (!confirmResult) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const count = scope === "SUB"
|
||||
? await importSubTree(treeId.value, currentPerson.value!.PersonId, payload)
|
||||
: await importTree(treeId.value, payload);
|
||||
importVisible.value = false;
|
||||
toast(t("FamilyTreePage.exchangeImportDone").replace("{count}", String(count)));
|
||||
// 导入/还原后全员重编号,旧 currentPerson 已失效:先清上下文再重拉,避免带旧 Id 查询
|
||||
data.value = new FamilyTreeData();
|
||||
await load();
|
||||
}
|
||||
catch (error: any) {
|
||||
toast(error?.message || t("FamilyTreePage.exchangeFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 只读文案小工具 =====
|
||||
function genderText(gender?: string): string {
|
||||
return getPolicyText("GenderPolicy", gender ?? "");
|
||||
@@ -1160,18 +1455,24 @@ defineExpose({
|
||||
<u-icon name="file-text" size="22" color="#9a8f82" />
|
||||
</view>
|
||||
<view class="ft-hero__badges">
|
||||
<text v-if="tree.PublicIndc === 'Y'" class="ft-badge ft-badge--pub">{{ t("FamilyTreePage.chipPublic") }}</text>
|
||||
<text v-if="tree.PublicIndc === 'Y'" class="ft-badge ft-badge--pub">{{
|
||||
t("FamilyTreePage.chipPublic") }}</text>
|
||||
<text v-else class="ft-badge ft-badge--priv">{{ t("FamilyTreePage.chipPrivate") }}</text>
|
||||
<!-- 公开开关属谱级设置:仅管理员可见可操作,成员/普通用户只读徽章 -->
|
||||
<u-switch v-if="data.IsManager" :model-value="tree.PublicIndc === 'Y'" size="40" :active-color="primary" @change="onPublicToggle" />
|
||||
<text v-if="data.IsManager" class="ft-badge ft-badge--admin">{{ t("FamilyTreePage.managerTag") }}</text>
|
||||
<u-switch v-if="data.IsManager" :model-value="tree.PublicIndc === 'Y'" size="40"
|
||||
:active-color="primary" @change="onPublicToggle" />
|
||||
<text v-if="data.IsManager" class="ft-badge ft-badge--admin">{{ t("FamilyTreePage.managerTag")
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ft-hero__stats">
|
||||
<view class="ft-stat"><text class="ft-stat__n">{{ memberCount }}</text><text class="ft-stat__l">{{ t("FamilyTreePage.statMembers") }}</text></view>
|
||||
<view class="ft-stat"><text class="ft-stat__n">{{ managerPermissions.length }}</text><text class="ft-stat__l">{{ t("FamilyTreePage.statManagers") }}</text></view>
|
||||
<view class="ft-stat"><text class="ft-stat__n">{{ pendingPermissions.length }}</text><text class="ft-stat__l">{{ t("FamilyTreePage.statPending") }}</text></view>
|
||||
<view class="ft-stat"><text class="ft-stat__n">{{ memberCount }}</text><text class="ft-stat__l">{{
|
||||
t("FamilyTreePage.statMembers") }}</text></view>
|
||||
<view class="ft-stat"><text class="ft-stat__n">{{ managerPermissions.length }}</text><text
|
||||
class="ft-stat__l">{{ t("FamilyTreePage.statManagers") }}</text></view>
|
||||
<view class="ft-stat"><text class="ft-stat__n">{{ pendingPermissions.length }}</text><text
|
||||
class="ft-stat__l">{{ t("FamilyTreePage.statPending") }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1181,12 +1482,14 @@ defineExpose({
|
||||
<text>{{ t("FamilyTreePage.tabOverview") }}</text>
|
||||
</view>
|
||||
<!-- 成员 tab:管理员与已绑定成员可见;普通用户仅概览(查看公开信息 + 两谱图) -->
|
||||
<view v-if="data.IsManager || data.IsMember" class="ft-tab" :class="{ 'ft-tab--on': activeTab === 'members' }" @tap="switchTab('members')">
|
||||
<view v-if="data.IsManager || data.IsMember" class="ft-tab"
|
||||
:class="{ 'ft-tab--on': activeTab === 'members' }" @tap="switchTab('members')">
|
||||
<text>{{ t("FamilyTreePage.tabMembers") }}</text>
|
||||
<text class="ft-tab__cnt">{{ memberCount }}</text>
|
||||
</view>
|
||||
<!-- 权限 tab:审批/管理名单属全谱人事权,仅管理员可见 -->
|
||||
<view v-if="data.IsManager" class="ft-tab" :class="{ 'ft-tab--on': activeTab === 'perms' }" @tap="switchTab('perms')">
|
||||
<view v-if="data.IsManager" class="ft-tab" :class="{ 'ft-tab--on': activeTab === 'perms' }"
|
||||
@tap="switchTab('perms')">
|
||||
<text>{{ t("FamilyTreePage.tabPerms") }}</text>
|
||||
<view v-if="pendingPermissions.length" class="ft-tab__dot" />
|
||||
</view>
|
||||
@@ -1206,19 +1509,24 @@ defineExpose({
|
||||
<view v-if="loaded" class="ft-card">
|
||||
<view class="ft-card__head">
|
||||
<text class="ft-card__title">{{ t("FamilyTreePage.ancestorInfo") }}</text>
|
||||
<text v-if="ancestorPerson && data.IsCanAmend" class="ft-card__link" @tap="openEditPerson(ancestorPerson.PersonId)">{{ t("FamilyTreePage.edit") }}</text>
|
||||
<text v-if="ancestorPerson && data.IsCanAmend" class="ft-card__link"
|
||||
@tap="openEditPerson(ancestorPerson.PersonId)">{{ t("FamilyTreePage.edit") }}</text>
|
||||
<!-- 没始祖时补设属谱级动作:挂始祖只跟管理员,成员支系内可编辑已有始祖档案但不可改挂 -->
|
||||
<text v-else-if="!ancestorPerson && data.IsManager" class="ft-card__link" @tap="openQuickCreate('ancestor')">{{ t("FamilyTreePage.addAncestor") }}</text>
|
||||
<text v-else-if="!ancestorPerson && data.IsManager" class="ft-card__link"
|
||||
@tap="openQuickCreate('ancestor')">{{ t("FamilyTreePage.addAncestor") }}</text>
|
||||
</view>
|
||||
<template v-if="ancestorPerson">
|
||||
<view class="person-hero">
|
||||
<view class="person-hero__avatar" :style="avatarFallbackStyle(!!ancestorPerson.AvatarUrl, ancestorPerson.Gender)">
|
||||
<image v-if="ancestorPerson.AvatarUrl" class="person-hero__img" :src="ancestorPerson.AvatarUrl" mode="aspectFill" />
|
||||
<view class="person-hero__avatar"
|
||||
:style="avatarFallbackStyle(!!ancestorPerson.AvatarUrl, ancestorPerson.Gender)">
|
||||
<image v-if="ancestorPerson.AvatarUrl" class="person-hero__img" :src="ancestorPerson.AvatarUrl"
|
||||
mode="aspectFill" />
|
||||
<u-icon v-else name="account" size="56" :color="genderTheme(ancestorPerson.Gender)" />
|
||||
</view>
|
||||
<view class="person-hero__main">
|
||||
<view class="person-hero__line">
|
||||
<text class="person-hero__name">{{ `${ancestorPerson.Surname ?? ""}${ancestorPerson.GenealogyName ?? ""}` || "-" }}</text>
|
||||
<text class="person-hero__name">{{ `${ancestorPerson.Surname ??
|
||||
""}${ancestorPerson.GenealogyName ?? ""}` || "-" }}</text>
|
||||
<text class="person-hero__gender">{{ genderText(ancestorPerson.Gender) || "-" }}</text>
|
||||
</view>
|
||||
<view class="person-hero__chips">
|
||||
@@ -1227,20 +1535,31 @@ defineExpose({
|
||||
</view>
|
||||
</view>
|
||||
<view class="ft-rows">
|
||||
<view class="ft-row"><text class="ft-row__k">{{ t("FamilyPersonPage.genealogyName") }}</text><text class="ft-row__v">{{ ancestorPerson.GenealogyName || "-" }}</text></view>
|
||||
<view class="ft-row"><text class="ft-row__k">{{ t("FamilyPersonPage.commonName") }}</text><text class="ft-row__v">{{ ancestorPerson.CommonName || "-" }}</text></view>
|
||||
<view class="ft-row"><text class="ft-row__k">{{ t("FamilyPersonPage.genealogyName") }}</text><text
|
||||
class="ft-row__v">{{ ancestorPerson.GenealogyName || "-" }}</text></view>
|
||||
<view class="ft-row"><text class="ft-row__k">{{ t("FamilyPersonPage.commonName") }}</text><text
|
||||
class="ft-row__v">{{ ancestorPerson.CommonName || "-" }}</text></view>
|
||||
</view>
|
||||
</template>
|
||||
<view v-else class="ft-empty"><text>{{ t("FamilyTreePage.noAncestor") }}</text></view>
|
||||
</view>
|
||||
|
||||
<view class="ft-card">
|
||||
<view class="ft-card__head"><text class="ft-card__title">{{ t("FamilyTreePage.quickActions") }}</text></view>
|
||||
<view class="ft-card__head"><text class="ft-card__title">{{ t("FamilyTreePage.quickActions") }}</text>
|
||||
</view>
|
||||
<view class="ft-quick">
|
||||
<!-- 编辑资料改的是族谱本体(谱级):仅管理员;普通用户/成员不可见 -->
|
||||
<view v-if="data.IsManager" class="ft-qa" @tap="openTreeEditor"><u-icon name="edit-pen" size="30" :color="primary" /><text>{{ t("FamilyTreePage.editProfile") }}</text></view>
|
||||
<view v-if="data.IsManager" class="ft-qa" @tap="openTreeEditor"><u-icon name="edit-pen" size="30"
|
||||
:color="primary" /><text>{{ t("FamilyTreePage.editProfile") }}</text></view>
|
||||
<!-- 申请管理三档都可用(已是管理员时点击只提示不重复发) -->
|
||||
<view class="ft-qa" @tap="requestManager"><u-icon name="server-man" size="30" :color="primary" /><text>{{ t("FamilyTreePage.applyManage") }}</text></view>
|
||||
<view class="ft-qa" @tap="requestManager"><u-icon name="server-man" size="30"
|
||||
:color="primary" /><text>{{
|
||||
t("FamilyTreePage.applyManage") }}</text></view>
|
||||
<!-- 交换文件:导出需成员及以上(D8 管理员不限);导入入口同样成员可见(小系替换 P6 / 备份还原 R1 均已开成员闸),仅整谱替换留管理员 -->
|
||||
<view v-if="canExport" class="ft-qa" @tap="openExchangeExport"><u-icon name="download" size="30"
|
||||
:color="primary" /><text>{{ t("FamilyTreePage.exchangeExportEntry") }}</text></view>
|
||||
<view v-if="canExport" class="ft-qa" @tap="openExchangeImport"><u-icon name="upload" size="30"
|
||||
:color="primary" /><text>{{ t("FamilyTreePage.exchangeImportEntry") }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1249,10 +1568,13 @@ defineExpose({
|
||||
<view v-show="activeTab === 'members'" class="ft-pane">
|
||||
<!-- 当前族员:从概览移入,夫妻/子女列表都是相对此人 -->
|
||||
<view v-if="loaded && currentPerson" class="ft-card">
|
||||
<view class="ft-card__head"><text class="ft-card__title">{{ t("FamilyTreePage.currentPerson") }}</text></view>
|
||||
<view class="ft-card__head"><text class="ft-card__title">{{ t("FamilyTreePage.currentPerson")
|
||||
}}</text></view>
|
||||
<view class="person-hero">
|
||||
<view class="person-hero__avatar" :style="avatarFallbackStyle(!!currentPerson?.AvatarUrl, currentPerson?.Gender)">
|
||||
<image v-if="currentPerson?.AvatarUrl" class="person-hero__img" :src="currentPerson.AvatarUrl" mode="aspectFill" />
|
||||
<view class="person-hero__avatar"
|
||||
:style="avatarFallbackStyle(!!currentPerson?.AvatarUrl, currentPerson?.Gender)">
|
||||
<image v-if="currentPerson?.AvatarUrl" class="person-hero__img" :src="currentPerson.AvatarUrl"
|
||||
mode="aspectFill" />
|
||||
<u-icon v-else name="account" size="56" :color="genderTheme(currentPerson?.Gender)" />
|
||||
</view>
|
||||
<view class="person-hero__main">
|
||||
@@ -1261,108 +1583,197 @@ defineExpose({
|
||||
<text class="person-hero__gender">{{ genderText(currentPerson?.Gender) || "-" }}</text>
|
||||
</view>
|
||||
<view class="person-hero__chips">
|
||||
<text v-if="currentPerson?.PersonId" class="person-hero__chip">{{ personChipText(currentPerson) }}</text>
|
||||
<text v-if="currentPerson?.PersonId" class="person-hero__chip">{{ personChipText(currentPerson)
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="op-btns">
|
||||
<view v-if="currentPerson?.PersonId && data.IsCanAmend" class="op-btn" @tap="openEditPerson(currentPerson.PersonId)"><u-icon name="edit-pen" size="26" :color="primary" /><text class="op-btn-text">{{ t("FamilyPersonPage.editBut") }}</text></view>
|
||||
<view class="op-btn" @tap="pickCurrentPerson"><u-icon name="search" size="26" :color="primary" /><text class="op-btn-text">{{ t("FamilyTreePage.personSearch") }}</text></view>
|
||||
<view class="op-btn" @tap="subChartVisible = true"><view class="ft-glyph ft-glyph--sub" /><text class="op-btn-text">{{ t("FamilyTreePage.pedigreeChart") }}</text></view>
|
||||
<view v-if="currentPerson?.PersonId && data.IsCanAmend" class="op-btn"
|
||||
@tap="openEditPerson(currentPerson.PersonId)"><u-icon name="edit-pen" size="26"
|
||||
:color="primary" /><text class="op-btn-text">{{ t("FamilyPersonPage.editBut") }}</text></view>
|
||||
<view class="op-btn" @tap="pickCurrentPerson"><u-icon name="search" size="26"
|
||||
:color="primary" /><text class="op-btn-text">{{ t("FamilyTreePage.personSearch") }}</text>
|
||||
</view>
|
||||
<view class="op-btn" @tap="subChartVisible = true">
|
||||
<view class="ft-glyph ft-glyph--sub" /><text class="op-btn-text">{{
|
||||
t("FamilyTreePage.pedigreeChart") }}</text>
|
||||
</view>
|
||||
<!-- 绑定/解绑开关:当前族员已绑到自己权限行时显「解除绑定」,否则显「绑定账号」(需已过审且同名;申请入口仍在概览快捷区/底部条) -->
|
||||
<view v-if="!data.IsManager && isSelfBound" class="op-btn" @tap="unbindCurrentPerson"><u-icon name="close" size="26" :color="primary" /><text class="op-btn-text">{{ t("FamilyTreePage.unbindAccount") }}</text></view>
|
||||
<view v-else-if="!data.IsManager" class="op-btn" @tap="bindCurrentPerson"><u-icon name="scan" size="26" :color="primary" /><text class="op-btn-text">{{ t("FamilyTreePage.bindAccount") }}</text></view>
|
||||
<view v-if="!data.IsManager && isSelfBound" class="op-btn" @tap="unbindCurrentPerson"><u-icon
|
||||
name="close" size="26" :color="primary" /><text class="op-btn-text">{{
|
||||
t("FamilyTreePage.unbindAccount") }}</text></view>
|
||||
<view v-else-if="!data.IsManager" class="op-btn" @tap="bindCurrentPerson"><u-icon name="scan"
|
||||
size="26" :color="primary" /><text class="op-btn-text">{{ t("FamilyTreePage.bindAccount")
|
||||
}}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ft-search"><search-input v-model="memberKeyword" :placeholder="t('FamilyTreePage.searchMember')" /></view>
|
||||
<view class="ft-search"><search-input v-model="memberKeyword"
|
||||
:placeholder="t('FamilyTreePage.searchMember')" />
|
||||
</view>
|
||||
<view class="ft-chips">
|
||||
<text class="ft-chip" :class="{ 'ft-chip--on': memberFilter === 'all' }" @tap="memberFilter = 'all'">{{ t("FamilyTreePage.filterAll") }}</text>
|
||||
<text class="ft-chip" :class="{ 'ft-chip--on': memberFilter === 'male' }" @tap="memberFilter = 'male'">{{ t("FamilyTreePage.filterMale") }}</text>
|
||||
<text class="ft-chip" :class="{ 'ft-chip--on': memberFilter === 'female' }" @tap="memberFilter = 'female'">{{ t("FamilyTreePage.filterFemale") }}</text>
|
||||
<text class="ft-chip" :class="{ 'ft-chip--on': memberFilter === 'all' }"
|
||||
@tap="memberFilter = 'all'">{{
|
||||
t("FamilyTreePage.filterAll") }}</text>
|
||||
<text class="ft-chip" :class="{ 'ft-chip--on': memberFilter === 'male' }"
|
||||
@tap="memberFilter = 'male'">{{
|
||||
t("FamilyTreePage.filterMale") }}</text>
|
||||
<text class="ft-chip" :class="{ 'ft-chip--on': memberFilter === 'female' }"
|
||||
@tap="memberFilter = 'female'">{{
|
||||
t("FamilyTreePage.filterFemale") }}</text>
|
||||
</view>
|
||||
|
||||
<view class="ft-card">
|
||||
<!-- 新增入口与行尾操作同一把闸:当前锚点不可编(未绑定/出支系)时隐藏 -->
|
||||
<view class="ft-card__head"><view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.spouseTitle") }}</text><text class="ft-card__count">{{ spouseMaps.length }}</text></view><view v-if="data.IsCanAmend" class="ft-card__add" @tap="openQuickCreate('spouse')"><u-icon name="plus" size="26" :color="primary" /><text>{{ t("FamilyTreePage.addSpouse") }}</text></view></view>
|
||||
<view v-if="!visibleSpouses.length" class="ft-empty"><text>{{ t("FamilyTreePage.spouseEmpty") }}</text></view>
|
||||
<view class="ft-card__head">
|
||||
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.spouseTitle")
|
||||
}}</text><text class="ft-card__count">{{ spouseMaps.length }}</text></view>
|
||||
<view v-if="data.IsCanAmend" class="ft-card__add" @tap="openQuickCreate('spouse')"><u-icon
|
||||
name="plus" size="26" :color="primary" /><text>{{ t("FamilyTreePage.addSpouse") }}</text></view>
|
||||
</view>
|
||||
<view v-if="!visibleSpouses.length" class="ft-empty"><text>{{ t("FamilyTreePage.spouseEmpty")
|
||||
}}</text></view>
|
||||
<view v-for="record in visibleSpouses" :key="record.SpouseId" class="ft-mrow">
|
||||
<view class="ft-av" :style="avatarFallbackStyle(!!personAvatar(record.SpouseId), personGender(record.SpouseId, record.Gender))">
|
||||
<image v-if="personAvatar(record.SpouseId)" class="ft-av__img" :src="personAvatar(record.SpouseId)" mode="aspectFill" />
|
||||
<u-icon v-else name="account" size="40" :color="genderTheme(personGender(record.SpouseId, record.Gender))" />
|
||||
<view class="ft-av"
|
||||
:style="avatarFallbackStyle(!!personAvatar(record.SpouseId), personGender(record.SpouseId, record.Gender))">
|
||||
<image v-if="personAvatar(record.SpouseId)" class="ft-av__img"
|
||||
:src="personAvatar(record.SpouseId)" mode="aspectFill" />
|
||||
<u-icon v-else name="account" size="40"
|
||||
:color="genderTheme(personGender(record.SpouseId, record.Gender))" />
|
||||
</view>
|
||||
<view class="ft-mrow__main">
|
||||
<view class="ft-mrow__line"><text class="ft-mrow__name">{{ `${record.SpouseSurname ?? ""}${record.SpouseGenealogyName ?? ""}` }}</text><text class="ft-mrow__sex">{{ genderText(personGender(record.SpouseId, record.Gender)) }}</text></view>
|
||||
<view class="ft-mrow__line"><text class="ft-mrow__name">{{ `${record.SpouseSurname ??
|
||||
""}${record.SpouseGenealogyName ?? ""}` }}</text><text class="ft-mrow__sex">{{
|
||||
genderText(personGender(record.SpouseId, record.Gender)) }}</text></view>
|
||||
<text class="ft-mrow__code">{{ personCommonName(record.SpouseId) || record.SpouseId }}</text>
|
||||
</view>
|
||||
<!-- 行尾直达动作(旧编辑态式):↑↓ 调序属锚点关系列表维护留锚点级;编辑/解除按目标人逐个判,旁支(如兄弟)不显示 -->
|
||||
<view v-if="data.IsCanAmend" class="ft-mrow__ops" @tap.stop>
|
||||
<view class="ft-mrow__op" @tap.stop="onRelationMove('spouse', record, -1)"><u-icon name="arrow-up" size="28" :color="primary" /></view>
|
||||
<view class="ft-mrow__op" @tap.stop="onRelationMove('spouse', record, 1)"><u-icon name="arrow-down" size="28" :color="primary" /></view>
|
||||
<view v-if="canAmendPerson(record.SpouseId)" class="ft-mrow__op" @tap.stop="openEditPerson(record.SpouseId)"><u-icon name="edit-pen" size="28" :color="primary" /></view>
|
||||
<view v-if="canAmendPerson(record.SpouseId)" class="ft-mrow__op" @tap.stop="onRelationRemove('spouse', record)"><u-icon name="trash" size="28" color="#f0413e" /></view>
|
||||
<view class="ft-mrow__op" @tap.stop="onRelationMove('spouse', record, -1)"><u-icon name="arrow-up"
|
||||
size="28" :color="primary" /></view>
|
||||
<view class="ft-mrow__op" @tap.stop="onRelationMove('spouse', record, 1)"><u-icon
|
||||
name="arrow-down" size="28" :color="primary" /></view>
|
||||
<view v-if="canAmendPerson(record.SpouseId)" class="ft-mrow__op"
|
||||
@tap.stop="openEditPerson(record.SpouseId)">
|
||||
<u-icon name="edit-pen" size="28" :color="primary" />
|
||||
</view>
|
||||
<view v-if="canAmendPerson(record.SpouseId)" class="ft-mrow__op"
|
||||
@tap.stop="onRelationRemove('spouse', record)">
|
||||
<u-icon name="trash" size="28" color="#f0413e" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="ft-card">
|
||||
<view class="ft-card__head"><view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.childTitle") }}</text><text class="ft-card__count">{{ childMaps.length }}</text></view><view v-if="data.IsCanAmend" class="ft-card__add" @tap="openQuickCreate('child')"><u-icon name="plus" size="26" :color="primary" /><text>{{ t("FamilyTreePage.addChildBtn") }}</text></view></view>
|
||||
<view v-if="!visibleChildren.length" class="ft-empty"><text>{{ t("FamilyTreePage.childEmpty") }}</text></view>
|
||||
<view class="ft-card__head">
|
||||
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.childTitle")
|
||||
}}</text><text class="ft-card__count">{{ childMaps.length }}</text></view>
|
||||
<view v-if="data.IsCanAmend" class="ft-card__add" @tap="openQuickCreate('child')"><u-icon
|
||||
name="plus" size="26" :color="primary" /><text>{{ t("FamilyTreePage.addChildBtn") }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!visibleChildren.length" class="ft-empty"><text>{{ t("FamilyTreePage.childEmpty")
|
||||
}}</text></view>
|
||||
<view v-for="record in visibleChildren" :key="record.ChildrenId" class="ft-mrow">
|
||||
<view class="ft-av" :style="avatarFallbackStyle(!!personAvatar(record.ChildrenId), personGender(record.ChildrenId, record.Gender))">
|
||||
<image v-if="personAvatar(record.ChildrenId)" class="ft-av__img" :src="personAvatar(record.ChildrenId)" mode="aspectFill" />
|
||||
<u-icon v-else name="account" size="40" :color="genderTheme(personGender(record.ChildrenId, record.Gender))" />
|
||||
<view class="ft-av"
|
||||
:style="avatarFallbackStyle(!!personAvatar(record.ChildrenId), personGender(record.ChildrenId, record.Gender))">
|
||||
<image v-if="personAvatar(record.ChildrenId)" class="ft-av__img"
|
||||
:src="personAvatar(record.ChildrenId)" mode="aspectFill" />
|
||||
<u-icon v-else name="account" size="40"
|
||||
:color="genderTheme(personGender(record.ChildrenId, record.Gender))" />
|
||||
</view>
|
||||
<view class="ft-mrow__main">
|
||||
<view class="ft-mrow__line"><text class="ft-mrow__name">{{ `${record.ChildrenSurname ?? ""}${record.ChildrenGenealogyName ?? ""}` }}</text><text class="ft-mrow__sex">{{ genderText(personGender(record.ChildrenId, record.Gender)) }}</text></view>
|
||||
<view class="ft-mrow__line"><text class="ft-mrow__name">{{ `${record.ChildrenSurname ??
|
||||
""}${record.ChildrenGenealogyName ?? ""}` }}</text><text class="ft-mrow__sex">{{
|
||||
genderText(personGender(record.ChildrenId, record.Gender)) }}</text></view>
|
||||
<text class="ft-mrow__code">{{ personCommonName(record.ChildrenId) || record.ChildrenId }}</text>
|
||||
</view>
|
||||
<view v-if="data.IsCanAmend" class="ft-mrow__ops" @tap.stop>
|
||||
<view class="ft-mrow__op" @tap.stop="onRelationMove('child', record, -1)"><u-icon name="arrow-up" size="28" :color="primary" /></view>
|
||||
<view class="ft-mrow__op" @tap.stop="onRelationMove('child', record, 1)"><u-icon name="arrow-down" size="28" :color="primary" /></view>
|
||||
<view v-if="canAmendPerson(record.ChildrenId)" class="ft-mrow__op" @tap.stop="openEditPerson(record.ChildrenId)"><u-icon name="edit-pen" size="28" :color="primary" /></view>
|
||||
<view v-if="canAmendPerson(record.ChildrenId)" class="ft-mrow__op" @tap.stop="onRelationRemove('child', record)"><u-icon name="trash" size="28" color="#f0413e" /></view>
|
||||
<view class="ft-mrow__op" @tap.stop="onRelationMove('child', record, -1)"><u-icon name="arrow-up"
|
||||
size="28" :color="primary" /></view>
|
||||
<view class="ft-mrow__op" @tap.stop="onRelationMove('child', record, 1)"><u-icon name="arrow-down"
|
||||
size="28" :color="primary" /></view>
|
||||
<view v-if="canAmendPerson(record.ChildrenId)" class="ft-mrow__op"
|
||||
@tap.stop="openEditPerson(record.ChildrenId)"><u-icon name="edit-pen" size="28"
|
||||
:color="primary" /></view>
|
||||
<view v-if="canAmendPerson(record.ChildrenId)" class="ft-mrow__op"
|
||||
@tap.stop="onRelationRemove('child', record)"><u-icon name="trash" size="28" color="#f0413e" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="ft-ghost" @tap="fullChartVisible = true"><view class="ft-glyph ft-glyph--full" /><text>{{ t("FamilyTreePage.viewFullChart") }}</text></view>
|
||||
<view class="ft-ghost" @tap="fullChartVisible = true">
|
||||
<view class="ft-glyph ft-glyph--full" /><text>{{ t("FamilyTreePage.viewFullChart") }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 权限 ===== -->
|
||||
<view v-show="activeTab === 'perms'" class="ft-pane">
|
||||
<view v-if="data.IsManager && pendingPermissions.length" class="ft-card">
|
||||
<view class="ft-card__head"><view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.permPending") }}</text><text class="ft-card__count">{{ pendingPermissions.length }}</text></view></view>
|
||||
<view class="ft-card__head">
|
||||
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.permPending")
|
||||
}}</text><text class="ft-card__count">{{ pendingPermissions.length }}</text></view>
|
||||
</view>
|
||||
<view v-for="record in pendingPermissions" :key="record.UserName" class="ft-perm-row">
|
||||
<view class="ft-perm-row__main"><text class="ft-perm-row__name">{{ record.UserLocalName || record.UserName }}</text><text v-if="record.PersonLocalName" class="ft-perm-row__sub">{{ record.PersonLocalName }}</text></view>
|
||||
<view class="ft-perm-row__main"><text class="ft-perm-row__name">{{ record.UserLocalName ||
|
||||
record.UserName
|
||||
}}</text><text v-if="record.PersonLocalName" class="ft-perm-row__sub">{{ record.PersonLocalName
|
||||
}}</text>
|
||||
</view>
|
||||
<view class="ft-perm-ops">
|
||||
<view class="ft-perm-op" @tap="approvePermission(record, 'N')"><text>{{ t("FamilyTreePage.approve") }}</text></view>
|
||||
<view class="ft-perm-op" @tap="approvePermission(record, 'Y')"><text>{{ t("FamilyTreePage.setManager") }}</text></view>
|
||||
<view class="ft-perm-op ft-perm-op--danger" @tap="rejectPermission(record)"><text class="ft-perm-op__t--danger">{{ t("FamilyTreePage.reject") }}</text></view>
|
||||
<view class="ft-perm-op" @tap="approvePermission(record, 'N')"><text>{{
|
||||
t("FamilyTreePage.approve") }}</text>
|
||||
</view>
|
||||
<view class="ft-perm-op" @tap="approvePermission(record, 'Y')"><text>{{
|
||||
t("FamilyTreePage.setManager") }}</text>
|
||||
</view>
|
||||
<view class="ft-perm-op ft-perm-op--danger" @tap="rejectPermission(record)"><text
|
||||
class="ft-perm-op__t--danger">{{ t("FamilyTreePage.reject") }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="data.IsManager" class="ft-card">
|
||||
<view class="ft-card__head"><view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.permManager") }}</text><text class="ft-card__count">{{ managerPermissions.length }}</text></view></view>
|
||||
<view v-if="!managerPermissions.length" class="ft-empty"><text>{{ t("FamilyTreePage.permEmpty") }}</text></view>
|
||||
<view class="ft-card__head">
|
||||
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.permManager")
|
||||
}}</text><text class="ft-card__count">{{ managerPermissions.length }}</text></view>
|
||||
</view>
|
||||
<view v-if="!managerPermissions.length" class="ft-empty"><text>{{ t("FamilyTreePage.permEmpty")
|
||||
}}</text></view>
|
||||
<view v-for="record in managerPermissions" :key="record.UserName" class="ft-perm-row">
|
||||
<view class="ft-perm-row__main"><text class="ft-perm-row__name">{{ record.UserLocalName || record.UserName }}</text><text class="ft-perm-row__sub">{{ record.ManagerIndc === "Y" ? t("FamilyTreePage.managerTag") : t("FamilyTreePage.memberTag") }}</text></view>
|
||||
<view class="ft-perm-ops"><view class="ft-perm-op ft-perm-op--danger" @tap="rejectPermission(record)"><text class="ft-perm-op__t--danger">{{ t("FamilyTreePage.remove") }}</text></view></view>
|
||||
<view class="ft-perm-row__main"><text class="ft-perm-row__name">{{ record.UserLocalName ||
|
||||
record.UserName
|
||||
}}</text><text class="ft-perm-row__sub">{{ record.ManagerIndc === "Y" ?
|
||||
t("FamilyTreePage.managerTag") :
|
||||
t("FamilyTreePage.memberTag") }}</text></view>
|
||||
<view class="ft-perm-ops">
|
||||
<view class="ft-perm-op ft-perm-op--danger" @tap="rejectPermission(record)"><text
|
||||
class="ft-perm-op__t--danger">{{ t("FamilyTreePage.remove") }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 申请管理(旧系统同款):常驻卡——非管理员点按钮发申请,管理员显示当前状态;原邀请占位卡无后端直接取消 -->
|
||||
<view class="ft-card">
|
||||
<view class="ft-card__head"><text class="ft-card__title">{{ t("FamilyTreePage.applyManage") }}</text></view>
|
||||
<view v-if="data.IsManager" class="ft-empty"><text>{{ t("FamilyTreePage.alreadyManagerTip") }}</text></view>
|
||||
<view v-else class="ft-apply" @tap="requestManager"><u-icon name="server-man" size="28" color="#ffffff" /><text>{{ t("FamilyTreePage.applyManage") }}</text></view>
|
||||
<view class="ft-card__head"><text class="ft-card__title">{{ t("FamilyTreePage.applyManage") }}</text>
|
||||
</view>
|
||||
<view v-if="data.IsManager" class="ft-empty"><text>{{ t("FamilyTreePage.alreadyManagerTip") }}</text>
|
||||
</view>
|
||||
<view v-else class="ft-apply" @tap="requestManager"><u-icon name="server-man" size="28"
|
||||
color="#ffffff" /><text>{{
|
||||
t("FamilyTreePage.applyManage") }}</text></view>
|
||||
</view>
|
||||
|
||||
<!-- 删除族谱:原「...」更多菜单整体取消后,唯一入口放权限 tab 底部(仅有修改权) -->
|
||||
<view v-if="data.IsCanAmend" class="ft-card">
|
||||
<view class="ft-menu-item ft-menu-item--danger" @tap="onDeleteTree">
|
||||
<view class="ft-menu-item__ic"><u-icon name="trash" size="28" color="#f0413e" /></view>
|
||||
<view class="ft-menu-item__main"><text class="ft-menu-item__t ft-menu-item__t--danger">{{ t("FamilyTreePage.deleteTree") }}</text><text class="ft-menu-item__d">{{ t("FamilyTreePage.deleteTreeHint") }}</text></view>
|
||||
<view class="ft-menu-item__main"><text class="ft-menu-item__t ft-menu-item__t--danger">{{
|
||||
t("FamilyTreePage.deleteTree") }}</text><text class="ft-menu-item__d">{{
|
||||
t("FamilyTreePage.deleteTreeHint")
|
||||
}}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1374,10 +1785,16 @@ defineExpose({
|
||||
<!-- 底部主操作条 -->
|
||||
<view class="ft-bottombar">
|
||||
<!-- 主按钮:选定当前族员(添加配偶/子女都以它为基准;建员入口在概览快捷区与关系卡头) -->
|
||||
<view class="ft-bottombar__main" @tap="pickCurrentPerson"><u-icon name="account" size="30" color="#ffffff" /><text>{{ t("FamilyTreePage.selectMember") }}</text></view>
|
||||
<view class="ft-bottombar__main" @tap="pickCurrentPerson"><u-icon name="account" size="30"
|
||||
color="#ffffff" /><text>{{
|
||||
t("FamilyTreePage.selectMember") }}</text></view>
|
||||
<!-- 小系图/全系图:定制 SVG glyph(mask 随主题着色);原分享入口随邀请占位弹层一并取消 -->
|
||||
<view class="ft-bottombar__icon" @tap="subChartVisible = true"><view class="ft-glyph ft-glyph--bar ft-glyph--sub" /></view>
|
||||
<view class="ft-bottombar__icon" @tap="fullChartVisible = true"><view class="ft-glyph ft-glyph--bar ft-glyph--full" /></view>
|
||||
<view class="ft-bottombar__icon" @tap="subChartVisible = true">
|
||||
<view class="ft-glyph ft-glyph--bar ft-glyph--sub" />
|
||||
</view>
|
||||
<view class="ft-bottombar__icon" @tap="fullChartVisible = true">
|
||||
<view class="ft-glyph ft-glyph--bar ft-glyph--full" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1828,10 +2245,9 @@ defineExpose({
|
||||
<!-- 族员选择弹窗(族员搜索 / 选配偶共用):与邀请/编辑弹层同套底部 sheet,取消/确定齐全;行样式复用成员列表同款 -->
|
||||
<admin-pick-popup v-model:visible="personPickVisible"
|
||||
:title="personPickMode === 'current' ? t('FamilyTreePage.personSearch') : t('FamilyTreePage.pickSpouse')"
|
||||
:columns="personPickConfig.columns.value" :loader="personPickConfig.loader"
|
||||
:row-key="personPickConfig.rowKey" :title-key="personPickConfig.titleKey"
|
||||
:fixed-filters="personPickConfig.fixedFilters.value" @confirm="onPersonPicked"
|
||||
@close="personPickVisible = false">
|
||||
:columns="personPickConfig.columns.value" :loader="personPickConfig.loader" :row-key="personPickConfig.rowKey"
|
||||
:title-key="personPickConfig.titleKey" :fixed-filters="personPickConfig.fixedFilters.value"
|
||||
@confirm="onPersonPicked" @close="personPickVisible = false">
|
||||
<template #row="{ row }">
|
||||
<view class="pick-person">
|
||||
<view class="ft-av" :style="avatarFallbackStyle(!!row.AvatarUrl, row.Gender)">
|
||||
@@ -1868,6 +2284,144 @@ defineExpose({
|
||||
<!-- 族人档案弹层已取消:始祖卡单击不再弹窗,编辑走卡头「编辑资料」文字链 -->
|
||||
|
||||
<!-- ===== 编辑族谱信息弹层(仅名称/始祖谱名/简介/公开四字段,关系与成员分开维护) ===== -->
|
||||
<!-- ===== 交换文件导出弹层(信封加密在前端,密码只当密钥材料、后端不校验)===== -->
|
||||
<u-popup :model-value="exportVisible" mode="bottom" round="24" @close="exportVisible = false">
|
||||
<view class="ft-sheet">
|
||||
<view class="ft-sheet__head">
|
||||
<text class="ft-sheet__title">{{ t("FamilyTreePage.exchangeExportTitle") }}</text>
|
||||
<u-icon name="close" size="32" color="#8f959e" @click="exportVisible = false" />
|
||||
</view>
|
||||
<view class="ft-sheet__form">
|
||||
<view class="ft-chips">
|
||||
<text class="ft-chip" :class="{ 'ft-chip--on': exScope === 'ALL' }" @tap="exScope = 'ALL'">{{
|
||||
t("FamilyTreePage.exchangeScopeAll") }}</text>
|
||||
<text class="ft-chip" :class="{ 'ft-chip--on': exScope === 'SUB' }" @tap="exScope = 'SUB'">{{
|
||||
t("FamilyTreePage.exchangeScopeSub") }}</text>
|
||||
</view>
|
||||
<text v-if="exScope === 'SUB'" class="ft-exchange__sub">{{ t("FamilyTreePage.exchangeAnchorTip") }}:{{
|
||||
currentPersonName || "-" }}</text>
|
||||
<view class="ft-field">
|
||||
<text class="ft-field__label">{{ t("FamilyTreePage.exchangePwd") }}</text>
|
||||
<input v-model="exPassword" class="ft-field__input" :password="true" :maxlength="50"
|
||||
:placeholder="t('FamilyTreePage.exchangePwdHint')">
|
||||
</view>
|
||||
<text class="ft-exchange__warn">{{ t("FamilyTreePage.exchangePwdLostTip") }}</text>
|
||||
<view class="ft-switch-card">
|
||||
<view class="ft-switch-card__main">
|
||||
<text class="ft-switch-card__t">{{ t("FamilyTreePage.exchangeSaveLocal") }}</text>
|
||||
<text class="ft-switch-card__d">{{ t("FamilyTreePage.exchangeSaveLocalTip") }}</text>
|
||||
</view>
|
||||
<u-switch :model-value="exSaveLocal" :active-color="primary" @change="onExSaveChange" />
|
||||
</view>
|
||||
<view class="ft-switch-card">
|
||||
<view class="ft-switch-card__main">
|
||||
<text class="ft-switch-card__t">{{ t("FamilyTreePage.exchangeUploadServer") }}</text>
|
||||
<text class="ft-switch-card__d">{{ t("FamilyTreePage.exchangeUploadServerTip") }}</text>
|
||||
</view>
|
||||
<u-switch :model-value="exUploadServer" :active-color="primary" @change="onExUploadChange" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="ft-sheet__foot">
|
||||
<view class="ft-sheet__btn" @tap="exportVisible = false"><text>{{ t("AdminPage.cancelBut") }}</text></view>
|
||||
<view class="ft-sheet__btn ft-sheet__btn--primary" @tap="runExchangeExport"><text>{{ exchangeBusy ?
|
||||
t("FamilyTreePage.exchangeRunning") : t("FamilyTreePage.exchangeExportBut") }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<!-- ===== 交换文件导入弹层(一期仅全系整谱替换,二次确认里说清破坏性行为)===== -->
|
||||
<u-popup :model-value="importVisible" mode="bottom" round="24" @close="importVisible = false">
|
||||
<view class="ft-sheet">
|
||||
<view class="ft-sheet__head">
|
||||
<text class="ft-sheet__title">{{ t("FamilyTreePage.exchangeImportTitle") }}</text>
|
||||
<view class="ft-exchange__head-ops">
|
||||
<u-icon name="reload" size="32" color="#8f959e" @click="bkMode ? fetchBackups() : loadImportFiles()" />
|
||||
<u-icon name="close" size="32" color="#8f959e" @click="importVisible = false" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="ft-sheet__form">
|
||||
<!-- 导入模式:整谱替换(ALL)/ 支系替换(SUB)/ 备份还原(BK 明文列表,不需密码) -->
|
||||
<view class="ft-chips">
|
||||
<text v-if="data.IsManager" class="ft-chip" :class="{ 'ft-chip--on': imScope === 'ALL' && !bkMode }"
|
||||
@tap="pickImScope('ALL')">{{ t("FamilyTreePage.exchangeScopeAll") }}</text>
|
||||
<text class="ft-chip" :class="{ 'ft-chip--on': imScope === 'SUB' && !bkMode }"
|
||||
@tap="pickImScope('SUB')">{{
|
||||
t("FamilyTreePage.exchangeScopeSub") }}</text>
|
||||
<text class="ft-chip" :class="{ 'ft-chip--on': bkMode }" @tap="enterBkMode">{{
|
||||
t("FamilyTreePage.exchangeScopeBk") }}</text>
|
||||
</view>
|
||||
<template v-if="!bkMode">
|
||||
<text v-if="imScope === 'SUB' && !localFileName" class="ft-exchange__sub">{{
|
||||
t("FamilyTreePage.exchangeSubAnchorRow") }}:{{ currentPersonName || "-" }}</text>
|
||||
<!-- 本地上传通道:选中后暂时遮住服务器列表(两路互斥),点本地行可移除 -->
|
||||
<view v-if="!localFileName" class="ft-exchange__row" @tap="pickLocalFile">
|
||||
<view class="ft-exchange__row-main">
|
||||
<text class="ft-exchange__row-name">{{ t("FamilyTreePage.exchangeLocalPick") }}</text>
|
||||
<text class="ft-exchange__row-sub">{{ t("FamilyTreePage.exchangeLocalPickSub") }}</text>
|
||||
</view>
|
||||
<u-icon name="plus" size="28" :color="primary" />
|
||||
</view>
|
||||
<view v-else class="ft-exchange__row ft-exchange__row--on" @tap="clearLocalFile">
|
||||
<view class="ft-exchange__row-main">
|
||||
<text class="ft-exchange__row-name">{{ localFileName }}</text>
|
||||
<text class="ft-exchange__row-sub">{{ t("FamilyTreePage.exchangeLocalTag") }}</text>
|
||||
</view>
|
||||
<u-icon name="close" size="28" color="#8f959e" />
|
||||
</view>
|
||||
<!-- 服务器文件列表限高滚动:文件多了不挤掉密码框/警告/按钮 -->
|
||||
<scroll-view v-if="!localFileName" scroll-y class="ft-exchange__list">
|
||||
<view v-if="importFilesLoading" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeFileLoading")
|
||||
}}</text></view>
|
||||
<view v-else-if="!importFiles.length" class="ft-empty"><text>{{ t(imScope === "SUB" ?
|
||||
"FamilyTreePage.exchangeFileEmptySub" : "FamilyTreePage.exchangeFileEmpty") }}</text></view>
|
||||
<view v-for="file in importFiles" :key="file.FileName" class="ft-exchange__row"
|
||||
:class="{ 'ft-exchange__row--on': pickedFileName === file.FileName }"
|
||||
@tap="pickedFileName = file.FileName">
|
||||
<view class="ft-exchange__row-main">
|
||||
<text class="ft-exchange__row-name">{{ file.FileName }}</text>
|
||||
<text class="ft-exchange__row-sub">{{ formatFileSize(file.Size) }} · {{
|
||||
formatFileTime(file.LastModified) }}</text>
|
||||
</view>
|
||||
<u-icon v-if="pickedFileName === file.FileName" name="checkbox-mark" size="28" :color="primary" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
<!-- 备份还原模式:本谱 BK 列表(一年内、时刻降序),选中即待还原回滚点;限高滚动防备份多了撑爆弹层 -->
|
||||
<template v-else>
|
||||
<scroll-view scroll-y class="ft-exchange__list">
|
||||
<view v-if="backupLoading" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeFileLoading") }}</text>
|
||||
</view>
|
||||
<view v-else-if="!backupFiles.length" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeBackupEmpty")
|
||||
}}</text></view>
|
||||
<view v-for="bk in backupFiles" :key="bk.FileName" class="ft-exchange__row"
|
||||
:class="{ 'ft-exchange__row--on': pickedBackup === bk.FileName }" @tap="pickedBackup = bk.FileName">
|
||||
<view class="ft-exchange__row-main">
|
||||
<text class="ft-exchange__row-name">{{ formatFileTime(bk.LastModified) }}</text>
|
||||
<text class="ft-exchange__row-sub">{{ bk.FileName }} · {{ formatFileSize(bk.Size) }}</text>
|
||||
</view>
|
||||
<u-icon v-if="pickedBackup === bk.FileName" name="checkbox-mark" size="28" :color="primary" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
<view v-if="!bkMode" class="ft-field">
|
||||
<text class="ft-field__label">{{ t("FamilyTreePage.exchangePwd") }}</text>
|
||||
<input v-model="imPassword" class="ft-field__input" :password="true" :maxlength="50"
|
||||
:placeholder="t('FamilyTreePage.exchangePwdHint')">
|
||||
</view>
|
||||
<text class="ft-exchange__warn">{{ t(bkMode ? "FamilyTreePage.exchangeRestoreWarn" :
|
||||
"FamilyTreePage.exchangeImportWarn") }}</text>
|
||||
</view>
|
||||
<view class="ft-sheet__foot">
|
||||
<view class="ft-sheet__btn" @tap="importVisible = false"><text>{{ t("AdminPage.cancelBut") }}</text></view>
|
||||
<view v-if="!bkMode" class="ft-sheet__btn ft-sheet__btn--primary" @tap="runExchangeImport"><text>{{
|
||||
exchangeBusy ?
|
||||
t("FamilyTreePage.exchangeRunning") : t("FamilyTreePage.exchangeImportBut") }}</text></view>
|
||||
<view v-else class="ft-sheet__btn ft-sheet__btn--primary" @tap="runRestoreBackup"><text>{{ exchangeBusy ?
|
||||
t("FamilyTreePage.exchangeRunning") : t("FamilyTreePage.exchangeRestoreBut") }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<u-popup :model-value="treeEditVisible" mode="bottom" round="24" @close="cancelTreeEdit">
|
||||
<view class="ft-sheet">
|
||||
<view class="ft-sheet__head">
|
||||
@@ -1877,16 +2431,19 @@ defineExpose({
|
||||
<view class="ft-sheet__form">
|
||||
<view class="ft-field">
|
||||
<text class="ft-field__label">{{ t("FamilyTreePage.treeName") }}</text>
|
||||
<input v-model="tree.TreeName" class="ft-field__input" :maxlength="50" :placeholder="t('FamilyTreePage.treeNameRequired')">
|
||||
<input v-model="tree.TreeName" class="ft-field__input" :maxlength="50"
|
||||
:placeholder="t('FamilyTreePage.treeNameRequired')">
|
||||
</view>
|
||||
<view class="ft-field">
|
||||
<text class="ft-field__label">{{ t("FamilyTreePage.ancestorGenealogyName") }}</text>
|
||||
<input v-model="tree.AncestorPersonGenealogyName" class="ft-field__input" :maxlength="50" :placeholder="t('FamilyTreePage.ancestorGenealogyName')">
|
||||
<input v-model="tree.AncestorPersonGenealogyName" class="ft-field__input" :maxlength="50"
|
||||
:placeholder="t('FamilyTreePage.ancestorGenealogyName')">
|
||||
</view>
|
||||
<view class="ft-field">
|
||||
<text class="ft-field__label">{{ t("FamilyTreePage.brief") }}</text>
|
||||
<view class="ft-field__area">
|
||||
<textarea v-model="tree.Brief" class="ft-field__textarea" :maxlength="60" :placeholder="t('FamilyTreePage.noBrief')" />
|
||||
<textarea v-model="tree.Brief" class="ft-field__textarea" :maxlength="60"
|
||||
:placeholder="t('FamilyTreePage.noBrief')" />
|
||||
<text class="ft-field__counter">{{ (tree.Brief || "").length }}/60</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1900,7 +2457,9 @@ defineExpose({
|
||||
</view>
|
||||
<view class="ft-sheet__foot">
|
||||
<view class="ft-sheet__btn" @tap="cancelTreeEdit"><text>{{ t("AdminPage.cancelBut") }}</text></view>
|
||||
<view class="ft-sheet__btn ft-sheet__btn--primary" @tap="saveTreeEdit"><text>{{ t("AdminPage.saveBut") }}</text></view>
|
||||
<view class="ft-sheet__btn ft-sheet__btn--primary" @tap="saveTreeEdit"><text>{{ t("AdminPage.saveBut")
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
@@ -2783,7 +3342,7 @@ defineExpose({
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
|
||||
& + &::before {
|
||||
&+&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 6rpx;
|
||||
@@ -3406,6 +3965,87 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 交换文件弹层专属:说明小字/警示/文件行(选中态跟主题 soft 底) ===== */
|
||||
.ft-exchange__sub {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
.ft-exchange__warn {
|
||||
display: block;
|
||||
margin: 8rpx 0 16rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
color: #f0413e;
|
||||
}
|
||||
|
||||
.ft-exchange__head-ops {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
/* 导入/还原弹层内的文件与备份列表:限高滚动,条目再多也不撑爆弹层(密码框/警告/按钮始终可见) */
|
||||
.ft-exchange__list {
|
||||
max-height: 42vh;
|
||||
}
|
||||
|
||||
/* H5 端给滚动条一个可见的细条样式(默认 overlay 条在桌面浏览器不可见,用户会以为列表被截断) */
|
||||
/* #ifdef H5 */
|
||||
.ft-exchange__list ::v-deep ::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.ft-exchange__list ::v-deep ::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
background: rgba(128, 128, 128, 0.35);
|
||||
}
|
||||
|
||||
.ft-exchange__list ::v-deep ::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
.ft-exchange__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
margin-bottom: 12rpx;
|
||||
border-radius: 20rpx;
|
||||
background: var(--fg-soft, rgba(0, 0, 0, 0.03));
|
||||
}
|
||||
|
||||
.ft-exchange__row--on {
|
||||
border: 2rpx solid var(--fg-primary, #12b35d);
|
||||
}
|
||||
|
||||
.ft-exchange__row-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ft-exchange__row-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ft-exchange__row-sub {
|
||||
font-size: 22rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
.ft-switch-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in new issue
Block a user