纳入 track-uniapp 前端项目 (扫码/任务树/记录/转交Picker)
This commit is contained in:
204
track-uniapp/src/pages/scan/components/TaskTreeNode.vue
Normal file
204
track-uniapp/src/pages/scan/components/TaskTreeNode.vue
Normal file
@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<view class="tnode">
|
||||
<view v-if="hasChildren" class="tree-line-vertical" />
|
||||
|
||||
<!-- 卡片 -->
|
||||
<view :class="['tnode-card', statusColor(task.status), { 'is-rework': task.is_rework }]">
|
||||
<!-- 信息区 -->
|
||||
<view class="tnode-body">
|
||||
<view class="tnode-name-row">
|
||||
<text v-if="task.is_rework" class="tag tag-rework">⚠返工</text>
|
||||
<text v-if="task.child_tasks && task.child_tasks.length > 1" class="tag tag-fission">裂变×{{ task.child_tasks.length }}</text>
|
||||
<text class="tnode-name">{{ task.task_name }}</text>
|
||||
</view>
|
||||
<view class="tnode-meta">
|
||||
<text v-if="task.assignee_id">负责人: {{ task.assignee_id }}</text>
|
||||
<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="record-top">
|
||||
<text class="record-time">{{ formatTime(rec.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>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="rec.remark" class="record-remark">{{ rec.remark }}</text>
|
||||
<view v-if="rec.images && rec.images.length" class="record-images">
|
||||
<image
|
||||
v-for="(img, i) in rec.images"
|
||||
:key="i"
|
||||
:src="imageUrl(img)"
|
||||
class="record-thumb"
|
||||
mode="aspectFill"
|
||||
@tap.stop="previewImage(rec.images, i)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态标签 -->
|
||||
<text :class="['badge', statusColor(task.status)]">{{ statusLabel(task.status) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<view v-if="task.status === 'PENDING' || task.status === 'WIP'" class="action-bar">
|
||||
<button v-if="task.status === 'PENDING'" class="act-btn act-receive" size="mini"
|
||||
@tap.stop="$emit('action', { task, type: 'receive' })">接收</button>
|
||||
<button v-if="task.status === 'PENDING'" class="act-btn act-reject" size="mini"
|
||||
@tap.stop="$emit('action', { task, type: 'reject' })">驳回</button>
|
||||
<button v-if="task.status === 'WIP'" class="act-btn act-transfer" size="mini"
|
||||
@tap.stop="$emit('action', { task, type: 'transfer' })">转交</button>
|
||||
<button v-if="task.status === 'WIP'" class="act-btn act-record" size="mini"
|
||||
@tap.stop="$emit('action', { task, type: 'record' })">📝 记录/拍照</button>
|
||||
</view>
|
||||
|
||||
<!-- 递归子树 -->
|
||||
<view v-if="task.child_tasks && task.child_tasks.length" class="tnode-children">
|
||||
<TaskTreeNode
|
||||
v-for="(child, idx) in task.child_tasks"
|
||||
:key="child.id"
|
||||
:task="child"
|
||||
:currentUser="currentUser"
|
||||
:currentUserId="currentUserId"
|
||||
:currentUsername="currentUsername"
|
||||
:isLast="idx === task.child_tasks.length - 1"
|
||||
@action="(e) => $emit('action', e)"
|
||||
@editRecord="(e) => $emit('editRecord', e)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TaskTreeNode",
|
||||
props: {
|
||||
task: { type: Object, required: true },
|
||||
isLast: { type: Boolean, default: false },
|
||||
hasChildren: { type: Boolean, default: false },
|
||||
currentUser: { type: Object, default: null },
|
||||
currentUserId: { type: [String, Number], default: "" },
|
||||
currentUsername: { type: String, default: "" },
|
||||
},
|
||||
emits: ["action", "editRecord"],
|
||||
computed: {
|
||||
canEditRecord() {
|
||||
// 双重宽松匹配:assignee_id 可能是数字ID或用户名字符串
|
||||
if (this.task.assignee_id == this.currentUserId) 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.username == this.task.assignee_id) return true;
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
imageUrl(url) {
|
||||
if (!url) return "";
|
||||
if (url.startsWith("http")) return url;
|
||||
// 后端返回 /api/v1/upload/files/xxx.jpg,需要补全域名
|
||||
const DOMAIN = "http://192.168.9.80:8011";
|
||||
return DOMAIN + (url.startsWith("/") ? url : "/" + url);
|
||||
},
|
||||
previewImage(urls, index) {
|
||||
const fullUrls = (urls || []).map(img => this.imageUrl(img));
|
||||
uni.previewImage({ urls: fullUrls, current: index });
|
||||
},
|
||||
confirmDelete(rec) {
|
||||
uni.showModal({
|
||||
title: "删除记录",
|
||||
content: "确定删除这条记录吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) this.$emit("action", { task: this.task, type: "deleteRecord", record: rec });
|
||||
},
|
||||
});
|
||||
},
|
||||
statusLabel(s) {
|
||||
const map = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
|
||||
return 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";
|
||||
}
|
||||
},
|
||||
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())}:${pad(d.getSeconds())}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tnode { position: relative; padding-left: 24px; margin-bottom: 4px; }
|
||||
.tree-line-vertical { position: absolute; left: 8px; top: 28px; bottom: 0; width: 2px; background: #e5e7eb; }
|
||||
|
||||
.tnode-card {
|
||||
display: flex; align-items: flex-start; justify-content: space-between;
|
||||
padding: 10px 12px; border-radius: 10px 10px 0 0;
|
||||
background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.is-rework { border-left-color: #ef4444 !important; }
|
||||
.s-yellow { border-left-color: #f59e0b; }
|
||||
.s-blue { border-left-color: #3b82f6; }
|
||||
.s-green { border-left-color: #22c55e; }
|
||||
.s-red { border-left-color: #ef4444; }
|
||||
|
||||
.tnode-body { flex: 1; min-width: 0; }
|
||||
.tnode-name-row { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
|
||||
.tnode-name { font-size: 14px; font-weight: 700; color: #1f2937; }
|
||||
.tag { font-size: 10px; padding: 1px 5px; border-radius: 6px; font-weight: 700; color: #fff; }
|
||||
.tag-rework { background: #ef4444; }
|
||||
.tag-fission { background: #7c3aed; }
|
||||
.tnode-meta { margin-top: 2px; font-size: 11px; color: #9ca3af; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.reject-reason { color: #ef4444; }
|
||||
|
||||
/* 进度记录 */
|
||||
.records-area { background-color: #f9f9f9; padding: 12rpx 16rpx; border-radius: 10rpx; margin-top: 16rpx; margin-bottom: 6rpx; }
|
||||
.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; }
|
||||
.record-actions { display: flex; gap: 12rpx; }
|
||||
.rec-act { font-size: 28rpx; padding: 4rpx; }
|
||||
.record-remark { font-size: 26rpx; color: #333; display: block; margin: 6rpx 0; line-height: 1.5; word-break: break-all; }
|
||||
.record-images { display: flex; gap: 8rpx; margin-top: 8rpx; flex-wrap: wrap; }
|
||||
.record-thumb { width: 100rpx; height: 100rpx; border-radius: 8rpx; border: 1px solid #e5e7eb; background: #f3f4f6; }
|
||||
.record-time { font-size: 22rpx; color: #9ca3af; }
|
||||
|
||||
.badge {
|
||||
font-size: 10px; padding: 2px 8px; border-radius: 20px; font-weight: 600;
|
||||
white-space: nowrap; flex-shrink: 0; margin-left: 8px;
|
||||
background: #f3f4f6; color: #6b7280;
|
||||
}
|
||||
.s-yellow .badge { background: #fef3c7; color: #b45309; }
|
||||
.s-blue .badge { background: #dbeafe; color: #1d4ed8; }
|
||||
.s-green .badge { background: #dcfce7; color: #15803d; }
|
||||
.s-red .badge { background: #fce4ec; color: #be123c; }
|
||||
|
||||
.action-bar {
|
||||
display: flex; justify-content: flex-end; gap: 20rpx;
|
||||
padding: 10px 12px; border-top: 1px solid #f3f4f6;
|
||||
border-radius: 0 0 10px 10px; background: #fafafa;
|
||||
}
|
||||
.act-btn { height: 56rpx; line-height: 56rpx; padding: 0 28rpx; font-size: 26rpx; font-weight: 600; border-radius: 10rpx; border: none; margin: 0; }
|
||||
.act-btn::after { border: none; }
|
||||
.act-receive { background: #dbeafe; color: #2563eb; }
|
||||
.act-reject { background: #fce4ec; color: #dc2626; }
|
||||
.act-transfer { background: #dcfce7; color: #16a34a; }
|
||||
.act-record { background: #fef3c7; color: #b45309; }
|
||||
|
||||
.tnode-children { position: relative; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user