This commit is contained in:
owenchen committed 2026-10-19 13:36:58 +08:00
1 parent 479762b57e
commit 27284040cc
30 files changed
+3042 -428

No files matched your search

@@ -136,9 +136,15 @@ function onSearch(): void {
function toggleSelect(row: AdminRow): void {
const key = keyOf(row);
if (!props.multiple) {
// 单选:直接确认
emit("confirm", [row]);
closePopup();
// 单选:点行高亮选中(再点同一条取消),待「确定」回传——与多选同构,取消/确定栏常驻
if (selectedKeys.value[0] === key) {
selectedKeys.value = [];
selectedPool.value = {};
}
else {
selectedKeys.value = [key];
selectedPool.value = { [key]: row };
}
return;
}
const idx = selectedKeys.value.indexOf(key);
@@ -249,18 +255,21 @@ watch(
<template v-else>
<view v-for="row in rows" :key="keyOf(row)" class="pick-popup__row"
:class="{ 'is-selected': isSelected(row) }" @tap="toggleSelect(row)">
<!-- 多选勾选框 -->
<view v-if="props.multiple" class="pick-popup__check" :class="{ 'is-on': isSelected(row) }">
<!-- 选择指示:多选方勾、单选圆圈,选中绿底白勾 -->
<view class="pick-popup__check" :class="{ 'is-on': isSelected(row), 'pick-popup__check--radio': !props.multiple }">
<u-icon v-if="isSelected(row)" name="checkbox-mark" size="24" color="#ffffff" />
</view>
<view class="pick-popup__content">
<text class="pick-popup__main">
{{ getTitle(row) }}
</text>
<view v-for="col in getSubtitles()" :key="col.prop" class="pick-popup__sub">
<text class="pick-popup__sub-label">{{ col.label }}</text>
<text class="pick-popup__sub-value">{{ row[col.prop] ?? "" }}</text>
</view>
<!-- row 插槽:调用方可自绘行内容(如族谱页成员行:头像+姓名带性别+常用名);不传则走默认标题+副字段 -->
<slot name="row" :row="row">
<text class="pick-popup__main">
{{ getTitle(row) }}
</text>
<view v-for="col in getSubtitles()" :key="col.prop" class="pick-popup__sub">
<text class="pick-popup__sub-label">{{ col.label }}</text>
<text class="pick-popup__sub-value">{{ row[col.prop] ?? "" }}</text>
</view>
</slot>
</view>
</view>
@@ -272,10 +281,10 @@ watch(
</template>
</scroll-view>
<!-- 底部操作栏(多选时) -->
<view v-if="props.multiple" class="pick-popup__actions">
<view class="pick-popup__action-left" @tap="toggleSelectAll">
<text class="pick-popup__toggle">
<!-- 底部操作栏(单选/多选常驻:取消、确定交互统一) -->
<view class="pick-popup__actions">
<view class="pick-popup__action-left">
<text v-if="props.multiple" class="pick-popup__toggle" @tap="toggleSelectAll">
{{ selectedKeys.length >= rows.length && rows.length > 0 ? t("AdminPage.unselectAllBut") :
t("AdminPage.selectAllBut") }}
</text>
@@ -379,6 +388,10 @@ $brand-green-light: #47a678;
border-color: $brand-green;
}
.pick-popup__check--radio {
border-radius: 50%;
}
.pick-popup__content {
flex: 1;
min-width: 0;
File diff suppressed because it is too large. Load diff
@@ -6,7 +6,7 @@
*/
import type { PropType } from "vue";
import type { FileItem, FileKind } from "@/data/custom/fileBrowser";
import { computed } from "vue";
import { computed, ref } from "vue";
import { useThemeTokens } from "@/composables/useThemeGradient";
const props = defineProps({
@@ -58,6 +58,18 @@ function visualOf(kind: FileKind): { icon: string; color: string } | null {
return kind === "folder" ? null : kindVisual[kind];
}
// 缩略图加载失败的卡记住失败态,回退类型图标(图被清理/静态目录无权限时不致白块)
const thumbFailed = ref(new Set<number>());
function onThumbError(file: FileItem): void {
thumbFailed.value.add(file.id);
}
/** 可显缩略图的地址:仅图片类型且后端 Url(Scale 压缩图)有值、未报错 */
function thumbSrcOf(file: FileItem): string {
return file.kind === "image" && file.thumbUrl && !thumbFailed.value.has(file.id) ? file.thumbUrl : "";
}
function isSelected(file: FileItem): boolean {
return props.selectedIds.includes(file.id);
}
@@ -108,6 +120,9 @@ function onCardTap(file: FileItem): void {
<view v-if="file.kind === 'folder'" class="fgl-card__folder" :style="folderStyle">
<view class="fgl-card__folder-tab" :style="folderTabStyle" />
</view>
<!-- 图片卡:后端 Url 指向 Scale 原图压缩缩略(旧系统宫格同款),失败回退图标 -->
<image v-else-if="thumbSrcOf(file)" class="fgl-card__thumb" :src="thumbSrcOf(file)"
mode="aspectFill" @error="onThumbError(file)" />
<u-icon v-else :name="visualOf(file.kind)!.icon" size="88" :color="visualOf(file.kind)!.color" />
<!-- 锁仅目录显示(旧系统:文件不挂锁):绿开锁=有权限,灰闭锁=无权限;
蓝分享标=已被收藏共享,两者文件/目录都可显示;
@@ -235,6 +250,15 @@ function onCardTap(file: FileItem): void {
border-radius: 10rpx 10rpx 0 0;
}
/* 图片缩略图:尺寸对齐类型图标区(u-icon 88),圆角卡化 */
.fgl-card__thumb {
width: 112rpx;
height: 112rpx;
margin: 4rpx 0;
border-radius: 14rpx;
background-color: rgba(31, 35, 41, 0.04);
}
.fgl-card__info {
width: 100%;
padding: 0 12rpx;
@@ -0,0 +1,951 @@
<script setup lang="ts">
/**
* 家族成员编辑器(底部抽屉,新增 / 修改共用)
*
* 把独立详情页 family-person-operation 的完整表单(基本 / 家庭 / 工作 / 简介 + 头像上传 + 身份证 OCR 回填)
* 收进一个弹层,供族谱页「新增夫妻 / 孩子 / 祖先」与关系行「修改」复用;创建走 Insert、编辑走 Update(按 CreatedBy 判定,旧页同款)。
* 组件自带数据与保存:create 用 props.treeId 落谱(可为空,祖先/新谱未落库时先建人再挂关系),
* edit 用 props.personId 经 FindByKey 载入。保存成功 emit("saved", person) 交调用方回填 / 刷新。
* 性别是 GenderPolicy 枚举,用 option-picker 单选;头像 / 身份证上传口径与详情页完全一致。
*/
import type { FamilyPersonDisplayData } from "@/data/auto/familyPersonDisplayData";
import { useLocale } from "uview-pro";
import { computed, ref, watch } from "vue";
import { datePart } from "@/admin/admin-date";
import { buildChunkFileName, uploadFile } from "@/common/http.interceptor";
import { useThemeTokens } from "@/composables";
import { usePolicyOptions } from "@/composables/usePolicyOptions";
import { FamilyPersonData } from "@/data/auto/familyPersonData";
import familyPersonService from "@/service/master/familyPersonService";
/** 单个字段描述:type 缺省为文本;switch/date/textarea/gender 走各自控件 */
interface FieldDef {
prop: string;
label: string;
type?: "switch" | "date" | "textarea" | "gender";
required?: boolean;
maxlength?: number;
}
const props = defineProps({
visible: { type: Boolean, default: false },
/** create=新增(Insert)/ edit=修改(Update) */
mode: { type: String as () => "create" | "edit", default: "create" },
/** edit 模式要载入并修改的成员主键 */
personId: { type: String, default: "" },
/** create 模式落到的族谱编号(可为空:祖先 / 新谱未落库时先建人) */
treeId: { type: String, default: "" },
/** create 模式姓的默认值(新增孩子传当前族员的姓;身份证 OCR 也按此姓拆分姓名) */
defaultSurname: { type: String, default: "" },
title: { type: String, default: "" },
});
const emit = defineEmits<{
(e: "update:visible", value: boolean): void;
(e: "saved", person: FamilyPersonDisplayData): void;
}>();
const { t } = useLocale();
// 主色:确认钮 / 勾选 / 开关随主题(渐变浅档白字发糊,用纯色)
const { primary } = useThemeTokens();
const { getPolicyOptions, getPolicyText } = usePolicyOptions();
const data = ref<FamilyPersonData>(new FamilyPersonData());
const person = computed(() => data.value.FamilyPerson);
const saving = ref(false);
const uploading = ref(false);
const idcardUploading = ref(false);
const genderPickerVisible = ref(false);
const genderItems = computed(() => getPolicyOptions("GenderPolicy"));
const genderLabel = computed(() => getPolicyText("GenderPolicy", person.value.Gender));
/** 弹层内四分区 tab(与 sections 一一对应)与基本区「更多资料」展开态:每次打开复位 */
const activeTab = ref(0);
const moreExpanded = ref(false);
/** 基本区收进「更多资料」的字段:别名/字/号/排行,默认收起不占主表单 */
const BASIC_MORE_PROPS = ["Alias", "Zi", "Hao", "BirthOrder"];
/** hero 名字行:姓+谱名实时拼接(未填时模板里回退到标题/用途名) */
const heroName = computed(() => `${person.value.Surname ?? ""}${person.value.GenealogyName ?? ""}`);
/** 当前 tab 要渲染的字段:基本区收起时剩主四行(姓/谱名/性别/常用名) */
const visibleFields = computed<FieldDef[]>(() => {
const fields = sections.value[activeTab.value]?.fields ?? [];
if (activeTab.value !== 0 || moreExpanded.value) {
return fields;
}
return fields.filter(f => !BASIC_MORE_PROPS.includes(f.prop));
});
/** 「更多资料」行右侧灰字提示:收起字段的 label 连写(别名 · 字 · 号 · 排行) */
const moreHint = computed(() =>
(sections.value[0]?.fields ?? [])
.filter(f => BASIC_MORE_PROPS.includes(f.prop))
.map(f => f.label)
.join(" · "),
);
/** 分区字段配置:字段与详情页同口径;title 仅作分段器短标签(tabXxx),与旧页分区标题解耦 */
const sections = computed<Array<{ title: string; fields: FieldDef[] }>>(() => [
{
title: t("FamilyPersonPage.tabBasic"),
fields: [
{ prop: "Surname", label: t("FamilyPersonPage.surname"), required: true },
{ prop: "GenealogyName", label: t("FamilyPersonPage.genealogyName"), required: true },
{ prop: "Gender", label: t("FamilyPersonPage.gender"), type: "gender" },
{ prop: "CommonName", label: t("FamilyPersonPage.commonName") },
{ prop: "Alias", label: t("FamilyPersonPage.alias") },
{ prop: "Zi", label: t("FamilyPersonPage.zi") },
{ prop: "Hao", label: t("FamilyPersonPage.hao") },
{ prop: "BirthOrder", label: t("FamilyPersonPage.birthOrder") },
],
},
{
title: t("FamilyPersonPage.tabFamily"),
fields: [
{ prop: "MaritalStatus", label: t("FamilyPersonPage.maritalStatus") },
{ prop: "FatherId", label: t("FamilyPersonPage.fatherId") },
{ prop: "MotherName", label: t("FamilyPersonPage.motherName") },
{ prop: "School", label: t("FamilyPersonPage.school") },
{ prop: "Major", label: t("FamilyPersonPage.major") },
{ prop: "Education", label: t("FamilyPersonPage.education") },
{ prop: "GraduationStatus", label: t("FamilyPersonPage.graduationStatus") },
{ prop: "Duty", label: t("FamilyPersonPage.duty") },
],
},
{
title: t("FamilyPersonPage.tabWork"),
fields: [
{ prop: "Title", label: t("FamilyPersonPage.title") },
{ prop: "LiveStatus", label: t("FamilyPersonPage.liveStatus") },
{ prop: "BindUserId", label: t("FamilyPersonPage.bindUserId") },
{ prop: "IsBindUser", label: t("FamilyPersonPage.bindUser"), type: "switch" },
{ prop: "BirthText", label: t("FamilyPersonPage.birthText") },
{ prop: "BirthDate", label: t("FamilyPersonPage.birthDate"), type: "date" },
{ prop: "DeathText", label: t("FamilyPersonPage.deathText") },
{ prop: "DeathDate", label: t("FamilyPersonPage.deathDate"), type: "date" },
],
},
{
title: t("FamilyPersonPage.tabIntro"),
fields: [
{ prop: "Introduction", label: t("FamilyPersonPage.introduction"), type: "textarea", maxlength: 2000 },
],
},
]);
// 展开时按模式装载:edit 拉详情,create 重置并带上目标族谱号
watch(
() => props.visible,
(on) => {
if (!on) {
return;
}
if (props.mode === "edit" && props.personId) {
loadForEdit(props.personId);
}
else {
resetForCreate();
}
// 弹层重开永远从「基本资料」起,更多资料回到收起态
activeTab.value = 0;
moreExpanded.value = false;
},
);
function resetForCreate(): void {
data.value = new FamilyPersonData();
data.value.FamilyPerson.TreeId = props.treeId || "";
// 新增孩子姓随亲(父/母):预填传入的默认姓,用户可改
data.value.FamilyPerson.Surname = props.defaultSurname || "";
}
async function loadForEdit(id: string): Promise<void> {
try {
const result = await familyPersonService.FindByKey(id);
if (result?.FamilyPerson?.PersonId) {
data.value = result;
}
else {
uni.showToast({ title: t("AdminPage.notExistTip"), icon: "none" });
}
}
catch (error) {
console.error(error);
uni.showToast({ title: t("FilePage.actionFailed"), icon: "none" });
}
}
function close(): void {
emit("update:visible", false);
}
/** 取字段的字符串值(Date/布尔等统一转串供控件绑定;null/undefined 归空串) */
function fv(prop: string): string {
const raw = (person.value as Record<string, any>)[prop];
return raw === null || raw === undefined ? "" : String(raw);
}
function setField(prop: string, value: unknown): void {
(person.value as Record<string, any>)[prop] = value;
}
function onFieldRowTap(field: FieldDef): void {
if (field.type === "gender") {
genderPickerVisible.value = true;
}
}
function onGenderConfirm(key: string): void {
person.value.Gender = key;
genderPickerVisible.value = false;
}
/** 头像上传:单图 -> DmsFile/UploadFile(FilePath 头 FamilyPersonAvatar);AvatarFilename 存分片物理名,与详情页同口径 */
function onUploadAvatar(): void {
if (uploading.value) {
return;
}
uni.chooseImage({
count: 1,
success: async (res: any) => {
const filePath = String(res?.tempFilePaths?.[0] ?? "");
const fileName = String(res?.tempFiles?.[0]?.name || `avatar-${Date.now()}.png`);
if (!filePath) {
return;
}
try {
uploading.value = true;
const physicalName = buildChunkFileName(fileName);
await uploadFile({
url: "/DmsFile/UploadFile",
filePath,
name: fileName,
fileName: physicalName,
header: { FilePath: "FamilyPersonAvatar" },
});
person.value.AvatarFilename = physicalName;
person.value.AvatarUrl = filePath;
uni.showToast({ title: t("FamilyPersonPage.avatarUploadedTip"), icon: "none" });
}
catch (error) {
console.error(error);
uni.showToast({ title: t("FamilyPersonPage.avatarUploadFailed"), icon: "none" });
}
finally {
uploading.value = false;
}
},
});
}
function onClearAvatar(): void {
if (person.value.AvatarUrl.startsWith("blob:") && typeof URL !== "undefined") {
URL.revokeObjectURL(person.value.AvatarUrl);
}
person.value.AvatarFilename = "";
person.value.AvatarUrl = "";
}
/** 身份证 OCR 性别文本反查 GenderPolicy 存储键:先按 key、再按 label 精确,最后含「男/女」模糊,全落空返回空串 */
function matchGenderKey(text: string): string {
const value = text.trim();
if (!value) {
return "";
}
const options = getPolicyOptions("GenderPolicy");
const exact = options.find(o => o.key === value || o.label === value);
if (exact) {
return exact.key;
}
const lower = value.toLowerCase();
const wantMale = value.includes("男") || lower.includes("male");
const wantFemale = value.includes("女") || lower.includes("female");
if (!wantMale && !wantFemale) {
return "";
}
const hit = options.find((o) => {
const label = `${o.key} ${o.label}`.toLowerCase();
return (wantMale && (o.label.includes("男") || label.includes("male")))
|| (wantFemale && (o.label.includes("女") || label.includes("female")));
});
return hit?.key ?? "";
}
/** 出生日期归一 yyyy-MM-dd(OCR 常给 "19791111",兼容带分隔符);不足 8 位数字返回空串 */
function formatBirthToYmd(raw: string): string {
const digits = String(raw ?? "").replace(/\D/g, "");
if (digits.length < 8) {
return "";
}
return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
}
/** 把 UploadIdCardFile 返回的识别信息回填:常用名/性别/出生日期回填各自列,其余无列项写入简介;有任一写入返回 true */
function applyIdCardOcr(info: any): boolean {
if (!info || typeof info !== "object") {
return false;
}
const pick = (pascal: string, camel: string): unknown => info[pascal] ?? info[camel];
let filled = 0;
const name = String(pick("CommonName", "commonName") ?? "").trim();
if (name) {
person.value.CommonName = name;
filled += 1;
// 按现有姓拆分身份证姓名:姓不变,去掉姓后的余名填入空白谱名(已有谱名不覆盖;姓为空或名字不含姓时只回填常用名)
const surname = String(person.value.Surname ?? "").trim();
const rest = surname && name.startsWith(surname) ? name.slice(surname.length).trim() : "";
if (rest && !String(person.value.GenealogyName ?? "").trim()) {
person.value.GenealogyName = rest;
filled += 1;
}
}
const genderKey = matchGenderKey(String(pick("Gender", "gender") ?? ""));
if (genderKey) {
person.value.Gender = genderKey;
filled += 1;
}
const ymd = formatBirthToYmd(String(pick("BirthDate", "birthDate") ?? ""));
if (ymd) {
setField("BirthDate", ymd);
filled += 1;
}
if (upsertIdCardIntro(info)) {
filled += 1;
}
return filled > 0;
}
/** 无列身份信息格式化成「身份证识别信息」块追加到简介末尾(重识别先截旧块再追加,避免堆叠);无可写项返回 false */
function upsertIdCardIntro(info: any): boolean {
const pick = (pascal: string, camel: string): unknown => info[pascal] ?? info[camel];
const labelMap: Array<[string, string, string]> = [
["nation", "Nation", "nation"],
["address", "Address", "address"],
["idNumber", "IdNumber", "idNumber"],
["issuer", "Issuer", "issuer"],
["validity", "Validity", "validity"],
];
const lines: string[] = [];
for (const [labelKey, pascal, camel] of labelMap) {
const val = String(pick(pascal, camel) ?? "").trim();
if (val) {
lines.push(`${t(`FamilyPersonPage.${labelKey}`)}: ${val}`);
}
}
if (!lines.length) {
return false;
}
const title = t("FamilyPersonPage.idcardIntroTitle");
const block = [title, ...lines].join("\n");
let intro = String(person.value.Introduction ?? "");
const idx = intro.indexOf(title);
if (idx >= 0) {
intro = intro.slice(0, idx).replace(/\s+$/, "");
}
person.value.Introduction = intro ? `${intro}\n\n${block}` : block;
return true;
}
/** 上传身份证:单图 -> 同源后端 DmsFile/UploadIdCardFile(落盘 idcard 目录 + server-to-server 调 OCR)-> 回填 */
function onUploadIdCard(): void {
if (idcardUploading.value) {
return;
}
uni.chooseImage({
count: 1,
success: async (res: any) => {
const filePath = String(res?.tempFilePaths?.[0] ?? "");
const fileName = String(res?.tempFiles?.[0]?.name || `idcard-${Date.now()}.jpg`);
if (!filePath) {
return;
}
try {
idcardUploading.value = true;
const physicalName = buildChunkFileName(fileName);
person.value.IdCardFilename = physicalName;
person.value.IdCardUrl = filePath;
const result = await uploadFile({
url: "/DmsFile/UploadIdCardFile",
filePath,
name: fileName,
fileName: physicalName,
header: { FilePath: "idcard" },
formData: { side: "auto" },
});
const recognized = result?.Data ?? result?.data ?? result;
if (applyIdCardOcr(recognized)) {
uni.showToast({ title: t("FamilyPersonPage.idcardUploadedTip"), icon: "none" });
}
else {
uni.showToast({ title: t("FamilyPersonPage.idcardNoResult"), icon: "none" });
}
}
catch (error) {
console.error(error);
uni.showToast({ title: t("FamilyPersonPage.idcardUploadFailed"), icon: "none" });
}
finally {
idcardUploading.value = false;
}
},
});
}
function onClearIdCard(): void {
if (person.value.IdCardUrl.startsWith("blob:") && typeof URL !== "undefined") {
URL.revokeObjectURL(person.value.IdCardUrl);
}
person.value.IdCardFilename = "";
person.value.IdCardUrl = "";
}
/** 保存:姓 / 谱名必填;按 CreatedBy 有无决定 Update / Insert;成功回传 person 并关闭 */
async function onSave(): Promise<void> {
if (saving.value) {
return;
}
if (!String(person.value.Surname ?? "").trim()) {
uni.showToast({ title: t("FamilyPersonPage.surnameRequired"), icon: "none" });
return;
}
if (!String(person.value.GenealogyName ?? "").trim()) {
uni.showToast({ title: t("FamilyPersonPage.genealogyNameRequired"), icon: "none" });
return;
}
try {
saving.value = true;
const isUpdate = String(person.value.CreatedBy ?? "").trim() !== "";
const result = await (isUpdate
? familyPersonService.Update(data.value)
: familyPersonService.Insert(data.value));
const saved = result?.FamilyPerson || data.value.FamilyPerson;
uni.showToast({ title: t("AdminPage.savedTip"), icon: "none" });
emit("saved", saved);
close();
}
catch (error) {
console.error(error);
uni.showToast({ title: t("FilePage.actionFailed"), icon: "none" });
}
finally {
saving.value = false;
}
}
</script>
<template>
<u-popup :model-value="props.visible" mode="bottom" round="24"
:close-on-click-overlay="!saving && !uploading && !idcardUploading" @close="close">
<view class="pe">
<!-- hero(图示同款):左大圆头像点开选图,右侧名字实时拼接 + 谱名灰字 + 红字清除头像;新增未填名时回退用途标题 -->
<view class="pe__hero">
<view class="pe__avatar" @tap="onUploadAvatar">
<image v-if="person.AvatarUrl" class="pe__avatar-img" :src="person.AvatarUrl" mode="aspectFill" />
<u-icon v-else name="account" size="52" color="#c4c8cf" />
<view class="pe__avatar-mask">
<text v-if="uploading" class="pe__avatar-mask-text">
{{ t("FamilyPersonPage.uploadingTip") }}
</text>
<u-icon v-else name="camera" size="26" color="#ffffff" />
</view>
</view>
<view class="pe__hero-main">
<text class="pe__hero-name">{{ heroName || props.title || t('FamilyPersonPage.detailTitle') }}</text>
<text class="pe__hero-genealogy">{{ t("FamilyPersonPage.genealogyName") }} {{ person.GenealogyName || "-" }}</text>
<text v-if="person.AvatarUrl || person.AvatarFilename" class="pe__hero-clear" @tap="onClearAvatar">
{{ t("FamilyPersonPage.clearAvatar") }}
</text>
</view>
<!-- 公开资料开关从任职区移到 hero 右侧:一眼可见的整档隐私闸 -->
<view class="pe__hero-privacy">
<text class="pe__hero-privacy-label">{{ t("FamilyPersonPage.privacy") }}</text>
<u-switch :model-value="person.Privacy === 'Y'" :active-color="primary"
@change="setField('Privacy', $event ? 'Y' : 'N')" />
</view>
</view>
<!-- 四分段器:一次只看一个分区(基本/家庭学业/任职状态/简介) -->
<view class="pe__tabs">
<view v-for="(section, index) in sections" :key="section.title" class="pe__tab"
:class="{ 'pe__tab--on': activeTab === index }" @tap="activeTab = index">
<text>{{ section.title }}</text>
</view>
</view>
<scroll-view scroll-y class="pe__body">
<!-- 当前分区字段:一行一字段,标签左 / 控件右 -->
<view class="pe__card">
<view v-for="field in visibleFields" :key="field.prop" class="pe__row"
:class="{ 'pe__row--tap': field.type === 'gender', 'pe__row--area': field.type === 'textarea' }"
@tap="onFieldRowTap(field)">
<!-- 简介区不展 label:整卡只剩一块大输入区 -->
<view v-if="field.type !== 'textarea'" class="pe__label">
<text>{{ field.label }}</text>
<text v-if="field.required" class="pe__required">
*
</text>
</view>
<view class="pe__control">
<!-- 性别:枚举行点开 option-picker 回写 -->
<view v-if="field.type === 'gender'" class="pe__picker">
<text :class="{ 'pe__picker-empty': !genderLabel }">
{{ genderLabel || t('FamilyPersonPage.genderPlaceholder') }}
</text>
<u-icon name="arrow-right" size="24" color="#c4c8cf" />
</view>
<u-switch v-else-if="field.type === 'switch'" :model-value="fv(field.prop) === 'Y'"
:active-color="primary" @change="setField(field.prop, $event ? 'Y' : 'N')" />
<textarea v-else-if="field.type === 'textarea'" class="pe__textarea"
:value="fv(field.prop)" :maxlength="field.maxlength ?? 2000"
:placeholder="t('FamilyPersonPage.tabIntro')"
@input="setField(field.prop, $event.detail.value)" />
<picker v-else-if="field.type === 'date'" mode="date" :value="datePart(fv(field.prop))"
@change="setField(field.prop, $event.detail.value)">
<view class="pe__picker">
<text :class="{ 'pe__picker-empty': !fv(field.prop) }">
{{ datePart(fv(field.prop)) || t('AdminPage.notSetTip') }}
</text>
<u-icon name="arrow-right" size="24" color="#c4c8cf" />
</view>
</picker>
<input v-else class="pe__input" :value="fv(field.prop)"
:maxlength="field.maxlength ?? 100" :placeholder="field.label"
@input="setField(field.prop, $event.detail.value)">
</view>
</view>
<!-- 基本区「更多资料」展开行:绿链 + 收起字段灰字提示,点开就地展开/收起 -->
<view v-if="activeTab === 0" class="pe__row pe__row--tap" @tap="moreExpanded = !moreExpanded">
<text class="pe__more-link">{{ t("FamilyPersonPage.moreInfo") }}</text>
<view class="pe__picker">
<text class="pe__more-hint">{{ moreHint }}</text>
<u-icon :name="moreExpanded ? 'arrow-up' : 'arrow-down'" size="22" color="#8f959e" />
</view>
</view>
</view>
<!-- 身份证上传块:仅基本区展示(图示同位),标题入卡内 -->
<view v-if="activeTab === 0" class="pe__group">
<view class="pe__card pe__idcard">
<text class="pe__idcard-title">
{{ t("FamilyPersonPage.idcardTitle") }}
</text>
<view class="pe__idcard-head">
<text class="pe__idcard-hint">
{{ t("FamilyPersonPage.idcardHint") }}
</text>
<text v-if="person.IdCardUrl || person.IdCardFilename" class="pe__idcard-clear"
@tap="onClearIdCard">
{{ t("FamilyPersonPage.idcardClear") }}
</text>
</view>
<view class="pe__idcard-thumb" @tap="onUploadIdCard">
<image v-if="person.IdCardUrl" class="pe__idcard-thumb-img" :src="person.IdCardUrl"
mode="aspectFill" />
<view v-else class="pe__idcard-empty">
<u-icon name="camera" size="40" color="#c4c8cf" />
<text class="pe__idcard-empty-text">
{{ t("FamilyPersonPage.idcardUpload") }}
</text>
</view>
<view class="pe__idcard-mask">
<text v-if="idcardUploading" class="pe__idcard-mask-text">
{{ t("FamilyPersonPage.uploadingTip") }}
</text>
<u-icon v-else-if="person.IdCardUrl" name="camera" size="30" color="#ffffff" />
</view>
</view>
</view>
</view>
<view class="pe__body-space" />
</scroll-view>
<view class="pe__footer">
<view class="pe__btn" @tap="close">
<text class="pe__btn-text">
{{ t("AdminPage.cancelBut") }}
</text>
</view>
<view class="pe__btn pe__btn--primary" :class="{ 'pe__btn--muted': saving }"
:style="{ background: primary }" @tap="onSave">
<text class="pe__btn-text pe__btn-text--primary">
{{ saving ? t("AdminPage.savingTip") : t("AdminPage.saveBut") }}
</text>
</view>
</view>
<view class="pe__safe" />
</view>
<option-picker :visible="genderPickerVisible" :title="t('FamilyPersonPage.gender')" :items="genderItems"
:empty-text="t('FamilyPersonPage.genderEmpty')" :confirm-text="t('AdminPage.confirmBut')"
@close="genderPickerVisible = false" @confirm="onGenderConfirm" />
</u-popup>
</template>
<style lang="scss" scoped>
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角卡分块(同 option-picker / person-quick-create) */
.pe {
padding: 32rpx 24rpx 0;
background-color: #f2f3f5;
/* hero:左大圆头像 + 右侧名字/谱名/清除链,图示同款左对齐结构 */
&__hero {
flex-shrink: 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 28rpx;
padding: 4rpx 12rpx 24rpx;
}
&__hero-main {
display: flex;
flex: 1;
flex-direction: column;
gap: 8rpx;
min-width: 0;
}
&__hero-name {
overflow: hidden;
font-size: 40rpx;
font-weight: 700;
color: #1f2329;
text-overflow: ellipsis;
white-space: nowrap;
}
&__hero-genealogy {
font-size: 24rpx;
color: #8f959e;
}
&__hero-clear {
font-size: 24rpx;
color: #fa3534;
}
/* hero 右侧:公开资料开关,小灰字题注 + 开关竖排 */
&__hero-privacy {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
}
&__hero-privacy-label {
font-size: 22rpx;
color: #8f959e;
}
/* 四分段器:灰底轨道 + 选中白底胶囊(同详情页 tab 观感) */
&__tabs {
flex-shrink: 0;
display: flex;
flex-direction: row;
gap: 8rpx;
padding: 8rpx;
margin-bottom: 24rpx;
background-color: #e8eaed;
border-radius: 999rpx;
}
&__tab {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 68rpx;
padding: 0 8rpx;
border-radius: 999rpx;
text {
font-size: 24rpx;
color: #5c626b;
white-space: nowrap;
}
&--on {
background-color: #ffffff;
box-shadow: 0 2rpx 8rpx rgba(31, 35, 41, 0.08);
text {
font-weight: 600;
color: #1f2329;
}
}
}
/* 头像:hero 左侧大圆图,点开选图上传,上传中遮罩转文字 */
&__avatar {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 128rpx;
height: 128rpx;
overflow: hidden;
background-color: #ffffff;
border: 3rpx solid #e5e6eb;
border-radius: 50%;
}
&__avatar-img {
width: 100%;
height: 100%;
}
&__avatar-mask {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.35);
}
&__avatar-mask-text {
font-size: 20rpx;
color: #ffffff;
}
&__body {
/* 明确封顶高度:scroll-view 在 uni-app/H5 需确定高度才会内部滚动,否则撑满内容把底部钮顶出可视区(同 option-picker) */
max-height: 56vh;
}
&__group {
margin-top: 24rpx;
margin-bottom: 24rpx;
}
&__card {
padding: 0 28rpx;
overflow: hidden;
background-color: #ffffff;
border-radius: 20rpx;
}
/* 一行一字段:标签左、控件右,行间细线(末行无线) */
&__row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 20rpx;
min-height: 92rpx;
padding: 16rpx 0;
border-bottom: 1rpx solid #eef0f3;
&:last-child {
border-bottom: none;
}
&--tap {
cursor: pointer;
}
/* 简介行:竖排整宽无标签,拉高输入区 */
&--area {
display: block;
.pe__textarea {
min-height: 360rpx;
}
}
}
&__label {
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
font-size: 28rpx;
color: #1f2329;
}
&__required {
margin-left: 6rpx;
color: #ff4d4f;
}
&__control {
display: flex;
flex: 1;
align-items: center;
justify-content: flex-end;
min-width: 0;
}
&__input {
width: 100%;
font-size: 28rpx;
color: #1f2329;
text-align: right;
}
&__textarea {
width: 100%;
min-height: 120rpx;
font-size: 28rpx;
line-height: 1.5;
color: #1f2329;
text-align: left;
}
&__picker {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 8rpx;
font-size: 28rpx;
color: #1f2329;
}
&__picker-empty {
color: #8f959e;
}
/* 「更多资料」行:左绿链 + 右灰字字段提示 + 箭头 */
&__more-link {
font-size: 28rpx;
font-weight: 600;
color: #19be6b;
}
&__more-hint {
font-size: 24rpx;
color: #8f959e;
}
/* 身份证块 */
&__idcard {
padding: 28rpx;
}
&__idcard-title {
display: block;
margin-bottom: 12rpx;
font-size: 30rpx;
font-weight: 700;
color: #1f2329;
}
&__idcard-head {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
&__idcard-hint {
flex: 1;
font-size: 22rpx;
color: #8f959e;
}
&__idcard-clear {
flex-shrink: 0;
margin-left: 16rpx;
font-size: 24rpx;
color: #fa3534;
}
&__idcard-thumb {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 260rpx;
overflow: hidden;
background-color: #f7f8fa;
border: 1rpx dashed #d4d7dd;
border-radius: 16rpx;
}
&__idcard-thumb-img {
width: 100%;
height: 100%;
}
&__idcard-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
&__idcard-empty-text {
font-size: 24rpx;
color: #8f959e;
}
&__idcard-mask {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.25);
}
&__idcard-mask-text {
font-size: 22rpx;
color: #ffffff;
}
&__body-space {
height: 16rpx;
}
&__footer {
flex-shrink: 0;
display: flex;
gap: 20rpx;
margin-top: 12rpx;
}
/* 底部按键统一规格(同 option-picker):等高圆角矩形 + 细描边,非胶囊 */
&__btn {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 96rpx;
border: 2rpx solid #e5e6eb;
border-radius: 24rpx;
background-color: #ffffff;
&--primary {
border-color: transparent;
}
&--muted {
opacity: 0.45;
}
}
&__btn-text {
font-size: 32rpx;
color: #5c626b;
&--primary {
font-weight: 600;
color: #ffffff;
}
}
&__safe {
flex-shrink: 0;
height: calc(32rpx + env(safe-area-inset-bottom));
}
}
</style>
@@ -1,259 +0,0 @@
<script setup lang="ts">
/**
* 快速新增家族成员(底部抽屉):旧 WebVue components/master/FamilyPersonQuickCreateModal.vue 的移动端等价物
*
* 族谱关系维护里「新增配偶 / 新增孩子 / 新增祖先」都调它:只填 姓* / 名(谱名)* / 性别*,
* 落库走 FamilyPersonService.Insert(TreeId 由调用方带入),成功后 emit("created", person),
* created 取 result?.FamilyPerson || payload.FamilyPerson,由页面按用途回填到对应关系列表。
* 性别用 option-picker 单选(数据来自 GenderPolicy 语言包),与成员详情页同口径。
*/
import type { FamilyPersonDisplayData } from "@/data/auto/familyPersonDisplayData";
import { useLocale } from "uview-pro";
import { computed, ref, watch } from "vue";
import { useThemeTokens } from "@/composables";
import { usePolicyOptions } from "@/composables/usePolicyOptions";
import { FamilyPersonData } from "@/data/auto/familyPersonData";
import { FamilyPersonDisplayData as FamilyPersonDisplayDataCls } from "@/data/auto/familyPersonDisplayData";
import familyPersonService from "@/service/master/familyPersonService";
const props = defineProps({
visible: { type: Boolean, default: false },
treeId: { type: String, default: "" },
title: { type: String, default: "" },
});
const emit = defineEmits<{
(e: "update:visible", value: boolean): void;
(e: "created", person: FamilyPersonDisplayData): void;
}>();
const { t } = useLocale();
// 确认钮纯色跟主题(渐变浅档白字发糊)
const { primary } = useThemeTokens();
const { getPolicyOptions, getPolicyText } = usePolicyOptions();
const genderItems = computed(() => getPolicyOptions("GenderPolicy"));
const surname = ref("");
const genealogyName = ref("");
const gender = ref("");
const saving = ref(false);
const genderPickerVisible = ref(false);
const genderLabel = computed(() => getPolicyText("GenderPolicy", gender.value));
watch(
() => props.visible,
(on) => {
if (on) {
surname.value = "";
genealogyName.value = "";
gender.value = "";
}
},
);
function close(): void {
emit("update:visible", false);
}
function onGenderConfirm(key: string): void {
gender.value = key;
genderPickerVisible.value = false;
}
async function onConfirm(): Promise<void> {
if (!surname.value.trim()) {
uni.showToast({ title: t("FamilyPersonPage.surnameRequired"), icon: "none" });
return;
}
if (!genealogyName.value.trim()) {
uni.showToast({ title: t("FamilyPersonPage.genealogyNameRequired"), icon: "none" });
return;
}
if (!gender.value) {
uni.showToast({ title: t("FamilyPersonPage.genderRequired"), icon: "none" });
return;
}
const payload = new FamilyPersonData();
payload.FamilyPerson = new FamilyPersonDisplayDataCls();
payload.FamilyPerson.Surname = surname.value.trim();
payload.FamilyPerson.GenealogyName = genealogyName.value.trim();
payload.FamilyPerson.Gender = gender.value;
payload.FamilyPerson.TreeId = props.treeId || "";
try {
saving.value = true;
const result = await familyPersonService.Insert(payload);
const created = result?.FamilyPerson || payload.FamilyPerson;
emit("created", created);
close();
}
catch (error) {
console.error(error);
uni.showToast({ title: t("FamilyPersonPage.quickCreateFailed"), icon: "none" });
}
finally {
saving.value = false;
}
}
</script>
<template>
<u-popup :model-value="props.visible" mode="bottom" round="24" :close-on-click-overlay="!saving" @close="close">
<view class="pqc">
<text class="pqc__title">
{{ props.title || t('FamilyPersonPage.quickCreateTitle') }}
</text>
<!-- 表单白卡(底部抽屉统一风格:灰底 + 白色圆角分块) -->
<view class="pqc__card">
<view class="pqc__field">
<text class="pqc__label">
{{ t("FamilyPersonPage.surname") }}<text class="pqc__required">*</text>
</text>
<u-input v-model="surname" :border="true" :maxlength="100"
:placeholder="t('FamilyPersonPage.surnamePlaceholder')" />
</view>
<view class="pqc__field">
<text class="pqc__label">
{{ t("FamilyPersonPage.genealogyName") }}<text class="pqc__required">*</text>
</text>
<u-input v-model="genealogyName" :border="true" :maxlength="100"
:placeholder="t('FamilyPersonPage.genealogyNamePlaceholder')" />
</view>
<view class="pqc__field">
<text class="pqc__label">
{{ t("FamilyPersonPage.gender") }}<text class="pqc__required">*</text>
</text>
<view class="pqc__picker" @tap="genderPickerVisible = true">
<text :class="{ 'pqc__picker-empty': !genderLabel }">
{{ genderLabel || t('FamilyPersonPage.genderPlaceholder') }}
</text>
<u-icon name="arrow-right" size="26" color="#c4c8cf" />
</view>
</view>
</view>
<view class="pqc__footer">
<view class="pqc__btn" @tap="close">
<text class="pqc__btn-text">
{{ t("AdminPage.cancelBut") }}
</text>
</view>
<view class="pqc__btn pqc__btn--primary" :class="{ 'pqc__btn--muted': saving }"
:style="{ background: primary }" @tap="onConfirm">
<text class="pqc__btn-text pqc__btn-text--primary">
{{ saving ? t("AdminPage.savingTip") : t("AdminPage.confirmBut") }}
</text>
</view>
</view>
<view class="pqc__safe" />
</view>
<option-picker :visible="genderPickerVisible" :title="t('FamilyPersonPage.gender')" :items="genderItems"
:empty-text="t('FamilyPersonPage.genderEmpty')" @close="genderPickerVisible = false"
@confirm="onGenderConfirm" />
</u-popup>
</template>
<style lang="scss" scoped>
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角卡分块 */
.pqc {
padding: 32rpx 24rpx 0;
background-color: #f2f3f5;
&__title {
display: block;
padding-bottom: 24rpx;
font-size: 32rpx;
font-weight: 600;
color: #1f2329;
text-align: center;
}
&__card {
padding: 28rpx 28rpx 4rpx;
border-radius: 20rpx;
background-color: #ffffff;
}
&__field {
margin-bottom: 24rpx;
}
&__label {
display: block;
margin-bottom: 12rpx;
font-size: 26rpx;
color: #646a73;
}
&__required {
margin-left: 2rpx;
color: #ff4d4f;
}
&__picker {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 76rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #1f2329;
border: 1px solid #e5e6eb;
border-radius: 12rpx;
}
&__picker-empty {
color: #8f959e;
}
&__footer {
display: flex;
gap: 20rpx;
margin-top: 28rpx;
}
/* 底部按键统一规格(同 option-picker):等高圆角矩形 + 细描边,非胶囊 */
&__btn {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 96rpx;
border: 2rpx solid #e5e6eb;
border-radius: 24rpx;
background-color: #ffffff;
&--primary {
border-color: transparent;
background-color: #2b8fff;
}
/* 提交中:纯色 + 透明度置灰(色相不脱节) */
&--muted {
opacity: 0.45;
}
}
&__btn-text {
font-size: 32rpx;
color: #5c626b;
&--primary {
font-weight: 600;
color: #ffffff;
}
}
&__safe {
height: calc(32rpx + env(safe-area-inset-bottom));
}
}
</style>
@@ -127,10 +127,13 @@ export function useDmsFiles() {
function toFileItem(record: DmsFileUploadViewDisplayData): FileItem {
const time = parseTime(record.CreatedDatetime);
const kind = kindOf(record);
return {
id: record.FileId,
name: record.OrgFileName || record.FileName,
kind: kindOf(record),
kind,
// 图片卡显 Scale 缩略图(后端 Url 字段已拼好,旧系统宫格同款)
thumbUrl: kind === "image" ? record.Url || undefined : undefined,
// 卡片元数据不显示用户:文件夹 sizeText 为空只剩时间,文件显示大小+时间
// FileSize 单位是 KB(旧系统 DmsFileEnquiry 下载缓冲 (FileSize+1)*1024 字节佐证),先换算成字节
sizeText: formatSize(
@@ -10,6 +10,8 @@
import type { PickColumn } from "@/components/admin-pick-popup/admin-pick-popup.vue";
import { useLocale } from "uview-pro";
import { computed, ref } from "vue";
import { FamilyPersonEntity } from "@/data/auto/familyPersonEntity";
import { FamilyPersonSearchCriteria } from "@/data/auto/familyPersonSearchCriteria";
import { SearchFilter } from "@/data/auto/searchFilter";
import { SecurityActorEntity } from "@/data/auto/securityActorEntity";
import { SecurityActorSearchCriteria } from "@/data/auto/securityActorSearchCriteria";
@@ -26,6 +28,7 @@ import { SecurityUserEntity } from "@/data/auto/securityUserEntity";
import { SecurityUserSearchCriteria } from "@/data/auto/securityUserSearchCriteria";
import { SearchFilterOperatorPolicy } from "@/data/custom/searchFilterOperatorPolicy";
import { SearchFilterTypePolicy } from "@/data/custom/searchFilterTypePolicy";
import familyPersonService from "@/service/master/familyPersonService";
import securityActorService from "@/service/security/securityActorService";
import securityFunctionService from "@/service/security/securityFunctionService";
import securityGroupService from "@/service/security/securityGroupService";
@@ -378,3 +381,47 @@ export function useHelpFeaturePickConfig() {
fixedFilters,
};
}
// ===== Family Person(族员选择:切换当前族员 / 选已有成员作配偶)=====
export function usePersonPickConfig() {
const { t } = useLocale();
const fixedFilters = ref<Record<string, string>>({});
const columns = computed<PickColumn[]>(() => [
{
label: t("FamilyPersonPage.genealogyName"),
prop: FamilyPersonEntity.GENEALOGY_NAME,
},
{ label: t("FamilyPersonPage.surname"), prop: FamilyPersonEntity.SURNAME },
{
label: t("FamilyPersonPage.commonName"),
prop: FamilyPersonEntity.COMMON_NAME,
},
// 成员编号列不显示:弹窗列表口径与族谱页统一用常用名辨识(rowKey 仍是 PersonId,仅不占行)
]);
async function loader(page: number, pageSize: number, keyword: string) {
const criteria = new FamilyPersonSearchCriteria();
criteria.GridColumns = [];
criteria.page = page;
criteria.limit = pageSize;
criteria.orderby = FamilyPersonEntity.DB_GENEALOGY_NAME;
const filters: SearchFilter[] = [];
if (keyword) {
filters.push(
containFilter(FamilyPersonEntity.DB_GENEALOGY_NAME, keyword),
);
}
criteria.Filter = filters;
const data = await familyPersonService.FindBySearchCriteria(criteria);
return { rows: data?.Datas ?? [], total: data?.Total ?? 0 };
}
return {
columns,
loader,
rowKey: FamilyPersonEntity.PERSON_ID,
titleKey: FamilyPersonEntity.GENEALOGY_NAME,
fixedFilters,
};
}
@@ -76,10 +76,13 @@ export function useShareHall() {
function toFileItem(record: FamilyDmsFileViewDisplayData): FileItem {
const time = parseTime(record.CreatedDatetime);
const kind = kindOf(record);
return {
id: record.FileId,
name: record.OrgFileName || record.FileName,
kind: kindOf(record),
kind,
// 图片卡显 Scale 缩略图(后端 Url 字段已拼好,旧系统宫格同款)
thumbUrl: kind === "image" ? record.Url || undefined : undefined,
// FileSize 单位是 KB(旧系统下载缓冲 (FileSize+1)*1024 字节佐证),先换算成字节
sizeText: formatSize(
record.FileSize ? record.FileSize * 1024 : undefined,
@@ -40,10 +40,13 @@ export function useTrashFiles() {
/** 删除日志 -> 卡片条目:时间列展示的是删除时间(记录 CreatedDatetime),path 留原目录备查 */
function toFileItem(record: DmsFileUploadDeleteLogViewDisplayData): FileItem {
const time = parseTime(record.CreatedDatetime);
const kind = kindOf(record);
return {
id: record.FileId,
name: record.OrgFileName || record.FileName,
kind: kindOf(record),
kind,
// 回收站也显缩略图(日志视图 Url 同口径拼的 Scale 图,文件删了图还在)
thumbUrl: kind === "image" ? record.Url || undefined : undefined,
// FileSize 单位是 KB(旧系统下载缓冲 (FileSize+1)*1024 字节佐证),先换算成字节
sizeText: formatSize(
record.FileSize ? record.FileSize * 1024 : undefined,
@@ -7,7 +7,10 @@ export class FamilyTreeData
public FamilyTree: FamilyTreeDisplayData = new FamilyTreeDisplayData();
public CurrentPerson: FamilyPersonData = new FamilyPersonData();
public IsManager: boolean =false;
public IsMember: boolean =false;
public IsCanAmend: boolean =false;
public SelfBindPersonId: string = "";
public CanAmendPersonIds: string[]= [];
public FamilyTreePermissions: FamilyTreePermissionUserViewEntity[] = [];
}
@@ -28,6 +28,8 @@ export interface FileItem {
permission?: "Y" | "N";
/** 已被收藏共享(PermissionIndc=Y 且 FavoriteIndc=Y):显示蓝色共享角标 */
shared?: boolean;
/** 图片缩略图地址(后端 PackUrl 拼的 Scale 压缩图,旧系统宫格同款);仅 image 类型且有值时卡片显图,否则显类型图标 */
thumbUrl?: string;
}
/** 快捷功能入口的单项:图标 + 渐变底色,页面给什么画什么 */
+47 -3
View File
@@ -161,12 +161,20 @@
"genealogyName": "Genealogy Name",
"commonName": "Common Name",
"personSearch": "Member Search",
"pedigreeChart": "Pedigree Chart",
"ancestryChart": "Ancestry Chart",
"pedigreeChart": "Branch Chart",
"ancestryChart": "Full Chart",
"chartEmpty": "No family tree data",
"applyManage": "Apply for Manager",
"bindAccount": "Bind Account",
"unbindAccount": "Unbind",
"unbindSuccessTip": "Binding with the member has been removed",
"bindSuccessTip": "Current member has been bound to your account",
"bindNoPersonTip": "Select the member to bind first",
"requestSentTip": "Request submitted, waiting for approval",
"alreadyManagerTip": "You are already a manager of this tree",
"addAncestor": "Add Ancestor",
"editAncestor": "Edit Ancestor",
"ancestorRequired": "Please add an ancestor first",
"spouseTitle": "Spouses",
"addSpouse": "Add Spouse",
"pickSpouse": "Pick Member",
@@ -203,11 +211,42 @@
"chipPublic": "Public",
"chipPrivate": "Private",
"searchPlaceholder": "Search tree name",
"listTitle": "Family Trees"
"listTitle": "Family Trees",
"tabOverview": "Overview",
"tabMembers": "Members",
"tabPerms": "Access",
"statMembers": "members",
"statManagers": "managers",
"statPending": "pending",
"introTitle": "Brief",
"quickActions": "Quick Actions",
"editProfile": "Edit Info",
"edit": "Edit",
"addMember": "Add Member",
"selectMember": "Select Current Member",
"deleteTree": "Delete Tree",
"deleteTreeHint": "All member profiles will be deleted too",
"searchMember": "Search name, genealogy name or member ID",
"filterAll": "All",
"filterMale": "Male",
"filterFemale": "Female",
"ancestorCard": "Ancestor",
"ancestorInfo": "Ancestor Info",
"noAncestor": "No ancestor info yet",
"createdAt": "Created",
"publicHint": "When public, others can search and request to join",
"addChildBtn": "Add Child",
"viewFullChart": "View Full Chart",
"editInfoTitle": "Edit Family Tree Info",
"removeRelation": "Remove Relation",
"removeRelationConfirm": "Remove the relation with {name}? It takes effect immediately after saving.",
"dupMemberTip": "This member is already linked, do not add again"
},
"FamilyPersonPage": {
"title": "Family Member Query",
"detailTitle": "Member Detail",
"editTitle": "Edit Member",
"editBut": "Edit",
"personId": "Member ID",
"treeId": "Tree ID",
"surname": "Surname",
@@ -223,6 +262,11 @@
"zi": "Courtesy Name (Zi)",
"hao": "Art Name (Hao)",
"birthOrder": "Birth Order",
"moreInfo": "More Info",
"tabBasic": "Basic",
"tabFamily": "Family",
"tabWork": "Career",
"tabIntro": "Bio",
"maritalStatus": "Marital Status",
"fatherId": "Father ID",
"motherName": "Mother Name",
+47 -3
View File
@@ -161,12 +161,20 @@
"genealogyName": "谱名",
"commonName": "常用名",
"personSearch": "族员搜索",
"pedigreeChart": "谱系图",
"ancestryChart": "祖谱系图",
"pedigreeChart": "小系图",
"ancestryChart": "全系图",
"chartEmpty": "暂无族谱数据",
"applyManage": "申请管理",
"bindAccount": "绑定账号",
"unbindAccount": "解除绑定",
"unbindSuccessTip": "已解除与族员的绑定",
"bindSuccessTip": "已把当前族员绑定到你的账号",
"bindNoPersonTip": "请先选择要绑定的族员",
"requestSentTip": "申请已提交,等待管理员审批",
"alreadyManagerTip": "您已是本族谱管理员,无需申请",
"addAncestor": "新增祖先",
"editAncestor": "修改祖先",
"ancestorRequired": "请先新增祖先",
"spouseTitle": "夫妻记录",
"addSpouse": "新增配偶",
"pickSpouse": "选择成员",
@@ -203,11 +211,42 @@
"chipPublic": "公开",
"chipPrivate": "私密",
"searchPlaceholder": "搜索族谱名称",
"listTitle": "有家族谱"
"listTitle": "有家族谱",
"tabOverview": "概览",
"tabMembers": "成员",
"tabPerms": "权限",
"statMembers": "位族人",
"statManagers": "管理员",
"statPending": "待审核",
"introTitle": "族谱简介",
"quickActions": "快捷操作",
"editProfile": "编辑资料",
"edit": "编辑",
"addMember": "添加族人",
"selectMember": "选择当前族员",
"deleteTree": "删除族谱",
"deleteTreeHint": "会同时删除全部族人档案",
"searchMember": "搜索姓名、谱名或成员编号",
"filterAll": "全部",
"filterMale": "男性",
"filterFemale": "女性",
"ancestorCard": "始祖",
"ancestorInfo": "始祖信息",
"noAncestor": "暂无始祖信息",
"createdAt": "创建时间",
"publicHint": "公开后他人可搜索并申请加入",
"addChildBtn": "添加子女",
"viewFullChart": "查看全系图",
"editInfoTitle": "编辑族谱信息",
"removeRelation": "解除关系",
"removeRelationConfirm": "确定解除与「{name}」的关系吗?解除后立即保存生效",
"dupMemberTip": "该成员已在关系中,请勿重复添加"
},
"FamilyPersonPage": {
"title": "家族成员查询",
"detailTitle": "成员详情",
"editTitle": "修改成员",
"editBut": "修改",
"personId": "成员编号",
"treeId": "族谱编号",
"surname": "姓",
@@ -223,6 +262,11 @@
"zi": "字",
"hao": "号",
"birthOrder": "排行",
"moreInfo": "更多资料",
"tabBasic": "基本资料",
"tabFamily": "家庭学业",
"tabWork": "任职状态",
"tabIntro": "简介",
"maritalStatus": "婚姻状况",
"fatherId": "父亲编号",
"motherName": "母亲姓名",
+3 -3
View File
@@ -58,13 +58,13 @@
- 组件 `components/admin-pick-popup/admin-pick-popup.vue`:`u-popup mode="bottom"`(80vh)+ `scroll-view` 手动分页(**不能用 z-paging**:它默认 `position:fixed` 撑满视口,在弹层内会打架);
- Props `visible / title / columns / loader / rowKey / titleKey / multiple / fixedFilters / pageSize`,Emits `update:visible / close / confirm(rows)`;
- 数据源在 `composables/usePickerConfigs.ts`:每个被选数据一个工厂函数(`usePermissionPickConfig`、`useDependentPickConfig`、`useActorPickConfig`、`useGroupPickConfig`、`useUserPickConfig`、`useFunctionPickConfig`、`useHelpFeaturePickConfig`),返回 `{ columns, loader, rowKey, titleKey, fixedFilters }`;
- 数据源在 `composables/usePickerConfigs.ts`:每个被选数据一个工厂函数(`usePermissionPickConfig`、`useDependentPickConfig`、`useActorPickConfig`、`useGroupPickConfig`、`useUserPickConfig`、`useFunctionPickConfig`、`useHelpFeaturePickConfig`、`usePersonPickConfig`),返回 `{ columns, loader, rowKey, titleKey, fixedFilters }`;
- 调用方只需 `const xxxConfig = useXxxPickConfig(); const showXxxPicker = ref(false);`,点入口置 `true`,`@confirm` 里做原来的去重合并逻辑。需要动态过滤(如扩展权限按 `dependent`)时写 `xxxConfig.fixedFilters.value = { dependent }`;
- 单选点行即 `confirm` + 关闭;多选勾选后点确定;X / 遮罩 / 取消三条退路都通。
- 单选/多选同构:点行高亮选中(单选圆圈、多选方勾,再点取消),底部「取消/确定」栏常驻,确定才 `confirm` + 关闭;多选另有全选钮;X / 遮罩 / 取消三条退路都通。
**security 域的 enquiry 页因此回归纯列表页**:`isPick` / `pickTarget` / `onLoad(channel)` / `deliver()` 全部删除,`:show-nav-back="true"` 恒定,`+` 新增入口不再被选择器态隐藏。
`useAdminPick` / `useAdminPickTarget` 机制本身**保留**:`family-person-enquiry` 仍作为「族员选择器」被首页 dashboard 以 `?channel=person` 调起。那条老路子的协议参数一律不做 `encodeURIComponent`(H5 侧 uni/vue-router 拼 URL 用 `encodeURI`,预编码会把 `%` 再编成 `%25`)。
`useAdminPick` / `useAdminPickTarget` 机制目前**无任何调用方**:最后一个跳页选择(族谱首页「族员搜索 / 选配偶」带 `?channel=person` 调 `family-person-enquiry`)已改为调用方内嵌 `<admin-pick-popup>`(数据源 `usePersonPickConfig`),enquiry 页侧协议代码同步移除。机制文件暂保留备用;若日后恢复跳页选择,注意那条老路子的协议参数一律不做 `encodeURIComponent`(H5 侧 uni/vue-router 拼 URL 用 `encodeURI`,预编码会把 `%` 再编成 `%25`)。
> 命名订正:`actor-enquiry` 就是旧 `ActorEnquiry.vue` 的移植,早期草稿里的 `actor-pick` 是造错的概念——它现在只是普通查询页,选择能力在调用方的弹窗里。
@@ -5,15 +5,15 @@
* 保留:四条筛选(谱名/常用名/族谱编号 文本包含、性别 单选枚举)、每页 20 条、行点击进详情。
* 旧页的新增/删除按钮本就被注释禁用,故这里不出新增入口、也不做批量删除,行为对齐。
*
* 兼作「族员选择器」:首页 dashboard 详情态的「族员搜索」带 ?channel=person 跳入,
* 单选回传后调用方按 PersonId 切换当前族员(useAdminPick / useAdminPickTarget 协议)。
* 本页不再承担选择器职责:族谱首页的「族员搜索 / 选配偶」已改为调用方内嵌
* <admin-pick-popup> 底部弹窗(数据源 composables/usePickerConfigs 的 usePersonPickConfig),
* 遗留的 useAdminPickTarget 协议代码已移除,与其他 enquiry 页同构。
*/
import type { AdminColumn, AdminQuery, AdminRow } from "@/admin/admin-types";
import { onLoad } from "@dcloudio/uni-app";
import { useLocale } from "uview-pro";
import { computed, nextTick, reactive, ref, watch } from "vue";
import { fillCriteria, makeFilterOption, toSelectOption } from "@/admin/admin-filter";
import { useAdminList, useAdminPickTarget, useAuthGuard, useThemeGradient, useThemeTokens } from "@/composables";
import { useAdminList, useAuthGuard, useThemeGradient, useThemeTokens } from "@/composables";
import { usePolicyOptions } from "@/composables/usePolicyOptions";
import { FamilyPersonEntity } from "@/data/auto/familyPersonEntity";
import { FamilyPersonSearchCriteria } from "@/data/auto/familyPersonSearchCriteria";
@@ -24,7 +24,6 @@ import familyPersonService from "@/service/master/familyPersonService";
const { t, currentLocale } = useLocale();
const { getPolicyOptions, getPolicyText } = usePolicyOptions();
const pickTarget = useAdminPickTarget();
// 统一风格接线(与族谱首页/媒体/共享页同套):主题渐变底 + 强调色 CSS 变量下发
const { themeBackground } = useThemeGradient();
@@ -36,9 +35,6 @@ const themeVars = computed(() => ({
"--fg-shadow": accentShadow.value,
}));
/** 选择器模式:带 channel 进来时点行回传而非跳详情 */
const isPick = ref(false);
const LABEL_KEYS: Record<string, string> = {
[FamilyPersonEntity.DB_GENEALOGY_NAME]: "FamilyPersonPage.genealogyName",
[FamilyPersonEntity.DB_COMMON_NAME]: "FamilyPersonPage.commonName",
@@ -133,26 +129,11 @@ function onSearch(): void {
}
function onRowTap(row: AdminRow): void {
if (isPick.value) {
if (pickTarget.multiple.value) {
return;
}
pickTarget.deliverOne(row);
return;
}
uni.navigateTo({
url: `${pageUrl("familyPersonOperation")}?id=${row.PersonId}&mode=view`,
});
}
function onConfirm(rows: AdminRow[]): void {
if (!rows.length) {
uni.showToast({ title: t("AdminPage.pickNoneTip"), icon: "none" });
return;
}
pickTarget.deliver(rows);
}
watch(currentLocale, () => {
for (const option of bindOptions) {
const key = LABEL_KEYS[option.BindValue];
@@ -165,11 +146,6 @@ watch(currentLocale, () => {
}
});
onLoad((options) => {
pickTarget.readOptions(options);
isPick.value = String(options?.channel ?? "").trim().length > 0;
});
useAuthGuard(() => {
// 每次显示都重拉第一页:详情修改后返回能看到最新数据(navigateBack 不重新挂页面)
nextTick(() => listRef.value?.refresh());
@@ -179,16 +155,12 @@ useAuthGuard(() => {
<template>
<view class="page" :style="[{ background: themeBackground }, themeVars]">
<admin-list ref="listRef" v-model:keyword="keyword" :nav-title="t('FamilyPersonPage.title')"
:show-nav-back="!isPick" :columns="columns" :loader="state.query" :row-key="FamilyPersonEntity.PERSON_ID"
title-key="GenealogyName" :selectable="isPick && pickTarget.multiple.value" :filter-count="filterCount"
:filter-visible="filterVisible" :filter-items="draft" :search-token="searchToken"
:keep-selection-on-reload="isPick" @search="onSearch" @open-filter="state.openFilter()"
@close-filter="state.closeFilter()" @apply-filter="state.applyFilter($event)"
@clear-filter="state.clearFiltersAndSearch()" @row-tap="onRowTap">
<template v-if="isPick" #actions="{ rows }">
<u-button type="primary" shape="circle" size="mini" :text="t('AdminPage.pickDoneBut')"
@click="onConfirm(rows)" />
</template>
:show-nav-back="true" :columns="columns" :loader="state.query" :row-key="FamilyPersonEntity.PERSON_ID"
title-key="GenealogyName" :filter-count="filterCount" :filter-visible="filterVisible"
:filter-items="draft" :search-token="searchToken" @search="onSearch"
@open-filter="state.openFilter()" @close-filter="state.closeFilter()"
@apply-filter="state.applyFilter($event)" @clear-filter="state.clearFiltersAndSearch()"
@row-tap="onRowTap">
</admin-list>
</view>
</template>
@@ -535,8 +535,8 @@ useAuthGuard(() => {
</template>
<option-picker :visible="genderPickerVisible" :title="t('FamilyPersonPage.gender')" :items="genderItems"
:empty-text="t('FamilyPersonPage.genderEmpty')" @close="genderPickerVisible = false"
@confirm="onGenderConfirm" />
:empty-text="t('FamilyPersonPage.genderEmpty')" :confirm-text="t('AdminPage.confirmBut')"
@close="genderPickerVisible = false" @confirm="onGenderConfirm" />
</template>
</admin-form>
</template>
+1 -1
View File
@@ -56,7 +56,7 @@ export const PAGE_PATHS = {
helpFeatureEnquiry: "pages/security/help-feature-enquiry",
helpFeatureOperation: "pages/security/help-feature-operation",
// 族谱管理已融合进首页 dashboard 两态页,旧 family-tree-enquiry/operation 两页已退役(不在 PAGE_PATHS 登记)
// 家族成员(短地址 /family-person-enquiry、/family-person-operation):旧 master/FamilyPerson 两页迁移,含族员选择器
// 家族成员(短地址 /family-person-enquiry、/family-person-operation):旧 master/FamilyPerson 两页迁移;族员选择已改为调用方内嵌弹窗
familyPersonEnquiry: "pages/security/family-person-enquiry",
familyPersonOperation: "pages/security/family-person-operation",
// 底部导航 tab 页(首页复用 dashboard):先各建一个简单测试页占位
@@ -86,6 +86,26 @@ class FamilyTreeService{
)
};
async BindSelfPerson(data: any) {
return request<FamilyTreeData>(
{
url: "/FamilyTree/BindSelfPerson",
method: "post",
data,
}
)
};
async UnbindSelfPerson(data: string) {
return request<FamilyTreeData>(
{
url: "/FamilyTree/UnbindSelfPerson?treeId="+data,
method: "post",
data,
}
)
};
async Update(data: FamilyTreeData) {
return request<FamilyTreeData>(
{
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M978.944 609.28C926.72 565.76 890.88 511.488 890.88 475.136c0-27.648 22.528-55.808 63.488-80.384 8.704-5.632 12.8-15.872 10.752-25.6-25.088-107.008-98.816-355.84-257.536-368.64-81.92-6.144-158.72 45.568-220.672 152.064-21.504 37.376-41.984 81.408-59.392 130.56-35.328-53.76-76.8-82.944-122.88-86.528-45.568-4.096-113.152 13.824-174.08 118.784-38.912 66.56-70.656 161.792-90.112 268.288-11.264 61.952 0.512 118.272 33.28 161.28 38.4 49.664 104.448 81.408 184.32 88.576v137.216c0 12.8 10.24 23.04 23.04 23.04 12.288 0 23.04-10.752 23.04-23.552V834.56c30.72-1.024 59.904-6.144 87.04-14.336 35.84-11.776 66.048-30.72 89.088-54.272 46.592 29.184 104.96 47.104 171.52 52.736h3.584v182.272c0 12.8 10.24 23.04 23.04 23.04s23.04-10.24 23.04-23.04v-181.248c57.344-1.536 111.616-13.312 156.672-34.816 68.096-32.768 112.64-85.504 128.512-152.576 2.048-8.192-0.512-17.408-7.68-23.04zM304.64 788.48v-258.048c0-12.8-10.24-23.04-23.04-23.04s-23.04 10.24-23.04 23.04v78.336L199.68 577.536c-11.264-6.144-25.6-1.536-31.232 9.728-6.144 11.264-1.536 25.6 9.728 31.232l79.872 42.496v126.464c-65.536-6.656-118.272-31.232-147.456-70.144-25.6-32.768-33.792-75.264-25.088-124.928 30.208-166.912 103.936-357.888 215.04-348.672 48.64 4.096 85.504 55.296 107.52 98.816-14.848 49.152-27.648 102.4-37.376 157.696-14.336 79.872 0.512 150.528 42.496 205.312 9.216 11.776 18.944 22.528 30.208 32.256-31.744 30.208-81.408 48.64-138.752 50.688z m397.312-14.848V504.832c3.072-1.536 5.632-3.072 7.68-5.632l116.736-132.096c8.704-9.728 7.168-24.064-2.048-32.768-9.728-8.704-24.064-7.168-32.768 2.048l-89.6 101.376V273.408c0-12.8-10.24-23.04-23.04-23.04s-23.04 10.24-23.04 23.04v338.944l-79.872-95.744c-8.704-10.24-23.04-11.776-32.768-3.584-10.24 8.704-11.776 23.04-3.584 32.768l104.96 125.952c2.56 3.584 6.656 6.144 10.752 7.168v93.184c-66.56-5.12-122.88-24.576-164.864-55.296l-0.512-0.512c-0.512 0-0.512-0.512-1.024-0.512-15.36-11.264-28.672-24.064-39.936-38.4-33.792-44.544-45.056-101.888-33.28-168.96 38.912-215.04 134.144-461.312 277.504-461.312h10.24c86.528 7.168 164.352 125.44 211.968 317.44-46.592 32.256-71.168 70.144-71.168 111.616 0 47.616 34.816 108.032 93.184 159.232-30.208 93.696-132.608 135.68-235.52 138.24z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M740.11 410.02c-21.24-50.2-50.79-105.07-87.83-163.08-62.83-98.38-126.61-173.13-127.24-173.87a16.978 16.978 0 0 0-12.92-5.94h-0.25c-5.05 0.07-9.81 2.39-12.99 6.32-0.66 0.82-66.61 82.76-130.48 184.64-86.93 138.66-128.96 246.79-124.92 321.38 3.2 59.05 22.96 114.54 57.14 160.49 34.38 46.22 80.78 79.33 134.18 95.75 8.98 2.76 18.5-2.28 21.26-11.26 2.76-8.98-2.28-18.5-11.26-21.26-46.44-14.28-86.86-43.17-116.89-83.54-30.18-40.57-47.63-89.68-50.47-142.03-3.6-66.44 37.75-170.59 119.59-301.18 46.11-73.57 93.32-136.57 115.42-165.19 21.68 26.75 66.76 84.46 111.38 154.37 56.08 87.87 122.92 213.49 122.92 312.92 0 52.11-16.72 101.44-48.34 142.67-30.64 39.94-74.02 69.45-122.16 83.1-9.04 2.56-14.29 11.96-11.72 21 2.12 7.49 8.94 12.37 16.35 12.37 1.54 0 3.1-0.21 4.65-0.65 55.14-15.63 104.81-49.41 139.87-95.12 36.22-47.22 55.37-103.72 55.37-163.38 0.01-48.04-13.67-104.74-40.66-168.51z"/><path d="M625.23 639.73c6.66-6.62 6.69-17.39 0.07-24.05s-17.39-6.69-24.05-0.07L529 687.42V512.1c1.68-0.74 3.27-1.75 4.69-3.07 0.23-0.21 23.16-21.47 45.79-43.28 13.37-12.88 24.06-23.46 31.76-31.45 14.72-15.26 19-20.9 19-28.86 0-9.39-7.61-17.01-17.01-17.01-7.04 0-13.09 4.28-15.67 10.38-6.09 8.08-36.6 37.96-68.57 68.04V345.44c0-9.39-7.61-17.01-17.01-17.01-9.39 0-17.01 7.61-17.01 17.01v229.79l-84.02-50.99c-8.03-4.88-18.49-2.31-23.36 5.72-4.87 8.03-2.31 18.49 5.72 23.36l101.67 61.7v324.85c0 9.39 7.61 17.01 17.01 17.01 9.39 0 17.01-7.61 17.01-17.01v-206.1c1.85-0.82 3.6-1.97 5.12-3.48l91.11-90.56z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB