fix: 修复移动端流转树全览空白,改用独立 FlowTree 递归树组件
- 修复 scroll-view 高度为 0 导致内容不可见 - 新建 FlowTree.vue 独立递归渲染(主线/分支标记、状态、转入在库、时间、记录) - 不依赖 TaskTreeNode,展示更贴近网页端流转树
This commit is contained in:
128
track-uniapp/src/pages/scan/components/FlowTree.vue
Normal file
128
track-uniapp/src/pages/scan/components/FlowTree.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<view class="ft-node">
|
||||||
|
<!-- 节点卡片 -->
|
||||||
|
<view class="ft-card" :class="statusColorClass(node.status)">
|
||||||
|
<view class="ft-head">
|
||||||
|
<text class="ft-badge" :class="isMainNode ? 'ft-badge-main' : 'ft-badge-sub'">{{ isMainNode ? '主线' : '分支' }}</text>
|
||||||
|
<text :class="['ft-status', statusColorClass(node.status)]">{{ statusLabel(node.status) }}</text>
|
||||||
|
<text v-if="node.is_rework" class="ft-rework">⚠返工</text>
|
||||||
|
</view>
|
||||||
|
<text class="ft-name">{{ node.task_name }}</text>
|
||||||
|
<view class="ft-meta">
|
||||||
|
<!-- 在库任务显示"转入在库",其余显示负责人 -->
|
||||||
|
<text v-if="isWarehouseTask" class="ft-warehouse-in">📥 转入在库: {{ formatUserName(node.assignee_id) }}</text>
|
||||||
|
<text v-else class="ft-assignee">👤 {{ formatUserName(node.assignee_id) || '未分配' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="ft-bottom">
|
||||||
|
<text class="ft-time">⏰ {{ fmtDate(node.created_at) }}{{ node.completed_at ? ' → ' + fmtDate(node.completed_at) : ' → 至今' }}</text>
|
||||||
|
<text v-if="node.records && node.records.length" class="ft-records" @tap.stop="$emit('viewRecords', node)">📋 {{ node.records.length }}条 ›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 子任务(递归缩进) -->
|
||||||
|
<view v-if="node.child_tasks && node.child_tasks.length" class="ft-children">
|
||||||
|
<FlowTree
|
||||||
|
v-for="child in node.child_tasks"
|
||||||
|
:key="child.id"
|
||||||
|
:node="child"
|
||||||
|
:current-user="currentUser"
|
||||||
|
:current-user-id="currentUserId"
|
||||||
|
:current-username="currentUsername"
|
||||||
|
@view-records="$emit('viewRecords', $event)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { formatUserName } from "../../../utils/format";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "FlowTree",
|
||||||
|
props: {
|
||||||
|
node: { type: Object, required: true },
|
||||||
|
currentUser: { type: Object, default: null },
|
||||||
|
currentUserId: { type: [String, Number], default: "" },
|
||||||
|
currentUsername: { type: String, default: "" },
|
||||||
|
},
|
||||||
|
emits: ["viewRecords"],
|
||||||
|
computed: {
|
||||||
|
isMainNode() {
|
||||||
|
return !this.node.parent_task_id
|
||||||
|
|| this.node.task_type === "TRANSFER"
|
||||||
|
|| this.node.task_type === "RECOVERY";
|
||||||
|
},
|
||||||
|
isWarehouseTask() {
|
||||||
|
const name = String(this.node.task_name || "");
|
||||||
|
return (name.includes("在库") || name.includes("入库")) || this.node.assignee_id === "virtual_warehouse";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
statusLabel(s) {
|
||||||
|
const map = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库", OUTBOUND: "已出库" };
|
||||||
|
return map[s] || s;
|
||||||
|
},
|
||||||
|
statusColorClass(s) {
|
||||||
|
switch (s) {
|
||||||
|
case "PENDING": return "s-yellow";
|
||||||
|
case "WIP": return "s-blue";
|
||||||
|
case "COMPLETED": return "s-green";
|
||||||
|
case "REJECTED": return "s-red";
|
||||||
|
case "ARCHIVED": return "s-archived";
|
||||||
|
case "OUTBOUND": return "s-outbound";
|
||||||
|
default: return "s-gray";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fmtDate(t) {
|
||||||
|
if (!t) return "";
|
||||||
|
const d = new Date(t);
|
||||||
|
const p = (n) => String(n).padStart(2, "0");
|
||||||
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ft-node { margin-bottom: 10px; }
|
||||||
|
.ft-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-left-width: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
/* 状态左色条 */
|
||||||
|
.ft-card.s-yellow { border-left-color: #f59e0b; }
|
||||||
|
.ft-card.s-blue { border-left-color: #3b82f6; }
|
||||||
|
.ft-card.s-green { border-left-color: #22c55e; }
|
||||||
|
.ft-card.s-red { border-left-color: #ef4444; }
|
||||||
|
.ft-card.s-archived { border-left-color: #8b5cf6; }
|
||||||
|
.ft-card.s-outbound { border-left-color: #4f46e5; }
|
||||||
|
.ft-card.s-gray { border-left-color: #9ca3af; }
|
||||||
|
|
||||||
|
.ft-head { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
||||||
|
.ft-badge { font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 4px; color: #fff; }
|
||||||
|
.ft-badge-main { background: #2563eb; }
|
||||||
|
.ft-badge-sub { background: #7c3aed; }
|
||||||
|
.ft-status { font-size: 11px; font-weight: 700; padding: 1px 8px; border-radius: 10px; }
|
||||||
|
.s-yellow .ft-status { background: #fef3c7; color: #b45309; }
|
||||||
|
.s-blue .ft-status { background: #dbeafe; color: #1d4ed8; }
|
||||||
|
.s-green .ft-status { background: #dcfce7; color: #15803d; }
|
||||||
|
.s-red .ft-status { background: #fce4ec; color: #be123c; }
|
||||||
|
.s-archived .ft-status { background: #ede9fe; color: #7c3aed; }
|
||||||
|
.s-outbound .ft-status { background: #e0e7ff; color: #4338ca; }
|
||||||
|
.s-gray .ft-status { background: #f3f4f6; color: #6b7280; }
|
||||||
|
.ft-rework { font-size: 10px; background: #ef4444; color: #fff; padding: 1px 5px; border-radius: 4px; font-weight: 700; }
|
||||||
|
|
||||||
|
.ft-name { font-size: 14px; font-weight: 700; color: #1f2937; }
|
||||||
|
.ft-meta { margin-top: 4px; font-size: 12px; }
|
||||||
|
.ft-assignee { color: #4b5563; }
|
||||||
|
.ft-warehouse-in { color: #059669; font-weight: 700; }
|
||||||
|
.ft-bottom { display: flex; align-items: center; justify-content: space-between; margin-top: 6px; }
|
||||||
|
.ft-time { font-size: 11px; color: #9ca3af; }
|
||||||
|
.ft-records { font-size: 11px; color: #2563eb; font-weight: 600; }
|
||||||
|
|
||||||
|
.ft-children { margin-left: 16px; padding-left: 10px; border-left: 2px solid #e5e7eb; margin-top: 8px; }
|
||||||
|
</style>
|
||||||
@ -14,19 +14,16 @@
|
|||||||
<text class="zero-icon">📋</text><text class="zero-text">该产品暂无流转记录</text>
|
<text class="zero-icon">📋</text><text class="zero-text">该产品暂无流转记录</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<scroll-view v-else scroll-y class="tree-scroll">
|
<scroll-view v-else scroll-y class="tree-scroll" :style="{ height: scrollHeight + 'px' }">
|
||||||
<view class="tree-root">
|
<view class="tree-root">
|
||||||
<!-- 递归纵向树:主线 + 分支缩进,复用 TaskTreeNode 渲染 -->
|
<!-- 递归纵向流转树:主线 + 分支缩进 -->
|
||||||
<TaskTreeNode
|
<FlowTree
|
||||||
v-for="(t, idx) in product.task_tree"
|
v-for="t in product.task_tree"
|
||||||
:key="t.id"
|
:key="t.id"
|
||||||
:task="t"
|
:node="t"
|
||||||
:current-user="currentUser"
|
:current-user="currentUser"
|
||||||
:current-user-id="currentUserId"
|
:current-user-id="currentUserId"
|
||||||
:current-username="currentUsername"
|
:current-username="currentUsername"
|
||||||
:has-children="!!(t.child_tasks && t.child_tasks.length)"
|
|
||||||
:readonly="true"
|
|
||||||
:is-last="idx === product.task_tree.length - 1"
|
|
||||||
@view-records="$emit('viewRecords', $event)"
|
@view-records="$emit('viewRecords', $event)"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@ -43,11 +40,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TaskTreeNode from "./TaskTreeNode.vue";
|
import FlowTree from "./FlowTree.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "TreeCanvas",
|
name: "TreeCanvas",
|
||||||
components: { TaskTreeNode },
|
components: { FlowTree },
|
||||||
props: {
|
props: {
|
||||||
product: { type: Object, required: true },
|
product: { type: Object, required: true },
|
||||||
currentUser: { type: Object, default: null },
|
currentUser: { type: Object, default: null },
|
||||||
@ -55,6 +52,16 @@ export default {
|
|||||||
currentUsername: { type: String, default: "" },
|
currentUsername: { type: String, default: "" },
|
||||||
},
|
},
|
||||||
emits: ["viewRecords", "back", "swipe"],
|
emits: ["viewRecords", "back", "swipe"],
|
||||||
|
data() {
|
||||||
|
return { scrollHeight: 500 };
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
try {
|
||||||
|
const info = uni.getSystemInfoSync();
|
||||||
|
// 工具条高度 + 底部安全区预留约 70px,剩余给滚动区
|
||||||
|
this.scrollHeight = Math.max((info.windowHeight || 600) - 70, 300);
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -87,7 +94,7 @@ export default {
|
|||||||
.zero-icon { font-size: 40px; margin-bottom: 10px; }
|
.zero-icon { font-size: 40px; margin-bottom: 10px; }
|
||||||
.zero-text { font-size: 14px; color: #9ca3af; }
|
.zero-text { font-size: 14px; color: #9ca3af; }
|
||||||
|
|
||||||
.tree-scroll { flex: 1; height: 0; overflow: hidden; }
|
.tree-scroll { flex-shrink: 0; }
|
||||||
.tree-root { padding: 16px 12px 20px; }
|
.tree-root { padding: 16px 12px 20px; }
|
||||||
.tree-legend {
|
.tree-legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user