chore: 移除已废弃的工序树组件 FlowTree / TaskTreeNode

两者已被 TreeCanvas 取代:detail.vue 的 components 只注册
WorkspaceArea / TreeCanvas / TaskSwipeCards,全项目对 FlowTree 与
TaskTreeNode 已无任何 import(仅 detail.vue 内一句注释还提及后者)。

uni-app 不会编译未被引用的 .vue,留着不报错但会持续误导后续排查,
故清理。
This commit is contained in:
2026-09-15 15:52:08 +08:00
parent 1e46d6edd9
commit a5fa789657
2 changed files with 0 additions and 434 deletions

View File

@ -1,188 +0,0 @@
<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">
<!-- 1. 先渲染分支紧贴父节点下方缩进 + 左侧虚线框视觉上被父节点"兜住" -->
<view v-if="branchChildren.length" class="ft-branch-children">
<view class="ft-branch-indicator"> 分支延伸</view>
<FlowTree
v-for="child in branchChildren"
:key="child.id"
:node="child"
:is-branch-context="true"
:current-user="currentUser"
:current-user-id="currentUserId"
:current-username="currentUsername"
@view-records="$emit('viewRecords', $event)"
/>
</view>
<!-- 2. 再渲染主干道每个主线子节点前都画向下连线箭头含父长子 -->
<template v-for="(child, i) in mainChildren" :key="'mc-' + child.id">
<view v-if="i === 0 || i > 0" class="ft-flow-arrow"></view>
<FlowTree
:node="child"
:current-user="currentUser"
:current-user-id="currentUserId"
:current-username="currentUsername"
@view-records="$emit('viewRecords', $event)"
/>
</template>
</view>
</view>
</template>
<script>
import { formatUserName as fmtUserName } 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: "" },
// 分支上下文:一旦进入分支,其子子孙孙都强制缩进,不再回主干道
isBranchContext: { type: Boolean, default: false },
},
emits: ["viewRecords"],
computed: {
isMainNode() {
return !this.node.parent_task_id
|| this.node.task_type === "TRANSFER"
|| this.node.task_type === "RECOVERY";
},
// 主线子任务TRANSFER/RECOVERY继续主干道左对齐
// 分支上下文拦截:一旦处于分支,所有子任务强制归入分支,主线返回空
mainChildren() {
if (this.isBranchContext) return [];
return (this.node.child_tasks || []).filter((c) =>
!c.parent_task_id || c.task_type === "TRANSFER" || c.task_type === "RECOVERY"
);
},
// 分支子任务:其余(SPAWN 协助等),缩进显示
// 分支上下文拦截:处于分支时,全部子任务都作为分支继续缩进
branchChildren() {
if (this.isBranchContext) return this.node.child_tasks || [];
return (this.node.child_tasks || []).filter((c) =>
c.parent_task_id && c.task_type !== "TRANSFER" && c.task_type !== "RECOVERY"
);
},
isWarehouseTask() {
const name = String(this.node.task_name || "");
return (name.includes("在库") || name.includes("入库")) || this.node.assignee_id === "virtual_warehouse";
},
},
methods: {
// Vue2 Options API 中 import 的函数需挂到 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>
.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-flow-arrow {
text-align: center;
color: #9ca3af;
font-size: 16px;
font-weight: 700;
line-height: 1;
margin: 2px 0;
}
/* 分支子任务:缩进 + 左侧虚线框,视觉上被父节点"兜住" */
.ft-branch-children {
margin-left: 12px;
padding-left: 12px;
border-left: 2px dashed #cbd5e1;
margin-top: 8px;
}
/* 分支指示符:轻量级连接锚点 */
.ft-branch-indicator {
color: #9ca3af;
font-size: 14px;
margin-bottom: 4px;
padding-left: 4px;
}
</style>

View File

@ -1,246 +0,0 @@
<template>
<view class="tnode">
<view v-if="hasChildren" class="tree-line-vertical" />
<!-- 卡片 -->
<view :class="['tnode-card', statusColor(task.status), { 'is-rework': task.is_rework }]">
<!-- 第一层任务名称标签状态 Badge负责人 -->
<view class="tnode-body">
<view class="tnode-name-row">
<text v-if="task.is_rework" class="tag tag-rework">返工</text>
<text v-if="task.child_tasks && task.child_tasks.length > 1" class="tag tag-fission">裂变×{{ task.child_tasks.length }}</text>
<text class="tnode-name">{{ task.task_name }}</text>
</view>
<view class="tnode-meta">
<!-- 在库任务不单独显示"负责人"由下方"转入在库"统一展示实际操作人 -->
<text v-if="task.assignee_id && !isWarehouseTask">负责人: {{ formatUserName(task.assignee_id) }}</text>
<text v-if="task.reject_reason" class="reject-reason">驳回: {{ task.reject_reason }}</text>
</view>
<!-- 📥 在库任务显示实际执行"转入在库"的操作人任务负责人 -->
<text v-if="isWarehouseTask && task.assignee_id" class="warehouse-in">📥 转入在库: {{ formatUserName(task.assignee_id) }}</text>
<!-- 任务初始描述 / 交接备注常驻显示区别于动态日志 records -->
<view v-if="task.remark || task.description" class="task-initial-remark">
<text class="remark-label">📌 初始说明</text>
<text class="remark-text">{{ task.remark || task.description }}</text>
</view>
</view>
<!-- 状态标签 + 分支闭环标记 -->
<view style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0;margin-left:8px;">
<text :class="['badge', statusColor(task.status)]">{{ statusLabel(task.status) }}</text>
<text v-if="task.status === 'COMPLETED' && (!task.child_tasks || !task.child_tasks.length)" class="end-marker">🏁 已终止</text>
<text v-if="task.status === 'ARCHIVED'" class="end-marker end-warehouse">📦 已入库</text>
</view>
</view>
<!-- 第二层递归子任务树 -->
<view v-if="task.child_tasks && task.child_tasks.length" class="tnode-children">
<TaskTreeNode
v-for="(child, idx) in task.child_tasks"
:key="child.id"
:task="child"
:currentUser="currentUser"
:currentUserId="currentUserId"
:currentUsername="currentUsername"
:readonly="readonly"
:has-children="!!(child.child_tasks && child.child_tasks.length)"
:isLast="idx === task.child_tasks.length - 1"
@action="(e) => $emit('action', e)"
@editRecord="(e) => $emit('editRecord', e)"
@viewRecords="(t) => $emit('viewRecords', t)"
/>
</view>
<!-- 第三层最底部进度记录 最新一条 + 查看全部入口 -->
<view v-if="task.records && task.records.length" class="records-area">
<!-- 查看全部记录按钮 -->
<view class="records-bar" @tap.stop="$emit('viewRecords', task)">
<text class="records-bar-icon">📋</text>
<text class="records-bar-text">查看全部记录 ({{ task.records.length }})</text>
<text class="records-bar-arrow"></text>
</view>
<!-- 最新一条预览 -->
<view v-if="latestRecord" class="record-item">
<view class="record-top">
<text class="record-time">{{ formatTime(latestRecord.created_at) }}</text>
<view v-if="canEditRecord" class="record-actions">
<text class="rec-act" @tap.stop="$emit('editRecord', { task, record: latestRecord })"></text>
<text class="rec-act" @tap.stop="confirmDelete(latestRecord)">🗑</text>
</view>
</view>
<text v-if="latestRecord.remark" class="record-remark">{{ latestRecord.remark }}</text>
<view v-if="latestRecord.images && latestRecord.images.length" class="record-images">
<image
v-for="(img, i) in latestRecord.images"
:key="i"
:src="imageUrl(img)"
class="record-thumb"
mode="aspectFill"
@tap.stop="previewImage(latestRecord.images, i)"
/>
</view>
</view>
</view>
</view>
</template>
<script>
import { getBaseUrl } from "../../../utils/request";
import { formatUserName as fmtUserName } from "../../../utils/format";
export default {
name: "TaskTreeNode",
props: {
task: { type: Object, required: true },
isLast: { type: Boolean, default: false },
hasChildren: { type: Boolean, default: false },
currentUser: { type: Object, default: null },
currentUserId: { type: [String, Number], default: "" },
currentUsername: { type: String, default: "" },
readonly: { type: Boolean, default: false },
},
emits: ["action", "editRecord", "viewRecords"],
mounted() {
console.log("TaskTreeNode task keys:", Object.keys(this.task));
console.log("task.remark =", this.task.remark);
console.log("task.description =", this.task.description);
},
computed: {
isWarehouseTask() {
return !!(this.task && (
String(this.task.task_name || '').includes('在库')
|| String(this.task.task_name || '').includes('入库')
|| this.task.assignee_id === 'virtual_warehouse'
));
},
canEditRecord() {
if (this.readonly) return false;
if (this.task.assignee_id == this.currentUserId) return true;
if (this.task.assignee_id == this.currentUsername) return true;
if (this.currentUser && this.currentUser.id == this.task.assignee_id) return true;
if (this.currentUser && this.currentUser.username == this.task.assignee_id) return true;
return false;
},
latestRecord() {
const recs = this.task.records;
if (!recs || !recs.length) return null;
return [...recs].sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
},
},
methods: {
imageUrl(url) {
if (!url) return "";
if (url.startsWith("http")) return url;
// 后端返回 /api/v1/upload/files/xxx.jpg需要补全域名
const domain = getBaseUrl().replace(/\/api.*$/, '');
return domain + (url.startsWith("/") ? url : "/" + url);
},
previewImage(urls, index) {
const fullUrls = (urls || []).map(img => this.imageUrl(img));
uni.previewImage({ urls: fullUrls, current: index });
},
confirmDelete(rec) {
// 🚀 交给父页面统一做「双重确认倒计时」,避免原生 showModal 取消行为异常
this.$emit("action", { task: this.task, type: "deleteRecord", record: rec });
},
statusLabel(s) {
const map = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
return map[s] || s;
},
// Vue2 Options API 中 import 的函数需挂到 methods 才能被模板访问
formatUserName(userId) {
return fmtUserName(userId);
},
statusColor(s) {
switch (s) {
case "PENDING": return "s-yellow";
case "WIP": return "s-blue";
case "COMPLETED": return "s-green";
case "REJECTED": return "s-red";
case "OUTBOUND": return "s-outbound";
default: return "s-gray";
}
},
formatTime(t) {
if (!t) return "";
const d = new Date(t);
const pad = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
},
},
};
</script>
<style scoped>
.tnode { position: relative; padding-left: 12px; margin-bottom: 6px; }
.tree-line-vertical { position: absolute; left: 8px; top: 28px; bottom: 0; width: 2px; background: #e5e7eb; }
.tnode-card {
display: flex; align-items: flex-start; justify-content: space-between;
padding: 10px 12px; border-radius: 10px 10px 0 0;
background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.04);
border-left: 3px solid transparent;
}
.is-rework { border-left-color: #ef4444 !important; }
.s-yellow { border-left-color: #f59e0b; }
.s-blue { border-left-color: #3b82f6; }
.s-green { border-left-color: #22c55e; }
.s-red { border-left-color: #ef4444; }
.s-outbound { border-left-color: #4f46e5; }
.tnode-body { flex: 1; min-width: 0; }
.tnode-name-row { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.tnode-name { font-size: 14px; font-weight: 700; color: #1f2937; }
.tag { font-size: 10px; padding: 1px 5px; border-radius: 6px; font-weight: 700; color: #fff; }
.tag-rework { background: #ef4444; }
.tag-fission { background: #7c3aed; }
.tnode-meta { margin-top: 2px; font-size: 11px; color: #9ca3af; display: flex; gap: 8px; flex-wrap: wrap; }
.reject-reason { color: #ef4444; }
.warehouse-in { display: block; margin-top: 4px; font-size: 11px; color: #059669; font-weight: 700; }
/* 任务初始描述(常驻,区别于动态记录) */
.task-initial-remark {
margin-top: 10rpx; padding: 12rpx 16rpx;
background: linear-gradient(135deg, #fefce8, #fef9c3);
border-left: 6rpx solid #eab308; border-radius: 8rpx;
}
.remark-label { font-size: 22rpx; font-weight: 700; color: #a16207; }
.remark-text { font-size: 26rpx; color: #713f12; line-height: 1.5; word-break: break-all; }
/* 进度记录 */
.records-area { background-color: #f9f9f9; padding: 0; border-radius: 10rpx; margin-top: 16rpx; margin-bottom: 6rpx; overflow: hidden; }
.records-bar {
display: flex; align-items: center; gap: 8rpx;
padding: 16rpx 20rpx;
background: linear-gradient(135deg, #eff6ff, #dbeafe);
border-bottom: 1px solid #bfdbfe;
}
.records-bar-icon { font-size: 28rpx; }
.records-bar-text { flex: 1; font-size: 26rpx; font-weight: 700; color: #2563eb; }
.records-bar-arrow { font-size: 32rpx; color: #93c5fd; font-weight: 700; }
.record-item { padding: 12rpx 16rpx; }
.record-item { padding: 8rpx 0; border-bottom: 1px dashed #e5e7eb; }
.record-item:last-child { border-bottom: none; }
.record-top { display: flex; align-items: center; justify-content: space-between; }
.record-actions { display: flex; gap: 12rpx; }
.rec-act { font-size: 28rpx; padding: 4rpx; }
.record-remark { font-size: 26rpx; color: #333; display: block; margin: 6rpx 0; line-height: 1.5; word-break: break-all; }
.record-images { display: flex; gap: 8rpx; margin-top: 8rpx; flex-wrap: wrap; }
.record-thumb { width: 100rpx; height: 100rpx; border-radius: 8rpx; border: 1px solid #e5e7eb; background: #f3f4f6; }
.record-time { font-size: 22rpx; color: #9ca3af; }
.badge {
font-size: 10px; padding: 2px 8px; border-radius: 20px; font-weight: 600;
white-space: nowrap; flex-shrink: 0;
background: #f3f4f6; color: #6b7280;
}
.end-marker { font-size: 9px; padding: 1px 6px; border-radius: 8px; font-weight: 600; white-space: nowrap; background: #dcfce7; color: #16a34a; }
.end-warehouse { background: #ede9fe; color: #7c3aed; }
.s-yellow .badge { background: #fef3c7; color: #b45309; }
.s-blue .badge { background: #dbeafe; color: #1d4ed8; }
.s-green .badge { background: #dcfce7; color: #15803d; }
.s-red .badge { background: #fce4ec; color: #be123c; }
.s-outbound .badge { background: #e0e7ff; color: #4338ca; }
.tnode-children { position: relative; }
</style>