组件化拆分: TreeCanvas(2D画布) + WorkspaceArea(任务列表+锁定工作区) + detail瘦身(仅弹窗)
This commit is contained in:
101
track-uniapp/src/pages/scan/components/TreeCanvas.vue
Normal file
101
track-uniapp/src/pages/scan/components/TreeCanvas.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<view class="tree-area">
|
||||
<view v-if="!product.task_tree || !product.task_tree.length" class="zero-task">
|
||||
<text class="zero-icon">📋</text>
|
||||
<text class="zero-text">该产品暂无流转任务</text>
|
||||
</view>
|
||||
<template v-else>
|
||||
<view class="canvas-hint">🖐 2D 蓝图画布:支持随意拖拽探索分支走向</view>
|
||||
<movable-area class="tree-movable-area">
|
||||
<movable-view class="tree-movable-view" direction="all" :x="0" :y="0"
|
||||
:scale="true" :scale-min="0.5" :scale-max="2"
|
||||
:style="{ width: Math.max(canvasWidth, 1200) + 'px', height: Math.max(canvasHeight, 1200) + 'px' }">
|
||||
<view class="canvas-inner">
|
||||
<view v-for="line in treeLines" :key="line.key" class="canvas-line" :style="lineStyle(line)"></view>
|
||||
<view v-for="node in treeNodes" :key="node.id" class="canvas-node" :class="statusColor(node.status)"
|
||||
:style="{ left: node.x + 'px', top: node.y + 'px' }"
|
||||
@tap="node.records && node.records.length && $emit('viewRecords', node)">
|
||||
<view class="cn-header">
|
||||
<text class="cn-name">{{ node.task_name }}</text>
|
||||
<text :class="['cn-badge', statusColor(node.status)]">{{ statusLabel(node.status) }}</text>
|
||||
</view>
|
||||
<text class="cn-assignee">负责人: {{ node.assignee_id || '—' }}</text>
|
||||
<text v-if="getTaskRemark(node)" class="cn-remark">📌 {{ getTaskRemark(node) }}</text>
|
||||
<view class="cn-footer">
|
||||
<text v-if="node.status==='COMPLETED' && (!node.child_tasks || !node.child_tasks.length)" class="cn-end">🏁 终止</text>
|
||||
<text v-if="node.status==='ARCHIVED'" class="cn-end cn-end-wh">📦 入库</text>
|
||||
<text v-if="node.records && node.records.length" class="cn-records">📋 {{ node.records.length }}记录 ›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</movable-view>
|
||||
</movable-area>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const STATUS_MAP = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
|
||||
|
||||
export default {
|
||||
name: "TreeCanvas",
|
||||
props: { product: { type: Object, default: null } },
|
||||
emits: ["viewRecords"],
|
||||
computed: {
|
||||
treeNodes() {
|
||||
const nodes = []; const CARD_W = 200, CARD_H = 140, GAP_X = 60, GAP_Y = 60;
|
||||
const layout = (tasks, depth, startX) => {
|
||||
if (!tasks || !tasks.length) return startX;
|
||||
const y = 20 + depth * (CARD_H + GAP_Y); let x = startX; const childXs = [];
|
||||
for (const t of tasks) {
|
||||
nodes.push({ ...t, x, y }); const childStartX = x;
|
||||
if (t.child_tasks && t.child_tasks.length) { x = layout(t.child_tasks, depth + 1, x); } else { x += CARD_W + GAP_X; }
|
||||
childXs.push({ id: t.id, cx: childStartX + CARD_W / 2, cy: y + CARD_H, children: t.child_tasks || [] });
|
||||
}
|
||||
nodes._lines = nodes._lines || [];
|
||||
for (const cx of childXs) { if (cx.children.length) { const midX = cx.cx; for (const c of cx.children) { const childNode = nodes.find(n => n.id === c.id); if (childNode) { nodes._lines.push({ key: cx.id + '-' + c.id, x1: midX, y1: cx.cy, x2: childNode.x + CARD_W / 2, y2: childNode.y }); } } } }
|
||||
return x;
|
||||
};
|
||||
if (this.product && this.product.task_tree) { nodes._lines = []; layout(this.product.task_tree, 0, 40); }
|
||||
return nodes;
|
||||
},
|
||||
treeLines() { return this.treeNodes._lines || []; },
|
||||
canvasWidth() { const nodes = this.treeNodes; if (!nodes.length) return 400; return Math.max(400, Math.max(...nodes.map(n => n.x)) + 300); },
|
||||
canvasHeight() { const nodes = this.treeNodes; if (!nodes.length) return 400; return Math.max(400, Math.max(...nodes.map(n => n.y)) + 300); },
|
||||
},
|
||||
methods: {
|
||||
statusLabel(s) { return STATUS_MAP[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"; default: return "s-gray"; } },
|
||||
getTaskRemark(t) { if (!t) return ""; if (t.remark) return t.remark; if (t.records && t.records.length > 0) { const recs = [...t.records].reverse(); const rec = recs.find(r => r.remark && r.remark.length > 0); if (rec) return rec.remark; } return ""; },
|
||||
lineStyle(line) { const dx = line.x2 - line.x1; const dy = line.y2 - line.y1; const len = Math.sqrt(dx * dx + dy * dy); const angle = Math.atan2(dy, dx) * 180 / Math.PI; return { left: line.x1 + 'px', top: line.y1 + 'px', width: len + 'px', transform: `rotate(${angle}deg)`, transformOrigin: '0 0' }; },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tree-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; margin-top: 4px; }
|
||||
.canvas-hint { font-size: 11px; color: #9ca3af; text-align: center; padding: 4px 0; flex-shrink: 0; }
|
||||
.tree-movable-area { flex: 1; width: 100%; background-color: #ebedf0; border-radius: 12px; overflow: hidden; box-shadow: inset 0 0 20px rgba(0,0,0,0.05); }
|
||||
.canvas-inner { position: absolute; left: 0; top: 0; }
|
||||
.canvas-node { position: absolute; width: 180px; min-height: 100px; background: #ffffff; border-radius: 12px; padding: 14px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); border-left: 6px solid #3b82f6; border-top: none; z-index: 10; display: flex; flex-direction: column; gap: 6px; }
|
||||
.canvas-node.s-yellow { border-left-color: #f59e0b; }
|
||||
.canvas-node.s-blue { border-left-color: #3b82f6; }
|
||||
.canvas-node.s-green { border-left-color: #22c55e; }
|
||||
.canvas-node.s-red { border-left-color: #ef4444; }
|
||||
.cn-header { display: flex; align-items: center; justify-content: space-between; }
|
||||
.cn-name { font-size: 14px; font-weight: 800; color: #1f2937; }
|
||||
.cn-badge { font-size: 10px; padding: 2px 6px; border-radius: 8px; font-weight: 600; }
|
||||
.cn-badge.s-yellow { background: #fef3c7; color: #b45309; }
|
||||
.cn-badge.s-blue { background: #dbeafe; color: #1d4ed8; }
|
||||
.cn-badge.s-green { background: #dcfce7; color: #15803d; }
|
||||
.cn-assignee { font-size: 11px; color: #6b7280; }
|
||||
.cn-remark { font-size: 11px; color: #a16207; background: #fefce8; padding: 4px 8px; border-radius: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.cn-footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; padding-top: 6px; border-top: 1px solid #f3f4f6; }
|
||||
.cn-end { font-size: 10px; font-weight: 600; color: #16a34a; }
|
||||
.cn-end-wh { color: #7c3aed; }
|
||||
.cn-records { font-size: 11px; color: #2563eb; font-weight: 600; }
|
||||
.canvas-line { position: absolute; height: 3px; background: #94a3b8; z-index: 1; border-radius: 2px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
|
||||
.zero-task { display: flex; flex-direction: column; align-items: center; padding: 24px 0; }
|
||||
.zero-icon { font-size: 40px; margin-bottom: 8px; }
|
||||
.zero-text { font-size: 14px; color: #9ca3af; }
|
||||
</style>
|
||||
192
track-uniapp/src/pages/scan/components/WorkspaceArea.vue
Normal file
192
track-uniapp/src/pages/scan/components/WorkspaceArea.vue
Normal file
@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<view class="workspace-area">
|
||||
<!-- 0任务 -->
|
||||
<view v-if="!product.task_tree || !product.task_tree.length" class="zero-task">
|
||||
<text class="zero-icon">📋</text>
|
||||
<text class="zero-text">该产品暂无流转任务</text>
|
||||
</view>
|
||||
|
||||
<!-- 无本人任务 -->
|
||||
<view v-else-if="!focusTasks.length" class="zero-task">
|
||||
<text class="zero-icon">🔒</text>
|
||||
<text class="zero-text">当前没有需要你处理的任务</text>
|
||||
<text class="task-count">共 {{ countTasks(product.task_tree) }} 个任务</text>
|
||||
</view>
|
||||
|
||||
<!-- 任务列表(未锁定) -->
|
||||
<template v-else-if="!lockedTaskId">
|
||||
<view class="list-header">
|
||||
<text class="list-title">📋 待处理任务 ({{ focusTasks.length }})</text>
|
||||
<text class="list-hint">点击任务进入绝对锁定工作区</text>
|
||||
</view>
|
||||
<view class="task-list">
|
||||
<view v-for="t in focusTasks" :key="t.id" class="task-list-item"
|
||||
:class="statusColor(t.status)" @tap="lockTask(t.id)">
|
||||
<view class="tli-left">
|
||||
<text class="tli-name">{{ t.task_name }}</text>
|
||||
<text v-if="getTaskRemark(t)" class="tli-remark">{{ getTaskRemark(t) }}</text>
|
||||
</view>
|
||||
<view class="tli-right">
|
||||
<text :class="['tli-badge', statusColor(t.status)]">{{ statusLabel(t.status) }}</text>
|
||||
<text v-if="t.is_rework" class="tag tag-rework-sm" style="margin-top:2px;">⚠返工</text>
|
||||
<text class="tli-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 锁定工作区(单任务详情) -->
|
||||
<template v-else>
|
||||
<view class="lock-bar">
|
||||
<text class="lock-back" @tap="lockedTaskId = null">← 返回列表</text>
|
||||
<text class="lock-title">🔒 沉浸工作区</text>
|
||||
</view>
|
||||
|
||||
<view class="focus-card" :class="statusColor(lockedTask.status)">
|
||||
<view class="fc-header">
|
||||
<text class="fc-status">{{ statusLabel(lockedTask.status) }}</text>
|
||||
<text v-if="lockedTask.is_rework" class="tag tag-rework-sm">⚠返工</text>
|
||||
</view>
|
||||
<text class="fc-name">{{ lockedTask.task_name }}</text>
|
||||
|
||||
<view v-if="getTaskRemark(lockedTask)" style="margin-top:20rpx;padding:20rpx;background:#FFFBE8;border-left:8rpx solid #FADB14;border-radius:12rpx;">
|
||||
<text style="font-size:26rpx;color:#8C6A00;font-weight:bold;">📌 初始/交接备注:</text>
|
||||
<view style="font-size:28rpx;color:#333;margin-top:10rpx;">{{ getTaskRemark(lockedTask) }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="lockedTask.parent_task_id && parentTaskOf(lockedTask)" class="fc-link fc-up">
|
||||
<text class="fc-link-label">⬆ 上游工序</text>
|
||||
<text class="fc-link-name">{{ parentTaskOf(lockedTask).task_name }} → {{ parentTaskOf(lockedTask).assignee_id || '—' }}</text>
|
||||
</view>
|
||||
<view v-if="lockedTask.child_tasks && lockedTask.child_tasks.length" class="fc-link fc-down">
|
||||
<text class="fc-link-label">⬇ 下游分支 ({{ lockedTask.child_tasks.length }})</text>
|
||||
<text v-for="c in lockedTask.child_tasks" :key="c.id" class="fc-link-name">
|
||||
· {{ c.task_name }} → {{ c.assignee_id || '—' }}
|
||||
<text v-if="c.status==='COMPLETED'" class="branch-done">✓已完成</text>
|
||||
<text v-else-if="c.status==='ARCHIVED'" class="branch-done">📦已入库</text>
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="lockedTask.records && lockedTask.records.length" class="fc-records-bar" @tap="$emit('viewRecords', lockedTask)">
|
||||
📋 {{ lockedTask.records.length }} 条干活记录 ›
|
||||
</view>
|
||||
<view class="fc-time">{{ formatTaskTime(lockedTask) }}</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-actions">
|
||||
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-transfer"
|
||||
@tap="$emit('action', { task: lockedTask, type: 'transfer' })">🔄 完工转交</button>
|
||||
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-record"
|
||||
@tap="$emit('action', { task: lockedTask, type: 'record' })">📝 记录/拍照</button>
|
||||
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-end"
|
||||
@tap="$emit('action', { task: lockedTask, type: 'end' })">🏁 结束分支</button>
|
||||
<button v-if="lockedTask.status === 'PENDING'" class="footer-btn footer-receive"
|
||||
@tap="$emit('action', { task: lockedTask, type: 'receive' })">✅ 接收任务</button>
|
||||
<button v-if="lockedTask.status === 'PENDING'" class="footer-btn footer-reject"
|
||||
@tap="$emit('action', { task: lockedTask, type: 'reject' })">❌ 驳回任务</button>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const STATUS_MAP = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
|
||||
|
||||
export default {
|
||||
name: "WorkspaceArea",
|
||||
props: {
|
||||
product: { type: Object, default: null },
|
||||
currentUserId: { type: String, default: "" },
|
||||
currentUsername: { type: String, default: "" },
|
||||
},
|
||||
emits: ["action", "viewRecords"],
|
||||
data() {
|
||||
return { lockedTaskId: null };
|
||||
},
|
||||
computed: {
|
||||
taskMap() {
|
||||
const m = {};
|
||||
const walk = (tasks) => { if (!tasks) return; for (const t of tasks) { m[t.id] = t; walk(t.child_tasks); } };
|
||||
if (this.product) walk(this.product.task_tree);
|
||||
return m;
|
||||
},
|
||||
focusTasks() {
|
||||
const result = [];
|
||||
const walk = (tasks) => {
|
||||
if (!tasks) return;
|
||||
for (const t of tasks) {
|
||||
const isMine = t.status === 'WIP' || t.status === 'PENDING';
|
||||
if (isMine && (t.assignee_id == this.currentUserId || t.assignee_id == this.currentUsername)) result.push(t);
|
||||
walk(t.child_tasks);
|
||||
}
|
||||
};
|
||||
if (this.product) walk(this.product.task_tree);
|
||||
return result;
|
||||
},
|
||||
lockedTask() { return this.focusTasks.find(t => t.id === this.lockedTaskId) || null; },
|
||||
},
|
||||
methods: {
|
||||
statusLabel(s) { return STATUS_MAP[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"; default: return "s-gray"; } },
|
||||
countTasks(tree) { return tree ? tree.reduce((s, t) => s + 1 + this.countTasks(t.child_tasks), 0) : 0; },
|
||||
getTaskRemark(t) { if (!t) return ""; if (t.remark) return t.remark; if (t.records && t.records.length > 0) { const recs = [...t.records].reverse(); const rec = recs.find(r => r.remark && r.remark.length > 0); if (rec) return rec.remark; } return ""; },
|
||||
lockTask(taskId) { this.lockedTaskId = taskId; },
|
||||
parentTaskOf(t) { return t && t.parent_task_id ? (this.taskMap[t.parent_task_id] || null) : null; },
|
||||
formatTaskTime(t) { if (!t) return ""; const parts = []; if (t.created_at) parts.push("创建: " + this.formatTime(t.created_at)); if (t.received_at) parts.push("接收: " + this.formatTime(t.received_at)); return parts.join(" | "); },
|
||||
formatTime(d) { if (!d) return ""; const dt = new Date(d); const pad = (n) => String(n).padStart(2, "0"); return `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`; },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.workspace-area { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||
.list-header { display: flex; align-items: baseline; justify-content: space-between; padding: 8px 4px; flex-shrink: 0; }
|
||||
.list-title { font-size: 14px; font-weight: 700; color: #1f2937; }
|
||||
.list-hint { font-size: 11px; color: #9ca3af; }
|
||||
.task-list { flex: 1; overflow-y: auto; min-height: 0; }
|
||||
.task-list-item { display: flex; align-items: flex-start; justify-content: space-between; background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border-left: 4px solid transparent; }
|
||||
.task-list-item.s-yellow { border-left-color: #f59e0b; }
|
||||
.task-list-item.s-blue { border-left-color: #3b82f6; }
|
||||
.tli-left { flex: 1; min-width: 0; }
|
||||
.tli-name { font-size: 15px; font-weight: 700; color: #1f2937; display: block; }
|
||||
.tli-remark { font-size: 12px; color: #a16207; font-weight: bold; margin-top: 4px; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.tli-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; margin-left: 8px; }
|
||||
.tli-badge { font-size: 11px; padding: 2px 10px; border-radius: 12px; font-weight: 600; }
|
||||
.tli-badge.s-yellow { background: #fef3c7; color: #b45309; }
|
||||
.tli-badge.s-blue { background: #dbeafe; color: #1d4ed8; }
|
||||
.tli-arrow { font-size: 18px; color: #d1d5db; }
|
||||
.lock-bar { display: flex; align-items: center; gap: 12px; padding: 8px 0; flex-shrink: 0; }
|
||||
.lock-back { font-size: 13px; color: #2563eb; font-weight: 600; }
|
||||
.lock-title { font-size: 14px; font-weight: 700; color: #1f2937; }
|
||||
.branch-done { font-size: 11px; font-weight: 600; color: #16a34a; }
|
||||
.task-count { font-size: 12px; color: #9ca3af; display: block; margin-top: 4px; }
|
||||
.focus-card { height: 100%; margin: 0 4px; padding: 20px 16px; border-radius: 16px; background: #fff; box-shadow: 0 2px 12px rgba(0,0,0,0.08); overflow-y: auto; border-top: 5px solid #3b82f6; }
|
||||
.focus-card.s-yellow { border-top-color: #f59e0b; }
|
||||
.focus-card.s-blue { border-top-color: #3b82f6; }
|
||||
.focus-card.s-green { border-top-color: #22c55e; }
|
||||
.focus-card.s-red { border-top-color: #ef4444; }
|
||||
.fc-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.fc-status { font-size: 12px; padding: 2px 10px; border-radius: 12px; font-weight: 700; background: #f3f4f6; color: #6b7280; }
|
||||
.s-yellow .fc-status { background: #fef3c7; color: #b45309; }
|
||||
.s-blue .fc-status { background: #dbeafe; color: #1d4ed8; }
|
||||
.tag-rework-sm { font-size: 10px; padding: 2px 6px; border-radius: 8px; background: #ef4444; color: #fff; }
|
||||
.fc-name { font-size: 22px; font-weight: 800; color: #1f2937; display: block; margin-bottom: 10px; }
|
||||
.fc-link { padding: 6px 10px; border-radius: 8px; margin-bottom: 4px; font-size: 12px; margin-top: 10px; }
|
||||
.fc-up { background: #f0fdf4; color: #16a34a; }
|
||||
.fc-down { background: #eff6ff; color: #2563eb; }
|
||||
.fc-link-label { font-weight: 700; display: block; }
|
||||
.fc-link-name { display: block; margin-top: 2px; }
|
||||
.fc-records-bar { padding: 8px 12px; background: linear-gradient(135deg,#eff6ff,#dbeafe); border-radius: 8px; font-size: 13px; font-weight: 700; color: #2563eb; margin-top: 10px; margin-bottom: 8px; }
|
||||
.fc-time { font-size: 11px; color: #9ca3af; margin-top: auto; padding-top: 8px; border-top: 1px solid #f3f4f6; }
|
||||
.zero-task { display: flex; flex-direction: column; align-items: center; padding: 24px 0; }
|
||||
.zero-icon { font-size: 40px; margin-bottom: 8px; }
|
||||
.zero-text { font-size: 14px; color: #9ca3af; }
|
||||
.footer-actions { display: flex; gap: 20rpx; padding: 20rpx 30rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05); flex-shrink: 0; }
|
||||
.footer-btn { flex: 1; height: 88rpx; border: none; border-radius: 12rpx; font-size: 30rpx; font-weight: 700; line-height: 88rpx; }
|
||||
.footer-btn::after { border: none; }
|
||||
.footer-transfer { background: #dcfce7; color: #16a34a; }
|
||||
.footer-record { background: #eff6ff; color: #2563eb; }
|
||||
.footer-receive { background: #dbeafe; color: #1d4ed8; }
|
||||
.footer-reject { background: #fce4ec; color: #dc2626; }
|
||||
.footer-end { background: #fef3c7; color: #b45309; }
|
||||
</style>
|
||||
@ -6,9 +6,7 @@
|
||||
<template v-if="product && !loading">
|
||||
<view class="overall-bar" @tap="showStatusPicker = true">
|
||||
<text class="overall-label">宏观状态</text>
|
||||
<text :class="['overall-val', product.overall_status ? '' : 'overall-empty']">
|
||||
{{ product.overall_status || '点击设定' }}
|
||||
</text>
|
||||
<text :class="['overall-val', product.overall_status ? '' : 'overall-empty']">{{ product.overall_status || '点击设定' }}</text>
|
||||
<text class="overall-arrow">▾</text>
|
||||
</view>
|
||||
|
||||
@ -17,9 +15,7 @@
|
||||
<text class="card-title">📦 产品信息</text>
|
||||
<view class="card-header-right">
|
||||
<text :class="['badge', statusColor(product.status)]">{{ statusLabel(product.status) }}</text>
|
||||
<text class="mode-toggle" @tap="toggleMode">
|
||||
{{ currentMode === 'workspace' ? '🌳 流转树' : '🛠️ 工作区' }}
|
||||
</text>
|
||||
<text class="mode-toggle" @tap="toggleMode">{{ currentMode === 'workspace' ? '🌳 流转树' : '🛠️ 工作区' }}</text>
|
||||
<text class="edit-btn" @tap="openEditProduct">✏️</text>
|
||||
</view>
|
||||
</view>
|
||||
@ -30,116 +26,16 @@
|
||||
<view class="info-item"><text class="label">订单编号</text><text class="value">{{ product.order_no || '—' }}</text></view>
|
||||
<view class="info-item" v-if="product.current_location_id">
|
||||
<text class="label">当前位置</text>
|
||||
<text :class="['value', product.current_location_id === 'virtual_warehouse' ? 'warehouse' : '']">
|
||||
{{ product.current_location_id === 'virtual_warehouse' ? '🏭 仓库' : product.current_location_id }}
|
||||
</text>
|
||||
<text :class="['value', product.current_location_id === 'virtual_warehouse' ? 'warehouse' : '']">{{ product.current_location_id === 'virtual_warehouse' ? '🏭 仓库' : product.current_location_id }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 🛠️ 个人工作区 -->
|
||||
<view v-if="currentMode === 'workspace'" class="workspace-area">
|
||||
<view v-if="!product.task_tree || !product.task_tree.length" class="zero-task">
|
||||
<text class="zero-icon">📋</text><text class="zero-text">该产品暂无流转任务</text>
|
||||
<button class="btn-start" @tap="openCreateFirstTask">🚀 发起首道工序</button>
|
||||
</view>
|
||||
<view v-else-if="!focusTasks.length" class="zero-task">
|
||||
<text class="zero-icon">🔒</text><text class="zero-text">当前没有需要你处理的任务</text>
|
||||
<text class="task-count">共 {{ countTasks(product.task_tree) }} 个任务</text>
|
||||
</view>
|
||||
<template v-else-if="!lockedTaskId">
|
||||
<view class="list-header">
|
||||
<text class="list-title">📋 待处理任务 ({{ focusTasks.length }})</text>
|
||||
<text class="list-hint">点击任务进入绝对锁定工作区</text>
|
||||
</view>
|
||||
<view class="task-list">
|
||||
<view v-for="t in focusTasks" :key="t.id" class="task-list-item" :class="statusColor(t.status)" @tap="lockTask(t.id)">
|
||||
<view class="tli-left">
|
||||
<text class="tli-name">{{ t.task_name }}</text>
|
||||
<text v-if="getTaskRemark(t)" class="tli-remark">{{ getTaskRemark(t) }}</text>
|
||||
</view>
|
||||
<view class="tli-right">
|
||||
<text :class="['tli-badge', statusColor(t.status)]">{{ statusLabel(t.status) }}</text>
|
||||
<text v-if="t.is_rework" class="tag tag-rework-sm" style="margin-top:2px;">⚠返工</text>
|
||||
<text class="tli-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="lock-bar">
|
||||
<text class="lock-back" @tap="lockedTaskId = null">← 返回列表</text>
|
||||
<text class="lock-title">🔒 沉浸工作区</text>
|
||||
</view>
|
||||
<view class="focus-card" :class="statusColor(lockedTask.status)">
|
||||
<view class="fc-header">
|
||||
<text class="fc-status">{{ statusLabel(lockedTask.status) }}</text>
|
||||
<text v-if="lockedTask.is_rework" class="tag tag-rework-sm">⚠返工</text>
|
||||
</view>
|
||||
<text class="fc-name">{{ lockedTask.task_name }}</text>
|
||||
<view v-if="getTaskRemark(lockedTask)" style="margin-top:20rpx;padding:20rpx;background:#FFFBE8;border-left:8rpx solid #FADB14;border-radius:12rpx;">
|
||||
<text style="font-size:26rpx;color:#8C6A00;font-weight:bold;">📌 初始/交接备注:</text>
|
||||
<view style="font-size:28rpx;color:#333;margin-top:10rpx;">{{ getTaskRemark(lockedTask) }}</view>
|
||||
</view>
|
||||
<view v-if="lockedTask.parent_task_id && parentTaskOf(lockedTask)" class="fc-link fc-up">
|
||||
<text class="fc-link-label">⬆ 上游工序</text>
|
||||
<text class="fc-link-name">{{ parentTaskOf(lockedTask).task_name }} → {{ parentTaskOf(lockedTask).assignee_id || '—' }}</text>
|
||||
</view>
|
||||
<view v-if="lockedTask.child_tasks && lockedTask.child_tasks.length" class="fc-link fc-down">
|
||||
<text class="fc-link-label">⬇ 下游分支 ({{ lockedTask.child_tasks.length }})</text>
|
||||
<text v-for="c in lockedTask.child_tasks" :key="c.id" class="fc-link-name">
|
||||
· {{ c.task_name }} → {{ c.assignee_id || '—' }}
|
||||
<text v-if="c.status==='COMPLETED'" class="branch-done">✓已完成</text>
|
||||
<text v-else-if="c.status==='ARCHIVED'" class="branch-done">📦已入库</text>
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="lockedTask.records && lockedTask.records.length" class="fc-records-bar" @tap="handleViewRecords(lockedTask)">
|
||||
📋 {{ lockedTask.records.length }} 条干活记录 ›
|
||||
</view>
|
||||
<view class="fc-time">{{ formatTaskTime(lockedTask) }}</view>
|
||||
</view>
|
||||
<view class="footer-actions">
|
||||
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-transfer" @tap="handleTaskAction({ task: lockedTask, type: 'transfer' })">🔄 完工转交</button>
|
||||
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-record" @tap="handleTaskAction({ task: lockedTask, type: 'record' })">📝 记录/拍照</button>
|
||||
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-end" @tap="handleTaskAction({ task: lockedTask, type: 'end' })">🏁 结束分支</button>
|
||||
<button v-if="lockedTask.status === 'PENDING'" class="footer-btn footer-receive" @tap="handleTaskAction({ task: lockedTask, type: 'receive' })">✅ 接收任务</button>
|
||||
<button v-if="lockedTask.status === 'PENDING'" class="footer-btn footer-reject" @tap="handleTaskAction({ task: lockedTask, type: 'reject' })">❌ 驳回任务</button>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
<WorkspaceArea v-if="currentMode === 'workspace'" :product="product"
|
||||
:currentUserId="currentUserId" :currentUsername="currentUsername"
|
||||
@action="handleTaskAction" @viewRecords="handleViewRecords" />
|
||||
|
||||
<!-- 🌳 全局流转树 (2D movable-area 画布) -->
|
||||
<view v-if="currentMode === 'tree'" class="tree-area">
|
||||
<view v-if="!product.task_tree || !product.task_tree.length" class="zero-task">
|
||||
<text class="zero-icon">📋</text><text class="zero-text">该产品暂无流转任务</text>
|
||||
</view>
|
||||
<template v-else>
|
||||
<view class="canvas-hint">🖐 2D 蓝图画布:支持随意拖拽探索分支走向</view>
|
||||
<movable-area class="tree-movable-area">
|
||||
<movable-view class="tree-movable-view" direction="all" :x="0" :y="0" :scale="true" :scale-min="0.5" :scale-max="2"
|
||||
:style="{ width: Math.max(canvasWidth, 1200) + 'px', height: Math.max(canvasHeight, 1200) + 'px' }">
|
||||
<view class="canvas-inner">
|
||||
<view v-for="line in treeLines" :key="line.key" class="canvas-line" :style="lineStyle(line)"></view>
|
||||
<view v-for="node in treeNodes" :key="node.id" class="canvas-node" :class="statusColor(node.status)"
|
||||
:style="{ left: node.x + 'px', top: node.y + 'px' }"
|
||||
@tap="node.records && node.records.length && handleViewRecords(node)">
|
||||
<view class="cn-header">
|
||||
<text class="cn-name">{{ node.task_name }}</text>
|
||||
<text :class="['cn-badge', statusColor(node.status)]">{{ statusLabel(node.status) }}</text>
|
||||
</view>
|
||||
<text class="cn-assignee">负责人: {{ node.assignee_id || '—' }}</text>
|
||||
<text v-if="getTaskRemark(node)" class="cn-remark">📌 {{ getTaskRemark(node) }}</text>
|
||||
<view class="cn-footer">
|
||||
<text v-if="node.status==='COMPLETED' && (!node.child_tasks || !node.child_tasks.length)" class="cn-end">🏁 终止</text>
|
||||
<text v-if="node.status==='ARCHIVED'" class="cn-end cn-end-wh">📦 入库</text>
|
||||
<text v-if="node.records && node.records.length" class="cn-records">📋 {{ node.records.length }}记录 ›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</movable-view>
|
||||
</movable-area>
|
||||
</template>
|
||||
</view>
|
||||
<TreeCanvas v-if="currentMode === 'tree'" :product="product" @viewRecords="handleViewRecords" />
|
||||
</template>
|
||||
|
||||
<!-- 状态定调 -->
|
||||
@ -187,7 +83,7 @@
|
||||
<view v-for="n in recordForm.pendingCount" :key="'p'+n" class="img-cell img-cell-loading"><text class="img-loading-text">⏳</text></view>
|
||||
</view>
|
||||
<button v-if="recordForm.images.length + recordForm.pendingCount < 9" class="btn-upload" @tap="handleChooseImage" :disabled="isUploading">{{ isUploading ? '上传中...' : `📷 拍照/选图 (${recordForm.images.length + recordForm.pendingCount}/9)` }}</button>
|
||||
<view class="popup-btns"><button class="btn-cancel" @tap="closeRecordPopup">取消</button><button class="btn-primary" :disabled="recordSaving || isUploading" @tap="doSaveRecord">{{ isUploading ? `上传中` : (recordSaving ? '保存中...' : (recordForm.recordId ? '更新记录' : '保存记录')) }}</button></view>
|
||||
<view class="popup-btns"><button class="btn-cancel" @tap="closeRecordPopup">取消</button><button class="btn-primary" :disabled="recordSaving || isUploading" @tap="doSaveRecord">{{ isUploading ? '上传中' : (recordSaving ? '保存中...' : (recordForm.recordId ? '更新记录' : '保存记录')) }}</button></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 任务操作 -->
|
||||
@ -226,12 +122,15 @@
|
||||
|
||||
<script>
|
||||
import request, { get, post, patch, put } from "../../utils/request";
|
||||
import WorkspaceArea from "./components/WorkspaceArea.vue";
|
||||
import TreeCanvas from "./components/TreeCanvas.vue";
|
||||
|
||||
const OVERALL_OPTIONS = ["备货", "生产", "测试", "维修", "在库"];
|
||||
const TASK_NAME_OPTIONS = ["备货", "生产", "测试", "维修", "在库"];
|
||||
const STATUS_MAP = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
|
||||
|
||||
export default {
|
||||
components: { WorkspaceArea, TreeCanvas },
|
||||
data() {
|
||||
return {
|
||||
OVERALL_OPTIONS, loading: true, error: "", product: null,
|
||||
@ -239,7 +138,7 @@ export default {
|
||||
users: [], TASK_NAME_OPTIONS,
|
||||
createFirstVisible: false, firstForm: { task_name: "", taskNameIdx: 0, assignee_id: "", assigneeLabel: "", assigneeIdx: 0, note: "", autoReceive: true }, firstSaving: false,
|
||||
recordPopup: { visible: false, task: null }, recordForm: { recordId: null, remark: "", images: [], pendingCount: 0 }, recordSaving: false, isUploading: false,
|
||||
currentUser: null, currentUserId: "", currentUsername: "", currentMode: "tree", lockedTaskId: null,
|
||||
currentUser: null, currentUserId: "", currentUsername: "", currentMode: "tree",
|
||||
processOptions: [], userOptions: [],
|
||||
actionPopup: { visible: false, type: "", task: null }, actionLoading: false, rejectReason: "", receiveRemark: "",
|
||||
transferForm: { branches: [{ task_name: "", assignee_id: "" }], note: "" },
|
||||
@ -249,54 +148,31 @@ export default {
|
||||
userLabels() { return this.users.map(u => `${u.full_name} (${u.username})`); },
|
||||
canDeleteImage() { if (!this.recordPopup.task) return true; if (!this.currentUser) return true; return this.currentUser.username === this.recordPopup.task.assignee_id; },
|
||||
canSubmitTransfer() { return this.transferForm.branches.every(b => { if (!b.task_name) return false; if (b.task_name === "🏭 入库 (virtual_warehouse)") return true; return !!b.assignee_id; }); },
|
||||
taskMap() { const m = {}; const walk = (tasks) => { if (!tasks) return; for (const t of tasks) { m[t.id] = t; walk(t.child_tasks); } }; if (this.product) walk(this.product.task_tree); return m; },
|
||||
focusTasks() { const result = []; const walk = (tasks) => { if (!tasks) return; for (const t of tasks) { const isMine = t.status === 'WIP' || t.status === 'PENDING'; const matchId = t.assignee_id == this.currentUserId; const matchName = t.assignee_id == this.currentUsername; if (isMine && (matchId || matchName)) result.push(t); walk(t.child_tasks); } }; if (this.product) walk(this.product.task_tree); return result; },
|
||||
lockedTask() { return this.focusTasks.find(t => t.id === this.lockedTaskId) || null; },
|
||||
hasMyActiveTask() { return this.focusTasks.length > 0; },
|
||||
treeNodes() {
|
||||
const nodes = []; const CARD_W = 200, CARD_H = 140, GAP_X = 60, GAP_Y = 60;
|
||||
const layout = (tasks, depth, startX) => {
|
||||
if (!tasks || !tasks.length) return startX;
|
||||
const y = 20 + depth * (CARD_H + GAP_Y); let x = startX; const childXs = [];
|
||||
for (const t of tasks) {
|
||||
nodes.push({ ...t, x, y }); const childStartX = x;
|
||||
if (t.child_tasks && t.child_tasks.length) { x = layout(t.child_tasks, depth + 1, x); } else { x += CARD_W + GAP_X; }
|
||||
childXs.push({ id: t.id, cx: childStartX + CARD_W / 2, cy: y + CARD_H, children: t.child_tasks || [] });
|
||||
}
|
||||
nodes._lines = nodes._lines || [];
|
||||
for (const cx of childXs) { if (cx.children.length) { const midX = cx.cx; for (const c of cx.children) { const childNode = nodes.find(n => n.id === c.id); if (childNode) { nodes._lines.push({ key: cx.id + '-' + c.id, x1: midX, y1: cx.cy, x2: childNode.x + CARD_W / 2, y2: childNode.y }); } } } }
|
||||
return x;
|
||||
};
|
||||
if (this.product && this.product.task_tree) { nodes._lines = []; layout(this.product.task_tree, 0, 40); }
|
||||
return nodes;
|
||||
hasMyActiveTask() {
|
||||
const find = (tasks) => { if (!tasks) return false; for (const t of tasks) { if ((t.status === 'WIP' || t.status === 'PENDING') && (t.assignee_id == this.currentUserId || t.assignee_id == this.currentUsername)) return true; if (find(t.child_tasks)) return true; } return false; };
|
||||
return this.product ? find(this.product.task_tree) : false;
|
||||
},
|
||||
treeLines() { return this.treeNodes._lines || []; },
|
||||
canvasWidth() { const nodes = this.treeNodes; if (!nodes.length) return 400; return Math.max(400, Math.max(...nodes.map(n => n.x)) + 300); },
|
||||
canvasHeight() { const nodes = this.treeNodes; if (!nodes.length) return 400; return Math.max(400, Math.max(...nodes.map(n => n.y)) + 300); },
|
||||
},
|
||||
onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) this.doQuery(sn); },
|
||||
methods: {
|
||||
statusLabel(s) { return STATUS_MAP[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"; default: return "s-gray"; } },
|
||||
countTasks(tree) { return tree ? tree.reduce((s, t) => s + 1 + this.countTasks(t.child_tasks), 0) : 0; },
|
||||
|
||||
getTaskRemark(t) { if (!t) return ""; if (t.remark) return t.remark; if (t.records && t.records.length > 0) { const recs = [...t.records].reverse(); const rec = recs.find(r => r.remark && r.remark.length > 0); if (rec) return rec.remark; } return ""; },
|
||||
|
||||
async doQuery(sn) { this.loading = true; this.error = ""; try { this.product = await get(`/products/scan/${sn}`); if (!this.product.overall_status) this.showStatusPicker = true; this.$nextTick(() => { this.currentMode = this.hasMyActiveTask ? 'workspace' : 'tree'; this.lockedTaskId = null; }); } catch (e) { this.error = e?.data?.detail || "查询失败"; } finally { this.loading = false; } },
|
||||
toggleMode() { this.currentMode = this.currentMode === 'workspace' ? 'tree' : 'workspace'; if (this.currentMode === 'workspace') this.lockedTaskId = null; },
|
||||
lockTask(taskId) { this.lockedTaskId = taskId; },
|
||||
lineStyle(line) { const dx = line.x2 - line.x1; const dy = line.y2 - line.y1; const len = Math.sqrt(dx * dx + dy * dy); const angle = Math.atan2(dy, dx) * 180 / Math.PI; return { left: line.x1 + 'px', top: line.y1 + 'px', width: len + 'px', transform: `rotate(${angle}deg)`, transformOrigin: '0 0' }; },
|
||||
async doQuery(sn) { this.loading = true; this.error = ""; try { this.product = await get(`/products/scan/${sn}`); if (!this.product.overall_status) this.showStatusPicker = true; this.$nextTick(() => { this.currentMode = this.hasMyActiveTask ? 'workspace' : 'tree'; }); } catch (e) { this.error = e?.data?.detail || "查询失败"; } finally { this.loading = false; } },
|
||||
toggleMode() { this.currentMode = this.currentMode === 'workspace' ? 'tree' : 'workspace'; },
|
||||
|
||||
async handleSetOverallStatus(status) { try { this.product = await patch(`/products/scan/${this.product.serial_number}/status`, { status }); uni.showToast({ title: `状态已更新: ${status}`, icon: "success" }); this.showStatusPicker = false; } catch {} },
|
||||
openEditProduct() { this.editForm = { order_no: this.product.order_no || "", external_serial: this.product.external_serial || "" }; this.editProductVisible = true; },
|
||||
async doEditProduct() { this.editSaving = true; try { this.product = await patch(`/products/${this.product.id}`, { order_no: this.editForm.order_no.trim(), external_serial: this.editForm.external_serial.trim() }); uni.showToast({ title: "已保存", icon: "success" }); this.editProductVisible = false; } catch {} finally { this.editSaving = false; } },
|
||||
|
||||
async loadUsers() { try { this.users = await get("/users/", { limit: 200 }); this.userOptions = (this.users || []).map(u => ({ id: u.username || u.id, name: u.full_name || u.username })); } catch {} },
|
||||
loadCurrentUser() { try { let user = uni.getStorageSync("user"); if (typeof user === "string" && user) { try { user = JSON.parse(user); } catch (e) { user = null; } } if (user && typeof user === "object") { this.currentUser = user; this.currentUserId = String(user.id || ""); this.currentUsername = user.username || ""; } } catch {} },
|
||||
|
||||
onTaskNameChange(e) { const idx = e.detail.value; this.firstForm.taskNameIdx = idx; this.firstForm.task_name = TASK_NAME_OPTIONS[idx]; },
|
||||
onAssigneeChange(e) { const idx = e.detail.value; const u = this.users[idx]; if (u) { this.firstForm.assigneeIdx = idx; this.firstForm.assignee_id = u.username; this.firstForm.assigneeLabel = `${u.full_name} (${u.username})`; } },
|
||||
openCreateFirstTask() { this.firstForm = { task_name: TASK_NAME_OPTIONS[0], taskNameIdx: 0, assignee_id: this.users.length > 0 ? this.users[0].username : "", assigneeLabel: this.users.length > 0 ? `${this.users[0].full_name} (${this.users[0].username})` : "", assigneeIdx: 0, note: "", autoReceive: true }; this.createFirstVisible = true; },
|
||||
async doCreateFirstTask() { this.firstSaving = true; try { const task = await post("/tasks/", { product_id: this.product.id, task_name: this.firstForm.task_name, assignee_id: this.firstForm.assignee_id, notify_parent_on_complete: false, remark: this.firstForm.note.trim() || undefined }); if (this.firstForm.autoReceive) { try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${task.id}/receive?operator_id=${encodeURIComponent(opId)}`, { remark: this.firstForm.note.trim() || undefined }); } catch {} } uni.showToast({ title: this.firstForm.autoReceive ? "已创建并接收" : "已创建", icon: "success" }); this.createFirstVisible = false; this.doQuery(this.product.serial_number); } catch {} finally { this.firstSaving = false; } },
|
||||
async doDeleteRecord(record) { try { await request({ url: `/records/${record.id}`, method: "DELETE" }); uni.showToast({ title: "记录已删除", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} },
|
||||
|
||||
openRecordPopup(task) { this.recordPopup = { visible: true, task }; this.recordForm = { recordId: null, remark: "", images: [], pendingCount: 0 }; this.isUploading = false; },
|
||||
openEditRecord({ task, record }) { this.recordPopup = { visible: true, task }; this.recordForm = { recordId: record.id, remark: record.remark || "", images: record.images || [], pendingCount: 0 }; this.isUploading = false; },
|
||||
closeRecordPopup() { this.recordPopup = { visible: false, task: null }; },
|
||||
@ -305,13 +181,18 @@ export default {
|
||||
removeRecordImage(i) { this.recordForm.images.splice(i, 1); },
|
||||
previewRecordImage(i) { uni.previewImage({ urls: this.recordForm.images, current: i }); },
|
||||
async doSaveRecord() { if (this.isUploading) return; this.recordSaving = true; try { const payload = { remark: this.recordForm.remark.trim(), images: this.recordForm.images }; if (this.recordForm.recordId) await put(`/records/${this.recordForm.recordId}`, payload); else await patch(`/tasks/${this.recordPopup.task.id}/records`, payload); uni.showToast({ title: "已保存", icon: "success" }); this.closeRecordPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.recordSaving = false; } },
|
||||
async handleTaskAction({ task, type, record }) { if (type === "record") { this.openRecordPopup(task); return; } if (type === "deleteRecord") { this.doDeleteRecord(record); return; } if (type === "end") { this.confirmEndBranch(task); return; } if (type === "transfer") { uni.showLoading({ title: "加载数据..." }); try { if (!this.userOptions.length) await this.loadUsers(); if (!this.processOptions.length) this.processOptions = ["🏭 入库 (virtual_warehouse)", ...TASK_NAME_OPTIONS]; } finally { uni.hideLoading(); } } this.actionPopup = { visible: true, type, task }; this.rejectReason = ""; this.receiveRemark = ""; this.transferForm = { branches: [{ task_name: "", assignee_id: "" }], note: "" }; },
|
||||
|
||||
async handleTaskAction({ task, type, record }) {
|
||||
if (type === "record") { this.openRecordPopup(task); return; }
|
||||
if (type === "deleteRecord") { this.doDeleteRecord(record); return; }
|
||||
if (type === "end") { this.confirmEndBranch(task); return; }
|
||||
if (type === "transfer") { uni.showLoading({ title: "加载数据..." }); try { if (!this.userOptions.length) await this.loadUsers(); if (!this.processOptions.length) this.processOptions = ["🏭 入库 (virtual_warehouse)", ...TASK_NAME_OPTIONS]; } finally { uni.hideLoading(); } }
|
||||
this.actionPopup = { visible: true, type, task }; this.rejectReason = ""; this.receiveRemark = ""; this.transferForm = { branches: [{ task_name: "", assignee_id: "" }], note: "" };
|
||||
},
|
||||
handleViewRecords(task) { uni.navigateTo({ url: `/pages/scan/records?taskId=${task.id}` }); },
|
||||
async doDeleteRecord(record) { try { await request({ url: `/records/${record.id}`, method: "DELETE" }); uni.showToast({ title: "记录已删除", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} },
|
||||
confirmEndBranch(task) { uni.showModal({ title: "结束分支", content: `确定结束「${task.task_name}」吗?`, success: (res) => { if (res.confirm) this.doEndBranch(task); } }); },
|
||||
async doEndBranch(task) { try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${task.id}/end?operator_id=${encodeURIComponent(opId)}`); uni.showToast({ title: "分支已结束", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} },
|
||||
parentTaskOf(t) { return t && t.parent_task_id ? (this.taskMap[t.parent_task_id] || null) : null; },
|
||||
formatTaskTime(t) { if (!t) return ""; const parts = []; if (t.created_at) parts.push("创建: " + this.formatTime(t.created_at)); if (t.received_at) parts.push("接收: " + this.formatTime(t.received_at)); return parts.join(" | "); },
|
||||
formatTime(d) { if (!d) return ""; const dt = new Date(d); const pad = (n) => String(n).padStart(2, "0"); return `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`; },
|
||||
closeActionPopup() { this.actionPopup = { visible: false, type: "", task: null }; },
|
||||
async doReceive() { this.actionLoading = true; try { const remark = this.receiveRemark.trim() || undefined; const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/receive?operator_id=${encodeURIComponent(opId)}`, { remark }); uni.showToast({ title: "已接收", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
|
||||
async doReject() { this.actionLoading = true; try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/reject?operator_id=${encodeURIComponent(opId)}`, { reason: this.rejectReason.trim() }); uni.showToast({ title: "已驳回", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
|
||||
@ -336,67 +217,6 @@ export default {
|
||||
.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; }
|
||||
.workspace-area { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||
.list-header { display: flex; align-items: baseline; justify-content: space-between; padding: 8px 4px; flex-shrink: 0; }
|
||||
.list-title { font-size: 14px; font-weight: 700; color: #1f2937; }
|
||||
.list-hint { font-size: 11px; color: #9ca3af; }
|
||||
.task-list { flex: 1; overflow-y: auto; min-height: 0; }
|
||||
.task-list-item { display: flex; align-items: flex-start; justify-content: space-between; background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border-left: 4px solid transparent; }
|
||||
.task-list-item.s-yellow { border-left-color: #f59e0b; }
|
||||
.task-list-item.s-blue { border-left-color: #3b82f6; }
|
||||
.tli-left { flex: 1; min-width: 0; }
|
||||
.tli-name { font-size: 15px; font-weight: 700; color: #1f2937; display: block; }
|
||||
.tli-remark { font-size: 12px; color: #a16207; font-weight: bold; margin-top: 4px; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.tli-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; margin-left: 8px; }
|
||||
.tli-badge { font-size: 11px; padding: 2px 10px; border-radius: 12px; font-weight: 600; }
|
||||
.tli-badge.s-yellow { background: #fef3c7; color: #b45309; }
|
||||
.tli-badge.s-blue { background: #dbeafe; color: #1d4ed8; }
|
||||
.tli-arrow { font-size: 18px; color: #d1d5db; }
|
||||
.lock-bar { display: flex; align-items: center; gap: 12px; padding: 8px 0; flex-shrink: 0; }
|
||||
.lock-back { font-size: 13px; color: #2563eb; font-weight: 600; }
|
||||
.lock-title { font-size: 14px; font-weight: 700; color: #1f2937; }
|
||||
.branch-done { font-size: 11px; font-weight: 600; color: #16a34a; }
|
||||
.task-count { font-size: 12px; color: #9ca3af; display: block; margin-top: 4px; }
|
||||
.focus-card { height: 100%; margin: 0 4px; padding: 20px 16px; border-radius: 16px; background: #fff; box-shadow: 0 2px 12px rgba(0,0,0,0.08); overflow-y: auto; border-top: 5px solid #3b82f6; }
|
||||
.focus-card.s-yellow { border-top-color: #f59e0b; }
|
||||
.focus-card.s-blue { border-top-color: #3b82f6; }
|
||||
.focus-card.s-green { border-top-color: #22c55e; }
|
||||
.focus-card.s-red { border-top-color: #ef4444; }
|
||||
.fc-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.fc-status { font-size: 12px; padding: 2px 10px; border-radius: 12px; font-weight: 700; background: #f3f4f6; color: #6b7280; }
|
||||
.s-yellow .fc-status { background: #fef3c7; color: #b45309; }
|
||||
.s-blue .fc-status { background: #dbeafe; color: #1d4ed8; }
|
||||
.tag-rework-sm { font-size: 10px; padding: 2px 6px; border-radius: 8px; background: #ef4444; color: #fff; }
|
||||
.fc-name { font-size: 22px; font-weight: 800; color: #1f2937; display: block; margin-bottom: 10px; }
|
||||
.fc-link { padding: 6px 10px; border-radius: 8px; margin-bottom: 4px; font-size: 12px; margin-top: 10px; }
|
||||
.fc-up { background: #f0fdf4; color: #16a34a; }
|
||||
.fc-down { background: #eff6ff; color: #2563eb; }
|
||||
.fc-link-label { font-weight: 700; display: block; }
|
||||
.fc-link-name { display: block; margin-top: 2px; }
|
||||
.fc-records-bar { padding: 8px 12px; background: linear-gradient(135deg,#eff6ff,#dbeafe); border-radius: 8px; font-size: 13px; font-weight: 700; color: #2563eb; margin-top: 10px; margin-bottom: 8px; }
|
||||
.fc-time { font-size: 11px; color: #9ca3af; margin-top: auto; padding-top: 8px; border-top: 1px solid #f3f4f6; }
|
||||
.tree-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; margin-top: 4px; }
|
||||
.canvas-hint { font-size: 11px; color: #9ca3af; text-align: center; padding: 4px 0; flex-shrink: 0; }
|
||||
.tree-movable-area { flex: 1; width: 100%; background-color: #ebedf0; border-radius: 12px; overflow: hidden; box-shadow: inset 0 0 20px rgba(0,0,0,0.05); }
|
||||
.canvas-inner { position: absolute; left: 0; top: 0; }
|
||||
.canvas-node { position: absolute; width: 180px; min-height: 100px; background: #ffffff; border-radius: 12px; padding: 14px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); border-left: 6px solid #3b82f6; border-top: none; z-index: 10; display: flex; flex-direction: column; gap: 6px; }
|
||||
.canvas-node.s-yellow { border-left-color: #f59e0b; }
|
||||
.canvas-node.s-blue { border-left-color: #3b82f6; }
|
||||
.canvas-node.s-green { border-left-color: #22c55e; }
|
||||
.canvas-node.s-red { border-left-color: #ef4444; }
|
||||
.cn-header { display: flex; align-items: center; justify-content: space-between; }
|
||||
.cn-name { font-size: 14px; font-weight: 800; color: #1f2937; }
|
||||
.cn-badge { font-size: 10px; padding: 2px 6px; border-radius: 8px; font-weight: 600; }
|
||||
.cn-badge.s-yellow { background: #fef3c7; color: #b45309; }
|
||||
.cn-badge.s-blue { background: #dbeafe; color: #1d4ed8; }
|
||||
.cn-badge.s-green { background: #dcfce7; color: #15803d; }
|
||||
.cn-assignee { font-size: 11px; color: #6b7280; }
|
||||
.cn-remark { font-size: 11px; color: #a16207; background: #fefce8; padding: 4px 8px; border-radius: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.cn-footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; padding-top: 6px; border-top: 1px solid #f3f4f6; }
|
||||
.cn-end { font-size: 10px; font-weight: 600; color: #16a34a; }
|
||||
.cn-end-wh { color: #7c3aed; }
|
||||
.cn-records { font-size: 11px; color: #2563eb; font-weight: 600; }
|
||||
.canvas-line { position: absolute; height: 3px; background: #94a3b8; z-index: 1; border-radius: 2px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
|
||||
.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; }
|
||||
@ -413,10 +233,6 @@ export default {
|
||||
.s-green .badge, .s-green { color: #15803d; }
|
||||
.s-red .badge, .s-red { color: #be123c; }
|
||||
.s-gray .badge, .s-gray { color: #6b7280; }
|
||||
.zero-task { display: flex; flex-direction: column; align-items: center; padding: 24px 0; }
|
||||
.zero-icon { font-size: 40px; margin-bottom: 8px; }
|
||||
.zero-text { font-size: 14px; color: #9ca3af; margin-bottom: 16px; }
|
||||
.btn-start { width: 220px; height: 44px; background: linear-gradient(135deg, #2563EB, #3B82F6); border: none; border-radius: 12px; color: #fff; font-size: 15px; font-weight: 700; line-height: 44px; box-shadow: 0 4px 12px rgba(37,99,235,0.3); }
|
||||
.overlay { position: fixed; inset: 0; z-index: 999; background: rgba(0,0,0,0.45); display: flex; align-items: flex-end; justify-content: center; }
|
||||
.sheet { width: 100%; max-width: 480px; background: #fff; border-radius: 20px 20px 0 0; padding: 20px 16px 32px; }
|
||||
.sheet-title { font-size: 17px; font-weight: 700; display: block; text-align: center; }
|
||||
@ -456,14 +272,6 @@ export default {
|
||||
.img-loading-text { font-size: 36rpx; }
|
||||
.img-del { position: absolute; top: -12rpx; right: -12rpx; width: 40rpx; height: 40rpx; background: #ef4444; color: #fff; border-radius: 20rpx; font-size: 24rpx; text-align: center; line-height: 40rpx; z-index: 2; }
|
||||
.btn-upload { width: 100%; height: 42px; border: 1px dashed #d1d5db; border-radius: 10px; background: #f9fafb; color: #6b7280; font-size: 14px; line-height: 42px; margin: 8px 0; }
|
||||
.footer-actions { display: flex; gap: 20rpx; padding: 20rpx 30rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05); z-index: 99; flex-shrink: 0; }
|
||||
.footer-btn { flex: 1; height: 88rpx; border: none; border-radius: 12rpx; font-size: 30rpx; font-weight: 700; line-height: 88rpx; }
|
||||
.footer-btn::after { border: none; }
|
||||
.footer-transfer { background: #dcfce7; color: #16a34a; }
|
||||
.footer-record { background: #eff6ff; color: #2563eb; }
|
||||
.footer-receive { background: #dbeafe; color: #1d4ed8; }
|
||||
.footer-reject { background: #fce4ec; color: #dc2626; }
|
||||
.footer-end { background: #fef3c7; color: #b45309; }
|
||||
.form-item { margin: 10px 0; }
|
||||
.form-label { font-size: 14px; font-weight: 600; color: #374151; display: block; margin-bottom: 4px; }
|
||||
.picker-value { display: flex; align-items: center; justify-content: space-between; width: 100%; height: 42px; padding: 0 12px; border: 1px solid #e5e7eb; border-radius: 10px; background: #f9fafb; font-size: 14px; box-sizing: border-box; }
|
||||
|
||||
Reference in New Issue
Block a user