diff --git a/uniapp/youjiala/src/components/family-tree-chart/family-tree-chart.vue b/uniapp/youjiala/src/components/family-tree-chart/family-tree-chart.vue
index bdda11d..619fa32 100644
--- a/uniapp/youjiala/src/components/family-tree-chart/family-tree-chart.vue
+++ b/uniapp/youjiala/src/components/family-tree-chart/family-tree-chart.vue
@@ -275,6 +275,81 @@ function toggleDirection(): void {
vertical.value = !vertical.value;
}
+// ===== 控制条可拖拽:按住顶部握把行挪位(拖拽热区只在握把,不碰动作钮),
+// ctrlOffset 为整条相对默认右上锚点的位移 =====
+const ctrlOffset = ref({ x: 0, y: 0 });
+let dragStart: { tx: number; ty: number; ox: number; oy: number } | null = null;
+
+function onCtrlTouchStart(e: TouchEvent): void {
+ const t0 = e.touches?.[0];
+ if (!t0) {
+ return;
+ }
+ dragStart = { tx: t0.clientX, ty: t0.clientY, ox: ctrlOffset.value.x, oy: ctrlOffset.value.y };
+}
+
+function onCtrlTouchMove(e: TouchEvent): void {
+ const t0 = e.touches?.[0];
+ if (!dragStart || !t0) {
+ return;
+ }
+ const sys = uni.getSystemInfoSync();
+ const barW = uni.upx2px(88);
+ const barH = uni.upx2px(384); // 握把 + 4 动作钮各 76 + 分隔线
+ const edge = uni.upx2px(16);
+ // 默认锚点(top/right 定位)换算成 left/top 基准,再按屏幕边界夹取
+ const baseLeft = sys.windowWidth - uni.upx2px(24) - barW;
+ const baseTop = uni.upx2px(120) + (sys.safeAreaInsets?.top || 0);
+ const minX = edge - baseLeft;
+ const maxX = sys.windowWidth - edge - barW - baseLeft;
+ const minY = edge + uni.upx2px(110) + (sys.safeAreaInsets?.top || 0) - baseTop;
+ const maxY = sys.windowHeight - edge - barH - baseTop;
+ ctrlOffset.value = {
+ x: Math.min(Math.max(dragStart.ox + t0.clientX - dragStart.tx, minX), maxX),
+ y: Math.min(Math.max(dragStart.oy + t0.clientY - dragStart.ty, minY), maxY),
+ };
+}
+
+function onCtrlTouchEnd(): void {
+ dragStart = null;
+}
+
+// ===== 双指捏合缩放:画布触摸层两指距离比换算 zoom(5% 步进,比 ± 钮细腻)=====
+let pinchStartDist = 0;
+let pinchStartZoom = 1;
+
+function touchGap(t0: Touch, t1: Touch): number {
+ return Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY);
+}
+
+function onChartTouchStart(e: TouchEvent): void {
+ if (e.touches?.length === 2) {
+ pinchStartDist = touchGap(e.touches[0], e.touches[1]);
+ pinchStartZoom = zoom.value;
+ } else {
+ pinchStartDist = 0;
+ }
+}
+
+function onChartTouchMove(e: TouchEvent): void {
+ if (e.touches?.length !== 2 || pinchStartDist <= 0) {
+ return;
+ }
+ const ratio = touchGap(e.touches[0], e.touches[1]) / pinchStartDist;
+ const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(pinchStartZoom * ratio * 20) / 20));
+ if (next !== zoom.value) {
+ // H5:捏合期间拦掉默认滚动,避免双指被 scroll-view 当成拖动
+ e.preventDefault?.();
+ zoom.value = next;
+ }
+}
+
+function onChartTouchEnd(e: TouchEvent): void {
+ if (!e.touches || e.touches.length < 2) {
+ pinchStartDist = 0;
+ }
+}
+
function requestLoad(personId: string): void {
emit("select-child", personId);
}
@@ -457,15 +532,18 @@ function close(): void {
{{ props.title || t('FamilyTreePage.pedigreeChart') }}
-
-
-
- {{ t("FamilyTreePage.cancel") }}
-
+
+
+
-
-
+
+
+
+ ⠿
+
+
@@ -479,7 +557,9 @@ function close(): void {
⇅
-
+
+
@@ -518,18 +598,17 @@ function close(): void {
color: #1f2329;
}
-.ftc__cancel {
- padding: 12rpx 40rpx;
- border-radius: 999rpx;
+.ftc__close {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
background-color: #f2f3f5;
}
-.ftc__cancel-text {
- font-size: 26rpx;
- color: #5c626b;
-}
-
-/* 悬浮控制条:右侧竖排,白底圆角轻阴影 */
+/* 悬浮控制条:右侧竖排,白底圆角轻阴影;整条可拖拽(见 ctrlOffset) */
.ftc__ctrl {
position: absolute;
top: calc(120rpx + env(safe-area-inset-top));
@@ -543,6 +622,24 @@ function close(): void {
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
}
+/* 顶部握把行:拖拽热区(touch-action:none 只加在这里,动作钮手势不受影响);
+ 与动作钮等高,手指易按住 */
+.ftc__ctrl-grip {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 76rpx;
+ border-bottom: 2rpx solid #f2f3f5;
+ cursor: move;
+ touch-action: none;
+}
+
+.ftc__ctrl-grip-dot {
+ font-size: 22rpx;
+ line-height: 1;
+ color: #c0c4cc;
+}
+
.ftc__ctrl-btn {
display: flex;
align-items: center;
diff --git a/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue b/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue
new file mode 100644
index 0000000..1606619
--- /dev/null
+++ b/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue
@@ -0,0 +1,1730 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("FamilyTreePage.listTitle") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("FilePage.loading") }}
+
+
+ {{ errorText }}
+ {{ t("FilePage.retryBut") }}
+
+
+
+ {{ t("FamilyTreePage.chartEmpty") }}
+
+
+
+
+
+
+ {{ row.TreeName || t("FamilyTreePage.unnamed") }}
+ {{ row.Brief || t("FamilyTreePage.noBrief") }}
+
+ {{ row.TreeId }}
+
+ {{ t("FamilyTreePage.chipPublic") }}
+
+ {{ t("FamilyTreePage.chipPrivate") }}
+
+ {{ `${row.Surname}${row.GenealogyName}` }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tree.TreeName || t("FamilyTreePage.unnamed") }}
+
+ {{ tree.TreeId || t("FamilyTreePage.treeIdPlaceholder") }}
+
+
+
+ {{ t("FamilyTreePage.chipPublic") }}
+
+
+ {{ t("FamilyTreePage.chipPrivate") }}
+
+
+ {{ t("FamilyTreePage.managerTag") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("AdminPage.cancelBut") }}
+
+
+ {{ t("AdminPage.saveBut") }}
+
+
+
+
+
+
+ {{ t("FamilyTreePage.infoTitle") }}
+
+
+
+
+ {{ t("FamilyTreePage.treeId") }}
+ {{ tree.TreeId || "-" }}
+
+
+ {{ t("FamilyTreePage.treeName") }}
+
+
+
+ {{ t("FamilyTreePage.brief") }}
+
+
+
+ {{ t("FamilyTreePage.treeCover") }}
+
+
+
+ {{ t("FamilyTreePage.publicIndc") }}
+
+
+
+ {{ t("FamilyTreePage.ancestor") }}
+ {{ tree.AncestorPersonGenealogyName || "-" }}
+
+ + {{ t("FamilyTreePage.addAncestor") }}
+
+
+
+
+
+
+
+
+ {{ t("FamilyTreePage.currentPerson") }}
+
+
+
+
+ {{ t("FamilyTreePage.gender") }}
+ {{ genderText(currentPerson?.Gender) || "-" }}
+
+
+ {{ t("FamilyTreePage.genealogyName") }}
+ {{ currentPerson?.GenealogyName || "-" }}
+
+
+ {{ t("FamilyTreePage.commonName") }}
+ {{ currentPerson?.CommonName || "-" }}
+
+
+
+
+ {{ t("FamilyTreePage.personSearch") }}
+
+
+
+ {{ t("FamilyTreePage.pedigreeChart") }}
+
+
+
+ {{ t("FamilyTreePage.ancestryChart") }}
+
+
+
+ {{ t("FamilyTreePage.applyManage") }}
+
+
+
+
+
+
+
+
+
+ {{ t("FamilyTreePage.spouseTitle") }}
+ {{ spouseMaps.length }}
+
+
+
+
+ + {{ t("FamilyTreePage.addSpouse") }}
+
+
+ {{ t("FamilyTreePage.pickSpouse") }}
+
+
+
+
+
+
+
+ {{ t("FamilyTreePage.spouseEmpty") }}
+
+
+
+
+ {{ record.SpouseSurname }}{{ record.SpouseGenealogyName }}
+ {{ genderText(record.Gender) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("FamilyTreePage.marryDateText") }}
+
+
+
+
+ {{ t("FamilyTreePage.marryDate") }}
+
+ {{ datePart(record.MarryDate) || t("AdminPage.notSetTip")
+ }}
+
+
+
+
+
+ {{ t("FamilyTreePage.divorceDateText") }}
+
+
+
+
+ {{ t("FamilyTreePage.divorceDate") }}
+
+ {{
+ datePart(record.DivorceDate) || t("AdminPage.notSetTip")
+ }}
+
+
+
+
+
+ {{ t("FamilyTreePage.marryType") }}
+
+
+
+ {{ t("FamilyTreePage.isCurrent") }}
+
+
+
+ {{ t("FamilyTreePage.remark") }}
+
+
+
+
+
+
+ {{ t("FamilyTreePage.marryDate") }}
+ {{ datePart(record.MarryDate) || "-" }}
+
+
+ {{ t("FamilyTreePage.marryDateText") }}
+ {{ record.MarryDateText }}
+
+
+ {{ t("FamilyTreePage.divorceDate") }}
+ {{ datePart(record.DivorceDate) || "-" }}
+
+
+ {{ t("FamilyTreePage.marryType") }}
+ {{ record.MarryType }}
+
+
+ {{ t("FamilyTreePage.isCurrent") }}
+
+ {{ record.IsCurrent === "Y" ? t("FamilyTreePage.yes") : t("FamilyTreePage.no") }}
+
+
+
+ {{ t("FamilyTreePage.remark") }}
+ {{ record.Remark }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("FamilyTreePage.childTitle") }}
+ {{ childMaps.length }}
+
+
+
+ + {{ t("FamilyTreePage.addChild") }}
+
+
+
+
+
+
+ {{ t("FamilyTreePage.childEmpty") }}
+
+
+
+
+ {{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }}
+ {{ genderText(record.Gender) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t("FamilyTreePage.parent") }}
+
+ {{ spouseLabel(record.SpouseId) }}
+
+
+
+
+ {{ t("FamilyTreePage.relationType") }}
+
+ {{ relationLabel(record.RelationType) }}
+
+
+
+
+ {{ t("FamilyTreePage.remark") }}
+
+
+
+
+
+
+ {{ t("FamilyTreePage.parent") }}
+ {{ spouseLabel(record.SpouseId) }}
+
+
+ {{ t("FamilyTreePage.relationType") }}
+ {{ relationLabel(record.RelationType) }}
+
+
+ {{ t("FamilyTreePage.remark") }}
+ {{ record.Remark }}
+
+
+
+
+
+
+
+
+
+
+ {{ t("FamilyTreePage.permTitle") }}
+
+
+
+ {{ t("FamilyTreePage.permPending") }}
+
+ {{ t("FamilyTreePage.permEmpty") }}
+
+
+
+ {{ record.UserLocalName || record.UserName }}
+ {{ record.PersonLocalName }}
+
+
+
+ {{ t("FamilyTreePage.approve") }}
+
+
+ {{ t("FamilyTreePage.setManager") }}
+
+
+ {{ t("FamilyTreePage.reject") }}
+
+
+
+
+ {{ t("FamilyTreePage.permManager") }}
+
+ {{ t("FamilyTreePage.permEmpty") }}
+
+
+
+ {{ record.UserLocalName || record.UserName }}
+ {{
+ record.ManagerIndc === "Y" ? t("FamilyTreePage.managerTag") : t("FamilyTreePage.memberTag")
+ }}
+
+
+
+ {{ t("FamilyTreePage.remove") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uniapp/youjiala/src/components/float-add-btn/float-add-btn.vue b/uniapp/youjiala/src/components/float-add-btn/float-add-btn.vue
index 071164c..ecade22 100644
--- a/uniapp/youjiala/src/components/float-add-btn/float-add-btn.vue
+++ b/uniapp/youjiala/src/components/float-add-btn/float-add-btn.vue
@@ -43,13 +43,17 @@ function sizePx(): number {
return uni.upx2px(112);
}
-/** 夹在屏幕可视区内,四边留 8px 呼吸位 */
+/** 夹在屏幕可视区内:左右/底缘留 8px 呼吸位;顶让出导航栏条带、底让出 tabbar 条带——
+ * 两带是 z-index 更高的 fixed/sticky 层(page-navbar 为 9,本球仅 8),球一旦进入,
+ * mousedown/touchstart 会被上层截走,表现为"球卡死拖不动"(位置全页面共享存储,一处卡处处卡) */
function clampPos(x: number, y: number): { x: number; y: number } {
- const { windowWidth, windowHeight } = uni.getSystemInfoSync();
+ const { windowWidth, windowHeight, statusBarHeight = 0, safeAreaInsets } = uni.getSystemInfoSync();
const size = sizePx();
+ const minY = (statusBarHeight || 0) + uni.upx2px(100);
+ const maxY = windowHeight - size - (safeAreaInsets?.bottom ?? 0) - uni.upx2px(110);
return {
x: Math.min(Math.max(8, x), windowWidth - size - 8),
- y: Math.min(Math.max(8, y), windowHeight - size - 8),
+ y: Math.min(Math.max(minY, y), Math.max(minY, maxY)),
};
}
diff --git a/uniapp/youjiala/src/components/page-navbar/page-navbar.vue b/uniapp/youjiala/src/components/page-navbar/page-navbar.vue
index 40ffed6..373a50a 100644
--- a/uniapp/youjiala/src/components/page-navbar/page-navbar.vue
+++ b/uniapp/youjiala/src/components/page-navbar/page-navbar.vue
@@ -90,6 +90,8 @@ const transferIcon = computed(() => transferGlyph(props.transferring ? TRANSFER_
+
+
diff --git a/uniapp/youjiala/src/components/video-player/video-player.vue b/uniapp/youjiala/src/components/video-player/video-player.vue
index 56ed232..6bfad9b 100644
--- a/uniapp/youjiala/src/components/video-player/video-player.vue
+++ b/uniapp/youjiala/src/components/video-player/video-player.vue
@@ -183,6 +183,13 @@ function attachHls(): void {
hls.loadSource(url);
hls.attachMedia(el);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
+ // m3u8 清单解析完即有总时长:先取 level 的 totalDuration,再兜底读元素
+ const d = Number(hls?.levels?.[0]?.totalDuration) || 0;
+ if (d > 0 && isFinite(d)) {
+ duration.value = d;
+ } else {
+ syncDuration();
+ }
if (pending != null) {
el.currentTime = pending;
}
@@ -382,8 +389,24 @@ function onWaiting(): void {
function onPlaying(): void {
buffering.value = false;
}
-function onCanplay(): void {
+function onCanplay(e: { detail?: { duration?: number } }): void {
buffering.value = false;
+ // 未点播放也先亮总时长:uni canplay 的 detail.duration(H5/App/小程序都有)
+ const d = Number(e?.detail?.duration) || 0;
+ if (d > 0 && isFinite(d)) {
+ duration.value = d;
+ return;
+ }
+ syncDuration();
+}
+/** H5 兜底:从内层