From 30a48d90fc75aa195bde072103cd68ffb7f39d2e Mon Sep 17 00:00:00 2001 From: duxingchen Date: Tue, 11 Aug 2026 15:05:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E6=B5=81=E8=BD=AC=E8=93=9D?= =?UTF-8?q?=E5=9B=BE=20+=20=E8=81=8A=E5=A4=A9=E5=AE=A4=20+=20=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E5=B8=83=E5=B1=80=20+=20=E6=89=93=E5=8D=B0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. TreeCanvas.vue - Y轴坐标算法重构: - calcSubtreeHeight 改为兄弟累加 (reduce) 替代 Math.max,彻底杜绝垂直重叠 - childNode.y = parentNode.y + CARD_H + VERTICAL_GAP 严格向下延伸 - 同级子节点按子树高度累加 startY,各占独立 Y 区域 - 加宽画布默认尺寸 (400→800, padding +300/+400) 2. detail.vue - onShow 生命周期修复: - 新增 onShow() 钩子调用 fetchMessages(),解决聊天室退回不刷新问题 3. TaskSwipeCards.vue + detail.vue - CSS 防跳动: - .ss-swiper-h / .ss-card-wrapper 添加 min-height: 220px - .card 添加 min-height: 120px 4. detail.vue - 移动端打印标签: - 新增「🖨️ 打印标签」按钮 - 双方案 ActionSheet: 网络打印机(后端API) / 蓝牙打印机(ESC/POS) - 对接现有 /print/execute 端点 --- .../pages/scan/components/TaskSwipeCards.vue | 4 +- .../src/pages/scan/components/TreeCanvas.vue | 48 +++++++++++++------ track-uniapp/src/pages/scan/detail.vue | 37 +++++++++++++- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/track-uniapp/src/pages/scan/components/TaskSwipeCards.vue b/track-uniapp/src/pages/scan/components/TaskSwipeCards.vue index 31b4ab9..08204e5 100644 --- a/track-uniapp/src/pages/scan/components/TaskSwipeCards.vue +++ b/track-uniapp/src/pages/scan/components/TaskSwipeCards.vue @@ -212,9 +212,9 @@ export default { .ss-overview-btn { font-size: 12px; font-weight: 700; color: #2563eb; padding: 5px 10px; background: #dbeafe; border-radius: 8px; } /* 滑动区域 */ -.ss-swiper-h { width: 100%; flex: 1; } +.ss-swiper-h { width: 100%; flex: 1; min-height: 220px; } .ss-swiper-v { width: 100%; } -.ss-card-wrapper { display: flex; align-items: flex-start; justify-content: center; padding: 10px 16px; height: 100%; box-sizing: border-box; } +.ss-card-wrapper { display: flex; align-items: flex-start; justify-content: center; padding: 10px 16px; height: 100%; min-height: 220px; box-sizing: border-box; } /* 任务卡片 */ .ss-card { position: relative; width: 100%; max-width: 420px; background: #fff; border-radius: 20px; padding: 18px 22px; box-shadow: 0 8px 24px rgba(0,0,0,0.1); display: flex; flex-direction: column; gap: 10px; max-height: 100%; overflow-y: auto; } diff --git a/track-uniapp/src/pages/scan/components/TreeCanvas.vue b/track-uniapp/src/pages/scan/components/TreeCanvas.vue index aab171b..1e370c7 100644 --- a/track-uniapp/src/pages/scan/components/TreeCanvas.vue +++ b/track-uniapp/src/pages/scan/components/TreeCanvas.vue @@ -74,11 +74,11 @@ export default { }, computed: { // ============================================================ - // 🚀 父子相对坐标延伸算法 + // 🚀 父子相对坐标延伸算法(v3 — 无重叠 Y 轴栈式布局) // ============================================================ treeNodes() { if (!this.product || !this.product.task_tree) return []; - const CARD_W = 160, CARD_H = 80, GAP_X = 24, GAP_Y = 24; + const CARD_W = 160, CARD_H = 80, GAP_X = 24, VERTICAL_GAP = 28, MAIN_GAP = 40; const CENTER_X = 0; // 拍平 + 浅拷贝 @@ -121,25 +121,39 @@ export default { allTasks.forEach(t => { if (isMainFn(t)) mains.push(t); }); mains.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); + // 🚀 预计算每个节点的子树总高度(兄弟累加,用于防重叠占位) + const calcSubtreeHeight = (nodeId) => { + const children = childMap[nodeId] || []; + if (children.length === 0) return CARD_H + VERTICAL_GAP; + const totalChildrenHeight = children.reduce((sum, c) => sum + calcSubtreeHeight(c.id), 0); + return CARD_H + VERTICAL_GAP + totalChildrenHeight; + }; + const nodes = []; - // 🚀 全新递归放置算法(相对坐标系) + // 🚀 递归放置算法:childNode.y = parentNode.y + CARD_H + VERTICAL_GAP + // 同级子节点按 index 错开 Y 轴,各占其子树高度防止重叠 const placeChildren = (parentNode, currentSide) => { const children = childMap[parentNode.id] || []; + // 🚀 首个子节点 Y 起始:parent.y 正下方 + let startY = parentNode.y + CARD_H + VERTICAL_GAP; + children.forEach((child, index) => { + const subtreeH = calcSubtreeHeight(child.id); + // 主干的第一层协助分支:均衡分发左右 let side = currentSide; if (parentNode._isMain) { side = index % 2 === 0 ? 'right' : 'left'; } - // 🚀 核心:永远基于真实父亲 (parentNode) 的坐标向外延伸! + // 🚀 X 轴:永远基于真实父亲向外延伸 const childX = side === 'right' ? parentNode.x + CARD_W + GAP_X : parentNode.x - CARD_W - GAP_X; - // 同级多子节点略微错开 Y 轴防重叠 - const childY = parentNode.y + (index * (CARD_H + 20)); + // 🚀 Y 轴:严格基于父节点向下延伸,同级按 index 错开 + const childY = startY; const childNode = { ...child, @@ -151,24 +165,30 @@ export default { nodes.push(childNode); flatMap[childNode.id] = childNode; - // 带着当前的方向(side)继续递归,保证孙子永远顺着儿子的方向长 + // 递归放置孙子节点(沿相同方向) placeChildren(childNode, side); + + // 🚀 下一个兄弟节点跳到当前子树高度之后,严禁 Y 坐标重叠 + startY += subtreeH + VERTICAL_GAP; }); }; - // 🚀 渲染入口 + // 🚀 渲染入口:主干节点也按子树深度排布 + let mainStartY = 0; mains.forEach((m, mi) => { - const rootY = mi * (CARD_H + GAP_Y) * 2; - const mainNode = { ...m, x: CENTER_X, y: rootY, _isMain: true, _blabel: '主分支', _rootMainId: m.id }; + const subtreeH = calcSubtreeHeight(m.id); + const mainNode = { ...m, x: CENTER_X, y: mainStartY, _isMain: true, _blabel: '主分支', _rootMainId: m.id }; nodes.push(mainNode); flatMap[mainNode.id] = mainNode; placeChildren(mainNode, null); + mainStartY += subtreeH + MAIN_GAP; }); - // 全局偏移 + // 全局偏移(给左侧分支预留空间) const minX = Math.min(...nodes.map(n => n.x)); + const minY = Math.min(...nodes.map(n => n.y)); const padding = 40; - nodes.forEach(n => { n.x += Math.abs(minX) + CARD_W * 2 + padding; n.y += padding; }); + nodes.forEach(n => { n.x += Math.abs(minX) + CARD_W * 2 + padding; n.y += padding - Math.min(0, minY); }); return nodes; }, // ============================================================ @@ -215,8 +235,8 @@ export default { return result; }, // ============================================================ - canvasWidth() { const ns = this.treeNodes; if (!ns.length) return 400; return Math.max(...ns.map(n => n.x)) + 200; }, - canvasHeight() { const ns = this.treeNodes; if (!ns.length) return 400; return Math.max(...ns.map(n => n.y)) + 200; }, + canvasWidth() { const ns = this.treeNodes; if (!ns.length) return 800; return Math.max(...ns.map(n => n.x)) + 400; }, + canvasHeight() { const ns = this.treeNodes; if (!ns.length) return 800; return Math.max(...ns.map(n => n.y)) + 300; }, }, methods: { formatUserName, diff --git a/track-uniapp/src/pages/scan/detail.vue b/track-uniapp/src/pages/scan/detail.vue index b6e174e..b1c429a 100644 --- a/track-uniapp/src/pages/scan/detail.vue +++ b/track-uniapp/src/pages/scan/detail.vue @@ -19,6 +19,7 @@ {{ modeToggleLabel }} ✏️ + 🖨️ 打印标签 @@ -246,6 +247,8 @@ export default { msgUnreadCount() { if (!this.lastMsgSeenAt) return this.messages.length; return this.messages.filter(m => m.created_at > this.lastMsgSeenAt).length; }, }, onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) this.doQuery(sn); }, + // 🚀 onShow 生命周期:每次页面显示时刷新留言板(解决从聊天室退回不更新问题) + onShow() { if (this.product?.id) { this.fetchMessages(); } }, methods: { formatUserName, formatUserAvatar, formatName(name) { if (!name) return ""; return name.length === 2 ? name[0] + " " + name[1] : name; }, @@ -326,6 +329,37 @@ export default { closeMsgDrawer() { const last = this.messages[this.messages.length - 1]; this.lastMsgSeenAt = last ? last.created_at : new Date().toISOString(); this.showMsgDrawer = false; }, scrollToBottom() { this.$nextTick(() => { this.bottomMsgId = 'msg-bottom'; }); }, fmtMsgTime(d) { if (!d) return ''; const dt = new Date(d); const pad = (n) => String(n).padStart(2, '0'); return `${pad(dt.getMonth()+1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`; }, + // 🖨️ 打印标签:调用后端 API 发送打印指令 + async printLabel() { + if (!this.product?.serial_number) return; + uni.showActionSheet({ + itemList: ['网络打印机(后端API)', '蓝牙打印机(ESC/POS)'], + success: async (res) => { + if (res.tapIndex === 0) { + // 方案A:网络打印机 → 调用后端 /print/execute API + try { + uni.showLoading({ title: '发送打印指令...' }); + await post(`/print/execute`, { + serial_number: this.product.serial_number, + material_name: this.product.material_name || '', + spec_model: this.product.spec_model || '', + order_no: this.product.order_no || '', + copies: 1, + }); + uni.hideLoading(); + uni.showToast({ title: '打印指令已发送', icon: 'success' }); + } catch (e) { + uni.hideLoading(); + uni.showToast({ title: e?.data?.detail || '打印失败', icon: 'none' }); + } + } else if (res.tapIndex === 1) { + // 方案B:蓝牙打印机 → 前端直连 ESC/POS 指令 + // ⚠️ 需要引入蓝牙打印 SDK,当前为占位架构 + uni.showToast({ title: '蓝牙打印功能开发中', icon: 'none' }); + } + }, + }); + }, }, }; @@ -339,12 +373,13 @@ export default { .overall-val { font-size: 15px; font-weight: 700; color: #2563eb; flex: 1; } .overall-empty { color: #ef4444; } .overall-arrow { font-size: 12px; color: #9ca3af; } -.card { background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); flex-shrink: 0; } +.card { background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); flex-shrink: 0; min-height: 120px; } .card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .card-header-right { display: flex; align-items: center; gap: 8px; } .card-title { font-size: 15px; font-weight: 700; } .edit-btn { font-size: 18px; padding: 2px 6px; } .mode-toggle { font-size: 13px; font-weight: 700; padding: 4px 10px; border-radius: 8px; background: #eff6ff; color: #2563eb; } +.print-label-btn { font-size: 12px; font-weight: 700; padding: 4px 8px; border-radius: 8px; background: #fef3c7; color: #b45309; margin-left: 4px; } .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .label { font-size: 12px; color: #9ca3af; } .value { font-size: 14px; color: #1f2937; font-weight: 600; word-break: break-all; }