ow
This commit is contained in:
1 parent
dcc112c076
commit
b858386e86
15 files changed
+3634
-69
No files matched your search
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 家族成员查询(列表页):旧 WebVue views/master/FamilyPersonEnquiry.vue 的移动端等价物
|
||||
*
|
||||
* 保留:四条筛选(谱名/常用名/族谱编号 文本包含、性别 单选枚举)、每页 20 条、行点击进详情。
|
||||
* 旧页的新增/删除按钮本就被注释禁用,故这里不出新增入口、也不做批量删除,行为对齐。
|
||||
*
|
||||
* 兼作「族员选择器」:family-tree-operation 的「族员搜索」带 ?channel=person 跳入,
|
||||
* 单选回传后调用方按 PersonId 切换当前族员(useAdminPick / useAdminPickTarget 协议)。
|
||||
*/
|
||||
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 } from "@/composables";
|
||||
import { usePolicyOptions } from "@/composables/usePolicyOptions";
|
||||
import { FamilyPersonEntity } from "@/data/auto/familyPersonEntity";
|
||||
import { FamilyPersonSearchCriteria } from "@/data/auto/familyPersonSearchCriteria";
|
||||
import { SearchFilterOperatorPolicy } from "@/data/custom/searchFilterOperatorPolicy";
|
||||
import { SearchFilterTypePolicy } from "@/data/custom/searchFilterTypePolicy";
|
||||
import { pageUrl } from "@/router/routes";
|
||||
import familyPersonService from "@/service/master/familyPersonService";
|
||||
|
||||
const { t, currentLocale } = useLocale();
|
||||
const { getPolicyOptions, getPolicyText } = usePolicyOptions();
|
||||
const pickTarget = useAdminPickTarget();
|
||||
|
||||
/** 选择器模式:带 channel 进来时点行回传而非跳详情 */
|
||||
const isPick = ref(false);
|
||||
|
||||
const LABEL_KEYS: Record<string, string> = {
|
||||
[FamilyPersonEntity.DB_GENEALOGY_NAME]: "FamilyPersonPage.genealogyName",
|
||||
[FamilyPersonEntity.DB_COMMON_NAME]: "FamilyPersonPage.commonName",
|
||||
[FamilyPersonEntity.DB_GENDER]: "FamilyPersonPage.gender",
|
||||
[FamilyPersonEntity.DB_TREE_ID]: "FamilyPersonPage.treeId",
|
||||
};
|
||||
|
||||
const genderChoices = computed(() =>
|
||||
getPolicyOptions("GenderPolicy").map(o => ({ label: o.label, value: o.key })),
|
||||
);
|
||||
|
||||
const bindOptions = reactive([
|
||||
makeFilterOption(
|
||||
FamilyPersonEntity.DB_GENEALOGY_NAME,
|
||||
t(LABEL_KEYS[FamilyPersonEntity.DB_GENEALOGY_NAME]),
|
||||
SearchFilterTypePolicy.TEXT,
|
||||
SearchFilterOperatorPolicy.CONTAIN,
|
||||
),
|
||||
makeFilterOption(
|
||||
FamilyPersonEntity.DB_COMMON_NAME,
|
||||
t(LABEL_KEYS[FamilyPersonEntity.DB_COMMON_NAME]),
|
||||
SearchFilterTypePolicy.TEXT,
|
||||
SearchFilterOperatorPolicy.CONTAIN,
|
||||
),
|
||||
makeFilterOption(
|
||||
FamilyPersonEntity.DB_GENDER,
|
||||
t(LABEL_KEYS[FamilyPersonEntity.DB_GENDER]),
|
||||
SearchFilterTypePolicy.SINGLE_SELECTION,
|
||||
SearchFilterOperatorPolicy.SINGLE,
|
||||
genderChoices.value,
|
||||
),
|
||||
makeFilterOption(
|
||||
FamilyPersonEntity.DB_TREE_ID,
|
||||
t(LABEL_KEYS[FamilyPersonEntity.DB_TREE_ID]),
|
||||
SearchFilterTypePolicy.TEXT,
|
||||
SearchFilterOperatorPolicy.CONTAIN,
|
||||
),
|
||||
]);
|
||||
|
||||
async function fetchData(query: AdminQuery) {
|
||||
const criteria = new FamilyPersonSearchCriteria();
|
||||
criteria.GridColumns = [];
|
||||
fillCriteria(criteria, query.filters, query);
|
||||
const data = await familyPersonService.FindBySearchCriteria(criteria);
|
||||
return { rows: data?.Datas ?? [], total: data?.Total ?? 0 };
|
||||
}
|
||||
|
||||
const state = useAdminList({
|
||||
bindOptions,
|
||||
loader: fetchData,
|
||||
orderby: FamilyPersonEntity.DB_GENEALOGY_NAME,
|
||||
});
|
||||
|
||||
const { filterCount, filterVisible, draft, searchToken } = state;
|
||||
|
||||
const columns = computed<AdminColumn[]>(() => [
|
||||
{
|
||||
label: t("FamilyPersonPage.surname"),
|
||||
prop: FamilyPersonEntity.SURNAME,
|
||||
},
|
||||
{
|
||||
label: t("FamilyPersonPage.genealogyName"),
|
||||
prop: FamilyPersonEntity.GENEALOGY_NAME,
|
||||
},
|
||||
{
|
||||
label: t("FamilyPersonPage.gender"),
|
||||
prop: FamilyPersonEntity.GENDER,
|
||||
format: (row: AdminRow) => getPolicyText("GenderPolicy", row.Gender),
|
||||
},
|
||||
{
|
||||
label: t("FamilyPersonPage.commonName"),
|
||||
prop: FamilyPersonEntity.COMMON_NAME,
|
||||
},
|
||||
{
|
||||
label: t("FamilyPersonPage.createdDatetime"),
|
||||
prop: FamilyPersonEntity.CREATED_DATETIME,
|
||||
date: "datetime",
|
||||
},
|
||||
]);
|
||||
|
||||
const keyword = ref("");
|
||||
const listRef = ref<any>(null);
|
||||
|
||||
function onSearch(): void {
|
||||
const option = bindOptions.find(
|
||||
item => item.BindValue === FamilyPersonEntity.DB_GENEALOGY_NAME,
|
||||
);
|
||||
if (option) {
|
||||
option.Value = keyword.value;
|
||||
}
|
||||
state.reload();
|
||||
}
|
||||
|
||||
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];
|
||||
if (key) {
|
||||
option.DisplayName = t(key);
|
||||
}
|
||||
if (option.BindValue === FamilyPersonEntity.DB_GENDER) {
|
||||
option.SelectOptions = genderChoices.value.map(toSelectOption);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onLoad((options) => {
|
||||
pickTarget.readOptions(options);
|
||||
isPick.value = String(options?.channel ?? "").trim().length > 0;
|
||||
});
|
||||
|
||||
useAuthGuard(() => {
|
||||
// 每次显示都重拉第一页:详情修改后返回能看到最新数据(navigateBack 不重新挂页面)
|
||||
nextTick(() => listRef.value?.refresh());
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page">
|
||||
<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>
|
||||
</admin-list>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: $u-bg-color;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,236 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 族谱查询(列表页):旧 WebVue views/master/FamilyTreeEnquiry.vue 的移动端等价物
|
||||
*
|
||||
* 保留:三条筛选(族谱名称/简介 文本包含、公开? 单选)、每页 20 条、
|
||||
* 行点击进详情、多选批量删除(旧页面同样是循环 Delete(TreeId))。
|
||||
* 丢掉:多列网格、列配置、打印。
|
||||
*
|
||||
* 挂在「安全管理」枢纽下(族谱属 master 域但按产品要求与权限体系同入口),
|
||||
* 未登记 PAGE_PERMISSIONS:路由级 fail-open,编辑能力由数据级 IsManager/IsCanAmend 把关。
|
||||
*/
|
||||
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 } from "@/admin/admin-filter";
|
||||
import { useAdminList, useAdminPickTarget, useAuthGuard } from "@/composables";
|
||||
import { FamilyTreeSearchCriteria } from "@/data/auto/familyTreeSearchCriteria";
|
||||
import { FamilyTreeViewEntity } from "@/data/auto/familyTreeViewEntity";
|
||||
import { SearchFilterOperatorPolicy } from "@/data/custom/searchFilterOperatorPolicy";
|
||||
import { SearchFilterTypePolicy } from "@/data/custom/searchFilterTypePolicy";
|
||||
import { usePagePermission } from "@/permission/permission-context";
|
||||
import { pageUrl } from "@/router/routes";
|
||||
import familyTreeService from "@/service/master/familyTreeService";
|
||||
|
||||
const { t, currentLocale } = useLocale();
|
||||
const perm = usePagePermission("familyTreeEnquiry");
|
||||
const pickTarget = useAdminPickTarget();
|
||||
|
||||
/** 选择器模式:带 channel 进来时点行回传而非跳详情 */
|
||||
const isPick = ref(false);
|
||||
|
||||
const LABEL_KEYS: Record<string, string> = {
|
||||
[FamilyTreeViewEntity.DB_TREE_NAME]: "FamilyTreePage.treeName",
|
||||
[FamilyTreeViewEntity.DB_BRIEF]: "FamilyTreePage.brief",
|
||||
[FamilyTreeViewEntity.DB_PUBLIC_INDC]: "FamilyTreePage.publicIndc",
|
||||
};
|
||||
|
||||
function yesNo() {
|
||||
return [
|
||||
{ label: t("FamilyTreePage.yes"), value: "Y" },
|
||||
{ label: t("FamilyTreePage.no"), value: "N" },
|
||||
];
|
||||
}
|
||||
|
||||
const bindOptions = reactive([
|
||||
makeFilterOption(
|
||||
FamilyTreeViewEntity.DB_TREE_NAME,
|
||||
t(LABEL_KEYS[FamilyTreeViewEntity.DB_TREE_NAME]),
|
||||
SearchFilterTypePolicy.TEXT,
|
||||
SearchFilterOperatorPolicy.CONTAIN,
|
||||
),
|
||||
makeFilterOption(
|
||||
FamilyTreeViewEntity.DB_BRIEF,
|
||||
t(LABEL_KEYS[FamilyTreeViewEntity.DB_BRIEF]),
|
||||
SearchFilterTypePolicy.TEXT,
|
||||
SearchFilterOperatorPolicy.CN_CONTAIN,
|
||||
),
|
||||
makeFilterOption(
|
||||
FamilyTreeViewEntity.DB_PUBLIC_INDC,
|
||||
t(LABEL_KEYS[FamilyTreeViewEntity.DB_PUBLIC_INDC]),
|
||||
SearchFilterTypePolicy.SINGLE_SELECTION,
|
||||
SearchFilterOperatorPolicy.SINGLE,
|
||||
yesNo(),
|
||||
),
|
||||
]);
|
||||
|
||||
async function fetchData(query: AdminQuery) {
|
||||
const criteria = new FamilyTreeSearchCriteria();
|
||||
criteria.GridColumns = [];
|
||||
fillCriteria(criteria, query.filters, query);
|
||||
const data = await familyTreeService.FindBySearchCriteria(criteria);
|
||||
return { rows: data?.Datas ?? [], total: data?.Total ?? 0 };
|
||||
}
|
||||
|
||||
const state = useAdminList({
|
||||
bindOptions,
|
||||
loader: fetchData,
|
||||
orderby: FamilyTreeViewEntity.DB_TREE_NAME,
|
||||
});
|
||||
|
||||
const { filterCount, filterVisible, draft, searchToken } = state;
|
||||
|
||||
const columns = computed<AdminColumn[]>(() => [
|
||||
{
|
||||
label: t("FamilyTreePage.treeName"),
|
||||
prop: FamilyTreeViewEntity.TREE_NAME,
|
||||
},
|
||||
{
|
||||
label: t("FamilyTreePage.brief"),
|
||||
prop: FamilyTreeViewEntity.BRIEF,
|
||||
},
|
||||
{
|
||||
label: t("FamilyTreePage.publicIndc"),
|
||||
prop: FamilyTreeViewEntity.PUBLIC_INDC,
|
||||
format: (row: AdminRow) => {
|
||||
if (row.PublicIndc === "Y") {
|
||||
return t("FamilyTreePage.yes");
|
||||
}
|
||||
return row.PublicIndc === "N" ? t("FamilyTreePage.no") : "";
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("FamilyTreePage.ancestorSurname"),
|
||||
prop: FamilyTreeViewEntity.SURNAME,
|
||||
},
|
||||
{
|
||||
label: t("FamilyTreePage.ancestorGenealogyName"),
|
||||
prop: FamilyTreeViewEntity.GENEALOGY_NAME,
|
||||
},
|
||||
]);
|
||||
|
||||
const keyword = ref("");
|
||||
const listRef = ref<any>(null);
|
||||
|
||||
function onSearch(): void {
|
||||
const option = bindOptions.find(
|
||||
item => item.BindValue === FamilyTreeViewEntity.DB_TREE_NAME,
|
||||
);
|
||||
if (option) {
|
||||
option.Value = keyword.value;
|
||||
}
|
||||
state.reload();
|
||||
}
|
||||
|
||||
function onRowTap(row: AdminRow): void {
|
||||
if (isPick.value) {
|
||||
if (pickTarget.multiple.value) {
|
||||
return;
|
||||
}
|
||||
pickTarget.deliverOne(row);
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `${pageUrl("familyTreeOperation")}?id=${row.TreeId}&mode=view`,
|
||||
});
|
||||
}
|
||||
|
||||
function onCreate(): void {
|
||||
uni.navigateTo({ url: `${pageUrl("familyTreeOperation")}?mode=create` });
|
||||
}
|
||||
|
||||
function onConfirm(rows: AdminRow[]): void {
|
||||
if (!rows.length) {
|
||||
uni.showToast({ title: t("AdminPage.pickNoneTip"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
pickTarget.deliver(rows);
|
||||
}
|
||||
|
||||
async function onDelete(rows: AdminRow[]): Promise<void> {
|
||||
if (!rows.length) {
|
||||
uni.showToast({ title: t("AdminPage.pickNoneTip"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: t("AdminPage.confirmDeleteTitle"),
|
||||
success: async (res: any) => {
|
||||
if (!res.confirm) {
|
||||
return;
|
||||
}
|
||||
for (const row of rows) {
|
||||
await familyTreeService.Delete(String(row.TreeId));
|
||||
}
|
||||
listRef.value?.clearSelection();
|
||||
state.reload();
|
||||
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
watch(currentLocale, () => {
|
||||
for (const option of bindOptions) {
|
||||
const key = LABEL_KEYS[option.BindValue];
|
||||
if (key) {
|
||||
option.DisplayName = t(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onLoad((options) => {
|
||||
pickTarget.readOptions(options);
|
||||
isPick.value = String(options?.channel ?? "").trim().length > 0;
|
||||
});
|
||||
|
||||
useAuthGuard(() => {
|
||||
// 每次显示都重拉第一页:增/改/删后从详情返回能看到最新数据(navigateBack 不重新挂页面)
|
||||
nextTick(() => listRef.value?.refresh());
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page">
|
||||
<admin-list ref="listRef" v-model:keyword="keyword" :nav-title="t('FamilyTreePage.title')"
|
||||
:show-nav-back="!isPick" :columns="columns" :loader="state.query" :row-key="FamilyTreeViewEntity.TREE_ID"
|
||||
title-key="TreeName" :selectable="isPick ? pickTarget.multiple.value : Boolean(perm.canDelete.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 && perm.canCreate.value" #toolbar-actions>
|
||||
<view class="create-entry" @click="onCreate">
|
||||
<u-icon name="plus" size="40" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template #actions="{ rows }">
|
||||
<u-button v-if="isPick" type="primary" shape="circle" size="mini" :text="t('AdminPage.pickDoneBut')"
|
||||
@click="onConfirm(rows)" />
|
||||
<u-button v-else-if="perm.canDelete.value" type="error" shape="circle" size="mini"
|
||||
:text="t('AdminPage.deleteBut')" @click="onDelete(rows)" />
|
||||
</template>
|
||||
</admin-list>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: $u-bg-color;
|
||||
}
|
||||
|
||||
.create-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
margin-left: 12rpx;
|
||||
color: $u-type-primary;
|
||||
background-color: $u-bg-color;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -14,8 +14,8 @@ import { pageUrl } from "@/router/routes";
|
||||
|
||||
/** 一条功能入口:i18n 标题/副标题键 + 目标列表页名(null=旧系统尚未迁移) */
|
||||
interface SecurityEntry {
|
||||
key: string;
|
||||
target: PageName | null;
|
||||
key: string;
|
||||
target: PageName | null;
|
||||
}
|
||||
|
||||
const { t } = useLocale();
|
||||
@@ -23,109 +23,109 @@ const { t } = useLocale();
|
||||
useAuthGuard();
|
||||
|
||||
const entries: SecurityEntry[] = [
|
||||
{ key: "user", target: "userEnquiry" },
|
||||
{ key: "actor", target: "actorEnquiry" },
|
||||
{ key: "group", target: "groupEnquiry" },
|
||||
{ key: "function", target: "functionEnquiry" },
|
||||
{ key: "permission", target: "permissionEnquiry" },
|
||||
{ key: "generator", target: "generatorEnquiry" },
|
||||
{ key: "master", target: "masterEnquiry" },
|
||||
{ key: "helpFunction", target: "helpFunctionEnquiry" },
|
||||
{ key: "helpFeature", target: "helpFeatureEnquiry" },
|
||||
{ key: "user", target: "userEnquiry" },
|
||||
{ key: "actor", target: "actorEnquiry" },
|
||||
{ key: "group", target: "groupEnquiry" },
|
||||
{ key: "function", target: "functionEnquiry" },
|
||||
{ key: "permission", target: "permissionEnquiry" },
|
||||
{ key: "generator", target: "generatorEnquiry" },
|
||||
{ key: "master", target: "masterEnquiry" },
|
||||
{ key: "helpFunction", target: "helpFunctionEnquiry" },
|
||||
{ key: "helpFeature", target: "helpFeatureEnquiry" },
|
||||
];
|
||||
|
||||
function onPick(entry: SecurityEntry): void {
|
||||
if (!entry.target) {
|
||||
uni.showToast({ title: t("SecurityPage.pendingTip"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: pageUrl(entry.target) });
|
||||
if (!entry.target) {
|
||||
uni.showToast({ title: t("SecurityPage.pendingTip"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: pageUrl(entry.target) });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page :nav-title="t('SecurityPage.title')">
|
||||
<view class="security-page">
|
||||
<text class="security-page__hint">
|
||||
{{ t("SecurityPage.pageHint") }}
|
||||
<app-page :nav-title="t('SecurityPage.title')">
|
||||
<view class="security-page">
|
||||
<text class="security-page__hint">
|
||||
{{ t("SecurityPage.pageHint") }}
|
||||
</text>
|
||||
<view class="security-list">
|
||||
<view v-for="entry in entries" :key="entry.key" class="security-cell" @tap="onPick(entry)">
|
||||
<view class="security-cell__body">
|
||||
<text class="security-cell__title">
|
||||
{{ t(`SecurityPage.${entry.key}`) }}
|
||||
</text>
|
||||
<view class="security-list">
|
||||
<view v-for="entry in entries" :key="entry.key" class="security-cell" @tap="onPick(entry)">
|
||||
<view class="security-cell__body">
|
||||
<text class="security-cell__title">
|
||||
{{ t(`SecurityPage.${entry.key}`) }}
|
||||
</text>
|
||||
<text class="security-cell__desc">
|
||||
{{ t(`SecurityPage.${entry.key}Desc`) }}
|
||||
</text>
|
||||
</view>
|
||||
<text v-if="!entry.target" class="security-cell__tag">
|
||||
{{ t("SecurityPage.pendingTag") }}
|
||||
</text>
|
||||
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="security-cell__desc">
|
||||
{{ t(`SecurityPage.${entry.key}Desc`) }}
|
||||
</text>
|
||||
</view>
|
||||
<text v-if="!entry.target" class="security-cell__tag">
|
||||
{{ t("SecurityPage.pendingTag") }}
|
||||
</text>
|
||||
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
|
||||
</view>
|
||||
</app-page>
|
||||
</view>
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.security-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.security-page__hint {
|
||||
padding: 0 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #9aa0a6;
|
||||
padding: 0 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
|
||||
/* 白底圆角卡片承载列表,行间用细分隔线,与「我的」页卡片语言一致 */
|
||||
.security-list {
|
||||
overflow: hidden;
|
||||
border-radius: 24rpx;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
border-radius: 24rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.security-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
border-bottom: 2rpx solid #f2f3f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
border-bottom: 2rpx solid #f2f3f5;
|
||||
}
|
||||
|
||||
.security-cell:last-child {
|
||||
border-bottom: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.security-cell__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.security-cell__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.security-cell__desc {
|
||||
font-size: 24rpx;
|
||||
color: #9aa0a6;
|
||||
font-size: 24rpx;
|
||||
color: #9aa0a6;
|
||||
}
|
||||
|
||||
/* 未迁移条目的灰色小标签 */
|
||||
.security-cell__tag {
|
||||
margin-right: 12rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
font-size: 20rpx;
|
||||
color: #9aa0a6;
|
||||
background-color: #f2f3f5;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 12rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
font-size: 20rpx;
|
||||
color: #9aa0a6;
|
||||
background-color: #f2f3f5;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -23,6 +23,16 @@ function goTheme(): void {
|
||||
function goSecurity(): void {
|
||||
uni.navigateTo({ url: pageUrl("securityPage") });
|
||||
}
|
||||
|
||||
/** 进入「族谱管理」列表页(/family-tree-enquiry) */
|
||||
function goFamilyTree(): void {
|
||||
uni.navigateTo({ url: pageUrl("familyTreeEnquiry") });
|
||||
}
|
||||
|
||||
/** 进入「家族成员」列表页(/family-person-enquiry) */
|
||||
function goFamilyPerson(): void {
|
||||
uni.navigateTo({ url: pageUrl("familyPersonEnquiry") });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -44,6 +54,22 @@ function goSecurity(): void {
|
||||
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
|
||||
</view>
|
||||
|
||||
<!-- 族谱管理:族谱列表与谱系图维护(/family-tree-enquiry) -->
|
||||
<view class="mine-cell" @tap="goFamilyTree">
|
||||
<text class="mine-cell__title">
|
||||
{{ t("MinePage.familyTree") }}
|
||||
</text>
|
||||
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
|
||||
</view>
|
||||
|
||||
<!-- 家族成员:族人档案检索(/family-person-enquiry) -->
|
||||
<view class="mine-cell" @tap="goFamilyPerson">
|
||||
<text class="mine-cell__title">
|
||||
{{ t("MinePage.familyPerson") }}
|
||||
</text>
|
||||
<u-icon name="arrow-right" size="32" color="#9aa0a6" />
|
||||
</view>
|
||||
|
||||
<!-- 关于:应用名 + 当前版本 + 运行平台 -->
|
||||
<view class="mine-card">
|
||||
<text class="mine-card__title">
|
||||
|
||||
Reference in new issue
Block a user