From 501c92d87c9dedeef8fdc2f802774c7940d9ef0c Mon Sep 17 00:00:00 2001 From: owen Date: Thu, 17 Sep 2026 11:49:35 +0800 Subject: [PATCH] ow --- .../src/components/basic/OperatorStation.vue | 28 +- .../src/components/basic/PhotoSlidePlayer.vue | 2255 +++++++++++++++++ WebVue/src/components/basic/Search.vue | 10 - WebVue/src/components/basic/SearchCard.vue | 16 +- .../src/components/basic/VinylAudioPlayer.vue | 5 +- .../master/FamilyTreeChartModal.vue | 194 +- WebVue/src/styles/ft-shell.less | 3 +- WebVue/src/views/master/DmsFileEnquiry.vue | 58 +- WebVue/src/views/master/DmsFileShareModal.vue | 4 +- .../src/views/master/FamilyShareOperation.vue | 6 +- .../src/views/master/FamilyTreeOperation.vue | 17 +- WebVue/src/views/master/Preview.vue | 96 +- WebVue/src/views/master/PreviewAudio.vue | 4 + 13 files changed, 2374 insertions(+), 322 deletions(-) create mode 100644 WebVue/src/components/basic/PhotoSlidePlayer.vue diff --git a/WebVue/src/components/basic/OperatorStation.vue b/WebVue/src/components/basic/OperatorStation.vue index 28fd752..0b8a633 100644 --- a/WebVue/src/components/basic/OperatorStation.vue +++ b/WebVue/src/components/basic/OperatorStation.vue @@ -21,7 +21,7 @@ @click="onDownload"> - + @@ -46,7 +46,7 @@ - + @@ -109,6 +109,7 @@ import { } from "@ant-design/icons-vue"; import { DmsFileUploadViewDisplayData } from "@/data/auto/dmsFileUploadViewDisplayData"; import FileUpload from "@/components/basic/FileUpload.vue"; +import fileTypePolicy from "@/policy/custom/fileTypePolicy"; const props = defineProps<{ selectedItems: DmsFileUploadViewDisplayData[]; @@ -180,6 +181,29 @@ const isSingleAudio = computed(() => { return isSingleItem.value && isAudio(selectedOrderKey.value); }); +/** + * 识别媒体类型,兼顾本地扩展名判断与全局 fileTypePolicy, + * 保证与发布时的类型判断一致。 + */ +const mediaKind = (key: string): "image" | "video" | "audio" | "" => { + if (isImage(key) || fileTypePolicy.isImage(key)) return "image"; + if (isVideo(key) || fileTypePolicy.isVideo(key)) return "video"; + if (isAudio(key) || fileTypePolicy.isAudio(key)) return "audio"; + return ""; +}; + +/** + * 多选时选中项是否全部属于同一媒体类型(图片/视频/音频)。 + * 满足时允许批量共享与发布。 + */ +const isSameMediaTypeSelected = computed(() => { + const items = props.selectedItems; + if (items.length <= 1) return false; + const kinds = items.map((x) => mediaKind(x.OrderKey || x.OrgFileName || "")); + const kind = kinds[0]; + return kind !== "" && kinds.every((k) => k === kind); +}); + const publishTooltip = computed(() => { if (isImage(selectedOrderKey.value)) return "发布相册"; if (isVideo(selectedOrderKey.value)) return "发布影视专辑"; diff --git a/WebVue/src/components/basic/PhotoSlidePlayer.vue b/WebVue/src/components/basic/PhotoSlidePlayer.vue new file mode 100644 index 0000000..5e91440 --- /dev/null +++ b/WebVue/src/components/basic/PhotoSlidePlayer.vue @@ -0,0 +1,2255 @@ + + + + + diff --git a/WebVue/src/components/basic/Search.vue b/WebVue/src/components/basic/Search.vue index d171d13..c0ef8ac 100644 --- a/WebVue/src/components/basic/Search.vue +++ b/WebVue/src/components/basic/Search.vue @@ -470,14 +470,4 @@ const { run, current, pageSize } = usePagination(queryData, { border-radius: 0; } -.page-footer { - z-index: 999; - padding-left: 8px; - padding-right: 18px; - padding-top: 8px; - width: 100%; - position: fixed; - bottom: 8px; - border-top: 1px solid silver; -} diff --git a/WebVue/src/components/basic/SearchCard.vue b/WebVue/src/components/basic/SearchCard.vue index cebaa83..71e132e 100644 --- a/WebVue/src/components/basic/SearchCard.vue +++ b/WebVue/src/components/basic/SearchCard.vue @@ -402,6 +402,11 @@ onMounted(() => { resizeObserver = new ResizeObserver(() => { void relayout(); }); + // 弹窗形态未显式传 is-modal 时自动识别:位于 .ant-modal-body 内即按弹窗布局 + // 否则分页栏会按「页面模式」position:fixed 钉在视口底部,压住弹窗/页面 footer + if (!isModal.value && searchRef.value?.closest(".ant-modal-body")) { + isModal.value = true; + } rebindObserver(); void setupModalLayout(); }); @@ -647,17 +652,6 @@ const { run } = usePagination(queryData, { min-width: 120px; } -.page-footer { - z-index: 999; - padding-left: 8px; - padding-right: 18px; - padding-top: 8px; - width: 100%; - position: fixed; - bottom: 8px; - border-top: 1px solid silver; -} - /* ==================== 结果区:卡片列表 ==================== */ .sc-list { overflow-x: hidden; diff --git a/WebVue/src/components/basic/VinylAudioPlayer.vue b/WebVue/src/components/basic/VinylAudioPlayer.vue index 43a6309..860d7bd 100644 --- a/WebVue/src/components/basic/VinylAudioPlayer.vue +++ b/WebVue/src/components/basic/VinylAudioPlayer.vue @@ -509,11 +509,10 @@ watch(current, async (t) => { } }); -// 曲目从无到有时默认展开播放列表(对齐原 APlayer showList 的行为) +// 播放列表默认收起,由用户点击列表按钮展开 watch( () => tracks.value.length, - (n, o) => { - if (n > 0 && !o) showList.value = true; + (n) => { if (index.value >= n) index.value = 0; } ); diff --git a/WebVue/src/components/master/FamilyTreeChartModal.vue b/WebVue/src/components/master/FamilyTreeChartModal.vue index 6cdc393..ece5b52 100644 --- a/WebVue/src/components/master/FamilyTreeChartModal.vue +++ b/WebVue/src/components/master/FamilyTreeChartModal.vue @@ -7,7 +7,6 @@ - @@ -162,8 +161,8 @@ const describePerson = (personId: string): any => { return null; }; -/** 构建横向布局图数据:深度向右排列,本人默认展开孩子,点击孩子可递归加载子树 */ -const buildHorizontalData = () => { +/** 构建图数据:树形竖向排列,本人默认展开孩子,点击孩子可递归加载子树 */ +const buildGraphData = () => { const nodes: any[] = []; const edges: any[] = []; const root = rootPersonId.value; @@ -264,166 +263,6 @@ const buildHorizontalData = () => { return { nodes, edges }; }; -/** - * 构建纵向布局图数据:每层一行,孩子横向铺开并居中于父节点,配偶与本人并排。 - * 仅改变排列方式,卡片内容与样式不变。 - */ -const buildVerticalData = () => { - const nodes: any[] = []; - const edges: any[] = []; - const root = rootPersonId.value; - if (!root) return { nodes, edges }; - - const CARD_WIDTH = 360; - const CARD_HEIGHT = 220; - const SPOUSE_GAP = 30; // 配偶紧贴本人 - const H_GAP = 60; // 兄弟子树水平间隔 - const Y_STEP = CARD_HEIGHT + 40; // 上下层间隔 - - /** 布局单元:本人 + 同行配偶,孩子排在下一行 */ - interface VUnit { - personId: string; - desc: any; - /** 需要展开的孩子(未展开则为空) */ - children: any[]; - spouses: any[]; - } - - const unitOf = (personId: string): VUnit | null => { - const desc = describePerson(personId); - if (!desc) return null; - return { - personId, - desc, - children: expandedIds.value.has(personId) ? desc.children : [], - spouses: desc.spouses ?? [], - }; - }; - - /** 本人 + 配偶并排一行的总宽 */ - const unitWidth = (u: VUnit) => CARD_WIDTH + u.spouses.length * (CARD_WIDTH + SPOUSE_GAP); - - const measuring = new Set(); - /** 子树宽度 = max(本行宽度, 孩子一行总宽) */ - const measure = (u: VUnit): number => { - if (measuring.has(u.personId)) return CARD_WIDTH; - measuring.add(u.personId); - let childrenWidth = 0; - u.children.forEach((c: any, i: number) => { - const child = unitOf(c.ChildrenId); - childrenWidth += (child ? measure(child) : CARD_WIDTH) + (i > 0 ? H_GAP : 0); - }); - measuring.delete(u.personId); - return Math.max(unitWidth(u), childrenWidth); - }; - - const place = (u: VUnit, depth: number, xLeft: number) => { - if (depth > 40) return; - const width = measure(u); - const p = u.desc.person; - const isRoot = u.personId === root; - const y = depth * Y_STEP; - // 本人 + 配偶并排在本行,整体居中于本子树 - const unitLeft = xLeft + (width - unitWidth(u)) / 2; - const selfX = unitLeft + CARD_WIDTH / 2; - // 父子连线的横向干线高度:本行底部与下一行顶部之间的中线 - const railY = (depth + 0.5) * Y_STEP; - - // 孩子排在下一行,整体居中于本子树 - const children = u.children.map((c: any) => ({ c, unit: unitOf(c.ChildrenId) })); - const childWidths = children.map((x) => (x.unit ? measure(x.unit) : CARD_WIDTH)); - const childrenWidth = - childWidths.reduce((a, b) => a + b, 0) + H_GAP * Math.max(0, childWidths.length - 1); - let cx = xLeft + (width - childrenWidth) / 2; - children.forEach(({ c, unit }, i) => { - const childX = cx + CARD_WIDTH / 2; - if (unit) { - place(unit, depth + 1, cx); - } else { - nodes.push({ - id: "n-" + c.ChildrenId, - style: { x: childX, y: (depth + 1) * Y_STEP }, - data: { - kind: "child", - personId: c.ChildrenId, - tag: c.RelationType ? relationLabel(c.RelationType) : "子女", - name: (c.ChildrenSurname ?? "") + (c.ChildrenGenealogyName ?? ""), - commonName: c.ChildrenCommonName, - gender: c.Gender, - avatarUrl: avatarMap.value[c.ChildrenId] ?? "", - hasChildren: false, - expanded: false, - loaded: false, - }, - }); - } - edges.push({ - source: "n-" + u.personId, - target: "n-" + c.ChildrenId, - // 折点:父卡底部中心下方 → 横向干线 → 子卡顶部中心上方 - data: { kind: "parent", points: [[selfX, railY], [childX, railY]] }, - }); - cx += childWidths[i] + H_GAP; - }); - - nodes.push({ - id: "n-" + u.personId, - style: { x: selfX, y }, - data: { - kind: isRoot ? "self" : "member", - personId: u.personId, - tag: isRoot ? "本人" : "族员", - name: (p?.Surname ?? "") + (p?.GenealogyName ?? ""), - commonName: p?.CommonName, - gender: p?.Gender, - avatarUrl: p?.AvatarUrl ?? avatarMap.value[u.personId] ?? "", - hasChildren: u.desc.children.length > 0, - expanded: expandedIds.value.has(u.personId), - loaded: u.desc.loaded, - }, - }); - - u.spouses.forEach((s: any, i: number) => { - const sid = "s-" + u.personId + "-" + i; - nodes.push({ - id: sid, - style: { x: selfX + (i + 1) * (CARD_WIDTH + SPOUSE_GAP), y }, - data: { - kind: "spouse", - personId: s.SpouseId, - tag: "配偶", - name: (s.SpouseSurname ?? "") + (s.SpouseGenealogyName ?? ""), - gender: s.Gender, - avatarUrl: avatarMap.value[s.SpouseId] ?? "", - loaded: true, - hasChildren: false, - expanded: false, - }, - }); - edges.push({ source: "n-" + u.personId, target: sid, data: { kind: "spouse" } }); - }); - }; - - const rootUnit = unitOf(root); - if (rootUnit) place(rootUnit, 0, 0); - return { nodes, edges }; -}; - -/** 布局方向:h=横向(深度向右),v=纵向(深度向下) */ -const layoutDir = ref<"h" | "v">("h"); - -/** 按当前方向构建图数据 */ -const buildGraphData = () => (layoutDir.value === "v" ? buildVerticalData() : buildHorizontalData()); - -/** 方向按钮提示(按钮上的箭头表示当前方向) */ -const dirTitle = computed(() => (layoutDir.value === "h" ? "当前横向排列,点击切换为纵向" : "当前纵向排列,点击切换为横向")); - -/** 横向 / 纵向排列切换 */ -const toggleDirection = () => { - layoutDir.value = layoutDir.value === "h" ? "v" : "h"; - initGraph(); -}; - const containerRef = ref(); let graph: any = null; @@ -440,13 +279,12 @@ const zoomBy = (delta: number) => { graph.zoomTo(Math.min(Math.max(next, MIN_ZOOM), 10)); }; -/** 还原为适配整个画布(缩到最小比例以下的需重新居中,否则整图可能落在视口之外) */ +/** 还原为适配整个画布 */ const fitGraph = async () => { if (!graph) return; await graph.fitView(); if (graph.getZoom() < MIN_ZOOM) { await graph.zoomTo(MIN_ZOOM); - if (typeof graph.fitCenter === "function") await graph.fitCenter(); } }; @@ -485,22 +323,13 @@ const initGraph = async () => { }, edge: { type: (d: any) => (d?.data?.kind === "spouse" ? "line" : "polyline"), - style: (d: any) => { - const isSpouse = d?.data?.kind === "spouse"; - return { - // 夫妻关系=紫色虚线,亲子关系=灰色实线 - stroke: isSpouse ? "#722ed1" : "#a0a0a0", - lineWidth: 1.5, - lineDash: isSpouse ? [4, 4] : undefined, - // 纵向布局:用布局阶段算好的折点(父卡底部中心 → 横向干线 → 子卡顶部中心) - // 横向布局:沿用正交路由 - ...(isSpouse - ? {} - : layoutDir.value === "v" - ? { controlPoints: d?.data?.points ?? [] } - : { router: { type: "orth" } }), - }; - }, + style: (d: any) => ({ + // 夫妻关系=紫色虚线,亲子关系=灰色实线 + stroke: d?.data?.kind === "spouse" ? "#722ed1" : "#a0a0a0", + lineWidth: 1.5, + lineDash: d?.data?.kind === "spouse" ? [4, 4] : undefined, + router: d?.data?.kind === "spouse" ? undefined : { type: "orth" }, + }), }, behaviors: ["drag-canvas", "zoom-canvas"], }); @@ -523,10 +352,9 @@ const initGraph = async () => { } }); await graph.render(); - // 世代过多导致 fitView 缩得太小时,放大到最小显示比例并重新居中,保证卡片可见可读 + // 世代过多导致 fitView 缩得太小时,放大到最小显示比例,保证卡片可读 if (graph.getZoom() < MIN_ZOOM) { await graph.zoomTo(MIN_ZOOM); - if (typeof graph.fitCenter === "function") await graph.fitCenter(); } }; diff --git a/WebVue/src/styles/ft-shell.less b/WebVue/src/styles/ft-shell.less index 16e4f13..4ea5ac5 100644 --- a/WebVue/src/styles/ft-shell.less +++ b/WebVue/src/styles/ft-shell.less @@ -39,7 +39,7 @@ padding-left: 12px; } -/* 底部固定操作栏 */ +/* 底部固定操作栏:全站统一不透明底色,避免下方内容透出 */ .page-footer { z-index: 999; padding-left: 8px; @@ -49,6 +49,7 @@ position: fixed; bottom: 8px; border-top: 1px solid silver; + background: #ffffff; } /* 表格行高亮:成员/子级选中等场景,与 FamilyTreeOperation 保持一致 */ diff --git a/WebVue/src/views/master/DmsFileEnquiry.vue b/WebVue/src/views/master/DmsFileEnquiry.vue index b90bd27..0628df2 100644 --- a/WebVue/src/views/master/DmsFileEnquiry.vue +++ b/WebVue/src/views/master/DmsFileEnquiry.vue @@ -96,7 +96,7 @@ interface BreadcrumbItem { const { width } = useWindowSize(); const showModalShare = ref(false); -const sharedFileRecord = ref(null); +const sharedFileRecords = ref([]); const showModalPublishPhoto = ref(false); const showModalPublishAudio = ref(false); const showModalPublishVideo = ref(false); @@ -487,8 +487,8 @@ const onPublishVideo = async () => { showModalPublishVideo.value = true; }; -const onShareFamily = async (record: Record) => { - sharedFileRecord.value = toUploadView(record); +const onShareFamily = async (records: DmsFileUploadViewDisplayData[]) => { + sharedFileRecords.value = records.map((r) => toUploadView(r)); showModalShare.value = true; }; @@ -801,18 +801,20 @@ const onReload = async () => { const handleSelectShare = async () => { if (shareEnquiry.value) { - if (shareEnquiry.value.keys.length > 0) { + if (shareEnquiry.value.keys.length > 0 && sharedFileRecords.value.length > 0) { let fgps: FamilyGroupFolderEntity[] = []; - let fgp: FamilyGroupFolderEntity; - shareEnquiry.value.keys.forEach((element) => { - fgp = new FamilyGroupFolderEntity(); - fgp.FamilyName = element.FamilyName; - fgp.TopFolderName = element.TopFolderName; - fgp.FolderName = sharedFileRecord.value?.FileName ?? ""; - fgp.FolderLocalName = sharedFileRecord.value?.OrgFileName ?? ""; - fgp.FilePath = element.FolderName; + // 每个选中的目标共享目录 × 每个待共享的文件,各生成一条群组文件夹记录 + shareEnquiry.value.keys.forEach((target) => { + sharedFileRecords.value.forEach((record) => { + let fgp: FamilyGroupFolderEntity = new FamilyGroupFolderEntity(); + fgp.FamilyName = target.FamilyName; + fgp.TopFolderName = target.TopFolderName; + fgp.FolderName = record.FileName ?? ""; + fgp.FolderLocalName = record.OrgFileName ?? ""; + fgp.FilePath = target.FolderName; - fgps.push(fgp); + fgps.push(fgp); + }); }); await dmsFileService.CreateFamilyGroupFolder(fgps); @@ -997,28 +999,26 @@ const onDownloadSelected = (items: DmsFileUploadViewDisplayData[]) => { }; const onShareSelected = (items: DmsFileUploadViewDisplayData[]) => { - if (items.length > 0) { - onShareFamily(items[0]); + const files = items.filter((item) => item.IsDirectoryIndc != "Y"); + if (files.length > 0) { + onShareFamily(files); } }; const onPublishSelected = (items: DmsFileUploadViewDisplayData[]) => { - if (items.length === 0) return; - const first = items[0]; - if ( - fileTypePolicy.isImage(first.OrderKey) || - fileTypePolicy.isImage(first.OrgFileName) - ) { + const files = items.filter((item) => item.IsDirectoryIndc != "Y"); + if (files.length === 0) return; + const first = files[0]; + const mediaKey = (item: DmsFileUploadViewDisplayData) => + item.OrderKey || item.OrgFileName || ""; + if (fileTypePolicy.isImage(mediaKey(first))) { + keys.value = files.filter((item) => fileTypePolicy.isImage(mediaKey(item))); onPublishPhoto(); - } else if ( - fileTypePolicy.isVideo(first.OrderKey) || - fileTypePolicy.isVideo(first.OrgFileName) - ) { + } else if (fileTypePolicy.isVideo(mediaKey(first))) { + keys.value = files.filter((item) => fileTypePolicy.isVideo(mediaKey(item))); onPublishVideo(); - } else if ( - fileTypePolicy.isAudio(first.OrderKey) || - fileTypePolicy.isAudio(first.OrgFileName) - ) { + } else if (fileTypePolicy.isAudio(mediaKey(first))) { + keys.value = files.filter((item) => fileTypePolicy.isAudio(mediaKey(item))); onPublishAudio(); } }; diff --git a/WebVue/src/views/master/DmsFileShareModal.vue b/WebVue/src/views/master/DmsFileShareModal.vue index eef7774..4f9775b 100644 --- a/WebVue/src/views/master/DmsFileShareModal.vue +++ b/WebVue/src/views/master/DmsFileShareModal.vue @@ -4,11 +4,11 @@ @on-select-change="onSelectChange"> diff --git a/WebVue/src/views/master/FamilyShareOperation.vue b/WebVue/src/views/master/FamilyShareOperation.vue index ac1838f..d486e30 100644 --- a/WebVue/src/views/master/FamilyShareOperation.vue +++ b/WebVue/src/views/master/FamilyShareOperation.vue @@ -15,6 +15,10 @@ @on-select-change="onSelectChange" > +