Files
Docker/uniapp/youjiala/src/pages/security/family-person-operation.vue
T
2026-10-15 23:20:36 +08:00

467 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>