下载安装包,有进度条,自动安装
This commit is contained in:
1 parent
181207fc7a
commit
d97a8884aa
14 files changed
+665
-220
No files matched your search
@@ -15,9 +15,9 @@ import type { FamilyTreeChildrenMapViewEntity } from "@/data/auto/familyTreeChil
|
||||
import type { FamilyTreeData } from "@/data/auto/familyTreeData";
|
||||
import type { FamilyTreeSpouseMapViewEntity } from "@/data/auto/familyTreeSpouseMapViewEntity";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, provide, ref, watch } from "vue";
|
||||
import { computed, onBeforeUnmount, provide, ref, watch } from "vue";
|
||||
import { usePolicyOptions } from "@/composables/usePolicyOptions";
|
||||
import familyTreeService from "@/service/master/familyTreeService";
|
||||
import { request } from "@/utils/request";
|
||||
import { FAMILY_TREE_CHART_API } from "./types";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -56,6 +56,10 @@ interface FullIndex {
|
||||
currentId: string;
|
||||
/** 主系链:父 PersonId -> 该父子女中位于「祖→当前族人」链上的那一个,排序时置顶(旧图主系居首同款) */
|
||||
lineageChildOf: Map<string, string>;
|
||||
/** 主系脊(始祖→当前族人的直连祖先链,含两端):默认展开恒开,保证当前族人可达可见 */
|
||||
spine: Set<string>;
|
||||
/** 各人世代号(始祖=0,按亲子视图自顶向下 BFS):±5 代窗口据此判定 */
|
||||
genOf: Map<string, number>;
|
||||
}
|
||||
const fullIndex = ref<FullIndex | null>(null);
|
||||
|
||||
@@ -250,6 +254,41 @@ function toggle(personId: string): void {
|
||||
expanded.value = { ...expanded.value, [personId]: !expanded.value[personId] };
|
||||
}
|
||||
|
||||
// ===== 全系图默认展开口径:主系脊恒开 + 当前族人 ±5 代窗口展开,其余折叠待点击懒展开 =====
|
||||
const MAIN_LINE_UP = 5;
|
||||
const MAIN_LINE_DOWN = 5;
|
||||
/** 一键展开全部(大谱会变卡,由用户自担);关闭回到 ±5 代窗口默认 */
|
||||
const expandAll = ref(false);
|
||||
|
||||
/** 依当前口径算展开集:expandAll=全展开;否则主系脊 ∪ 世代窗口内节点 */
|
||||
function computeExpanded(): Record<string, boolean> {
|
||||
const index = fullIndex.value;
|
||||
const next: Record<string, boolean> = {};
|
||||
if (!index) {
|
||||
return next;
|
||||
}
|
||||
if (index.rootId) {
|
||||
next[index.rootId] = true;
|
||||
}
|
||||
const curGen = index.genOf.get(index.currentId);
|
||||
index.childrenOf.forEach((_list: any[], parentId: string) => {
|
||||
if (expandAll.value || index.spine.has(parentId)) {
|
||||
next[parentId] = true;
|
||||
return;
|
||||
}
|
||||
const g = index.genOf.get(parentId);
|
||||
if (curGen !== undefined && g !== undefined && g >= curGen - MAIN_LINE_UP && g <= curGen + MAIN_LINE_DOWN) {
|
||||
next[parentId] = true;
|
||||
}
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function toggleExpandAll(): void {
|
||||
expandAll.value = !expandAll.value;
|
||||
expanded.value = computeExpanded();
|
||||
}
|
||||
|
||||
// ===== 缩放与布局方向(弹窗右上控制条)=====
|
||||
const ZOOM_MIN = 0.5;
|
||||
const ZOOM_MAX = 2;
|
||||
@@ -295,7 +334,9 @@ function onCtrlTouchMove(e: TouchEvent): void {
|
||||
}
|
||||
const sys = uni.getSystemInfoSync();
|
||||
const barW = uni.upx2px(88);
|
||||
const barH = uni.upx2px(384); // 握把 + 4 动作钮各 76 + 分隔线
|
||||
// 握把 + 动作钮各 76 + 分隔线;全系图多一枚「展开全部」钮,高度随之 +76
|
||||
const btnCount = props.mode === "full" ? 5 : 4;
|
||||
const barH = uni.upx2px(76 + btnCount * 76 + 4);
|
||||
const edge = uni.upx2px(16);
|
||||
// 默认锚点(top/right 定位)换算成 left/top 基准,再按屏幕边界夹取
|
||||
const baseLeft = sys.windowWidth - uni.upx2px(24) - barW;
|
||||
@@ -350,6 +391,93 @@ function onChartTouchEnd(e: TouchEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== H5 鼠标拖拽平移:scroll-view 桌面端只能滚轮/触控板滚,按住拖动不动,补一套 mousedown→改 scroll-left/top =====
|
||||
// 走受控 scroll-left/scroll-top(uni 官方属性,跨版本稳):拖动中改这两个 ref 让 scroll-view 滚,
|
||||
// 非拖动的滚轮/触摸由 @scroll 回填 ref(panning 时不回填,避免和手打架)。
|
||||
const scrollLeft = ref(0);
|
||||
const scrollTop = ref(0);
|
||||
const isPanning = ref(false);
|
||||
let panning = false;
|
||||
let panMoved = false;
|
||||
let panStartX = 0;
|
||||
let panStartY = 0;
|
||||
let panStartLeft = 0;
|
||||
let panStartTop = 0;
|
||||
// 拖拽刚结束的时刻:node 用它丢弃「拖完松手那一下」误触发的 tap
|
||||
let panEndedAt = 0;
|
||||
|
||||
function onPanStart(e: MouseEvent): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
// 只认鼠标左键;右键/中键交回浏览器,触摸平移走原生滚动
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
panning = true;
|
||||
panMoved = false;
|
||||
isPanning.value = true;
|
||||
panStartX = e.clientX;
|
||||
panStartY = e.clientY;
|
||||
panStartLeft = scrollLeft.value;
|
||||
panStartTop = scrollTop.value;
|
||||
// 不能 preventDefault:它会取消后续原生 click,卡片 @tap 再也收不到(float-add-btn 同款教训);
|
||||
// 文本选中与图片拖影改由 CSS user-select:none + -webkit-user-drag:none 拦
|
||||
window.addEventListener("mousemove", onPanMove);
|
||||
window.addEventListener("mouseup", onPanEnd);
|
||||
}
|
||||
|
||||
function onPanMove(e: MouseEvent): void {
|
||||
if (!panning) {
|
||||
return;
|
||||
}
|
||||
const dx = e.clientX - panStartX;
|
||||
const dy = e.clientY - panStartY;
|
||||
if (Math.abs(dx) > 4 || Math.abs(dy) > 4) {
|
||||
panMoved = true;
|
||||
}
|
||||
// 抓住内容反向拖:鼠标右移→视口左移(scrollLeft 减小)
|
||||
scrollLeft.value = Math.max(0, panStartLeft - dx);
|
||||
scrollTop.value = Math.max(0, panStartTop - dy);
|
||||
}
|
||||
|
||||
function onPanEnd(): void {
|
||||
if (!panning) {
|
||||
return;
|
||||
}
|
||||
panning = false;
|
||||
isPanning.value = false;
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener("mousemove", onPanMove);
|
||||
window.removeEventListener("mouseup", onPanEnd);
|
||||
}
|
||||
if (panMoved) {
|
||||
panEndedAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/** 滚轮/触摸滚动时把真实偏移同步回受控值(拖动中跳过,避免覆盖刚算出的目标位置) */
|
||||
function onChartScroll(e: any): void {
|
||||
if (panning) {
|
||||
return;
|
||||
}
|
||||
scrollLeft.value = e?.detail?.scrollLeft ?? scrollLeft.value;
|
||||
scrollTop.value = e?.detail?.scrollTop ?? scrollTop.value;
|
||||
}
|
||||
|
||||
/** 拖拽平移刚结束的一瞬为真:node 用它丢弃误触 tap */
|
||||
function didPanDrag(): boolean {
|
||||
return Date.now() - panEndedAt < 250;
|
||||
}
|
||||
|
||||
// 拖拽中途弹窗被关(组件卸载)也要摘掉 window 监听,避免泄漏与卸载后仍改 ref
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener("mousemove", onPanMove);
|
||||
window.removeEventListener("mouseup", onPanEnd);
|
||||
}
|
||||
});
|
||||
|
||||
function requestLoad(personId: string): void {
|
||||
emit("select-child", personId);
|
||||
}
|
||||
@@ -366,6 +494,7 @@ provide(FAMILY_TREE_CHART_API, {
|
||||
requestPick,
|
||||
isMale,
|
||||
vertical,
|
||||
didPanDrag,
|
||||
});
|
||||
|
||||
/** 全树:从 FamilyTreeData 拍平三张索引表(照抄旧 buildIndex 的字段映射) */
|
||||
@@ -450,7 +579,26 @@ function buildFullIndex(resp: FamilyTreeData): FullIndex {
|
||||
lineageChildOf.set(parentId, cursor);
|
||||
cursor = parentId;
|
||||
}
|
||||
return { rootId, personMap, spousesOf, childrenOf, parentsOf, currentId, lineageChildOf };
|
||||
// 主系脊:visited 即「当前族人 + 其历代父辈直至始祖」,默认展开据此恒开
|
||||
const spine = new Set(visited);
|
||||
|
||||
// 世代号:自始祖按亲子视图 BFS(一人多父取首父世代,图以主承担父渲染子树,够用且稳)
|
||||
const genOf = new Map<string, number>();
|
||||
if (rootId) {
|
||||
genOf.set(rootId, 0);
|
||||
const queue: string[] = [rootId];
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
const id = queue[i];
|
||||
const g = genOf.get(id) as number;
|
||||
(childrenOf.get(id) ?? []).forEach((c: any) => {
|
||||
if (!genOf.has(c.id)) {
|
||||
genOf.set(c.id, g + 1);
|
||||
queue.push(c.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return { rootId, personMap, spousesOf, childrenOf, parentsOf, currentId, lineageChildOf, spine, genOf };
|
||||
}
|
||||
|
||||
async function loadFull(): Promise<void> {
|
||||
@@ -461,7 +609,14 @@ async function loadFull(): Promise<void> {
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
const resp = await familyTreeService.FindAllByTreeId(treeId);
|
||||
// 整谱一次性下发(大谱数千族员),体量远超常规接口,走生成 service 拿不到单次超时会被全局 20s 掐断
|
||||
// 报「请求错误:未知」,故这里直连请求层并放宽到 90s(后端已同步瘦身,只下发卡片渲染所需列)
|
||||
const resp = await request<FamilyTreeData>({
|
||||
url: "/FamilyTree/FindAllByTreeId?treeId=" + treeId,
|
||||
method: "post",
|
||||
data: treeId,
|
||||
timeout: 90000,
|
||||
});
|
||||
if (resp?.CurrentPerson) {
|
||||
fullIndex.value = buildFullIndex(resp);
|
||||
}
|
||||
@@ -478,7 +633,7 @@ async function loadFull(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 弹窗打开:重置展开态到根节点;full 默认全树展开(旧祖谱系图同款),sub 仅展开根待懒加载 */
|
||||
/** 弹窗打开:重置展开态;full 默认只铺主系脊+当前族人±5代窗口(旁系/更深世代折叠懒展开,可一键全展开),sub 仅展开根待懒加载 */
|
||||
watch(
|
||||
() => props.visible,
|
||||
(on) => {
|
||||
@@ -487,16 +642,10 @@ watch(
|
||||
}
|
||||
expanded.value = {};
|
||||
if (props.mode === "full") {
|
||||
expandAll.value = false;
|
||||
loadFull().then(() => {
|
||||
const next: Record<string, boolean> = {};
|
||||
if (rootPersonId.value) {
|
||||
next[rootPersonId.value] = true;
|
||||
}
|
||||
// 所有有子女的族员一律展开;非主承担父由 node 的 expandable 门控,不会重复渲染子树
|
||||
fullIndex.value?.childrenOf.forEach((_list: any[], parentId: string) => {
|
||||
next[parentId] = true;
|
||||
});
|
||||
expanded.value = next;
|
||||
// 默认只铺主系脊与当前族人 ±5 代窗口,旁系/更深世代折叠,靠 node 点击懒展开(避免整谱全展开卡死 H5)
|
||||
expanded.value = computeExpanded();
|
||||
});
|
||||
}
|
||||
else if (rootPersonId.value) {
|
||||
@@ -556,10 +705,17 @@ function close(): void {
|
||||
<view class="ftc__ctrl-btn" @tap="toggleDirection">
|
||||
<text class="ftc__ctrl-glyph">⇅</text>
|
||||
</view>
|
||||
<!-- 全系图专用:一键展开全部 / 回到 ±5 代窗口默认(sub 懒加载模式无整谱概念,不显示) -->
|
||||
<view v-if="props.mode === 'full'" class="ftc__ctrl-btn"
|
||||
:class="{ 'ftc__ctrl-btn--active': expandAll }" @tap="toggleExpandAll">
|
||||
<text class="ftc__ctrl-glyph">{{ expandAll ? '⊟' : '⊞' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 画布:双指捏合缩放(touch 层只识别两指,单指滚动不受影响) -->
|
||||
<scroll-view class="ftc__scroll" scroll-x scroll-y :show-scrollbar="true" @touchstart="onChartTouchStart"
|
||||
@touchmove="onChartTouchMove" @touchend="onChartTouchEnd" @touchcancel="onChartTouchEnd">
|
||||
<!-- 画布:双指捏合缩放(touch 层只识别两指,单指滚动不受影响);H5 另支持鼠标按住拖拽平移(@mousedown 走模板绑定,见 float-add-btn 同款) -->
|
||||
<scroll-view class="ftc__scroll" :class="{ 'ftc__scroll--panning': isPanning }" scroll-x scroll-y
|
||||
:show-scrollbar="true" :scroll-left="scrollLeft" :scroll-top="scrollTop" @scroll="onChartScroll"
|
||||
@mousedown="onPanStart" @touchstart="onChartTouchStart" @touchmove="onChartTouchMove"
|
||||
@touchend="onChartTouchEnd" @touchcancel="onChartTouchEnd">
|
||||
<view v-if="hasRoot" class="ftc__tree" :style="{ zoom }">
|
||||
<family-tree-node :person-id="rootPersonId" :preview="null" :expandable="true" :depth="0" />
|
||||
</view>
|
||||
@@ -664,6 +820,16 @@ function close(): void {
|
||||
border-bottom-color: #f2f3f5;
|
||||
}
|
||||
|
||||
/* 展开全部态:青底白字,与默认「±5 代窗口」折叠态区分(对齐 node 动作钮青配色) */
|
||||
.ftc__ctrl-btn--active {
|
||||
.ftc__ctrl-glyph {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
background-color: #13c2c2;
|
||||
border-bottom-color: #13c2c2;
|
||||
}
|
||||
|
||||
.ftc__ctrl-glyph {
|
||||
font-size: 34rpx;
|
||||
color: #1f2329;
|
||||
@@ -678,6 +844,21 @@ function close(): void {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
/* H5 鼠标拖拽平移:手型光标 + 禁选(图是示意不是文本,拖动时不选中卡内文字/不拖出图片幽灵) */
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.ftc__scroll--panning {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* 拖拽平移时不把头像拖出原生图片幽灵(preventDefault 会误伤 @tap,改由 CSS 拦) */
|
||||
.ftc__scroll :deep(img) {
|
||||
-webkit-user-drag: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ftc__tree {
|
||||
|
||||
Reference in new issue
Block a user