This commit is contained in:
owenchen committed 2026-10-15 23:20:36 +08:00
1 parent dcc112c076
commit b858386e86
15 files changed
+3634 -69

No files matched your search

@@ -0,0 +1,466 @@
<script setup lang="ts">
/**
* 家族成员详情(查看 / 修改):旧 WebVue views/master/FamilyPersonOperation.vue 的移动端等价物
*
* 对齐旧逻辑:
* - FindByKey(PersonId) 取 FamilyPersonData,主对象 FamilyPerson;
* - 头像上传 file-path=FamilyPersonAvatar(旧 FileUpload 的移动端等价:chooseImage 单图 +
* uploadFile 直传 DmsFile/UploadDmsFile,FilePath 走请求头、form 字段名=原始文件名);
* - 性别是 GenderPolicy 枚举,admin-form 无 select,用 #extra 行点开 option-picker 回写;
* - 只读门控:!IsCanAmend 且 Privacy!=='Y' 时只显「基本资料」卡、隐藏其余卡与修改/删除;
* - 保存按 CreatedBy 有无决定 Update / Insert。
* 旧页的新增入口在查询页即被注释,本页只承担 view/amend 两态(create 走快速建员弹层)。
*/
import type { AdminFormAction, AdminFormField, AdminFormSection } from "@/admin/admin-types";
import { onLoad } from "@dcloudio/uni-app";
import { useLocale } from "uview-pro";
import { computed, ref } from "vue";
import { uploadFile } from "@/common/http.interceptor";
import { useAuthGuard } from "@/composables";
import { usePolicyOptions } from "@/composables/usePolicyOptions";
import { FamilyPersonData } from "@/data/auto/familyPersonData";
import { smartBack } from "@/router/routes";
import familyPersonService from "@/service/master/familyPersonService";
const { t } = useLocale();
const { getPolicyOptions, getPolicyText } = usePolicyOptions();
const data = ref<FamilyPersonData>(new FamilyPersonData());
const person = computed(() => data.value.FamilyPerson);
const readonly = ref(true);
const routeId = ref("");
const genderPickerVisible = ref(false);
const uploading = ref(false);
const genderItems = computed(() => getPolicyOptions("GenderPolicy"));
const genderLabel = computed(() => getPolicyText("GenderPolicy", person.value.Gender));
/** 只读门控(旧页同款):无权修改且本人未公开详细资料时,只保留基本资料卡 */
const readonlyBasicOnly = computed(
() => routeId.value !== "" && !person.value.IsCanAmend && person.value.Privacy !== "Y",
);
const heroTitle = computed(
() => `${person.value.Surname ?? ""}${person.value.GenealogyName ?? ""}`,
);
const heroChips = computed(() => {
const chips: string[] = [];
if (person.value.PersonId) {
chips.push(`${t("FamilyPersonPage.personId")}: ${person.value.PersonId}`);
}
if (person.value.TreeId) {
chips.push(`${t("FamilyPersonPage.treeId")}: ${person.value.TreeId}`);
}
return chips;
});
const sections = computed<AdminFormSection[]>(() => {
const basic: AdminFormSection = {
title: t("FamilyPersonPage.basicTitle"),
fields: [
{ label: t("FamilyPersonPage.surname"), prop: "Surname", required: true },
{ label: t("FamilyPersonPage.genealogyName"), prop: "GenealogyName", required: true },
{ label: t("FamilyPersonPage.commonName"), prop: "CommonName" },
{ label: t("FamilyPersonPage.alias"), prop: "Alias" },
{ label: t("FamilyPersonPage.zi"), prop: "Zi" },
{ label: t("FamilyPersonPage.hao"), prop: "Hao" },
{ label: t("FamilyPersonPage.birthOrder"), prop: "BirthOrder" },
],
};
if (readonlyBasicOnly.value) {
return [basic];
}
return [
basic,
{
title: t("FamilyPersonPage.familyTitle"),
fields: [
{ label: t("FamilyPersonPage.maritalStatus"), prop: "MaritalStatus" },
{ label: t("FamilyPersonPage.fatherId"), prop: "FatherId" },
{ label: t("FamilyPersonPage.motherName"), prop: "MotherName" },
{ label: t("FamilyPersonPage.school"), prop: "School" },
{ label: t("FamilyPersonPage.major"), prop: "Major" },
{ label: t("FamilyPersonPage.education"), prop: "Education" },
{ label: t("FamilyPersonPage.graduationStatus"), prop: "GraduationStatus" },
{ label: t("FamilyPersonPage.duty"), prop: "Duty" },
],
},
{
title: t("FamilyPersonPage.workTitle"),
fields: [
{ label: t("FamilyPersonPage.title"), prop: "Title" },
{
label: t("FamilyPersonPage.privacy"),
prop: "Privacy",
type: "switch",
display: model => (model.Privacy === "Y" ? t("FamilyPersonPage.yes") : t("FamilyPersonPage.no")),
},
{ label: t("FamilyPersonPage.liveStatus"), prop: "LiveStatus" },
{ label: t("FamilyPersonPage.bindUserId"), prop: "BindUserId" },
{
label: t("FamilyPersonPage.bindUser"),
prop: "IsBindUser",
type: "switch",
display: model => (model.IsBindUser === "Y" ? t("FamilyPersonPage.yes") : t("FamilyPersonPage.no")),
},
{ label: t("FamilyPersonPage.birthText"), prop: "BirthText" },
{ label: t("FamilyPersonPage.birthDate"), prop: "BirthDate", type: "date", date: true },
{ label: t("FamilyPersonPage.deathText"), prop: "DeathText" },
{ label: t("FamilyPersonPage.deathDate"), prop: "DeathDate", type: "date", date: true },
],
},
{
title: t("FamilyPersonPage.introTitle"),
fields: [
{
label: t("FamilyPersonPage.introduction"),
prop: "Introduction",
type: "textarea",
maxlength: 2000,
},
],
},
];
});
const actions = computed<AdminFormAction[]>(() => {
const list: AdminFormAction[] = [];
if (readonlyBasicOnly.value) {
return list;
}
list.push({ key: "amend", label: t("AdminPage.amendBut"), when: "view" });
// 旧页口径:仅未挂族谱(TreeId 为空)的成员允许删除
if (person.value.TreeId === "") {
list.push({
key: "delete",
label: t("AdminPage.deleteBut"),
tone: "error",
confirm: true,
when: "view",
});
}
list.push({ key: "save", label: t("AdminPage.saveBut"), when: "edit" });
list.push({ key: "cancel", label: t("AdminPage.cancelBut"), when: "edit" });
return list;
});
async function load(): Promise<void> {
const result = await familyPersonService.FindByKey(routeId.value);
if (!result?.FamilyPerson?.PersonId) {
uni.showToast({ title: t("AdminPage.notExistTip"), icon: "none" });
return;
}
data.value = result;
}
function onFieldChange(prop: string, value: unknown): void {
if (prop === "Privacy" || prop === "IsBindUser") {
person.value[prop] = value ? "Y" : "N";
return;
}
(person.value as Record<string, any>)[prop] = value;
}
function onGenderConfirm(key: string): void {
person.value.Gender = key;
genderPickerVisible.value = false;
}
/** 头像上传:单图 -> DmsFile/UploadDmsFile(FilePath=FamilyPersonAvatar),成功后本地预览 */
async function onUploadAvatar(): Promise<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;
await uploadFile({
url: "/DmsFile/UploadDmsFile",
filePath,
name: fileName,
header: { FilePath: "FamilyPersonAvatar" },
});
person.value.AvatarFilename = fileName;
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 {
// H5 本地预览是 blob: URL,清掉引用防泄漏;其余端是本地临时路径,无需回收
if (person.value.AvatarUrl.startsWith("blob:") && typeof URL !== "undefined") {
URL.revokeObjectURL(person.value.AvatarUrl);
}
person.value.AvatarFilename = "";
person.value.AvatarUrl = "";
}
async function onSave(): Promise<void> {
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;
}
const isUpdate = String(person.value.CreatedBy ?? "").trim() !== "";
const result = await (isUpdate
? familyPersonService.Update(data.value)
: familyPersonService.Insert(data.value));
if (result?.FamilyPerson?.PersonId) {
data.value = result;
routeId.value = result.FamilyPerson.PersonId;
}
readonly.value = true;
uni.showToast({ title: t("AdminPage.savedTip"), icon: "none" });
}
async function onDelete(): Promise<void> {
await familyPersonService.Delete(routeId.value);
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
setTimeout(() => smartBack("mineTab"), 600);
}
function onAction(action: AdminFormAction): void {
switch (action.key) {
case "amend":
readonly.value = false;
break;
case "save":
onSave();
break;
case "delete":
onDelete();
break;
case "cancel":
if (!routeId.value) {
smartBack("mineTab");
return;
}
readonly.value = true;
load();
break;
default:
break;
}
}
function onBack(): void {
smartBack("mineTab");
}
onLoad((options) => {
routeId.value = String(options?.id ?? "");
const mode = String(options?.mode ?? "view");
readonly.value = mode !== "create" && mode !== "amend";
});
let booted = false;
useAuthGuard(() => {
if (booted) {
return;
}
booted = true;
if (routeId.value) {
load();
}
});
</script>
<template>
<admin-form :nav-title="t('FamilyPersonPage.detailTitle')" :show-nav-back="true" :hero-title="heroTitle"
:hero-chips="heroChips" :sections="sections" :model="person" :readonly="readonly" :actions="actions"
@back="onBack" @action="onAction" @field-change="onFieldChange">
<template #extra="{ readonly: ro }">
<!-- 只读门控提示:无修改权限且对方未公开详细资料 -->
<view v-if="readonlyBasicOnly" class="ro-tip">
<text>
{{ t("FamilyPersonPage.readonlyTip") }}
</text>
</view>
<template v-else>
<!-- 头像 + 性别(admin-form 无 select,枚举行在这里点开的 option-picker 里回写) -->
<view class="group">
<view class="group-head">
<text class="group-title">
{{ t("FamilyPersonPage.avatarTitle") }}
</text>
</view>
<view class="group-card avatar-card">
<view class="avatar-box" :class="{ 'avatar-box--tap': !ro }" @tap="!ro && onUploadAvatar()">
<image v-if="person.AvatarUrl" class="avatar-img" :src="person.AvatarUrl"
mode="aspectFill" />
<u-icon v-else name="account-fill" size="72" color="#c4c8cf" />
</view>
<view v-if="!ro" class="avatar-ops">
<view class="avatar-op" @tap="onUploadAvatar">
<u-icon name="camera" size="30" color="#059669" />
<text class="avatar-op-text">
{{ uploading ? t("FamilyPersonPage.uploadingTip") :
t("FamilyPersonPage.uploadAvatar") }}
</text>
</view>
<view v-if="person.AvatarUrl || person.AvatarFilename" class="avatar-op"
@tap="onClearAvatar">
<u-icon name="close-circle" size="30" color="#c4c8cf" />
<text class="avatar-op-text">
{{ t("FamilyPersonPage.clearAvatar") }}
</text>
</view>
</view>
</view>
</view>
<view class="group">
<view class="group-card">
<view class="enum-row" :class="{ 'enum-row--tap': !ro }"
@tap="!ro && (genderPickerVisible = true)">
<text class="enum-label">
{{ t("FamilyPersonPage.gender") }}
</text>
<view class="enum-value-box">
<text class="enum-value" :class="{ 'enum-value-empty': !genderLabel }">
{{ genderLabel || t("FamilyPersonPage.genderPlaceholder") }}
</text>
<u-icon v-if="!ro" name="arrow-right" size="24" color="#c4c8cf" />
</view>
</view>
</view>
</view>
</template>
<option-picker :visible="genderPickerVisible" :title="t('FamilyPersonPage.gender')" :items="genderItems"
:empty-text="t('FamilyPersonPage.genderEmpty')" @close="genderPickerVisible = false"
@confirm="onGenderConfirm" />
</template>
</admin-form>
</template>
<style lang="scss" scoped>
.ro-tip {
padding: 28rpx;
margin-bottom: 28rpx;
font-size: 24rpx;
color: $u-tips-color;
background-color: #ffffff;
border-radius: 20rpx;
}
.group {
margin-bottom: 28rpx;
}
.group-head {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 12rpx 12rpx;
}
.group-title {
font-size: 24rpx;
color: $u-tips-color;
}
.group-card {
padding: 0 28rpx;
overflow: hidden;
background-color: #ffffff;
border-radius: 20rpx;
}
.avatar-card {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 28rpx;
}
.avatar-box {
display: flex;
align-items: center;
justify-content: center;
width: 160rpx;
height: 160rpx;
overflow: hidden;
background-color: $u-bg-color;
border: 2rpx solid $u-border-color;
border-radius: 50%;
}
.avatar-box--tap {
border-style: dashed;
}
.avatar-img {
width: 100%;
height: 100%;
}
.avatar-ops {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.avatar-op {
display: flex;
flex-direction: row;
align-items: center;
}
.avatar-op-text {
margin-left: 8rpx;
font-size: 26rpx;
color: $u-content-color;
}
.enum-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
min-height: 92rpx;
}
.enum-label {
font-size: 28rpx;
color: $u-main-color;
}
.enum-value-box {
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
}
.enum-value {
font-size: 26rpx;
color: $u-content-color;
}
.enum-value-empty {
color: $u-tips-color;
}
</style>