This commit is contained in:
itcsx committed 2026-09-17 11:49:35 +08:00
1 parent 2b963e4bfe
commit 501c92d87c
13 files changed
+2374 -322

No files matched your search

@@ -21,7 +21,7 @@
@click="onDownload"></a-button>
</a-tooltip>
</a-col>
<a-col :span="2" v-if="!isDirectorySelected && isSingleItem">
<a-col :span="2" v-if="!isDirectorySelected && (isSingleItem || isSameMediaTypeSelected)">
<a-tooltip title="共享">
<a-button class="nav-btn" :icon="h(ShareAltOutlined)" @click="onShare"></a-button>
</a-tooltip>
@@ -46,7 +46,7 @@
</a-popconfirm>
</a-tooltip>
</a-col>
<a-col :span="2" v-if="!isDirectorySelected && isSingleItem">
<a-col :span="2" v-if="!isDirectorySelected && (isSingleItem || isSameMediaTypeSelected)">
<a-tooltip :title="publishTooltip">
<a-button class="nav-btn" :icon="h(NotificationOutlined)" @click="onPublish"></a-button>
</a-tooltip>
@@ -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 "发布影视专辑";
File diff suppressed because it is too large. Load diff
-10
View File
@@ -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;
}
</style>
+5 -11
View File
@@ -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;
@@ -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;
}
);
@@ -7,7 +7,6 @@
<button class="ftc-zoom-btn" title="放大" @click="zoomBy(ZOOM_STEP)">+</button>
<button class="ftc-zoom-btn" title="缩小" @click="zoomBy(-ZOOM_STEP)">-</button>
<button class="ftc-zoom-btn" title="适应画布" @click="fitGraph">⤢</button>
<button class="ftc-zoom-btn" :title="dirTitle" @click="toggleDirection">{{ layoutDir === "h" ? "⇄" : "⇅" }}</button>
</div>
</div>
<a-empty v-else description="暂无当前族员数据" />
@@ -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<string>();
/** 子树宽度 = 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<HTMLDivElement>();
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();
}
};