feat: 移动端双Token队列拦截器 + 通知列表 + 登录页 + TabBar红点 + v1.0.1

This commit is contained in:
2026-08-07 11:44:20 +08:00
parent 721cfe1504
commit 0df7215134
13 changed files with 1013 additions and 206 deletions

View File

@ -4,45 +4,55 @@
<view v-if="error" class="error-box">{{ error }}</view>
<template v-if="product && !loading">
<view class="overall-bar" @tap="handleOverallBarClick">
<text class="overall-label">宏观状态</text>
<text :class="['overall-val', product.overall_status ? '' : 'overall-empty']">{{ product.overall_status || '未激活 — 点击发起首道工序' }}</text>
<text v-if="product.task_tree && product.task_tree.length" class="overall-arrow">▾</text>
</view>
<!-- 工作区模式:显示产品信息栏 -->
<template v-if="currentMode === 'workspace'">
<view class="overall-bar" @tap="handleOverallBarClick">
<text class="overall-label">宏观状态</text>
<text :class="['overall-val', product.overall_status ? '' : 'overall-empty']">{{ product.overall_status || '未激活 — 点击发起首道工序' }}</text>
<text v-if="product.task_tree && product.task_tree.length" class="overall-arrow">▾</text>
</view>
<view class="card">
<view class="card-header">
<text class="card-title">📦 产品信息</text>
<view class="card-header-right">
<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>
<view class="card">
<view class="card-header">
<text class="card-title">📦 产品信息</text>
<view class="card-header-right">
<text class="mode-toggle" @tap="toggleMode">{{ modeToggleLabel }}</text>
<text class="edit-btn" @tap="openEditProduct">✏️</text>
</view>
</view>
<view class="info-grid">
<view class="info-item"><text class="label">身份证</text><text class="value sn">{{ product.serial_number }}</text></view>
<view class="info-item" v-if="product.external_serial"><text class="label">产品序列号</text><text class="value sn">{{ product.external_serial }}</text></view>
<view class="info-item"><text class="label">物料名称</text><text class="value">{{ product.material_name || product.material_id || '—' }}</text></view>
<view class="info-item"><text class="label">规格型号</text><text class="value">{{ product.spec_model || '—' }}</text></view>
<view class="info-item"><text class="label">订单编号</text><text class="value">{{ product.order_no || '—' }}</text></view>
<view class="info-item" v-if="product.current_location_id">
<text class="label">当前位置</text>
<text :class="['value', product.current_location_id === 'virtual_warehouse' ? 'warehouse' : '']">{{ product.current_location_id === 'virtual_warehouse' ? '🏭 仓库' : product.current_location_id }}</text>
</view>
</view>
</view>
<view class="info-grid">
<view class="info-item"><text class="label">身份证</text><text class="value sn">{{ product.serial_number }}</text></view>
<view class="info-item"><text class="label">物料名称</text><text class="value">{{ product.material_name || product.material_id || '—' }}</text></view>
<view class="info-item"><text class="label">规格型号</text><text class="value">{{ product.spec_model || '—' }}</text></view>
<view class="info-item"><text class="label">订单编号</text><text class="value">{{ product.order_no || '—' }}</text></view>
<view class="info-item" v-if="product.current_location_id">
<text class="label">当前位置</text>
<text :class="['value', product.current_location_id === 'virtual_warehouse' ? 'warehouse' : '']">{{ product.current_location_id === 'virtual_warehouse' ? '🏭 仓库' : product.current_location_id }}</text>
</view>
<view v-if="product && product.current_location_id === 'virtual_warehouse' && !hasMyActiveTask"
class="warehouse-transfer-banner" @tap="openWarehouseTransfer">
<text class="wt-icon">📤</text>
<text class="wt-text">该产品在仓库中 — 点击此处转出并派发给指定人员</text>
</view>
</view>
<!-- 📤 仓库转出横幅(当前用户有活跃任务时隐藏) -->
<view v-if="product && product.current_location_id === 'virtual_warehouse' && !hasMyActiveTask"
class="warehouse-transfer-banner" @tap="openWarehouseTransfer">
<text class="wt-icon">📤</text>
<text class="wt-text">该产品在仓库中 — 点击此处转出并派发给指定人员</text>
</view>
</template>
<!-- 工作区视图 -->
<WorkspaceArea v-if="currentMode === 'workspace'" :product="product"
:currentUserId="currentUserId" :currentUsername="currentUsername"
:initialLockTaskId="autoLockTaskId"
@action="handleTaskAction" @viewRecords="handleViewRecords" />
<TreeCanvas v-if="currentMode === 'tree'" :product="product" @viewRecords="handleViewRecords" />
<!-- 📇 流转卡片:探探式单张滑动 -->
<TaskSwipeCards v-if="currentMode === 'swipe'" :product="product"
@back="currentMode = 'workspace'" @overview="currentMode = 'tree'"
@viewRecords="handleViewRecords" />
<!-- 🌳 流转树:全屏独立视图 -->
<TreeCanvas v-if="currentMode === 'tree'" :product="product" @viewRecords="handleViewRecords" @back="currentMode = 'workspace'" @swipe="currentMode = 'swipe'" />
</template>
<!-- 状态定调 -->
@ -155,13 +165,14 @@
import request, { get, post, patch, put } from "../../utils/request";
import WorkspaceArea from "./components/WorkspaceArea.vue";
import TreeCanvas from "./components/TreeCanvas.vue";
import TaskSwipeCards from "./components/TaskSwipeCards.vue";
const OVERALL_OPTIONS = ["备货", "生产", "测试", "维修", "在库"];
const TASK_NAME_OPTIONS = ["备货", "生产", "测试", "维修", "在库"];
const STATUS_MAP = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库", CANCELED: "已撤回" };
export default {
components: { WorkspaceArea, TreeCanvas },
components: { WorkspaceArea, TreeCanvas, TaskSwipeCards },
data() {
return {
OVERALL_OPTIONS, loading: true, error: "", product: null,
@ -169,7 +180,7 @@ export default {
users: [], TASK_NAME_OPTIONS,
createFirstVisible: false, isWarehouseTransfer: false, firstForm: { task_name: "", taskNameIdx: 0, assignee_id: "", assigneeLabel: "", assigneeIdx: 0, note: "" }, firstSaving: false,
recordPopup: { visible: false, task: null }, recordForm: { recordId: null, remark: "", images: [], pendingCount: 0 }, recordSaving: false, isUploading: false,
currentUser: null, currentUserId: "", currentUsername: "", currentMode: "tree",
currentUser: null, currentUserId: "", currentUsername: "", currentMode: "workspace", autoLockTaskId: "",
processOptions: [], userOptions: [],
actionPopup: { visible: false, type: "", task: null }, actionLoading: false, rejectReason: "", receiveRemark: "", receiveTaskName: "",
transferForm: { selectedUserId: "", isWarehouse: false, note: "" },
@ -182,6 +193,7 @@ export default {
transferUserName() { const u = this.userOptions.find(u => u.id === this.transferForm.selectedUserId); return u ? u.name : ""; },
spawnUserName() { const u = this.userOptions.find(u => u.id === this.spawnForm.assignee_id); return u ? u.name : ""; },
userGridOptions() { return (this.userOptions || []).map(u => ({ id: u.id, name: u.name })); },
modeToggleLabel() { if (this.currentMode === 'workspace') return '📇 流转卡片'; if (this.currentMode === 'swipe') return '🌳 流转树'; return '🛠️ 工作区'; },
hasMyActiveTask() {
const find = (tasks) => { if (!tasks) return false; for (const t of tasks) { if ((t.status === 'WIP' || t.status === 'PENDING') && (t.assignee_id == this.currentUserId || t.assignee_id == this.currentUsername)) return true; if (find(t.child_tasks)) return true; } return false; };
return this.product ? find(this.product.task_tree) : false;
@ -193,8 +205,24 @@ export default {
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"; } },
async doQuery(sn) { this.loading = true; this.error = ""; try { this.product = await get(`/products/scan/${sn}`); this.$nextTick(() => { this.currentMode = this.hasMyActiveTask ? 'workspace' : 'tree'; if (!this.product.task_tree || !this.product.task_tree.length) { this.openCreateFirstTask(); } }); } catch (e) { this.error = e?.data?.detail || "查询失败"; } finally { this.loading = false; } },
toggleMode() { this.currentMode = this.currentMode === 'workspace' ? 'tree' : 'workspace'; },
async doQuery(sn) { this.loading = true; this.error = ""; try { this.product = await get(`/products/scan/${sn}`); this.$nextTick(() => { this.currentMode = 'workspace'; this.autoLockTaskId = this.findMyImmersiveTask() || ''; if (!this.product.task_tree || !this.product.task_tree.length) { this.openCreateFirstTask(); } }); } catch (e) { this.error = e?.data?.detail || "查询失败"; } finally { this.loading = false; } },
findMyImmersiveTask() {
// 🚀 扫描用户的 WIP/PENDING 任务,自动沉浸锁定
if (!this.product || !this.product.task_tree) return null;
let wipTask = null, pendingTask = null;
const walk = (tasks) => {
if (!tasks) return;
for (const t of tasks) {
const isMine = t.assignee_id == this.currentUserId || t.assignee_id == this.currentUsername;
if (isMine && t.status === 'WIP') wipTask = t;
if (isMine && t.status === 'PENDING' && !pendingTask) pendingTask = t;
walk(t.child_tasks);
}
};
walk(this.product.task_tree);
return (wipTask || pendingTask) ? (wipTask || pendingTask).id : null;
},
toggleMode() { if (this.currentMode === 'workspace') this.currentMode = 'swipe'; else if (this.currentMode === 'swipe') this.currentMode = 'tree'; else this.currentMode = 'workspace'; },
async handleSetOverallStatus(status) { try { this.product = await patch(`/products/scan/${this.product.serial_number}/status`, { status }); uni.showToast({ title: `状态已更新: ${status}`, icon: "success" }); this.showStatusPicker = false; } catch {} },
openEditProduct() { this.editForm = { order_no: this.product.order_no || "", external_serial: this.product.external_serial || "" }; this.editProductVisible = true; },