refactor: 移动端流转树全面复刻 Web 端 childMap 左中右布局

- 废弃递归纵向排版(FlowTree),改用拍平 + childMap 重构
- 横向 scroll-x + 每条主线一个 Row:[左翼]-[中央]-[右翼]
- 中央绝对定位垂直线贯穿;分支按奇偶拆左右翼,横向展开
- 新建 FlowCard(节点卡片)与 FlowBranch(递归分支)
This commit is contained in:
2026-09-01 14:37:29 +08:00
parent 9808742f94
commit 327f5f7829
3 changed files with 332 additions and 42 deletions

View File

@ -0,0 +1,112 @@
<template>
<view class="fc-card" :class="statusColorClass(node.status)">
<view class="fc-head">
<!-- 主线/分支 标签(仅中央主线显示) -->
<text v-if="showBadge" class="fc-badge" :class="isMainNode ? 'fc-badge-main' : 'fc-badge-sub'">{{ isMainNode ? '主线' : '分支' }}</text>
<text :class="['fc-status', statusColorClass(node.status)]">{{ statusLabel(node.status) }}</text>
<text v-if="node.is_rework" class="fc-rework">⚠返工</text>
</view>
<text class="fc-name">{{ node.task_name }}</text>
<view class="fc-meta">
<!-- 在库任务显示"转入在库",其余显示负责人 -->
<text v-if="isWarehouseTask" class="fc-warehouse-in">📥 转入在库: {{ formatUserName(node.assignee_id) }}</text>
<text v-else class="fc-assignee">👤 {{ formatUserName(node.assignee_id) || '未分配' }}</text>
</view>
<view class="fc-bottom">
<text class="fc-time">⏰ {{ fmtDate(node.created_at) }}{{ node.completed_at ? '→' + fmtDate(node.completed_at) : '→至今' }}</text>
<text v-if="node.records && node.records.length" class="fc-records" @tap.stop="$emit('viewRecords', node)">📋{{ node.records.length }}条 ›</text>
</view>
</view>
</template>
<script>
import { formatUserName as fmtUserName } from "../../../utils/format";
export default {
name: "FlowCard",
props: {
node: { type: Object, required: true },
showBadge: { type: Boolean, default: false },
},
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: {
formatUserName(userId) {
return fmtUserName(userId);
},
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>
.fc-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);
width: 168px;
flex-shrink: 0;
}
.fc-card.s-yellow { border-left-color: #f59e0b; }
.fc-card.s-blue { border-left-color: #3b82f6; }
.fc-card.s-green { border-left-color: #22c55e; }
.fc-card.s-red { border-left-color: #ef4444; }
.fc-card.s-archived { border-left-color: #8b5cf6; }
.fc-card.s-outbound { border-left-color: #4f46e5; }
.fc-card.s-gray { border-left-color: #9ca3af; }
.fc-head { display: flex; align-items: center; gap: 4px; margin-bottom: 4px; flex-wrap: wrap; }
.fc-badge { font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 4px; color: #fff; }
.fc-badge-main { background: #2563eb; }
.fc-badge-sub { background: #7c3aed; }
.fc-status { font-size: 11px; font-weight: 700; padding: 1px 8px; border-radius: 10px; }
.s-yellow .fc-status { background: #fef3c7; color: #b45309; }
.s-blue .fc-status { background: #dbeafe; color: #1d4ed8; }
.s-green .fc-status { background: #dcfce7; color: #15803d; }
.s-red .fc-status { background: #fce4ec; color: #be123c; }
.s-archived .fc-status { background: #ede9fe; color: #7c3aed; }
.s-outbound .fc-status { background: #e0e7ff; color: #4338ca; }
.s-gray .fc-status { background: #f3f4f6; color: #6b7280; }
.fc-rework { font-size: 10px; background: #ef4444; color: #fff; padding: 1px 5px; border-radius: 4px; font-weight: 700; }
.fc-name { font-size: 13px; font-weight: 700; color: #1f2937; }
.fc-meta { margin-top: 4px; font-size: 11px; }
.fc-assignee { color: #4b5563; }
.fc-warehouse-in { color: #059669; font-weight: 700; }
.fc-bottom { display: flex; align-items: center; justify-content: space-between; gap: 4px; margin-top: 6px; }
.fc-time { font-size: 10px; color: #9ca3af; }
.fc-records { font-size: 10px; color: #2563eb; font-weight: 600; }
</style>