ow
This commit is contained in:
1 parent
3b2ee8523b
commit
b5d2a2ede0
17 files changed
+2061
-1484
No files matched your search
@@ -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 {
|
||||
<text class="ftc__title">
|
||||
{{ props.title || t('FamilyTreePage.pedigreeChart') }}
|
||||
</text>
|
||||
<!-- 退出钮统一为「取消」文字胶囊(底部抽屉风格语言,不再用 X) -->
|
||||
<view class="ftc__cancel" @tap="close">
|
||||
<text class="ftc__cancel-text">
|
||||
{{ t("FamilyTreePage.cancel") }}
|
||||
</text>
|
||||
<!-- 退出钮按用户要求改为 × 关闭图标(灰底圆钮) -->
|
||||
<view class="ftc__close" @tap="close">
|
||||
<u-icon name="close" size="28" color="#5c626b" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- 悬浮控制条:放大/缩小/百分比复位/横纵布局切换(对齐旧图 G6 工具钮) -->
|
||||
<view class="ftc__ctrl">
|
||||
<!-- 悬浮控制条:放大/缩小/百分比复位/横纵布局切换(对齐旧图 G6 工具钮);
|
||||
会挡树图内容,按住顶部握把行可拖拽挪位;动作钮只响应单击,与拖拽互不干扰 -->
|
||||
<view class="ftc__ctrl" :style="{ transform: `translate(${ctrlOffset.x}px, ${ctrlOffset.y}px)` }">
|
||||
<view class="ftc__ctrl-grip" @touchstart="onCtrlTouchStart" @touchmove="onCtrlTouchMove"
|
||||
@touchend="onCtrlTouchEnd" @touchcancel="onCtrlTouchEnd">
|
||||
<text class="ftc__ctrl-grip-dot">⠿</text>
|
||||
</view>
|
||||
<view class="ftc__ctrl-btn" @tap="zoomIn">
|
||||
<text class="ftc__ctrl-glyph">+</text>
|
||||
</view>
|
||||
@@ -479,7 +557,9 @@ function close(): void {
|
||||
<text class="ftc__ctrl-glyph">⇅</text>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="ftc__scroll" scroll-x scroll-y :show-scrollbar="true">
|
||||
<!-- 画布:双指捏合缩放(touch 层只识别两指,单指滚动不受影响) -->
|
||||
<scroll-view class="ftc__scroll" scroll-x scroll-y :show-scrollbar="true" @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>
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,8 @@ const transferIcon = computed(() => transferGlyph(props.transferring ? TRANSFER_
|
||||
<view v-if="props.showQuickToggle" class="page-navbar__btn" @tap="emit('toggle-quick')">
|
||||
<u-icon name="grid" size="38" :color="props.quickOn ? '#2b8fff' : '#8f959e'" />
|
||||
</view>
|
||||
<!-- 页面自定义动作区(如首页「新建族谱」圆钮):追加在最右,不填不影响既有页面 -->
|
||||
<slot name="actions" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -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 兜底:从内层 <video> 元素读 duration(loadedmetadata 后即可用) */
|
||||
function syncDuration(): void {
|
||||
// #ifdef H5
|
||||
const d = Number(getInnerVideo()?.duration) || 0;
|
||||
if (d > 0 && isFinite(d)) {
|
||||
duration.value = d;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
function onTimeUpdate(e: { detail: { currentTime: number; duration: number } }): void {
|
||||
// 画面在推进就说明已经播出来了,清掉缓冲转圈(uni video 的 playing/canplay 在 H5 不一定派发)
|
||||
@@ -392,12 +415,33 @@ function onTimeUpdate(e: { detail: { currentTime: number; duration: number } }):
|
||||
if (e.detail?.duration && isFinite(e.detail.duration)) {
|
||||
duration.value = e.detail.duration;
|
||||
}
|
||||
syncBuffer();
|
||||
}
|
||||
function onProgress(e: { detail: { buffered: number } }): void {
|
||||
const b = e.detail?.buffered;
|
||||
if (typeof b === "number") {
|
||||
bufferedRatio.value = Math.min(1, Math.max(bufferedRatio.value, b / 100));
|
||||
}
|
||||
syncBuffer();
|
||||
}
|
||||
/**
|
||||
* H5 校准缓冲条:原生 buffered 常留最后零点几秒不放(要播到才认),
|
||||
* 导致进度条尾部永远露出一截轨道底色、与缓冲条不重合;距结束 1s 内视为满缓冲。
|
||||
*/
|
||||
function syncBuffer(): void {
|
||||
// #ifdef H5
|
||||
const el = getInnerVideo();
|
||||
if (!el?.buffered?.length || !duration.value) {
|
||||
return;
|
||||
}
|
||||
let end = 0;
|
||||
for (let i = 0; i < el.buffered.length; i++) {
|
||||
end = Math.max(end, Number(el.buffered.end(i)) || 0);
|
||||
}
|
||||
if (end >= duration.value - 1) {
|
||||
bufferedRatio.value = 1;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
function onLoadedmetadata(e: { detail: { width: number; height: number } }): void {
|
||||
const w = e.detail?.width ?? 0;
|
||||
@@ -405,6 +449,8 @@ function onLoadedmetadata(e: { detail: { width: number; height: number } }): voi
|
||||
if (w && h) {
|
||||
landscape.value = w > h;
|
||||
}
|
||||
// 元数据就绪即可拿时长,不等播放(H5)
|
||||
syncDuration();
|
||||
if (pendingSeek != null) {
|
||||
ensureCtx();
|
||||
ctx?.seek(pendingSeek);
|
||||
|
||||
Reference in new issue
Block a user