布局升级: 流转树撑满空间+独立历史记录页+查看全部记录入口

This commit is contained in:
2026-08-05 15:09:09 +08:00
parent 5e53cd7f79
commit 430c0d2342
4 changed files with 274 additions and 39 deletions

View 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>