This commit is contained in:
owenchen committed 2026-10-22 13:55:27 +08:00
1 parent c6eb00b406
commit 1c91f62ff6
58 files changed
+3888 -69

No files matched your search

@@ -20,6 +20,7 @@ import { brandGlyph } from "@/common/brand";
import { buildChunkFileName, uploadFile } from "@/common/http.interceptor";
import { userTypeGlyph, userTypePillBg } from "@/common/userTypeIcons";
import { useIdCardVerify, useThemeGradient, useThemeTokens } from "@/composables";
import { useArticles } from "@/composables/useArticles";
import { formatFileSize, formatFileTime, useFamilyTreeExchange } from "@/composables/useFamilyTreeExchange";
import { usePersonPickConfig } from "@/composables/usePickerConfigs";
import { usePolicyOptions } from "@/composables/usePolicyOptions";
@@ -37,8 +38,9 @@ import familyPersonService from "@/service/master/familyPersonService";
import familyTreeService from "@/service/master/familyTreeService";
import dmsFileService from "@/service/master/dmsFileService";
import { useLoginProfileStore } from "@/stores";
import { pageUrl } from "@/router/routes";
import { chooseFiles } from "@/utils/chooseFiles";
import { absoluteFileUrl } from "@/utils/fileUrl";
import { absoluteFileUrl, backupFileUrl } from "@/utils/fileUrl";
/** ready:组件挂载完成通知(时序口径见文件底部 onMounted 注释) */
const emit = defineEmits<{ (e: "ready"): void }>();
@@ -239,6 +241,60 @@ async function onRetry(): Promise<void> {
await loadTrees();
}
// ===== 有家文化传承(文章)卡片:与族谱列表并列,数据源见 composables/useArticles =====
const articlesApi = useArticles();
const articleKeyword = ref("");
const showArticleSearch = ref(false);
const articleCollapsed = ref(false);
/** 首页「+」新建弹层可见(编辑层不另开页,直接底部弹出,同相册/群编辑) */
const articleEditorVisible = ref(false);
/** 首页卡只展示最新 5 条(列表已按置顶、创建时间排序) */
const homeArticles = computed(() => articlesApi.list.value.slice(0, 5));
/** 无封面时的书卷字形:内联 SVG(同 treeGlyph 套路),白描卷轴 */
const articleGlyph = `data:image/svg+xml,${encodeURIComponent([
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='none' stroke='#ffffff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'>",
"<path d='M10 12c0-2 1.6-3.6 3.6-3.6H30v30H13.6C11.6 38.4 10 36.8 10 34.8Z'/>",
"<path d='M30 8.4h6.4c2 0 3.6 1.6 3.6 3.6v22.8c0 2-1.6 3.6-3.6 3.6H30'/>",
"<path d='M15 16h10M15 22h10'/>",
"</svg>",
].join(""))}`;
async function loadArticles(): Promise<void> {
await articlesApi.loadList();
}
function onToggleArticleSearch(): void {
showArticleSearch.value = !showArticleSearch.value;
}
async function onArticleSearch(): Promise<void> {
await articlesApi.loadList(articleKeyword.value);
}
function onCreateArticle(): void {
// 首页「+」直接弹底部编辑层(不另开页):新建空白整包,保存走 useArticles.save
articlesApi.startCreate();
articleEditorVisible.value = true;
}
function closeArticleEditor(): void {
articleEditorVisible.value = false;
void articlesApi.cancelEdit();
}
async function onArticleSaved(): Promise<void> {
articleEditorVisible.value = false;
await loadArticles();
}
function onOpenArticle(name: string): void {
if (!name) {
return;
}
uni.navigateTo({ url: `${pageUrl("articlePage")}?name=${encodeURIComponent(name)}` });
}
// ===== 详情态数据(原 family-tree-operation 全量照搬) =====
const data = ref<FamilyTreeData>(new FamilyTreeData());
const tree = computed(() => data.value.FamilyTree);
@@ -1520,6 +1576,7 @@ defineExpose({
reloadIfList(): void {
if (!inDetail.value) {
loadTrees();
void loadArticles();
void fetchStorage();
}
},
@@ -1528,7 +1585,8 @@ defineExpose({
<template>
<!-- 列表态显示 tabbar,进详情(或弹层展开)收起;未认证底部弹层也在列表态弹出,须一并隐 tabbar,否则盖住底部按钮 -->
<app-page :hide-nav="true" :show-tabbar="!inDetail && !idPopup">
<!-- 文章底部编辑层同理:不隐 tabbar 会盖住弹层底部的「取消/保存」(tabbar 与弹层不在同一层叠上下文,z-index 压不过) -->
<app-page :hide-nav="true" :show-tabbar="!inDetail && !idPopup && !articleEditorVisible">
<view class="tree-page" :style="[{ background: themeBackground }, themeVars]">
<!-- ===== 列表态:软底分区面板(标题 + 搜索/新建/伸缩圆钮组 + 白底族谱卡) ===== -->
<!-- 隐藏原生滚动条:否则 Chrome 滚动条占右侧 ~17px,面板左右留白不对称 -->
@@ -1569,7 +1627,7 @@ defineExpose({
<text class="home-card__stitle">{{ t("storage.title") }}</text>
</view>
<text class="home-card__spct" :class="{ 'home-card__spct--over': storageOver }">{{ storagePercentText
}}</text>
}}</text>
</view>
<view class="home-card__bar">
<view class="home-card__bar-fill" :class="{ 'home-card__bar-fill--over': storageOver }"
@@ -1634,6 +1692,69 @@ defineExpose({
</template>
</view>
</view>
<!-- ===== 有家文化传承:与族谱面板并列的文章卡 ===== -->
<view class="tree-panel article-panel">
<view class="tree-panel__head">
<text class="tree-panel__title">{{ t("article.title") }}</text>
<view class="tree-panel__acts">
<view class="tree-panel__act" @tap="onToggleArticleSearch">
<u-icon name="search" size="34" :color="primary" />
</view>
<view class="tree-panel__act tree-panel__act--add" @tap="onCreateArticle">
<u-icon name="plus" size="30" color="#ffffff" />
</view>
<view class="tree-panel__act" @tap="articleCollapsed = !articleCollapsed">
<u-icon :name="articleCollapsed ? 'arrow-up' : 'arrow-down'" size="34" :color="primary" />
</view>
</view>
</view>
<view v-if="showArticleSearch" class="tree-panel__search">
<search-input v-model="articleKeyword" :placeholder="t('article.searchPlaceholder')"
@search="onArticleSearch" />
</view>
<view v-if="!articleCollapsed" class="tree-panel__list">
<view v-if="articlesApi.loading.value" class="tree-page__state">
<text class="tree-page__state-text">{{ t("FilePage.loading") }}</text>
</view>
<view v-else-if="articlesApi.errorText.value" class="tree-page__state">
<text class="tree-page__state-text tree-page__state-text--error">{{ articlesApi.errorText.value }}</text>
<text class="tree-page__retry" @click="loadArticles">{{ t("FilePage.retryBut") }}</text>
</view>
<template v-else>
<view v-if="!homeArticles.length" class="tree-page__empty">
<text class="tree-page__state-text">{{ t("article.empty") }}</text>
</view>
<view v-for="row in homeArticles" :key="row.ArticleName" class="article-card"
@tap="onOpenArticle(row.ArticleName)">
<view class="article-card__cover">
<image v-if="row.CoverUrl || row.ArticleCoverFilename" class="article-card__cover-img"
:src="absoluteFileUrl(row.CoverUrl) || backupFileUrl('ArticleCover', row.ArticleCoverFilename)"
mode="aspectFill" />
<image v-else class="article-card__glyph" :src="articleGlyph" mode="aspectFit" />
</view>
<view class="article-card__main">
<view class="article-card__titlerow">
<text class="article-card__name">{{ row.ArticleTitle || t("article.unnamed") }}</text>
<text v-if="row.TopIndc === 'Y'" class="article-card__chip article-card__chip--top">{{
t("article.tagTop") }}</text>
<text v-if="row.ApprovedIndc !== 'Y'" class="article-card__chip article-card__chip--pending">{{
t("article.tagPending") }}</text>
</view>
<text class="article-card__desc">{{ articlesApi.plainText(row.Brief, 100) || t("article.noBrief")
}}</text>
<view class="article-card__meta">
<image v-if="row.AuthorAvatarUrl" class="article-card__avatar"
:src="absoluteFileUrl(row.AuthorAvatarUrl)" mode="aspectFill" />
<view v-else class="article-card__avatar article-card__avatar--empty" />
<text class="article-card__author">{{ row.AuthorLocalName || t("article.unknownAuthor") }}</text>
<text class="article-card__date">{{ articlesApi.formatDate(row.CreatedDatetime) }}</text>
</view>
</view>
<u-icon name="arrow-right" size="30" color="#c0c4cc" />
</view>
</template>
</view>
</view>
</scroll-view>
<!-- ===== 详情态:族谱管理 ===== -->
@@ -1665,7 +1786,7 @@ defineExpose({
<u-switch v-if="data.IsManager" :model-value="tree.PublicIndc === 'Y'" size="40"
:active-color="primary" @change="onPublicToggle" />
<text v-if="data.IsManager" class="ft-badge ft-badge--admin">{{ t("FamilyTreePage.managerTag")
}}</text>
}}</text>
</view>
</view>
</view>
@@ -1772,7 +1893,7 @@ defineExpose({
<!-- 当前族员:从概览移入,夫妻/子女列表都是相对此人 -->
<view v-if="loaded && currentPerson" class="ft-card">
<view class="ft-card__head"><text class="ft-card__title">{{ t("FamilyTreePage.currentPerson")
}}</text></view>
}}</text></view>
<view class="person-hero">
<view class="person-hero__avatar"
:style="avatarFallbackStyle(!!currentPerson?.AvatarUrl, currentPerson?.Gender)">
@@ -1787,7 +1908,7 @@ defineExpose({
</view>
<view class="person-hero__chips">
<text v-if="currentPerson?.PersonId" class="person-hero__chip">{{ personChipText(currentPerson)
}}</text>
}}</text>
</view>
</view>
</view>
@@ -1830,12 +1951,12 @@ defineExpose({
<!-- 新增入口与行尾操作同一把闸:当前锚点不可编(未绑定/出支系)时隐藏 -->
<view class="ft-card__head">
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.spouseTitle")
}}</text><text class="ft-card__count">{{ spouseMaps.length }}</text></view>
}}</text><text class="ft-card__count">{{ spouseMaps.length }}</text></view>
<view v-if="data.IsCanAmend" class="ft-card__add" @tap="openQuickCreate('spouse')"><u-icon
name="plus" size="26" :color="primary" /><text>{{ t("FamilyTreePage.addSpouse") }}</text></view>
</view>
<view v-if="!visibleSpouses.length" class="ft-empty"><text>{{ t("FamilyTreePage.spouseEmpty")
}}</text></view>
}}</text></view>
<view v-for="record in visibleSpouses" :key="record.SpouseId" class="ft-mrow">
<view class="ft-av"
:style="avatarFallbackStyle(!!personAvatar(record.SpouseId), personGender(record.SpouseId, record.Gender))">
@@ -1871,13 +1992,13 @@ defineExpose({
<view class="ft-card">
<view class="ft-card__head">
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.childTitle")
}}</text><text class="ft-card__count">{{ childMaps.length }}</text></view>
}}</text><text class="ft-card__count">{{ childMaps.length }}</text></view>
<view v-if="data.IsCanAmend" class="ft-card__add" @tap="openQuickCreate('child')"><u-icon
name="plus" size="26" :color="primary" /><text>{{ t("FamilyTreePage.addChildBtn") }}</text>
</view>
</view>
<view v-if="!visibleChildren.length" class="ft-empty"><text>{{ t("FamilyTreePage.childEmpty")
}}</text></view>
}}</text></view>
<view v-for="record in visibleChildren" :key="record.ChildrenId" class="ft-mrow">
<view class="ft-av"
:style="avatarFallbackStyle(!!personAvatar(record.ChildrenId), personGender(record.ChildrenId, record.Gender))">
@@ -1917,7 +2038,7 @@ defineExpose({
<view v-if="data.IsManager && pendingPermissions.length" class="ft-card">
<view class="ft-card__head">
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.permPending")
}}</text><text class="ft-card__count">{{ pendingPermissions.length }}</text></view>
}}</text><text class="ft-card__count">{{ pendingPermissions.length }}</text></view>
</view>
<view v-for="record in pendingPermissions" :key="record.UserName" class="ft-perm-row">
<view class="ft-perm-row__main"><text class="ft-perm-row__name">{{ record.UserLocalName ||
@@ -1941,10 +2062,10 @@ defineExpose({
<view v-if="data.IsManager" class="ft-card">
<view class="ft-card__head">
<view class="ft-card__title-group"><text class="ft-card__title">{{ t("FamilyTreePage.permManager")
}}</text><text class="ft-card__count">{{ managerPermissions.length }}</text></view>
}}</text><text class="ft-card__count">{{ managerPermissions.length }}</text></view>
</view>
<view v-if="!managerPermissions.length" class="ft-empty"><text>{{ t("FamilyTreePage.permEmpty")
}}</text></view>
}}</text></view>
<view v-for="record in managerPermissions" :key="record.UserName" class="ft-perm-row">
<view class="ft-perm-row__main"><text class="ft-perm-row__name">{{ record.UserLocalName ||
record.UserName
@@ -2179,7 +2300,7 @@ defineExpose({
<view class="person-hero__line">
<text class="rel-name">{{ record.SpouseSurname }}{{ record.SpouseGenealogyName }}</text>
<text class="person-hero__gender">{{ genderText(personGender(record.SpouseId, record.Gender))
}}</text>
}}</text>
</view>
<view class="person-hero__chips">
<text class="person-hero__chip">
@@ -2216,7 +2337,7 @@ defineExpose({
<text class="sub-label">{{ t("FamilyTreePage.marryDate") }}</text>
<view class="sub-picker">
<text class="sub-picker-text">{{ datePart(record.MarryDate) || t("AdminPage.notSetTip")
}}</text>
}}</text>
<u-icon name="arrow-right" size="24" color="#c4c8cf" />
</view>
</view>
@@ -2232,7 +2353,7 @@ defineExpose({
<view class="sub-picker">
<text class="sub-picker-text">{{
datePart(record.DivorceDate) || t("AdminPage.notSetTip")
}}</text>
}}</text>
<u-icon name="arrow-right" size="24" color="#c4c8cf" />
</view>
</view>
@@ -2318,7 +2439,7 @@ defineExpose({
<view class="person-hero__line">
<text class="rel-name">{{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }}</text>
<text class="person-hero__gender">{{ genderText(personGender(record.ChildrenId, record.Gender))
}}</text>
}}</text>
</view>
<view class="person-hero__chips">
<text class="person-hero__chip">
@@ -2429,7 +2550,7 @@ defineExpose({
<text class="rel-name">{{ record.UserLocalName || record.UserName }}</text>
<text class="rel-sub">{{
record.ManagerIndc === "Y" ? t("FamilyTreePage.managerTag") : t("FamilyTreePage.memberTag")
}}</text>
}}</text>
</view>
<view class="perm-ops">
<view class="perm-op perm-op--danger" @tap="rejectPermission(record)">
@@ -2606,7 +2727,7 @@ defineExpose({
<!-- 服务器文件列表限高滚动:文件多了不挤掉密码框/警告/按钮 -->
<scroll-view v-if="!localFileName" scroll-y class="ft-exchange__list">
<view v-if="importFilesLoading" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeFileLoading")
}}</text></view>
}}</text></view>
<view v-else-if="!importFiles.length" class="ft-empty"><text>{{ t(imScope === "SUB" ?
"FamilyTreePage.exchangeFileEmptySub" : "FamilyTreePage.exchangeFileEmpty") }}</text></view>
<view v-for="file in importFiles" :key="file.FileName" class="ft-exchange__row"
@@ -2627,7 +2748,7 @@ defineExpose({
<view v-if="backupLoading" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeFileLoading") }}</text>
</view>
<view v-else-if="!backupFiles.length" class="ft-empty"><text>{{ t("FamilyTreePage.exchangeBackupEmpty")
}}</text></view>
}}</text></view>
<view v-for="bk in backupFiles" :key="bk.FileName" class="ft-exchange__row"
:class="{ 'ft-exchange__row--on': pickedBackup === bk.FileName }" @tap="pickedBackup = bk.FileName">
<view class="ft-exchange__row-main">
@@ -2693,11 +2814,15 @@ defineExpose({
<view class="ft-sheet__foot">
<view class="ft-sheet__btn" @tap="cancelTreeEdit"><text>{{ t("AdminPage.cancelBut") }}</text></view>
<view class="ft-sheet__btn ft-sheet__btn--primary" @tap="saveTreeEdit"><text>{{ t("AdminPage.saveBut")
}}</text>
}}</text>
</view>
</view>
</view>
</u-popup>
<!-- 有家文化传承:首页「+」新建底部编辑层(不另开页) -->
<article-editor :visible="articleEditorVisible" :api="articlesApi" @close="closeArticleEditor"
@saved="onArticleSaved" />
</view>
</app-page>
</template>
@@ -3264,6 +3389,127 @@ defineExpose({
color: #bfc3c9;
}
/* ===== 有家文化传承文章卡(复用 tree-panel 面板几何,卡体换横向图文) ===== */
.article-card {
display: flex;
align-items: center;
gap: 20rpx;
margin: 0 0 20rpx;
padding: 24rpx;
border-radius: 24rpx;
background-color: #ffffff;
box-shadow: 0 6rpx 18rpx rgba(31, 35, 41, 0.05);
}
.article-card:last-child {
margin-bottom: 0;
}
.article-card__cover {
display: flex;
flex: none;
align-items: center;
justify-content: center;
width: 128rpx;
height: 128rpx;
overflow: hidden;
border-radius: 20rpx;
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
}
.article-card__cover-img {
width: 100%;
height: 100%;
}
.article-card__glyph {
width: 60rpx;
height: 60rpx;
}
.article-card__main {
display: flex;
flex: 1;
min-width: 0;
flex-direction: column;
gap: 8rpx;
}
.article-card__titlerow {
display: flex;
align-items: center;
gap: 10rpx;
}
.article-card__name {
flex: 1;
min-width: 0;
font-size: 32rpx;
font-weight: 600;
color: #1f2329;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.article-card__chip {
flex: none;
padding: 2rpx 12rpx;
border-radius: 8rpx;
font-size: 20rpx;
color: #5c626b;
background-color: #f2f3f5;
}
.article-card__chip--top {
color: var(--fg-primary, #2b8fff);
background-color: var(--fg-soft, #eaf4ff);
}
.article-card__chip--pending {
color: #d48806;
background-color: #fff7e6;
}
.article-card__desc {
font-size: 24rpx;
line-height: 34rpx;
color: #8f959e;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.article-card__meta {
display: flex;
align-items: center;
gap: 10rpx;
}
.article-card__avatar {
flex: none;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background-color: #f2f3f5;
}
.article-card__avatar--empty {
background: var(--fg-soft, #eaf4ff);
}
.article-card__author {
font-size: 22rpx;
color: #5c626b;
}
.article-card__date {
margin-left: auto;
font-size: 20rpx;
color: #bfc3c9;
}
/* ===== 详情头卡 + 圆形动作钮(与共享页同套) ===== */
.detail-hero {
position: relative;