双模态详情页: workspace(swiper+footer) / tree(只读流转树) 智能分流+一键切换

This commit is contained in:
2026-08-05 16:43:32 +08:00
parent 21aa608db3
commit 78559db1fa
2 changed files with 66 additions and 24 deletions

View File

@ -35,6 +35,7 @@
:currentUser="currentUser" :currentUser="currentUser"
:currentUserId="currentUserId" :currentUserId="currentUserId"
:currentUsername="currentUsername" :currentUsername="currentUsername"
:readonly="readonly"
:isLast="idx === task.child_tasks.length - 1" :isLast="idx === task.child_tasks.length - 1"
@action="(e) => $emit('action', e)" @action="(e) => $emit('action', e)"
@editRecord="(e) => $emit('editRecord', e)" @editRecord="(e) => $emit('editRecord', e)"
@ -85,6 +86,7 @@ export default {
currentUser: { type: Object, default: null }, currentUser: { type: Object, default: null },
currentUserId: { type: [String, Number], default: "" }, currentUserId: { type: [String, Number], default: "" },
currentUsername: { type: String, default: "" }, currentUsername: { type: String, default: "" },
readonly: { type: Boolean, default: false },
}, },
emits: ["action", "editRecord", "viewRecords"], emits: ["action", "editRecord", "viewRecords"],
mounted() { mounted() {
@ -94,6 +96,7 @@ export default {
}, },
computed: { computed: {
canEditRecord() { canEditRecord() {
if (this.readonly) return false;
if (this.task.assignee_id == this.currentUserId) return true; if (this.task.assignee_id == this.currentUserId) return true;
if (this.task.assignee_id == this.currentUsername) 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.id == this.task.assignee_id) return true;

View File

@ -24,6 +24,9 @@
<text class="card-title">📦 产品信息</text> <text class="card-title">📦 产品信息</text>
<view class="card-header-right"> <view class="card-header-right">
<text :class="['badge', statusColor(product.status)]">{{ statusLabel(product.status) }}</text> <text :class="['badge', statusColor(product.status)]">{{ statusLabel(product.status) }}</text>
<text class="mode-toggle" @tap="toggleMode">
{{ currentMode === 'workspace' ? '🌳 流转树' : '🛠 工作区' }}
</text>
<text class="edit-btn" @tap="openEditProduct"></text> <text class="edit-btn" @tap="openEditProduct"></text>
</view> </view>
</view> </view>
@ -54,9 +57,9 @@
</view> </view>
<!-- ================================================================ --> <!-- ================================================================ -->
<!-- 任务卡片看板 (专注模式 + 左右滑动) --> <!-- 🛠 个人工作区 (workspace) -->
<!-- ================================================================ --> <!-- ================================================================ -->
<view class="swiper-area"> <view v-if="currentMode === 'workspace'" class="swiper-area">
<!-- 0任务 --> <!-- 0任务 -->
<view v-if="!product.task_tree || !product.task_tree.length" class="zero-task"> <view v-if="!product.task_tree || !product.task_tree.length" class="zero-task">
<text class="zero-icon">📋</text> <text class="zero-icon">📋</text>
@ -126,32 +129,51 @@
</swiper-item> </swiper-item>
</swiper> </swiper>
</template> </template>
<!-- 底部操作栏 workspace 模式 -->
<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="currentTask.status === 'WIP'" class="footer-btn footer-record"
@tap="handleTaskAction({ task: currentTask, type: 'record' })">
📝 记录/拍照
</button>
<button v-if="currentTask.status === 'WIP'" class="footer-btn footer-end"
@tap="handleTaskAction({ task: currentTask, type: 'end' })">
🏁 结束分支
</button>
<button v-if="currentTask.status === 'PENDING'" class="footer-btn footer-receive"
@tap="handleTaskAction({ task: currentTask, type: 'receive' })">
接收任务
</button>
<button v-if="currentTask.status === 'PENDING'" class="footer-btn footer-reject"
@tap="handleTaskAction({ task: currentTask, type: 'reject' })">
驳回任务
</button>
</view>
</view> </view>
<!-- ================================================================ --> <!-- ================================================================ -->
<!-- 底部操作栏钉死不随滚动 --> <!-- 🌳 全局流转树 (tree 模式 只读) -->
<!-- ================================================================ --> <!-- ================================================================ -->
<view v-if="currentTask" class="footer-actions"> <view v-if="currentMode === 'tree'" class="tree-area">
<button v-if="currentTask.status === 'WIP'" class="footer-btn footer-transfer" <view v-if="!product.task_tree || !product.task_tree.length" class="zero-task">
@tap="handleTaskAction({ task: currentTask, type: 'transfer' })"> <text class="zero-icon">📋</text>
🔄 完工转交 <text class="zero-text">该产品暂无流转任务</text>
</button> </view>
<button v-if="currentTask.status === 'WIP'" class="footer-btn footer-record" <TaskTreeNode
@tap="handleTaskAction({ task: currentTask, type: 'record' })"> v-for="(task, idx) in (product.task_tree || [])"
📝 记录/拍照 :key="task.id"
</button> :task="task"
<button v-if="currentTask.status === 'WIP'" class="footer-btn footer-end" :currentUser="currentUser"
@tap="handleTaskAction({ task: currentTask, type: 'end' })"> :currentUserId="currentUserId"
🏁 结束分支 :currentUsername="currentUsername"
</button> :hasChildren="task.child_tasks && task.child_tasks.length > 0"
<button v-if="currentTask.status === 'PENDING'" class="footer-btn footer-receive" :isLast="idx === (product.task_tree || []).length - 1"
@tap="handleTaskAction({ task: currentTask, type: 'receive' })"> :readonly="true"
接收任务 @viewRecords="handleViewRecords"
</button> />
<button v-if="currentTask.status === 'PENDING'" class="footer-btn footer-reject"
@tap="handleTaskAction({ task: currentTask, type: 'reject' })">
驳回任务
</button>
</view> </view>
</template> </template>
@ -397,6 +419,7 @@ export default {
currentUserId: "", currentUserId: "",
currentUsername: "", currentUsername: "",
swiperCurrent: 0, swiperCurrent: 0,
currentMode: "tree",
// Picker 数据源 // Picker 数据源
processOptions: [], processOptions: [],
userOptions: [], userOptions: [],
@ -453,6 +476,9 @@ export default {
currentTask() { currentTask() {
return this.focusTasks[this.swiperCurrent] || null; return this.focusTasks[this.swiperCurrent] || null;
}, },
hasMyActiveTask() {
return this.focusTasks.length > 0;
},
}, },
onLoad(options) { onLoad(options) {
this.loadUsers(); this.loadUsers();
@ -479,9 +505,18 @@ export default {
try { try {
this.product = await get(`/products/scan/${sn}`); this.product = await get(`/products/scan/${sn}`);
if (!this.product.overall_status) this.showStatusPicker = true; if (!this.product.overall_status) this.showStatusPicker = true;
// 智能分流:有本人活跃任务 → workspace否则 → tree
this.$nextTick(() => {
this.currentMode = this.hasMyActiveTask ? 'workspace' : 'tree';
this.swiperCurrent = 0;
});
} catch (e) { this.error = e?.data?.detail || "查询失败"; } } catch (e) { this.error = e?.data?.detail || "查询失败"; }
finally { this.loading = false; } finally { this.loading = false; }
}, },
toggleMode() {
this.currentMode = this.currentMode === 'workspace' ? 'tree' : 'workspace';
if (this.currentMode === 'workspace') this.swiperCurrent = 0;
},
// ---- 状态定调 ---- // ---- 状态定调 ----
async handleSetOverallStatus(status) { async handleSetOverallStatus(status) {
@ -915,10 +950,14 @@ export default {
.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-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; } .fc-time { font-size: 11px; color: #9ca3af; margin-top: auto; padding-top: 8px; border-top: 1px solid #f3f4f6; }
/* 流转树视图(只读) */
.tree-area { flex: 1; overflow-y: auto; min-height: 0; padding: 4px 0; -webkit-overflow-scrolling: touch; }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .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-header-right { display: flex; align-items: center; gap: 8px; }
.card-title { font-size: 15px; font-weight: 700; } .card-title { font-size: 15px; font-weight: 700; }
.edit-btn { font-size: 18px; padding: 2px 6px; } .edit-btn { font-size: 18px; padding: 2px 6px; }
.mode-toggle { font-size: 13px; font-weight: 700; padding: 4px 10px; border-radius: 8px; background: #eff6ff; color: #2563eb; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.label { font-size: 12px; color: #9ca3af; } .label { font-size: 12px; color: #9ca3af; }
.value { font-size: 14px; color: #1f2937; font-weight: 600; word-break: break-all; } .value { font-size: 14px; color: #1f2937; font-weight: 600; word-break: break-all; }