修复remark丢失(_to_response漏字段) + UI重构swiper专注卡片看板
This commit is contained in:
@ -104,6 +104,7 @@ def _to_response(task: Task) -> TaskResponse:
|
||||
status=task.status,
|
||||
notify_parent_on_complete=task.notify_parent_on_complete,
|
||||
is_rework=task.is_rework,
|
||||
remark=task.remark,
|
||||
reject_reason=task.reject_reason,
|
||||
received_at=task.received_at,
|
||||
completed_at=task.completed_at,
|
||||
|
||||
@ -54,57 +54,98 @@
|
||||
</view>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- 任务树(撑满剩余空间,可滚动) -->
|
||||
<!-- 任务卡片看板 (专注模式 + 左右滑动) -->
|
||||
<!-- ================================================================ -->
|
||||
<view class="tree-scroll-area">
|
||||
<view class="card card-tree">
|
||||
<view class="card-header">
|
||||
<text class="card-title">🌿 任务流转树</text>
|
||||
<text class="task-count">{{ countTasks(product.task_tree) }} 个任务</text>
|
||||
</view>
|
||||
|
||||
<!-- 0任务 → 发起首道工序 -->
|
||||
<view class="swiper-area">
|
||||
<!-- 0任务 -->
|
||||
<view v-if="!product.task_tree || !product.task_tree.length" class="zero-task">
|
||||
<text class="zero-icon">📋</text>
|
||||
<text class="zero-text">该产品暂无流转任务</text>
|
||||
<button class="btn-start" @tap="openCreateFirstTask">🚀 发起首道工序</button>
|
||||
</view>
|
||||
|
||||
<!-- 递归树 -->
|
||||
<TaskTreeNode
|
||||
v-for="(task, idx) in (product.task_tree || [])"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
:currentUser="currentUser"
|
||||
:currentUserId="currentUserId"
|
||||
:currentUsername="currentUsername"
|
||||
:hasChildren="task.child_tasks && task.child_tasks.length > 0"
|
||||
:isLast="idx === (product.task_tree || []).length - 1"
|
||||
@action="handleTaskAction"
|
||||
@editRecord="openEditRecord"
|
||||
@viewRecords="handleViewRecords"
|
||||
/>
|
||||
<!-- 无本人任务但有其他任务 -->
|
||||
<view v-else-if="!focusTasks.length" class="zero-task">
|
||||
<text class="zero-icon">🔒</text>
|
||||
<text class="zero-text">当前没有需要你处理的任务</text>
|
||||
<text class="task-count">共 {{ countTasks(product.task_tree) }} 个任务</text>
|
||||
</view>
|
||||
|
||||
<!-- Swiper 卡片 -->
|
||||
<template v-else>
|
||||
<view class="swipe-nav">
|
||||
<text class="swipe-info">{{ swiperCurrent + 1 }} / {{ focusTasks.length }}</text>
|
||||
<text class="swipe-hint">← 左右滑动切换任务 →</text>
|
||||
</view>
|
||||
|
||||
<swiper
|
||||
class="task-swiper"
|
||||
:current="swiperCurrent"
|
||||
@change="onSwiperChange"
|
||||
:indicator-dots="false"
|
||||
:circular="false"
|
||||
>
|
||||
<swiper-item v-for="(t, i) in focusTasks" :key="t.id">
|
||||
<view class="focus-card" :class="statusColor(t.status)">
|
||||
<!-- 卡片头 -->
|
||||
<view class="fc-header">
|
||||
<text class="fc-status">{{ statusLabel(t.status) }}</text>
|
||||
<text v-if="t.is_rework" class="tag tag-rework-sm">⚠返工</text>
|
||||
</view>
|
||||
|
||||
<!-- 任务名 -->
|
||||
<text class="fc-name">{{ t.task_name }}</text>
|
||||
|
||||
<!-- remark初始说明 -->
|
||||
<view v-if="t.remark" class="fc-remark">
|
||||
<text class="fc-remark-label">📌</text>
|
||||
<text class="fc-remark-text">{{ t.remark }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 来源/去向 -->
|
||||
<view class="fc-chain">
|
||||
<view v-if="t.parent_task_id && parentTaskOf(t)" class="fc-link fc-up">
|
||||
<text class="fc-link-label">⬆ 上一步</text>
|
||||
<text class="fc-link-name">{{ parentTaskOf(t).task_name }}</text>
|
||||
<text class="fc-link-who">→ {{ parentTaskOf(t).assignee_id || '—' }}</text>
|
||||
</view>
|
||||
<view v-if="t.child_tasks && t.child_tasks.length" class="fc-link fc-down">
|
||||
<text class="fc-link-label">⬇ 下一步 ({{ t.child_tasks.length }})</text>
|
||||
<text v-for="c in t.child_tasks" :key="c.id" class="fc-link-name">· {{ c.task_name }} → {{ c.assignee_id || '—' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新记录 -->
|
||||
<view v-if="t.records && t.records.length" class="fc-records-bar" @tap="handleViewRecords(t)">
|
||||
📋 {{ t.records.length }} 条记录 ›
|
||||
</view>
|
||||
|
||||
<!-- 时间 -->
|
||||
<view class="fc-time">{{ formatTaskTime(t) }}</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- 底部操作栏(钉死,不随滚动) -->
|
||||
<!-- ================================================================ -->
|
||||
<view v-if="myActiveTask" class="footer-actions">
|
||||
<button v-if="myActiveTask.status === 'WIP'" class="footer-btn footer-transfer"
|
||||
@tap="handleTaskAction({ task: myActiveTask, type: 'transfer' })">
|
||||
<view v-if="currentTask" class="footer-actions">
|
||||
<button v-if="currentTask.status === 'WIP'" class="footer-btn footer-transfer"
|
||||
@tap="handleTaskAction({ task: currentTask, type: 'transfer' })">
|
||||
🔄 完工转交
|
||||
</button>
|
||||
<button v-if="myActiveTask.status === 'WIP'" class="footer-btn footer-record"
|
||||
@tap="handleTaskAction({ task: myActiveTask, type: 'record' })">
|
||||
<button v-if="currentTask.status === 'WIP'" class="footer-btn footer-record"
|
||||
@tap="handleTaskAction({ task: currentTask, type: 'record' })">
|
||||
📝 记录/拍照
|
||||
</button>
|
||||
<button v-if="myActiveTask.status === 'PENDING'" class="footer-btn footer-receive"
|
||||
@tap="handleTaskAction({ task: myActiveTask, type: 'receive' })">
|
||||
<button v-if="currentTask.status === 'PENDING'" class="footer-btn footer-receive"
|
||||
@tap="handleTaskAction({ task: currentTask, type: 'receive' })">
|
||||
✅ 接收任务
|
||||
</button>
|
||||
<button v-if="myActiveTask.status === 'PENDING'" class="footer-btn footer-reject"
|
||||
@tap="handleTaskAction({ task: myActiveTask, type: 'reject' })">
|
||||
<button v-if="currentTask.status === 'PENDING'" class="footer-btn footer-reject"
|
||||
@tap="handleTaskAction({ task: currentTask, type: 'reject' })">
|
||||
❌ 驳回任务
|
||||
</button>
|
||||
</view>
|
||||
@ -351,6 +392,7 @@ export default {
|
||||
currentUser: null,
|
||||
currentUserId: "",
|
||||
currentUsername: "",
|
||||
swiperCurrent: 0,
|
||||
// Picker 数据源
|
||||
processOptions: [],
|
||||
userOptions: [],
|
||||
@ -378,38 +420,34 @@ export default {
|
||||
return !!b.assignee_id;
|
||||
});
|
||||
},
|
||||
activeTask() {
|
||||
// 查找树中第一个 WIP 或 PENDING 任务(不限归属)
|
||||
const findActive = (tasks) => {
|
||||
if (!tasks) return null;
|
||||
for (const t of tasks) {
|
||||
if (t.status === 'WIP' || t.status === 'PENDING') return t;
|
||||
if (t.child_tasks && t.child_tasks.length) {
|
||||
const found = findActive(t.child_tasks);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
// 构建 parent_task_id → task 的索引
|
||||
taskMap() {
|
||||
const m = {};
|
||||
const walk = (tasks) => {
|
||||
if (!tasks) return;
|
||||
for (const t of tasks) { m[t.id] = t; walk(t.child_tasks); }
|
||||
};
|
||||
return this.product ? findActive(this.product.task_tree) : null;
|
||||
if (this.product) walk(this.product.task_tree);
|
||||
return m;
|
||||
},
|
||||
myActiveTask() {
|
||||
// 只返回当前登录人自己的 WIP/PENDING 任务
|
||||
const findMine = (tasks) => {
|
||||
if (!tasks) return null;
|
||||
// 只返回当前用户需要处理的活跃任务 (PENDING/WIP)
|
||||
focusTasks() {
|
||||
const result = [];
|
||||
const walk = (tasks) => {
|
||||
if (!tasks) return;
|
||||
for (const t of tasks) {
|
||||
const isMine = t.status === 'WIP' || t.status === 'PENDING';
|
||||
const matchId = t.assignee_id == this.currentUserId;
|
||||
const matchName = t.assignee_id == this.currentUsername;
|
||||
if (isMine && (matchId || matchName)) return t;
|
||||
if (t.child_tasks && t.child_tasks.length) {
|
||||
const found = findMine(t.child_tasks);
|
||||
if (found) return found;
|
||||
if (isMine && (matchId || matchName)) result.push(t);
|
||||
walk(t.child_tasks);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return this.product ? findMine(this.product.task_tree) : null;
|
||||
if (this.product) walk(this.product.task_tree);
|
||||
return result;
|
||||
},
|
||||
currentTask() {
|
||||
return this.focusTasks[this.swiperCurrent] || null;
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
@ -709,6 +747,26 @@ export default {
|
||||
handleViewRecords(task) {
|
||||
uni.navigateTo({ url: `/pages/scan/records?taskId=${task.id}` });
|
||||
},
|
||||
onSwiperChange(e) {
|
||||
this.swiperCurrent = e.detail.current;
|
||||
},
|
||||
parentTaskOf(t) {
|
||||
if (!t || !t.parent_task_id) return null;
|
||||
return this.taskMap[t.parent_task_id] || null;
|
||||
},
|
||||
formatTaskTime(t) {
|
||||
if (!t) return "";
|
||||
const parts = [];
|
||||
if (t.created_at) parts.push("创建: " + this.formatTime(t.created_at));
|
||||
if (t.received_at) parts.push("接收: " + this.formatTime(t.received_at));
|
||||
return parts.join(" | ");
|
||||
},
|
||||
formatTime(d) {
|
||||
if (!d) return "";
|
||||
const dt = new Date(d);
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
|
||||
},
|
||||
closeActionPopup() { this.actionPopup = { visible: false, type: "", task: null }; },
|
||||
|
||||
async doReceive() {
|
||||
@ -795,10 +853,46 @@ export default {
|
||||
|
||||
/* 卡片 */
|
||||
.card { background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); flex-shrink: 0; }
|
||||
/* 任务树滚动区域 → 撑满剩余空间 */
|
||||
.tree-scroll-area { flex: 1; overflow-y: auto; min-height: 0; -webkit-overflow-scrolling: touch; }
|
||||
.card-tree { margin-bottom: 0; }
|
||||
.card-tree .card-header { flex-shrink: 0; }
|
||||
/* Swiper 卡片看板 */
|
||||
.swiper-area { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||
.swipe-nav { display: flex; align-items: center; justify-content: space-between; padding: 8px 4px; flex-shrink: 0; }
|
||||
.swipe-info { font-size: 12px; font-weight: 700; color: #2563eb; }
|
||||
.swipe-hint { font-size: 11px; color: #9ca3af; }
|
||||
.task-swiper { flex: 1; min-height: 0; }
|
||||
.task-count { font-size: 12px; color: #9ca3af; display: block; margin-top: 4px; }
|
||||
|
||||
.focus-card {
|
||||
height: 100%; margin: 0 4px; padding: 20px 16px; border-radius: 16px;
|
||||
background: #fff; box-shadow: 0 2px 12px rgba(0,0,0,0.08); overflow-y: auto;
|
||||
border-top: 5px solid #3b82f6;
|
||||
}
|
||||
.focus-card.s-yellow { border-top-color: #f59e0b; }
|
||||
.focus-card.s-blue { border-top-color: #3b82f6; }
|
||||
.focus-card.s-green { border-top-color: #22c55e; }
|
||||
.focus-card.s-red { border-top-color: #ef4444; }
|
||||
|
||||
.fc-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.fc-status { font-size: 12px; padding: 2px 10px; border-radius: 12px; font-weight: 700; background: #f3f4f6; color: #6b7280; }
|
||||
.s-yellow .fc-status { background: #fef3c7; color: #b45309; }
|
||||
.s-blue .fc-status { background: #dbeafe; color: #1d4ed8; }
|
||||
.tag-rework-sm { font-size: 10px; padding: 2px 6px; border-radius: 8px; background: #ef4444; color: #fff; }
|
||||
|
||||
.fc-name { font-size: 22px; font-weight: 800; color: #1f2937; display: block; margin-bottom: 10px; }
|
||||
|
||||
.fc-remark { display: flex; gap: 6px; padding: 10px 12px; background: #fefce8; border-radius: 10px; margin-bottom: 10px; border-left: 4px solid #eab308; }
|
||||
.fc-remark-label { font-size: 14px; flex-shrink: 0; }
|
||||
.fc-remark-text { font-size: 14px; color: #713f12; line-height: 1.5; word-break: break-all; }
|
||||
|
||||
.fc-chain { margin-bottom: 8px; }
|
||||
.fc-link { padding: 6px 10px; border-radius: 8px; margin-bottom: 4px; font-size: 12px; }
|
||||
.fc-up { background: #f0fdf4; color: #16a34a; }
|
||||
.fc-down { background: #eff6ff; color: #2563eb; }
|
||||
.fc-link-label { font-weight: 700; display: block; }
|
||||
.fc-link-name { display: block; margin-top: 2px; }
|
||||
.fc-link-who { color: #9ca3af; font-size: 11px; }
|
||||
|
||||
.fc-records-bar { padding: 8px 12px; background: linear-gradient(135deg,#eff6ff,#dbeafe); border-radius: 8px; font-size: 13px; font-weight: 700; color: #2563eb; margin-bottom: 8px; }
|
||||
.fc-time { font-size: 11px; color: #9ca3af; margin-top: auto; padding-top: 8px; border-top: 1px solid #f3f4f6; }
|
||||
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||||
.card-header-right { display: flex; align-items: center; gap: 8px; }
|
||||
.card-title { font-size: 15px; font-weight: 700; }
|
||||
|
||||
Reference in New Issue
Block a user