feat: 移动端全局人名映射 + 协同留言板 UI

- detail.vue: 留言板聊天室 UI + 乐观更新 + loadUsers 填充全局字典
- WorkspaceArea.vue: 任务列表/上游/下游负责人全部走 formatUserName
- TreeCanvas.vue: 节点负责人 + 嵌套协助提示走 formatUserName
- TaskSwipeCards.vue: 卡片负责人走 formatUserName
- 留言板头像取中文名末字(formatUserAvatar)
This commit is contained in:
2026-08-10 17:37:25 +08:00
parent 05003d3053
commit f9451b104b
4 changed files with 184 additions and 83 deletions

View File

@ -25,7 +25,7 @@
<view class="tli-left">
<text class="tag-branch">{{ branchLabelsMap[t.id] || '' }}</text>
<text class="tli-name">{{ t.task_name }}</text>
<text class="tli-assignee">→ {{ t.assignee_id || '未分配' }}</text>
<text class="tli-assignee">→ {{ formatUserName(t.assignee_id) || '未分配' }}</text>
<text v-if="getTaskRemark(t)" class="tli-remark">{{ getTaskRemark(t) }}</text>
</view>
<view class="tli-right">
@ -60,13 +60,13 @@
</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>
<text class="fc-link-label">{{ isNestedSpawn ? '⬆ 上游协助 (嵌套)' : '⬆ 上游工序' }}</text>
<text class="fc-link-name">{{ parentTaskOf(lockedTask).task_name }} → {{ formatUserName(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 || '—' }}
· {{ c.task_name }} → {{ formatUserName(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>
@ -102,6 +102,7 @@
</template>
<script>
import { formatUserName } from "../../../utils/format";
const STATUS_MAP = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库", CANCELED: "已撤回" };
export default {
@ -142,6 +143,7 @@ export default {
},
focusTasks() {
const result = [];
const isMainFn = (t) => !t.parent_task_id || t.task_type === 'TRANSFER' || t.task_type === 'RECOVERY';
const walk = (tasks) => {
if (!tasks) return;
for (const t of tasks) {
@ -151,8 +153,8 @@ export default {
};
if (this.product) walk(this.product.task_tree);
result.sort((a, b) => {
const aIsMain = !a.parent_task_id || a.task_type === 'TRANSFER' || a.task_type === 'RECOVERY';
const bIsMain = !b.parent_task_id || b.task_type === 'TRANSFER' || b.task_type === 'RECOVERY';
const aIsMain = isMainFn(a);
const bIsMain = isMainFn(b);
if (aIsMain && !bIsMain) return -1;
if (!aIsMain && bIsMain) return 1;
return new Date(a.created_at) - new Date(b.created_at);
@ -169,11 +171,17 @@ export default {
if (!parent) return false;
return parent.assignee_id == this.currentUserId || parent.assignee_id == this.currentUsername;
},
// 🚀 嵌套协助判定:直接父任务不是主线 → 孙子/曾孙协助
isNestedSpawn() {
if (!this.lockedTask || !this.lockedTask.parent_task_id) return false;
const parent = this.taskMap[this.lockedTask.parent_task_id];
if (!parent) return false;
return parent.parent_task_id && parent.task_type !== 'TRANSFER' && parent.task_type !== 'RECOVERY';
},
branchLabelsMap() {
const map = {};
if (!this.product || !this.product.task_tree) return map;
// 拍平树结构,方便随时查父节点状态
const flatMap = {};
const flatten = (tasks) => {
if (!tasks) return;
@ -181,22 +189,14 @@ export default {
};
flatten(this.product.task_tree);
// 🚀 仅凭基因字段判定,移除危险的状态兜底
const isMainFn = (t) => !t.parent_task_id || t.task_type === 'TRANSFER' || t.task_type === 'RECOVERY';
const traverse = (tasks, prefix) => {
if (!tasks) return;
let spawnIndex = 0;
tasks.forEach((t) => {
const parent = t.parent_task_id ? flatMap[t.parent_task_id] : null;
// 🚀 终极防御判定:
// 1. 没有父节点(打底任务) -> 主分支
// 2. 明确的基因 (TRANSFER/RECOVERY) -> 主分支
// 3. 兜底:如果基因丢失,但它的父节点已经完工(COMPLETED),那它必然是接力的转交任务 -> 主分支
const isMain = !t.parent_task_id
|| t.task_type === 'TRANSFER'
|| t.task_type === 'RECOVERY'
|| (!t.task_type && parent && (parent.status === 'COMPLETED' || parent.status === 'CANCELED'));
if (isMain) {
if (isMainFn(t)) {
map[t.id] = '主分支';
traverse(t.child_tasks, prefix);
} else {
@ -215,6 +215,7 @@ export default {
},
},
methods: {
formatUserName,
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; },