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

@ -44,7 +44,7 @@
<view class="tc-meta">
<view class="tc-meta-row">
<text class="tc-meta-label">👤 负责人</text>
<text class="tc-meta-val">{{ card.assignee_id || '未分配' }}</text>
<text class="tc-meta-val">{{ formatUserName(card.assignee_id) || '未分配' }}</text>
</view>
</view>
@ -94,6 +94,7 @@
</template>
<script>
import { formatUserName } from "../../../utils/format";
export default {
name: "TaskSwipeCards",
props: {
@ -116,14 +117,12 @@ export default {
tasks.forEach(t => { flatMap[t.id] = t; flatten(t.child_tasks); });
};
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;
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) { map[t.id] = '主分支'; traverse(t.child_tasks, prefix); }
if (isMainFn(t)) { map[t.id] = '主分支'; traverse(t.child_tasks, prefix); }
else { spawnIndex++; const num = prefix ? `${prefix}.${spawnIndex}` : `${spawnIndex}`; map[t.id] = `分支 ${num}`; traverse(t.child_tasks, num); }
});
};
@ -134,14 +133,14 @@ export default {
const result = [];
if (!this.product || !this.product.task_tree) return result;
// 分支0主分支
const mainLane = { _key: 'main', label: '主分支', cards: [], _cardIdx: 0 };
const isMainFn = (t) => !t.parent_task_id || t.task_type === 'TRANSFER' || t.task_type === 'RECOVERY';
const sortTasks = (tasks) => {
if (!tasks) return [];
return [...tasks].sort((a, b) => {
const aMain = (!a.parent_task_id || a.task_type === 'TRANSFER' || a.task_type === 'RECOVERY') ? -1 : 1;
const bMain = (!b.parent_task_id || b.task_type === 'TRANSFER' || b.task_type === 'RECOVERY') ? -1 : 1;
const aMain = isMainFn(a) ? -1 : 1;
const bMain = isMainFn(b) ? -1 : 1;
if (aMain !== bMain) return aMain - bMain;
return new Date(a.created_at) - new Date(b.created_at);
});
@ -152,17 +151,13 @@ export default {
const sorted = sortTasks(taskList);
for (const t of sorted) {
const isMain = !t.parent_task_id || t.task_type === 'TRANSFER' || t.task_type === 'RECOVERY'
|| (!t.task_type && t.parent_task_id);
if (isMain) {
if (isMainFn(t)) {
lane.cards.push({ ...t, _key: t.id + '_' + lane._key, _isMain: lane._key === 'main' });
// 递归:本分支下一步 = TRANSFER 子节点
if (t.child_tasks && t.child_tasks.length) {
const nextMain = t.child_tasks.filter(c =>
c.task_type === 'TRANSFER' || c.task_type === 'RECOVERY' || !c.task_type);
c.task_type === 'TRANSFER' || c.task_type === 'RECOVERY');
followMain(nextMain, lane);
// SPAWN 子节点 → 新分支
// SPAWN 子节点 → 新分支(含嵌套 spawn
const spawns = t.child_tasks.filter(c => c.task_type === 'SPAWN');
for (const sc of spawns) {
const blabel = this.branchLabelMap[sc.id] || '分支';
@ -190,6 +185,7 @@ export default {
} catch (e) { /* ignore */ }
},
methods: {
formatUserName,
onLaneSwipe(e) { this.currentLane = e.detail.current; },
onCardSwipe(e, laneIdx) {
if (this.lanes[laneIdx]) {