fix(mobile): 流转树全览居中 + 拖拽缩放性能优化
1. fitAll() 居中修复: 内容坐标系整体对齐视口中心 旧: mvX=0, mvY=0 (内容偏右下角) 新: mvX = (viewportW - canvasW * scale) / 2, mvY同理 2. movable-area 显式像素高度: 替代 flex:1 避免触摸区域计算不准确导致拖拽延迟 3. onScale 节流: rAF/setTimeout 限频 16ms 避免 @scale 每帧触发 Vue 响应式更新 4. 新增 onChange 同步手势结束后的位置 惯性动画结束后记录位置,下次 fitAll 参考使用 5. 添加 inertia+friction+damping 惯性动画参数
This commit is contained in:
@ -22,11 +22,12 @@
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<movable-area class="tree-movable-area">
|
||||
<movable-area class="tree-movable-area" :style="{ width: viewportW + 'px', height: areaHeight + 'px' }">
|
||||
<movable-view class="tree-movable-view" direction="all"
|
||||
:x="mvX" :y="mvY" :scale="true" :scale-min="0.1" :scale-max="5"
|
||||
:scale-value="mvScale" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
||||
@scale="onScale">
|
||||
:inertia="true" :friction="2" :damping="40"
|
||||
@scale="onScale" @change="onChange">
|
||||
|
||||
<view class="canvas-inner">
|
||||
<!-- 🔧 连线层 -->
|
||||
@ -68,10 +69,15 @@ export default {
|
||||
return {
|
||||
mvX: 0, mvY: 0, mvScale: 1,
|
||||
viewportW: 375, viewportH: 600,
|
||||
areaHeight: 500,
|
||||
_scalePending: false,
|
||||
_pendingScale: 1,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
try { const info = uni.getSystemInfoSync(); this.viewportW = info.windowWidth || 375; this.viewportH = info.windowHeight || 600; } catch {}
|
||||
// 🚀 给 movable-area 明确像素高度,避免 flex:1 导致触摸区域计算不准
|
||||
this.areaHeight = Math.max(this.viewportH - 100, 400);
|
||||
this.$nextTick(() => this.fitAll());
|
||||
},
|
||||
computed: {
|
||||
@ -204,11 +210,30 @@ export default {
|
||||
canvasHeight() { const ns = this.treeNodes; if (!ns.length) return 400; return Math.max(...ns.map(n => n.y)) + 200; },
|
||||
fitScale() {
|
||||
if (!this.treeNodes.length) return 1;
|
||||
return Math.max(0.1, Math.min((this.viewportW - 32) / this.canvasWidth, (this.viewportH - 100) / this.canvasHeight, 1));
|
||||
return Math.max(0.1, Math.min((this.viewportW - 32) / this.canvasWidth, (this.areaHeight - 16) / this.canvasHeight, 1));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onScale(e) { if (e?.detail?.scale) this.mvScale = e.detail.scale; },
|
||||
// 🚀 缩放事件节流:用 rAF 限频,避免每帧触发 Vue 响应式更新导致卡顿
|
||||
onScale(e) {
|
||||
const s = e?.detail?.scale;
|
||||
if (!s) return;
|
||||
this._pendingScale = s;
|
||||
if (!this._scalePending) {
|
||||
this._scalePending = true;
|
||||
// uni-app 环境优先用定时器(兼容小程序),H5 用 rAF
|
||||
const schedule = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : (fn) => setTimeout(fn, 16);
|
||||
schedule(() => {
|
||||
this.mvScale = this._pendingScale;
|
||||
this._scalePending = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
// 🚀 手势结束后同步位置(仅记录,不回绑 :x/:y 避免干扰原生拖拽)
|
||||
onChange(e) {
|
||||
if (e?.detail?.x !== undefined) this.mvX = e.detail.x;
|
||||
if (e?.detail?.y !== undefined) this.mvY = e.detail.y;
|
||||
},
|
||||
zoomIn() { this.mvScale = Math.min(5, +(this.mvScale + 0.2).toFixed(1)); },
|
||||
handleNodeTap(node) {
|
||||
if (node.records && node.records.length > 0) {
|
||||
@ -218,7 +243,18 @@ export default {
|
||||
}
|
||||
},
|
||||
zoomOut() { this.mvScale = Math.max(0.1, +(this.mvScale - 0.2).toFixed(1)); },
|
||||
fitAll() { this.mvScale = this.fitScale; this.mvX = 0; this.mvY = 0; },
|
||||
// 🚀 全览居中:缩放至适配 + 主分支水平居中 + 垂直起始位置
|
||||
fitAll() {
|
||||
const scale = this.fitScale;
|
||||
const canvasW = this.canvasWidth;
|
||||
const canvasH = this.canvasHeight;
|
||||
const viewW = this.viewportW;
|
||||
const viewH = this.areaHeight;
|
||||
// 内容整体在视口中居中
|
||||
this.mvScale = scale;
|
||||
this.mvX = (viewW - canvasW * scale) / 2;
|
||||
this.mvY = (viewH - canvasH * scale) / 2;
|
||||
},
|
||||
fmtDate(d) { if (!d) return ""; const dt = new Date(d); return (dt.getMonth() + 1) + '/' + dt.getDate(); },
|
||||
statusLabel(s) { const m = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库", CANCELED: "已撤回" }; return m[s] || s; },
|
||||
statusColor(s) { switch (s) { case "PENDING": return "s-yellow"; case "WIP": return "s-blue"; case "COMPLETED": return "s-green"; case "REJECTED": return "s-red"; case "CANCELED": return "s-canceled"; case "ARCHIVED": return "s-archived"; default: return "s-gray"; } },
|
||||
@ -243,7 +279,7 @@ export default {
|
||||
.zero-task { display: flex; flex-direction: column; align-items: center; padding: 60px 0; }
|
||||
.zero-icon { font-size: 40px; margin-bottom: 10px; }
|
||||
.zero-text { font-size: 14px; color: #9ca3af; }
|
||||
.tree-movable-area { flex: 1; width: 100%; background-color: #f0f2f5; background-image: linear-gradient(#dde1e6 1px, transparent 1px), linear-gradient(90deg, #dde1e6 1px, transparent 1px); background-size: 24px 24px; }
|
||||
.tree-movable-area { width: 100%; background-color: #f0f2f5; background-image: linear-gradient(#dde1e6 1px, transparent 1px), linear-gradient(90deg, #dde1e6 1px, transparent 1px); background-size: 24px 24px; overflow: hidden; }
|
||||
.canvas-inner { position: absolute; left: 0; top: 0; }
|
||||
|
||||
/* 🔧 连线(禁止吞事件) */
|
||||
|
||||
Reference in New Issue
Block a user