Files
track/track-uniapp/src/pages/scan/records.vue
duxingchen 8537bce30c fix(APP): 倒计时期间确认按钮禁用虚化 + 收窄倒计时范围
- 5秒倒计时期间确认按钮虚化禁用(点不了),只有取消可用,倒计时结束按钮解锁再点击才执行
- 收窄范围:仅 删除记录/删除图片/结束分支/驳回 保留5秒等待;接收/转交/派发/保存/创建/撤回转交 恢复直接确认
2026-08-28 12:05:13 +08:00

259 lines
10 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
<!-- 🛡 双重确认删除5 秒倒计时防误触 -->
<view v-if="confirmDlg.visible" class="dlg-overlay" @tap="confirmDlgCancel">
<view class="dlg-box" @tap.stop>
<text class="dlg-title">{{ confirmDlg.title }}</text>
<text class="dlg-content">{{ confirmDlg.content }}</text>
<text class="dlg-tip"> 5 秒确认等待中请核对信息倒计时结束后确认按钮才可点击</text>
<view class="dlg-btns">
<button class="dlg-btn dlg-cancel" @tap="confirmDlgCancel">取消</button>
<button class="dlg-btn dlg-danger" :disabled="confirming === 'dlg' && confirmCount > 0" @tap="confirmDlgConfirm">{{ confirmLabel('删除') }}</button>
</view>
</view>
</view>
</view>
</template>
<script>
import request, { get, put, getBaseUrl } from "../../utils/request";
export default {
data() {
return {
loading: true,
error: "",
task: null,
records: [],
currentUser: null,
// 🛡️ 双重确认删除5 秒倒计时防误触)
confirmDlg: { visible: false, title: "", content: "", target: null },
confirming: "",
confirmCount: 5,
confirmTimer: null,
};
},
computed: {
canEdit() {
if (!this.currentUser || !this.task) return false;
// 已完成/已驳回/已入库/已撤回 的任务禁止编辑
const frozen = ["COMPLETED", "REJECTED", "ARCHIVED", "CANCELED"];
if (frozen.includes(this.task.status)) 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;
}
},
// 🚀 页面卸载:清理确认倒计时定时器
onUnload() { this.clearConfirm(); },
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;
const domain = getBaseUrl().replace(/\/api.*$/, '');
return domain + (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) {
this.openConfirmDlg({ title: "删除记录", content: "确定删除这条记录吗?删除后不可恢复。", target: rec });
},
// ═══ 双重确认倒计时(防误触) ═══
openConfirmDlg({ title, content, target }) {
this.clearConfirm();
this.confirmDlg = { visible: true, title, content, target };
this.confirming = "dlg";
this.confirmCount = 5;
this.confirmTimer = setInterval(() => {
this.confirmCount -= 1;
if (this.confirmCount <= 0) clearInterval(this.confirmTimer);
}, 1000);
},
clearConfirm() {
if (this.confirmTimer) clearInterval(this.confirmTimer);
this.confirmTimer = null;
this.confirming = "";
this.confirmCount = 5;
},
confirmLabel(baseText) {
if (this.confirming === "dlg" && this.confirmCount > 0) return `${baseText} (${this.confirmCount}s)`;
return baseText;
},
confirmDlgConfirm() {
if (this.confirming === "dlg" && this.confirmCount > 0) return; // 倒计时中:忽略
if (this.confirming === "dlg") this.clearConfirm();
const rec = this.confirmDlg.target;
this.confirmDlg.visible = false;
this.confirmDlg.target = null;
if (rec) this.deleteRecord(rec);
},
confirmDlgCancel() {
this.clearConfirm();
this.confirmDlg.visible = false;
this.confirmDlg.target = null;
},
async deleteRecord(rec) {
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; }
/* 双重确认弹窗 */
.dlg-overlay { position: fixed; inset: 0; z-index: 999; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; }
.dlg-box { width: 80%; max-width: 360px; background: #fff; border-radius: 16px; padding: 28px 20px 20px; box-sizing: border-box; }
.dlg-title { display: block; text-align: center; font-size: 17px; font-weight: 700; color: #1f2937; }
.dlg-content { display: block; text-align: center; font-size: 14px; color: #6b7280; margin-top: 10px; line-height: 1.5; }
.dlg-tip { display: block; text-align: center; font-size: 12px; color: #b45309; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 8px; padding: 6px 10px; margin: 12px 0 0; line-height: 1.4; }
.dlg-btns { display: flex; gap: 12px; margin-top: 20px; }
.dlg-btn { flex: 1; height: 42px; line-height: 42px; border-radius: 10px; font-size: 15px; font-weight: 600; text-align: center; box-sizing: border-box; padding: 0; margin: 0; }
.dlg-btn::after { border: none; }
.dlg-cancel { background: #f3f4f6; color: #6b7280; }
.dlg-danger { background: #dc2626; color: #fff; }
.dlg-danger[disabled] { opacity: 0.5; }
</style>