布局升级: 流转树撑满空间+独立历史记录页+查看全部记录入口
This commit is contained in:
@ -1,11 +1,10 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/scan/detail",
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "产品详情",
|
||||
"navigationBarBackgroundColor": "#2563EB",
|
||||
"navigationBarTextStyle": "white"
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -16,6 +15,22 @@
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/scan/records",
|
||||
"style": {
|
||||
"navigationBarTitleText": "任务历史记录",
|
||||
"navigationBarBackgroundColor": "#2563EB",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/scan/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "产品详情",
|
||||
"navigationBarBackgroundColor": "#2563EB",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tasks/index",
|
||||
"style": {
|
||||
|
||||
@ -16,26 +16,32 @@
|
||||
<text v-if="task.reject_reason" class="reject-reason">驳回: {{ task.reject_reason }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 进度记录 — 浅灰背景独立区域 -->
|
||||
<!-- 进度记录 — 只展示最新一条 + 查看全部入口 -->
|
||||
<view v-if="task.records && task.records.length" class="records-area">
|
||||
<view v-for="rec in task.records" :key="rec.id" class="record-item">
|
||||
<!-- 顶部:时间 + 编辑/删除 -->
|
||||
<!-- 查看全部记录按钮 -->
|
||||
<view class="records-bar" @tap.stop="$emit('viewRecords', task)">
|
||||
<text class="records-bar-icon">📋</text>
|
||||
<text class="records-bar-text">查看全部记录 ({{ task.records.length }})</text>
|
||||
<text class="records-bar-arrow">›</text>
|
||||
</view>
|
||||
<!-- 最新一条预览 -->
|
||||
<view class="record-item">
|
||||
<view class="record-top">
|
||||
<text class="record-time">{{ formatTime(rec.created_at) }}</text>
|
||||
<text class="record-time">{{ formatTime(task.records[0].created_at) }}</text>
|
||||
<view v-if="canEditRecord" class="record-actions">
|
||||
<text class="rec-act" @tap.stop="$emit('editRecord', { task, record: rec })">✏️</text>
|
||||
<text class="rec-act" @tap.stop="confirmDelete(rec)">🗑️</text>
|
||||
<text class="rec-act" @tap.stop="$emit('editRecord', { task, record: task.records[0] })">✏️</text>
|
||||
<text class="rec-act" @tap.stop="confirmDelete(task.records[0])">🗑️</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="rec.remark" class="record-remark">{{ rec.remark }}</text>
|
||||
<view v-if="rec.images && rec.images.length" class="record-images">
|
||||
<text v-if="task.records[0].remark" class="record-remark">{{ task.records[0].remark }}</text>
|
||||
<view v-if="task.records[0].images && task.records[0].images.length" class="record-images">
|
||||
<image
|
||||
v-for="(img, i) in rec.images"
|
||||
v-for="(img, i) in task.records[0].images"
|
||||
:key="i"
|
||||
:src="imageUrl(img)"
|
||||
class="record-thumb"
|
||||
mode="aspectFill"
|
||||
@tap.stop="previewImage(rec.images, i)"
|
||||
@tap.stop="previewImage(task.records[0].images, i)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
@ -70,6 +76,7 @@
|
||||
:isLast="idx === task.child_tasks.length - 1"
|
||||
@action="(e) => $emit('action', e)"
|
||||
@editRecord="(e) => $emit('editRecord', e)"
|
||||
@viewRecords="(t) => $emit('viewRecords', t)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
@ -86,7 +93,7 @@ export default {
|
||||
currentUserId: { type: [String, Number], default: "" },
|
||||
currentUsername: { type: String, default: "" },
|
||||
},
|
||||
emits: ["action", "editRecord"],
|
||||
emits: ["action", "editRecord", "viewRecords"],
|
||||
computed: {
|
||||
canEditRecord() {
|
||||
// 双重宽松匹配:assignee_id 可能是数字ID或用户名字符串
|
||||
@ -167,7 +174,17 @@ export default {
|
||||
.reject-reason { color: #ef4444; }
|
||||
|
||||
/* 进度记录 */
|
||||
.records-area { background-color: #f9f9f9; padding: 12rpx 16rpx; border-radius: 10rpx; margin-top: 16rpx; margin-bottom: 6rpx; }
|
||||
.records-area { background-color: #f9f9f9; padding: 0; border-radius: 10rpx; margin-top: 16rpx; margin-bottom: 6rpx; overflow: hidden; }
|
||||
.records-bar {
|
||||
display: flex; align-items: center; gap: 8rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
background: linear-gradient(135deg, #eff6ff, #dbeafe);
|
||||
border-bottom: 1px solid #bfdbfe;
|
||||
}
|
||||
.records-bar-icon { font-size: 28rpx; }
|
||||
.records-bar-text { flex: 1; font-size: 26rpx; font-weight: 700; color: #2563eb; }
|
||||
.records-bar-arrow { font-size: 32rpx; color: #93c5fd; font-weight: 700; }
|
||||
.record-item { padding: 12rpx 16rpx; }
|
||||
.record-item { padding: 8rpx 0; border-bottom: 1px dashed #e5e7eb; }
|
||||
.record-item:last-child { border-bottom: none; }
|
||||
.record-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
|
||||
@ -54,14 +54,15 @@
|
||||
</view>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- 任务树 -->
|
||||
<!-- 任务树(撑满剩余空间,可滚动) -->
|
||||
<!-- ================================================================ -->
|
||||
<view class="card">
|
||||
<view class="card card-tree">
|
||||
<view class="card-header">
|
||||
<text class="card-title">🌿 任务流转树</text>
|
||||
<text class="task-count">{{ countTasks(product.task_tree) }} 个任务</text>
|
||||
</view>
|
||||
|
||||
<view class="tree-scroll">
|
||||
<!-- 0任务 → 发起首道工序 -->
|
||||
<view v-if="!product.task_tree || !product.task_tree.length" class="zero-task">
|
||||
<text class="zero-icon">📋</text>
|
||||
@ -81,8 +82,10 @@
|
||||
:isLast="idx === (product.task_tree || []).length - 1"
|
||||
@action="handleTaskAction"
|
||||
@editRecord="openEditRecord"
|
||||
@viewRecords="handleViewRecords"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
@ -605,6 +608,9 @@ export default {
|
||||
this.rejectReason = "";
|
||||
this.transferForm = { next_task_name: "", assignees: [], selectedUserId: "", warehouse: false, note: "" };
|
||||
},
|
||||
handleViewRecords(task) {
|
||||
uni.navigateTo({ url: `/pages/scan/records?taskId=${task.id}` });
|
||||
},
|
||||
closeActionPopup() { this.actionPopup = { visible: false, type: "", task: null }; },
|
||||
|
||||
async doReceive() {
|
||||
@ -655,7 +661,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; padding: 16px; padding-bottom: 100px; }
|
||||
.page { height: 100vh; display: flex; flex-direction: column; padding: 16px; padding-bottom: 0; overflow: hidden; box-sizing: border-box; }
|
||||
.loading { text-align: center; padding: 48px 0; color: #6b7280; }
|
||||
.error-box { padding: 12px; border-radius: 10px; background: #fef2f2; color: #dc2626; font-size: 13px; border: 1px solid #fecaca; }
|
||||
|
||||
@ -668,7 +674,11 @@ export default {
|
||||
.overall-arrow { font-size: 12px; color: #9ca3af; }
|
||||
|
||||
/* 卡片 */
|
||||
.card { background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
||||
.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; }
|
||||
/* 任务树卡片 → 撑满剩余空间 */
|
||||
.card-tree { flex: 1; display: flex; flex-direction: column; min-height: 0; margin-bottom: 0; overflow: hidden; }
|
||||
.card-tree .card-header { flex-shrink: 0; }
|
||||
.tree-scroll { flex: 1; overflow-y: auto; min-height: 0; -webkit-overflow-scrolling: touch; }
|
||||
.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; }
|
||||
|
||||
193
track-uniapp/src/pages/scan/records.vue
Normal file
193
track-uniapp/src/pages/scan/records.vue
Normal file
@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view v-if="loading" class="loading">加载中...</view>
|
||||
<view v-if="error" class="error-box">{{ error }}</view>
|
||||
|
||||
<template v-if="!loading && task">
|
||||
<!-- 任务信息头 -->
|
||||
<view class="task-header">
|
||||
<text class="task-name">{{ task.task_name }}</text>
|
||||
<text class="task-meta">负责人: {{ task.assignee_id || '—' }} · {{ statusLabel(task.status) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 记录时间轴 -->
|
||||
<view v-if="records.length" class="timeline">
|
||||
<view v-for="(rec, i) in records" :key="rec.id" class="tl-item">
|
||||
<!-- 时间轴竖线 + 圆点 -->
|
||||
<view class="tl-line">
|
||||
<view class="tl-dot" :class="i === 0 ? 'tl-dot-latest' : ''" />
|
||||
<view v-if="i < records.length - 1" class="tl-connector" />
|
||||
</view>
|
||||
|
||||
<!-- 内容卡片 -->
|
||||
<view class="tl-card">
|
||||
<view class="tl-top">
|
||||
<text class="tl-time">{{ formatTime(rec.created_at) }}</text>
|
||||
<view v-if="canEdit" class="tl-actions">
|
||||
<text class="tl-act" @tap="openEditRecord(rec)">✏️</text>
|
||||
<text class="tl-act" @tap="confirmDelete(rec)">🗑️</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="rec.remark" class="tl-remark">{{ rec.remark }}</text>
|
||||
<view v-if="rec.images && rec.images.length" class="tl-images">
|
||||
<image
|
||||
v-for="(img, j) in rec.images"
|
||||
:key="j"
|
||||
:src="imageUrl(img)"
|
||||
mode="aspectFill"
|
||||
class="tl-thumb"
|
||||
@tap="previewImage(rec.images, j)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="empty-timeline">
|
||||
<text class="empty-icon">📭</text>
|
||||
<text class="empty-text">暂无历史记录</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request, { get, put } from "../../utils/request";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
error: "",
|
||||
task: null,
|
||||
records: [],
|
||||
currentUser: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canEdit() {
|
||||
if (!this.currentUser || !this.task) return false;
|
||||
return (
|
||||
this.currentUser.id == this.task.assignee_id ||
|
||||
this.currentUser.username == this.task.assignee_id
|
||||
);
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
this.loadCurrentUser();
|
||||
const taskId = options.taskId || "";
|
||||
if (taskId) {
|
||||
this.loadTask(taskId);
|
||||
} else {
|
||||
this.error = "缺少任务ID";
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadCurrentUser() {
|
||||
try {
|
||||
let user = uni.getStorageSync("user");
|
||||
if (typeof user === "string" && user) {
|
||||
try { user = JSON.parse(user); } catch (e) { user = null; }
|
||||
}
|
||||
if (user && typeof user === "object") {
|
||||
this.currentUser = user;
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
|
||||
async loadTask(taskId) {
|
||||
this.loading = true;
|
||||
try {
|
||||
// 通过产品扫码接口反向获取任务(后端可能没有单独的任务查询接口)
|
||||
// 这里假设后端提供 GET /tasks/{taskId}
|
||||
this.task = await get(`/tasks/${taskId}`);
|
||||
this.records = this.task.records || [];
|
||||
} catch {
|
||||
this.error = "加载任务失败";
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
imageUrl(url) {
|
||||
if (!url) return "";
|
||||
if (url.startsWith("http")) return url;
|
||||
return "http://192.168.9.80:8011" + (url.startsWith("/") ? url : "/" + url);
|
||||
},
|
||||
|
||||
previewImage(urls, index) {
|
||||
const fullUrls = (urls || []).map((u) => this.imageUrl(u));
|
||||
uni.previewImage({ urls: fullUrls, current: index });
|
||||
},
|
||||
|
||||
formatTime(t) {
|
||||
if (!t) return "";
|
||||
const d = new Date(t);
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
},
|
||||
|
||||
statusLabel(s) {
|
||||
const map = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
|
||||
return map[s] || s;
|
||||
},
|
||||
|
||||
openEditRecord(rec) {
|
||||
// 复用 detail.vue 的编辑流程 — 通过全局事件或直接跳回
|
||||
// 简单方案:在当前页弹窗编辑
|
||||
uni.showToast({ title: "编辑功能请返回详情页操作", icon: "none", duration: 2000 });
|
||||
},
|
||||
|
||||
confirmDelete(rec) {
|
||||
uni.showModal({
|
||||
title: "删除记录",
|
||||
content: "确定删除这条记录吗?",
|
||||
confirmColor: "#dc2626",
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await request({ url: `/records/${rec.id}`, method: "DELETE" });
|
||||
uni.showToast({ title: "已删除", icon: "success" });
|
||||
this.records = this.records.filter((r) => r.id !== rec.id);
|
||||
} catch {
|
||||
uni.showToast({ title: "删除失败", icon: "none" });
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; padding: 16px; padding-bottom: 40px; background: #f3f4f6; }
|
||||
.loading { text-align: center; padding: 48px 0; color: #6b7280; }
|
||||
.error-box { padding: 12px; border-radius: 10px; background: #fef2f2; color: #dc2626; font-size: 13px; }
|
||||
|
||||
/* 任务头 */
|
||||
.task-header { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
||||
.task-name { font-size: 18px; font-weight: 700; color: #1f2937; display: block; }
|
||||
.task-meta { font-size: 13px; color: #9ca3af; margin-top: 4px; display: block; }
|
||||
|
||||
/* 时间轴 */
|
||||
.timeline { padding-left: 8px; }
|
||||
.tl-item { display: flex; gap: 12px; }
|
||||
.tl-line { display: flex; flex-direction: column; align-items: center; width: 24px; flex-shrink: 0; }
|
||||
.tl-dot { width: 12px; height: 12px; border-radius: 50%; background: #d1d5db; margin-top: 6px; }
|
||||
.tl-dot-latest { background: #2563eb; box-shadow: 0 0 0 4px rgba(37,99,235,0.15); }
|
||||
.tl-connector { flex: 1; width: 2px; background: #e5e7eb; min-height: 12px; }
|
||||
.tl-card { flex: 1; background: #fff; border-radius: 10px; padding: 12px; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }
|
||||
.tl-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.tl-time { font-size: 12px; color: #9ca3af; }
|
||||
.tl-actions { display: flex; gap: 12px; }
|
||||
.tl-act { font-size: 16px; padding: 2px; }
|
||||
.tl-remark { display: block; font-size: 14px; color: #374151; margin-top: 6px; line-height: 1.5; word-break: break-all; }
|
||||
.tl-images { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
|
||||
.tl-thumb { width: 80px; height: 80px; border-radius: 8px; background: #e5e7eb; }
|
||||
|
||||
.empty-timeline { display: flex; flex-direction: column; align-items: center; padding-top: 60px; }
|
||||
.empty-icon { font-size: 48px; margin-bottom: 8px; }
|
||||
.empty-text { font-size: 14px; color: #9ca3af; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user