fix(frontend): 流转蓝图 + 聊天室 + 卡片布局 + 打印功能修复

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 端点
This commit is contained in:
2026-08-11 15:05:39 +08:00
parent 88dc7381f6
commit 30a48d90fc
3 changed files with 72 additions and 17 deletions

View File

@ -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; }

View File

@ -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,

View File

@ -19,6 +19,7 @@
<view class="card-header-right">
<text class="mode-toggle" @tap="toggleMode">{{ modeToggleLabel }}</text>
<text class="edit-btn" @tap="openEditProduct"></text>
<text class="print-label-btn" @tap="printLabel">🖨 打印标签</text>
</view>
</view>
<view class="info-grid">
@ -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' });
}
},
});
},
},
};
</script>
@ -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; }