增加了手机操作端
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ElMessage, type InputInstance } from "element-plus";
|
||||
import {
|
||||
adminApi,
|
||||
type AdminFileAsset,
|
||||
type AdminAppraisalTaskDetail,
|
||||
type AdminAppraisalTaskListItem,
|
||||
type AdminAssignableAppraiserItem,
|
||||
@@ -24,6 +25,16 @@ const assigneeOptions = ref<AdminAssignableAppraiserItem[]>([]);
|
||||
const selectedAssigneeId = ref(0);
|
||||
const evidenceUploading = ref(false);
|
||||
const appraisalTemplateLoading = ref(false);
|
||||
const transferTagNo = ref("");
|
||||
const transferScanLoading = ref(false);
|
||||
const publishDialogVisible = ref(false);
|
||||
const publishMaterialTagInput = ref("");
|
||||
const publishMaterialTagInputRef = ref<InputInstance | null>(null);
|
||||
const publishMaterialTagSubmitting = ref(false);
|
||||
const zhongjianReportNo = ref("");
|
||||
const zhongjianReportFiles = ref<AdminFileAsset[]>([]);
|
||||
const zhongjianReportUploading = ref(false);
|
||||
const zhongjianReportSubmitting = ref(false);
|
||||
|
||||
const keyword = ref("");
|
||||
const taskStage = ref("");
|
||||
@@ -34,6 +45,7 @@ const activeWorkTab = ref("result");
|
||||
const formRenderKey = ref(0);
|
||||
const workbenchAsideRef = ref<HTMLElement | null>(null);
|
||||
const evidenceInputRef = ref<HTMLInputElement | null>(null);
|
||||
const zhongjianReportFileInputRef = ref<HTMLInputElement | null>(null);
|
||||
const materialTagInput = ref("");
|
||||
|
||||
const tasks = ref<AdminAppraisalTaskListItem[]>([]);
|
||||
@@ -265,9 +277,14 @@ const canBindMaterialTag = computed(() => {
|
||||
if (!detail.value?.report_summary) {
|
||||
return false;
|
||||
}
|
||||
if (detail.value.task_info.service_provider === "zhongjian") {
|
||||
return false;
|
||||
}
|
||||
return detail.value.report_summary.report_status !== "published" && !detail.value.material_tag;
|
||||
});
|
||||
|
||||
const isZhongjianTask = computed(() => detail.value?.task_info.service_provider === "zhongjian");
|
||||
const isPhysicalTask = computed(() => Boolean(detail.value) && !isZhongjianTask.value);
|
||||
const canRequestSupplement = computed(() => detail.value?.task_info.status !== "completed");
|
||||
const currentAdmin = computed(() => getAdminInfo());
|
||||
const canClaimTask = computed(() => {
|
||||
@@ -326,11 +343,12 @@ function resetSupplementForm() {
|
||||
function hydrateDetail(data: AdminAppraisalTaskDetail) {
|
||||
detail.value = data;
|
||||
detailTab.value = "overview";
|
||||
activeWorkTab.value = data.supplement_task ? "supplement" : "result";
|
||||
activeWorkTab.value = data.task_info.service_provider === "zhongjian" ? "zhongjian" : (data.supplement_task ? "supplement" : "result");
|
||||
applyProductForm(data.product_info);
|
||||
const resultSeed = resolveResultFormSeed(data);
|
||||
applyResultForm(resultSeed);
|
||||
applyAppraisalTemplate(data.appraisal_template, resultSeed.key_points || []);
|
||||
applyZhongjianReportForm(data);
|
||||
applySupplementForm(data);
|
||||
formRenderKey.value += 1;
|
||||
}
|
||||
@@ -416,6 +434,11 @@ function applySupplementForm(data: Pick<AdminAppraisalTaskDetail, "supplement_ta
|
||||
});
|
||||
}
|
||||
|
||||
function applyZhongjianReportForm(data: AdminAppraisalTaskDetail) {
|
||||
zhongjianReportNo.value = data.zhongjian_report?.report_no || "";
|
||||
zhongjianReportFiles.value = [...(data.zhongjian_report?.files || [])];
|
||||
}
|
||||
|
||||
function resetResultForm() {
|
||||
if (!detail.value) return;
|
||||
applyProductForm(detail.value.product_info);
|
||||
@@ -425,6 +448,11 @@ function resetResultForm() {
|
||||
formRenderKey.value += 1;
|
||||
}
|
||||
|
||||
function resetZhongjianReportForm() {
|
||||
if (!detail.value) return;
|
||||
applyZhongjianReportForm(detail.value);
|
||||
}
|
||||
|
||||
function openSupplementWorkbench() {
|
||||
if (isTaskReadonly.value) {
|
||||
return;
|
||||
@@ -517,10 +545,35 @@ async function openDetail(row: AdminAppraisalTaskListItem) {
|
||||
drawerVisible.value = true;
|
||||
detail.value = null;
|
||||
detailTab.value = "overview";
|
||||
activeWorkTab.value = "result";
|
||||
activeWorkTab.value = row.service_provider === "zhongjian" ? "zhongjian" : "result";
|
||||
await loadDetail(row.id);
|
||||
}
|
||||
|
||||
async function scanTransferTag() {
|
||||
const internalTagNo = transferTagNo.value.trim();
|
||||
if (!internalTagNo) {
|
||||
ElMessage.warning("请扫描内部流转码");
|
||||
return;
|
||||
}
|
||||
|
||||
transferScanLoading.value = true;
|
||||
try {
|
||||
const response = await adminApi.scanAppraisalTransferTag(internalTagNo);
|
||||
drawerVisible.value = true;
|
||||
detail.value = null;
|
||||
await loadDetail(response.data.task_id);
|
||||
detailTab.value = "actions";
|
||||
activeWorkTab.value = response.data.service_provider === "zhongjian" ? "zhongjian" : "result";
|
||||
ElMessage.success(`${response.data.service_provider_text}任务已打开`);
|
||||
await fetchTasks();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
ElMessage.error(error?.message || "内部流转码识别失败");
|
||||
} finally {
|
||||
transferScanLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function previewFiles(files: Array<{ file_url: string }>, current: string) {
|
||||
if (!files.length) return;
|
||||
window.open(current, "_blank", "noopener,noreferrer");
|
||||
@@ -556,6 +609,47 @@ async function handleEvidenceFileSelect(event: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
function triggerZhongjianReportUpload() {
|
||||
if (isTaskReadonly.value) {
|
||||
return;
|
||||
}
|
||||
zhongjianReportFileInputRef.value?.click();
|
||||
}
|
||||
|
||||
async function handleZhongjianReportFileSelect(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const files = Array.from(target.files || []);
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
zhongjianReportUploading.value = true;
|
||||
try {
|
||||
for (const file of files) {
|
||||
const response = await adminApi.uploadAppraisalEvidenceFile(file);
|
||||
zhongjianReportFiles.value.push(response.data);
|
||||
}
|
||||
ElMessage.success("中检报告文件已上传");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error("中检报告文件上传失败");
|
||||
} finally {
|
||||
zhongjianReportUploading.value = false;
|
||||
target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function removeZhongjianReportFile(fileUrl: string) {
|
||||
try {
|
||||
await adminApi.deleteAppraisalEvidenceFile(fileUrl);
|
||||
zhongjianReportFiles.value = zhongjianReportFiles.value.filter((item) => item.file_url !== fileUrl);
|
||||
ElMessage.success("报告文件已删除");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error("报告文件删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function removeEvidenceAttachment(fileUrl: string) {
|
||||
try {
|
||||
await adminApi.deleteAppraisalEvidenceFile(fileUrl);
|
||||
@@ -673,6 +767,11 @@ function hasProductFormValue() {
|
||||
|
||||
async function submitResult(action: "save" | "submit") {
|
||||
if (!detail.value) return;
|
||||
if (detail.value.task_info.service_provider === "zhongjian") {
|
||||
ElMessage.warning("中检订单请在中检报告录入页提交");
|
||||
activeWorkTab.value = "zhongjian";
|
||||
return;
|
||||
}
|
||||
if (action === "submit" && !resultForm.result_text.trim()) {
|
||||
ElMessage.warning("提交前请先填写鉴定结论");
|
||||
return;
|
||||
@@ -697,6 +796,12 @@ async function submitResult(action: "save" | "submit") {
|
||||
ElMessage.success(response.message || (action === "submit" ? "结论已提交" : "结论已保存"));
|
||||
await loadDetail(detail.value.task_info.id);
|
||||
await fetchTasks();
|
||||
if (action === "submit") {
|
||||
publishMaterialTagInput.value = "";
|
||||
publishDialogVisible.value = true;
|
||||
await nextTick();
|
||||
publishMaterialTagInputRef.value?.focus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error(action === "submit" ? "结论提交失败" : "结论保存失败");
|
||||
@@ -705,8 +810,23 @@ async function submitResult(action: "save" | "submit") {
|
||||
}
|
||||
}
|
||||
|
||||
async function bindMaterialTag() {
|
||||
async function publishCurrentTaskWithMaterialTag(qrInput: string) {
|
||||
if (!detail.value) return;
|
||||
if (!isPhysicalTask.value) {
|
||||
ElMessage.warning("中检订单不使用平台验真吊牌");
|
||||
return;
|
||||
}
|
||||
|
||||
await adminApi.publishAppraisalTaskWithMaterialTag({
|
||||
id: detail.value.task_info.id,
|
||||
qr_input: qrInput,
|
||||
});
|
||||
ElMessage.success("验真吊牌已绑定,报告已发布");
|
||||
await loadDetail(detail.value.task_info.id);
|
||||
await fetchTasks();
|
||||
}
|
||||
|
||||
async function bindMaterialTag() {
|
||||
const qrInput = materialTagInput.value.trim();
|
||||
if (!qrInput) {
|
||||
ElMessage.warning("请扫描或粘贴吊牌二维码链接");
|
||||
@@ -715,19 +835,72 @@ async function bindMaterialTag() {
|
||||
|
||||
materialTagBinding.value = true;
|
||||
try {
|
||||
await adminApi.bindAppraisalTaskMaterialTag({
|
||||
id: detail.value.task_info.id,
|
||||
qr_input: qrInput,
|
||||
});
|
||||
ElMessage.success("吊牌已绑定");
|
||||
await publishCurrentTaskWithMaterialTag(qrInput);
|
||||
materialTagInput.value = "";
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
ElMessage.error(error?.message || "验真吊牌绑定或报告发布失败");
|
||||
} finally {
|
||||
materialTagBinding.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function publishDialogMaterialTag() {
|
||||
const qrInput = publishMaterialTagInput.value.trim();
|
||||
if (!qrInput) {
|
||||
ElMessage.warning("请扫描验真吊牌二维码");
|
||||
return;
|
||||
}
|
||||
|
||||
publishMaterialTagSubmitting.value = true;
|
||||
try {
|
||||
await publishCurrentTaskWithMaterialTag(qrInput);
|
||||
publishDialogVisible.value = false;
|
||||
publishMaterialTagInput.value = "";
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
ElMessage.error(error?.message || "验真吊牌绑定或报告发布失败");
|
||||
} finally {
|
||||
publishMaterialTagSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function focusPublishMaterialTagInput() {
|
||||
nextTick(() => {
|
||||
publishMaterialTagInputRef.value?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
async function submitZhongjianReport() {
|
||||
if (!detail.value) return;
|
||||
if (!isZhongjianTask.value) {
|
||||
ElMessage.warning("当前不是中检订单");
|
||||
return;
|
||||
}
|
||||
if (!zhongjianReportNo.value.trim()) {
|
||||
ElMessage.warning("请填写中检报告编号");
|
||||
return;
|
||||
}
|
||||
if (!zhongjianReportFiles.value.length) {
|
||||
ElMessage.warning("请至少上传 1 个中检报告文件");
|
||||
return;
|
||||
}
|
||||
|
||||
zhongjianReportSubmitting.value = true;
|
||||
try {
|
||||
const response = await adminApi.saveZhongjianAppraisalReport({
|
||||
id: detail.value.task_info.id,
|
||||
zhongjian_report_no: zhongjianReportNo.value.trim(),
|
||||
report_files: zhongjianReportFiles.value,
|
||||
});
|
||||
ElMessage.success(response.message || "中检报告已录入并发布");
|
||||
await loadDetail(detail.value.task_info.id);
|
||||
await fetchTasks();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
ElMessage.error(error?.message || "吊牌绑定失败");
|
||||
ElMessage.error(error?.message || "中检报告录入失败");
|
||||
} finally {
|
||||
materialTagBinding.value = false;
|
||||
zhongjianReportSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,6 +986,28 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="panel-card appraisal-scan-card" shadow="never">
|
||||
<div class="appraisal-scan">
|
||||
<div class="appraisal-scan__main">
|
||||
<div class="appraisal-scan__title">鉴定作业台扫码入口</div>
|
||||
<div class="appraisal-scan__desc">扫描内部流转挂牌后自动打开对应鉴定任务。</div>
|
||||
</div>
|
||||
<div class="appraisal-scan__control">
|
||||
<el-input
|
||||
v-model="transferTagNo"
|
||||
size="large"
|
||||
placeholder="扫描内部流转码"
|
||||
clearable
|
||||
@keyup.enter="scanTransferTag"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :loading="transferScanLoading" @click="scanTransferTag">打开任务</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<div class="filters-row">
|
||||
<el-input v-model="keyword" placeholder="搜索订单号 / 外部订单号 / 商品名称" clearable style="width: 340px" />
|
||||
@@ -914,6 +1109,12 @@ onMounted(async () => {
|
||||
<el-button :loading="resultSubmitting" @click="submitResult('save')">保存结论</el-button>
|
||||
<el-button type="primary" :loading="resultSubmitting" @click="submitResult('submit')">提交结论</el-button>
|
||||
</template>
|
||||
<template v-else-if="activeWorkTab === 'zhongjian'">
|
||||
<el-button plain @click="openAssigneeDialog">分配处理人</el-button>
|
||||
<el-button v-if="canClaimTask" plain type="primary" :loading="assigneeSubmitting" @click="claimTask">认领给我</el-button>
|
||||
<el-button @click="resetZhongjianReportForm">重置内容</el-button>
|
||||
<el-button type="primary" :loading="zhongjianReportSubmitting" @click="submitZhongjianReport">提交并发布报告</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button plain @click="returnToResultWorkbench">返回结论操作</el-button>
|
||||
<el-button @click="resetSupplementForm">重置</el-button>
|
||||
@@ -1225,6 +1426,13 @@ onMounted(async () => {
|
||||
<el-tabs v-model="activeWorkTab" stretch class="task-work-tabs">
|
||||
<el-tab-pane label="填写结论" name="result">
|
||||
<div :key="`result-${formRenderKey}`" class="task-form-stack">
|
||||
<el-alert
|
||||
v-if="isZhongjianTask"
|
||||
title="中检订单不走平台验真吊牌流程,请切换到中检报告录入。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="task-form-block">
|
||||
<div class="task-form-block__title">物品信息</div>
|
||||
<div class="task-panel__desc">客户推送订单可能不带物品信息,请鉴定师根据实物和资料补全,报告草稿会使用这里的内容。</div>
|
||||
@@ -1268,8 +1476,16 @@ onMounted(async () => {
|
||||
|
||||
<div class="task-form-block">
|
||||
<div class="task-form-block__title">吊牌绑定</div>
|
||||
<div class="task-panel__desc">扫描枪输入吊牌二维码链接后绑定到当前报告草稿。报告发布后不可更换。</div>
|
||||
<div v-if="detail.material_tag" class="task-material-tag-bound">
|
||||
<div class="task-panel__desc">实物鉴定提交结论后扫描平台验真吊牌,绑定后发布报告。</div>
|
||||
<el-alert
|
||||
v-if="isZhongjianTask"
|
||||
title="中检订单不使用本平台验真吊牌。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-top: 12px;"
|
||||
/>
|
||||
<div v-else-if="detail.material_tag" class="task-material-tag-bound">
|
||||
<div class="task-info-grid">
|
||||
<div class="task-info-item task-info-item--full">
|
||||
<div class="task-info-item__label">二维码链接</div>
|
||||
@@ -1454,6 +1670,79 @@ onMounted(async () => {
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="isZhongjianTask" label="中检报告录入" name="zhongjian">
|
||||
<div :key="`zhongjian-${formRenderKey}`" class="task-form-stack">
|
||||
<el-alert
|
||||
title="中检订单不绑定平台验真吊牌,提交中检报告编号和文件后直接发布报告。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<div class="task-form-block">
|
||||
<div class="task-form-block__title">中检报告信息</div>
|
||||
<div class="task-form-grid">
|
||||
<div class="task-form-field task-form-field--full">
|
||||
<div class="task-form-field__label">中检报告编号 <span class="task-form-field__required">必填</span></div>
|
||||
<el-input
|
||||
v-model="zhongjianReportNo"
|
||||
:disabled="isTaskReadonly"
|
||||
placeholder="扫描或输入中检报告编号"
|
||||
clearable
|
||||
@keyup.enter="submitZhongjianReport"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="detail.zhongjian_report?.report_entry_admin_name" class="task-form-field">
|
||||
<div class="task-form-field__label">报告录入人</div>
|
||||
<div class="task-readonly-value">{{ detail.zhongjian_report.report_entry_admin_name }}</div>
|
||||
</div>
|
||||
<div v-if="detail.zhongjian_report?.report_entered_at" class="task-form-field">
|
||||
<div class="task-form-field__label">录入时间</div>
|
||||
<div class="task-readonly-value">{{ detail.zhongjian_report.report_entered_at }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-form-block">
|
||||
<div class="task-form-block__title">报告文件</div>
|
||||
<div class="task-panel__desc">至少上传 1 个中检报告文件,支持图片、视频或 PDF。</div>
|
||||
<input
|
||||
ref="zhongjianReportFileInputRef"
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*,video/*,.pdf,application/pdf"
|
||||
style="display: none"
|
||||
@change="handleZhongjianReportFileSelect"
|
||||
/>
|
||||
<div class="task-evidence-toolbar">
|
||||
<el-button :disabled="isTaskReadonly" :loading="zhongjianReportUploading" @click="triggerZhongjianReportUpload">上传报告文件</el-button>
|
||||
<span class="task-evidence-hint">{{ zhongjianReportFiles.length }} 个文件</span>
|
||||
</div>
|
||||
<div v-if="zhongjianReportFiles.length" class="task-evidence-list">
|
||||
<div v-for="file in zhongjianReportFiles" :key="file.file_id" class="task-evidence-card">
|
||||
<div
|
||||
class="task-evidence-card__preview"
|
||||
:class="{ 'is-image': file.file_type === 'image' }"
|
||||
@click="previewEvidence(file.file_url)"
|
||||
>
|
||||
<img v-if="file.file_type === 'image' && file.thumbnail_url" :src="file.thumbnail_url" :alt="file.name || '中检报告'" />
|
||||
<div v-else class="task-evidence-card__filetype">{{ evidenceTypeLabel(file.file_type) }}</div>
|
||||
</div>
|
||||
<div class="task-evidence-card__body">
|
||||
<div class="task-evidence-card__name">{{ file.name || file.file_url }}</div>
|
||||
<div class="task-evidence-card__meta">{{ evidenceTypeLabel(file.file_type) }}</div>
|
||||
<div class="task-evidence-card__actions">
|
||||
<el-button link type="primary" @click="previewEvidence(file.file_url)">查看</el-button>
|
||||
<el-button v-if="!isTaskReadonly" link type="danger" @click="removeZhongjianReportFile(file.file_url)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="task-empty task-empty--compact">当前还没有上传中检报告文件</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="发起补资料" name="supplement" :disabled="isTaskReadonly">
|
||||
<div :key="`supplement-${formRenderKey}`" class="task-form-stack">
|
||||
<el-alert
|
||||
@@ -1546,9 +1835,71 @@ onMounted(async () => {
|
||||
<el-button type="primary" :loading="assigneeSubmitting" @click="submitAssigneeAssign">确认分配</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="publishDialogVisible"
|
||||
title="绑定验真吊牌并发布报告"
|
||||
width="560px"
|
||||
@opened="focusPublishMaterialTagInput"
|
||||
>
|
||||
<div class="publish-dialog-body">
|
||||
<el-alert
|
||||
title="请扫描物品验真吊牌二维码,回车后发布正式报告。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-input
|
||||
ref="publishMaterialTagInputRef"
|
||||
v-model="publishMaterialTagInput"
|
||||
size="large"
|
||||
placeholder="扫描平台验真吊牌二维码"
|
||||
clearable
|
||||
@keyup.enter="publishDialogMaterialTag"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="publishDialogVisible = false">稍后处理</el-button>
|
||||
<el-button type="primary" :loading="publishMaterialTagSubmitting" @click="publishDialogMaterialTag">完成并发布报告</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.appraisal-scan-card {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.appraisal-scan {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) minmax(320px, 520px);
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.appraisal-scan__title {
|
||||
color: var(--admin-text-main);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.appraisal-scan__desc {
|
||||
margin-top: 6px;
|
||||
color: var(--admin-text-subtle);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.appraisal-scan__control {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.publish-dialog-body {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
:deep(.task-detail-drawer .el-drawer__body) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2269,6 +2620,16 @@ onMounted(async () => {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.task-readonly-value {
|
||||
min-height: 32px;
|
||||
padding: 7px 0;
|
||||
color: var(--admin-text-main);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.task-form-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -2342,6 +2703,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.appraisal-scan,
|
||||
.task-info-grid,
|
||||
.task-form-grid,
|
||||
.task-supplement-item__head {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { adminApi, type AdminMaterialBatchDetail, type AdminMaterialBatchItem, type AdminMaterialTagCode } from "../../api/admin";
|
||||
import OrderStatusTag from "../../components/OrderStatusTag.vue";
|
||||
@@ -7,6 +7,8 @@ import OrderStatusTag from "../../components/OrderStatusTag.vue";
|
||||
const loading = ref(false);
|
||||
const creating = ref(false);
|
||||
const downloadingId = ref<number | null>(null);
|
||||
const invalidatingBatchId = ref<number | null>(null);
|
||||
const invalidatingTagId = ref<number | null>(null);
|
||||
const detailLoading = ref(false);
|
||||
const createDialogVisible = ref(false);
|
||||
const detailDrawerVisible = ref(false);
|
||||
@@ -18,6 +20,14 @@ const verifyCode = ref("");
|
||||
const dateRange = ref<[string, string] | null>(null);
|
||||
const batches = ref<AdminMaterialBatchItem[]>([]);
|
||||
const detail = ref<AdminMaterialBatchDetail | null>(null);
|
||||
let pollingTimer: number | null = null;
|
||||
const PACKAGE_POLL_INTERVAL_MS = 3000;
|
||||
const PACKAGE_GENERATING_STATUSES = new Set(["pending", "generating"]);
|
||||
|
||||
interface RefreshOptions {
|
||||
silent?: boolean;
|
||||
schedulePolling?: boolean;
|
||||
}
|
||||
|
||||
const createForm = reactive({
|
||||
count: 100,
|
||||
@@ -27,15 +37,18 @@ const createForm = reactive({
|
||||
const stats = computed(() => {
|
||||
const totalCodes = batches.value.reduce((sum, item) => sum + item.total_count, 0);
|
||||
const totalBound = batches.value.reduce((sum, item) => sum + item.bound_count, 0);
|
||||
const totalQrImages = batches.value.reduce((sum, item) => sum + item.qr_image_generated_count, 0);
|
||||
const totalDownloads = batches.value.reduce((sum, item) => sum + item.download_count, 0);
|
||||
return [
|
||||
{ title: "批次数", value: batches.value.length, desc: "当前筛选结果内的物料批次" },
|
||||
{ title: "二维码数", value: totalCodes, desc: "已生成的吊牌二维码链接" },
|
||||
{ title: "吊牌图片", value: `${totalQrImages} / ${totalCodes}`, desc: "已生成的吊牌模板成品图" },
|
||||
{ title: "已绑定", value: totalBound, desc: "已关联鉴定报告的吊牌" },
|
||||
{ title: "下载次数", value: totalDownloads, desc: "Excel 打包下载总次数" },
|
||||
{ title: "下载次数", value: totalDownloads, desc: "压缩包下载总次数" },
|
||||
];
|
||||
});
|
||||
|
||||
const hasGeneratingPackage = computed(() => batches.value.some((item) => isPackageGenerating(item)));
|
||||
|
||||
function buildQueryParams() {
|
||||
return {
|
||||
keyword: keyword.value.trim(),
|
||||
@@ -46,16 +59,26 @@ function buildQueryParams() {
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchBatches() {
|
||||
loading.value = true;
|
||||
async function fetchBatches(options: RefreshOptions = {}) {
|
||||
const silent = options.silent === true;
|
||||
if (!silent) {
|
||||
loading.value = true;
|
||||
}
|
||||
try {
|
||||
const response = await adminApi.getMaterialBatches(buildQueryParams());
|
||||
batches.value = response.data.list;
|
||||
if (options.schedulePolling !== false) {
|
||||
restartPollingIfNeeded();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error("物料批次加载失败");
|
||||
if (!silent) {
|
||||
ElMessage.error("物料批次加载失败");
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (!silent) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,9 +116,18 @@ async function createBatch() {
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadBatch(row: Pick<AdminMaterialBatchItem, "id" | "batch_no">) {
|
||||
function triggerStaticFileDownload(url: string) {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
async function downloadBatch(row: Pick<AdminMaterialBatchItem, "id" | "batch_no" | "package_status">) {
|
||||
if (row.package_status !== "generated") {
|
||||
ElMessage.info(row.package_status === "purged" ? "系统仅保留3个批次图片" : "文件生成中,请稍后下载");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm("将打包下载完整批次的二维码链接与验真编码,并记录一次下载次数。", "下载物料批次", {
|
||||
await ElMessageBox.confirm("将下载完整批次的 Excel 和吊牌图片压缩包,并记录一次下载次数。", "下载物料批次", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认下载",
|
||||
cancelButtonText: "取消",
|
||||
@@ -106,16 +138,13 @@ async function downloadBatch(row: Pick<AdminMaterialBatchItem, "id" | "batch_no"
|
||||
|
||||
downloadingId.value = row.id;
|
||||
try {
|
||||
const blob = await adminApi.downloadMaterialBatch(row.id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `material-batch-${row.batch_no}.xlsx`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
ElMessage.success("物料批次已下载");
|
||||
const response = await adminApi.prepareMaterialBatchDownload(row.id);
|
||||
const url = String(response.data.url || "").trim();
|
||||
if (!url) {
|
||||
throw new Error("下载链接为空");
|
||||
}
|
||||
triggerStaticFileDownload(url);
|
||||
ElMessage.success("已开始下载");
|
||||
await fetchBatches();
|
||||
if (detail.value?.batch.id === row.id) {
|
||||
await loadDetail(row.id);
|
||||
@@ -128,16 +157,80 @@ async function downloadBatch(row: Pick<AdminMaterialBatchItem, "id" | "batch_no"
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail(id: number) {
|
||||
detailLoading.value = true;
|
||||
async function invalidateBatch(row: AdminMaterialBatchItem | AdminMaterialBatchDetail["batch"]) {
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt("失效后该批次下所有吊牌二维码都不能再绑定、扫码查看或验真。", "整批失效", {
|
||||
type: "warning",
|
||||
inputPlaceholder: "失效原因(选填)",
|
||||
confirmButtonText: "确认失效",
|
||||
cancelButtonText: "取消",
|
||||
});
|
||||
invalidatingBatchId.value = row.id;
|
||||
await adminApi.invalidateMaterialBatch({
|
||||
id: row.id,
|
||||
reason: String(value || "").trim(),
|
||||
});
|
||||
ElMessage.success("物料批次已失效");
|
||||
await fetchBatches();
|
||||
if (detail.value?.batch.id === row.id) {
|
||||
await loadDetail(row.id);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error === "cancel" || error === "close") return;
|
||||
console.error(error);
|
||||
ElMessage.error(error?.message || "物料批次失效失败");
|
||||
} finally {
|
||||
invalidatingBatchId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function invalidateTag(row: AdminMaterialTagCode) {
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt("失效后该吊牌二维码不能再绑定、扫码查看或验真。", "单个条码失效", {
|
||||
type: "warning",
|
||||
inputPlaceholder: "失效原因(选填)",
|
||||
confirmButtonText: "确认失效",
|
||||
cancelButtonText: "取消",
|
||||
});
|
||||
invalidatingTagId.value = row.id;
|
||||
await adminApi.invalidateMaterialTag({
|
||||
id: row.id,
|
||||
reason: String(value || "").trim(),
|
||||
});
|
||||
ElMessage.success("物料条码已失效");
|
||||
await fetchBatches();
|
||||
if (detail.value?.batch.id === row.batch_id) {
|
||||
await loadDetail(row.batch_id);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error === "cancel" || error === "close") return;
|
||||
console.error(error);
|
||||
ElMessage.error(error?.message || "物料条码失效失败");
|
||||
} finally {
|
||||
invalidatingTagId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail(id: number, options: RefreshOptions = {}) {
|
||||
const silent = options.silent === true;
|
||||
if (!silent) {
|
||||
detailLoading.value = true;
|
||||
}
|
||||
try {
|
||||
const response = await adminApi.getMaterialBatchDetail(id, detailKeyword.value.trim());
|
||||
detail.value = response.data;
|
||||
if (options.schedulePolling !== false) {
|
||||
restartPollingIfNeeded();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error("批次详情加载失败");
|
||||
if (!silent) {
|
||||
ElMessage.error("批次详情加载失败");
|
||||
}
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
if (!silent) {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +264,93 @@ function openReport(row: AdminMaterialTagCode) {
|
||||
window.location.hash = `#/reports?report_id=${row.report_id}`;
|
||||
}
|
||||
|
||||
onMounted(fetchBatches);
|
||||
function openUrl(url: string) {
|
||||
if (!url) return;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
function qrImageTagType(status: string) {
|
||||
if (status === "generated") return "success";
|
||||
if (status === "failed") return "danger";
|
||||
if (status === "generating") return "warning";
|
||||
if (status === "purged") return "info";
|
||||
return "info";
|
||||
}
|
||||
|
||||
function qrImageStatusText(row: AdminMaterialTagCode) {
|
||||
return row.qr_image_status_text || (row.qr_image_status === "generated" ? "已生成" : "待生成");
|
||||
}
|
||||
|
||||
function materialStatusTagType(status: string) {
|
||||
return status === "invalid" ? "danger" : "success";
|
||||
}
|
||||
|
||||
function packageTagType(status: string) {
|
||||
if (status === "generated") return "success";
|
||||
if (status === "failed") return "danger";
|
||||
if (status === "purged") return "info";
|
||||
return "warning";
|
||||
}
|
||||
|
||||
function packageButtonText(row: Pick<AdminMaterialBatchItem, "package_status"> | AdminMaterialBatchDetail["batch"]) {
|
||||
if (row.package_status === "generated") return "下载压缩包";
|
||||
if (row.package_status === "failed") return "生成失败";
|
||||
if (row.package_status === "purged") return "已清理";
|
||||
return "文件生成中";
|
||||
}
|
||||
|
||||
function isPackageDownloadDisabled(row: Pick<AdminMaterialBatchItem, "package_status"> | AdminMaterialBatchDetail["batch"]) {
|
||||
return row.package_status !== "generated";
|
||||
}
|
||||
|
||||
function isPackageGenerating(row: (Pick<AdminMaterialBatchItem, "package_status"> & { status?: string }) | AdminMaterialBatchDetail["batch"]) {
|
||||
return (row.status || "active") !== "invalid" && PACKAGE_GENERATING_STATUSES.has(row.package_status);
|
||||
}
|
||||
|
||||
function shouldPollPackageStatus() {
|
||||
return hasGeneratingPackage.value || (detailDrawerVisible.value && !!detail.value && isPackageGenerating(detail.value.batch));
|
||||
}
|
||||
|
||||
async function pollPackageStatus() {
|
||||
pollingTimer = null;
|
||||
if (!shouldPollPackageStatus()) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
await fetchBatches({ silent: true, schedulePolling: false });
|
||||
if (detailDrawerVisible.value && detail.value?.batch.id) {
|
||||
await loadDetail(detail.value.batch.id, { silent: true, schedulePolling: false });
|
||||
}
|
||||
restartPollingIfNeeded();
|
||||
}
|
||||
|
||||
function restartPollingIfNeeded() {
|
||||
if (!shouldPollPackageStatus()) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
if (pollingTimer !== null) return;
|
||||
pollingTimer = window.setTimeout(async () => {
|
||||
await pollPackageStatus();
|
||||
}, PACKAGE_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollingTimer !== null) {
|
||||
window.clearTimeout(pollingTimer);
|
||||
pollingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchBatches();
|
||||
});
|
||||
|
||||
watch(detailDrawerVisible, () => {
|
||||
restartPollingIfNeeded();
|
||||
});
|
||||
|
||||
onBeforeUnmount(stopPolling);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -195,8 +374,8 @@ onMounted(fetchBatches);
|
||||
end-placeholder="结束日期"
|
||||
style="width: 260px"
|
||||
/>
|
||||
<el-input v-model="keyword" placeholder="搜索二维码链接 / token / 验真编码" clearable style="width: 320px" />
|
||||
<el-input v-model="qrUrl" placeholder="二维码链接" clearable style="width: 260px" />
|
||||
<el-input v-model="keyword" placeholder="搜索吊牌链接 / token / 验真编码" clearable style="width: 320px" />
|
||||
<el-input v-model="qrUrl" placeholder="吊牌链接" clearable style="width: 260px" />
|
||||
<el-input v-model="verifyCode" placeholder="验真编码" clearable style="width: 160px" />
|
||||
<el-button type="primary" @click="fetchBatches">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
@@ -208,10 +387,31 @@ onMounted(fetchBatches);
|
||||
<el-card class="panel-card orders-table" shadow="never">
|
||||
<el-table :data="batches" stripe row-key="id">
|
||||
<el-table-column prop="batch_no" label="批次号" min-width="180" />
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="materialStatusTagType(row.status)">{{ row.status_text }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_count" label="链接数量" min-width="100" />
|
||||
<el-table-column label="绑定进度" min-width="130">
|
||||
<template #default="{ row }">{{ row.bound_count }} / {{ row.total_count }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="吊牌图片" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="qr-progress">
|
||||
<span>{{ row.qr_image_generated_count }} / {{ row.total_count }}</span>
|
||||
<el-tag v-if="row.qr_image_failed_count" size="small" type="danger">失败 {{ row.qr_image_failed_count }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下载包" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.package_error" :content="row.package_error" placement="top">
|
||||
<el-tag :type="packageTagType(row.package_status)">{{ row.package_status_text }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else :type="packageTagType(row.package_status)">{{ row.package_status_text }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="download_count" label="下载次数" min-width="100" />
|
||||
<el-table-column prop="created_by_name" label="创建人" min-width="110" />
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="170" />
|
||||
@@ -225,6 +425,10 @@ onMounted(fetchBatches);
|
||||
<div class="material-match-item__meta">
|
||||
验真编码 {{ item.verify_code }} · 扫码 {{ item.scan_count }} · 验真 {{ item.verify_count }}
|
||||
</div>
|
||||
<div class="material-match-item__meta">
|
||||
吊牌图片 {{ qrImageStatusText(item) }}
|
||||
<template v-if="item.qr_image_url"> · {{ item.qr_image_url }}</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else style="color: var(--admin-text-subtle);">-</span>
|
||||
@@ -233,7 +437,17 @@ onMounted(fetchBatches);
|
||||
<el-table-column label="操作" fixed="right" width="210">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetail(row)">查看详情</el-button>
|
||||
<el-button link type="success" :loading="downloadingId === row.id" @click="downloadBatch(row)">下载 Excel</el-button>
|
||||
<el-button
|
||||
v-if="row.status !== 'invalid'"
|
||||
link
|
||||
type="success"
|
||||
:loading="downloadingId === row.id || isPackageGenerating(row)"
|
||||
:disabled="isPackageDownloadDisabled(row)"
|
||||
@click="downloadBatch(row)"
|
||||
>
|
||||
{{ packageButtonText(row) }}
|
||||
</el-button>
|
||||
<el-button v-if="row.status !== 'invalid'" link type="danger" :loading="invalidatingBatchId === row.id" @click="invalidateBatch(row)">整批失效</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -261,12 +475,22 @@ onMounted(fetchBatches);
|
||||
<div class="detail-card__title">批次信息</div>
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">批次号</div>
|
||||
<div class="detail-value">{{ detail.batch.batch_no }}</div>
|
||||
<div class="detail-value">
|
||||
{{ detail.batch.batch_no }}
|
||||
<el-tag :type="materialStatusTagType(detail.batch.status)" size="small" style="margin-left: 8px">{{ detail.batch.status_text }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">数量 / 下载次数</div>
|
||||
<div class="detail-value">{{ detail.batch.total_count }} / {{ detail.batch.download_count }}</div>
|
||||
</div>
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">吊牌图片</div>
|
||||
<div class="detail-value">
|
||||
{{ detail.batch.qr_image_generated_count }} / {{ detail.batch.total_count }}
|
||||
<el-tag v-if="detail.batch.qr_image_failed_count" size="small" type="danger" style="margin-left: 8px">失败 {{ detail.batch.qr_image_failed_count }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-card">
|
||||
<div class="detail-card__title">生产备注</div>
|
||||
@@ -278,28 +502,87 @@ onMounted(fetchBatches);
|
||||
<div class="detail-label">最近下载</div>
|
||||
<div class="detail-value">{{ detail.batch.last_downloaded_at || "-" }}</div>
|
||||
</div>
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">下载包</div>
|
||||
<div class="detail-value">
|
||||
<el-tooltip v-if="detail.batch.package_error" :content="detail.batch.package_error" placement="top">
|
||||
<el-tag :type="packageTagType(detail.batch.package_status)">{{ detail.batch.package_status_text }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else :type="packageTagType(detail.batch.package_status)">{{ detail.batch.package_status_text }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="detail.batch.status === 'invalid'" class="detail-card__desc">
|
||||
<div class="detail-label">失效信息</div>
|
||||
<div class="detail-value">{{ detail.batch.invalidated_at || "-" }} / {{ detail.batch.invalidated_by_name || "-" }} / {{ detail.batch.invalid_reason || "-" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card class="panel-card" shadow="never" style="margin-top: 18px">
|
||||
<div class="filters-row" style="justify-content: space-between;">
|
||||
<div class="filters-row">
|
||||
<el-input v-model="detailKeyword" placeholder="筛选二维码链接 / token / 验真编码" clearable style="width: 340px" />
|
||||
<el-input v-model="detailKeyword" placeholder="筛选吊牌链接 / token / 验真编码" clearable style="width: 340px" />
|
||||
<el-button type="primary" @click="loadDetail(detail.batch.id)">筛选</el-button>
|
||||
</div>
|
||||
<el-button type="success" :loading="downloadingId === detail.batch.id" @click="downloadBatch(detail.batch)">下载 Excel</el-button>
|
||||
<div class="filters-row">
|
||||
<el-button
|
||||
v-if="detail.batch.status !== 'invalid'"
|
||||
type="success"
|
||||
:loading="downloadingId === detail.batch.id || isPackageGenerating(detail.batch)"
|
||||
:disabled="isPackageDownloadDisabled(detail.batch)"
|
||||
@click="downloadBatch(detail.batch)"
|
||||
>
|
||||
{{ packageButtonText(detail.batch) }}
|
||||
</el-button>
|
||||
<el-button v-if="detail.batch.status !== 'invalid'" type="danger" plain :loading="invalidatingBatchId === detail.batch.id" @click="invalidateBatch(detail.batch)">整批失效</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="panel-card orders-table" shadow="never">
|
||||
<el-table :data="detail.codes" stripe>
|
||||
<el-table-column prop="qr_url" label="二维码链接" min-width="360">
|
||||
<el-table-column label="吊牌图片" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<div style="word-break: break-all;">{{ row.qr_url }}</div>
|
||||
<el-button link type="primary" @click="copyText(row.qr_url, '二维码链接')">复制</el-button>
|
||||
<div class="qr-image-cell">
|
||||
<el-image
|
||||
v-if="row.qr_image_status === 'generated' && row.qr_image_url"
|
||||
:src="row.qr_image_url"
|
||||
fit="contain"
|
||||
class="qr-image-thumb"
|
||||
:preview-src-list="[row.qr_image_url]"
|
||||
preview-teleported
|
||||
/>
|
||||
<div v-else class="qr-image-placeholder">{{ qrImageStatusText(row) }}</div>
|
||||
<el-tooltip v-if="row.qr_image_error" :content="row.qr_image_error" placement="top">
|
||||
<el-tag size="small" :type="qrImageTagType(row.qr_image_status)">{{ qrImageStatusText(row) }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else size="small" :type="qrImageTagType(row.qr_image_status)">{{ qrImageStatusText(row) }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="吊牌图片链接" min-width="340">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.qr_image_url">
|
||||
<div class="link-text">{{ row.qr_image_url }}</div>
|
||||
<el-button link type="primary" @click="copyText(row.qr_image_url, '吊牌图片链接')">复制</el-button>
|
||||
<el-button link type="primary" @click="openUrl(row.qr_image_url)">打开/下载</el-button>
|
||||
</template>
|
||||
<span v-else style="color: var(--admin-text-subtle);">{{ qrImageStatusText(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="qr_url" label="吊牌内链接" min-width="360">
|
||||
<template #default="{ row }">
|
||||
<div class="link-text">{{ row.qr_url }}</div>
|
||||
<el-button link type="primary" @click="copyText(row.qr_url, '吊牌内链接')">复制</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="verify_code" label="验真编码" min-width="120" />
|
||||
<el-table-column label="条码状态" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="materialStatusTagType(row.status)">{{ row.status_text }}</el-tag>
|
||||
<div v-if="row.status === 'invalid'" class="status-note">{{ row.invalidated_at || "-" }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定状态" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<OrderStatusTag :status="row.bind_status_text" />
|
||||
@@ -315,6 +598,20 @@ onMounted(fetchBatches);
|
||||
<el-table-column prop="verify_count" label="验真次数" min-width="100" />
|
||||
<el-table-column prop="bound_by_name" label="绑定人" min-width="110" />
|
||||
<el-table-column prop="bound_at" label="绑定时间" min-width="170" />
|
||||
<el-table-column label="操作" fixed="right" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="detail?.batch.status !== 'invalid' && row.status !== 'invalid'"
|
||||
link
|
||||
type="danger"
|
||||
:loading="invalidatingTagId === row.id"
|
||||
@click="invalidateTag(row)"
|
||||
>
|
||||
失效
|
||||
</el-button>
|
||||
<span v-else style="color: var(--admin-text-subtle);">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -351,4 +648,46 @@ onMounted(fetchBatches);
|
||||
display: grid;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.qr-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.qr-image-cell {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.qr-image-thumb,
|
||||
.qr-image-placeholder {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.qr-image-placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 8px;
|
||||
color: var(--admin-text-subtle);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
word-break: break-all;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.status-note {
|
||||
margin-top: 4px;
|
||||
color: var(--admin-text-subtle);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -136,6 +136,12 @@ const imageEvidenceList = computed(() =>
|
||||
const fileEvidenceList = computed(() =>
|
||||
(detail.value?.evidence_attachments || []).filter((item) => item.file_type !== "image"),
|
||||
);
|
||||
const zhongjianReportImageList = computed(() =>
|
||||
(detail.value?.zhongjian_report_files || []).filter((item) => item.file_type === "image"),
|
||||
);
|
||||
const zhongjianReportFileList = computed(() =>
|
||||
(detail.value?.zhongjian_report_files || []).filter((item) => item.file_type !== "image"),
|
||||
);
|
||||
|
||||
function openInspectionCreate() {
|
||||
inspectionForm.value = createInspectionPayload();
|
||||
@@ -536,6 +542,20 @@ watch(
|
||||
<div class="detail-label">鉴定时间</div>
|
||||
<div class="detail-value">{{ detail.appraisal_info.appraisal_time || "-" }}</div>
|
||||
</div>
|
||||
<template v-if="detail.report_header.service_provider === 'zhongjian'">
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">中检报告编号</div>
|
||||
<div class="detail-value">{{ detail.report_header.zhongjian_report_no || "-" }}</div>
|
||||
</div>
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">报告录入人</div>
|
||||
<div class="detail-value">{{ detail.report_header.report_entry_admin_name || "-" }}</div>
|
||||
</div>
|
||||
<div class="detail-card__desc">
|
||||
<div class="detail-label">录入时间</div>
|
||||
<div class="detail-value">{{ detail.report_header.report_entered_at || "-" }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
@@ -593,7 +613,46 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-card" style="grid-column: 1 / -1">
|
||||
<div v-if="detail.report_header.service_provider === 'zhongjian'" class="detail-card" style="grid-column: 1 / -1">
|
||||
<div class="detail-card__title">中检报告文件</div>
|
||||
<div v-if="detail.zhongjian_report_files.length" class="report-evidence-stack">
|
||||
<div v-if="zhongjianReportImageList.length" class="report-evidence-section">
|
||||
<div class="report-evidence-section__title">报告图片</div>
|
||||
<div class="report-evidence-gallery">
|
||||
<div
|
||||
v-for="attachment in zhongjianReportImageList"
|
||||
:key="attachment.file_id"
|
||||
class="report-evidence-gallery__item"
|
||||
@click="previewEvidence(attachment.file_url)"
|
||||
>
|
||||
<img :src="attachment.thumbnail_url || attachment.file_url" :alt="attachment.name || '中检报告图片'" />
|
||||
<div class="report-evidence-gallery__caption">{{ attachment.name || "未命名图片" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="zhongjianReportFileList.length" class="report-evidence-section">
|
||||
<div class="report-evidence-section__title">报告文档</div>
|
||||
<div class="report-evidence-list">
|
||||
<div v-for="attachment in zhongjianReportFileList" :key="attachment.file_id" class="report-evidence-card">
|
||||
<div class="report-evidence-card__preview" @click="previewEvidence(attachment.file_url)">
|
||||
<div class="report-evidence-card__filetype">{{ evidenceTypeLabel(attachment.file_type) }}</div>
|
||||
</div>
|
||||
<div class="report-evidence-card__body">
|
||||
<div class="detail-value" style="margin-top: 0; word-break: break-word;">{{ attachment.name || attachment.file_url }}</div>
|
||||
<div class="detail-label" style="margin-top: 6px;">{{ evidenceTypeLabel(attachment.file_type) }}</div>
|
||||
<el-button size="small" style="margin-top: 10px" @click="previewEvidence(attachment.file_url)">查看文件</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="detail-card__desc">
|
||||
<div class="detail-value">当前报告未上传中检报告文件</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.report_header.service_provider !== 'zhongjian'" class="detail-card" style="grid-column: 1 / -1">
|
||||
<div class="detail-card__title">扫码与公开链接</div>
|
||||
<div style="display: grid; grid-template-columns: 220px 1fr; gap: 24px; align-items: start;">
|
||||
<div
|
||||
|
||||
544
admin-web/src/pages/warehouse-workbench/index.vue
Normal file
544
admin-web/src/pages/warehouse-workbench/index.vue
Normal file
@@ -0,0 +1,544 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, h, nextTick, ref, type PropType } from "vue";
|
||||
import { ElMessage, type InputInstance } from "element-plus";
|
||||
import { adminApi, type AdminWarehouseWorkbenchContext } from "../../api/admin";
|
||||
import OrderStatusTag from "../../components/OrderStatusTag.vue";
|
||||
|
||||
const activeMode = ref<"inbound" | "zhongjian" | "return">("inbound");
|
||||
const loading = ref(false);
|
||||
const actionLoading = ref(false);
|
||||
|
||||
const inboundTrackingNo = ref("");
|
||||
const inboundTagNo = ref("");
|
||||
const zhongjianTagNo = ref("");
|
||||
const returnTagNo = ref("");
|
||||
const returnMaterialQr = ref("");
|
||||
const returnExpressCompany = ref("");
|
||||
const returnTrackingNo = ref("");
|
||||
|
||||
const inboundContext = ref<AdminWarehouseWorkbenchContext | null>(null);
|
||||
const zhongjianContext = ref<AdminWarehouseWorkbenchContext | null>(null);
|
||||
const returnContext = ref<AdminWarehouseWorkbenchContext | null>(null);
|
||||
|
||||
const inboundTagInputRef = ref<InputInstance | null>(null);
|
||||
const returnMaterialInputRef = ref<InputInstance | null>(null);
|
||||
const returnTrackingInputRef = ref<InputInstance | null>(null);
|
||||
|
||||
const currentReturnIsZhongjian = computed(() => returnContext.value?.order_info.service_provider === "zhongjian");
|
||||
const returnConfirmed = computed(() => Boolean(returnContext.value?.transfer_flow?.return_confirmed_at));
|
||||
|
||||
const OrderContextCard = defineComponent({
|
||||
name: "OrderContextCard",
|
||||
props: {
|
||||
context: {
|
||||
type: Object as PropType<AdminWarehouseWorkbenchContext | null>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ["open-file"],
|
||||
setup(props, { emit }) {
|
||||
return () => {
|
||||
if (!props.context) {
|
||||
return h("div", { class: "detail-card empty-context" }, "等待扫码识别订单");
|
||||
}
|
||||
|
||||
const c = props.context;
|
||||
return h("div", { class: "detail-card context-card" }, [
|
||||
h("div", { class: "context-head" }, [
|
||||
h("div", [
|
||||
h("div", { class: "context-title" }, c.product_info.product_name || "待完善物品信息"),
|
||||
h("div", { class: "context-subtitle" }, `${c.order_info.order_no} / ${c.order_info.appraisal_no}`),
|
||||
]),
|
||||
h("div", { class: "context-tags" }, [
|
||||
h(OrderStatusTag, { status: c.order_info.display_status }),
|
||||
h("span", { class: "context-chip" }, c.order_info.service_provider_text),
|
||||
h("span", { class: "context-chip" }, c.order_info.source_channel_text),
|
||||
]),
|
||||
]),
|
||||
h("div", { class: "context-grid" }, [
|
||||
h("div", [h("span", "品类 / 品牌"), h("strong", `${c.product_info.category_name || "-"} / ${c.product_info.brand_name || "-"}`)]),
|
||||
h("div", [h("span", "内部挂牌"), h("strong", c.transfer_flow?.internal_tag_no || "-")]),
|
||||
h("div", [h("span", "流转阶段"), h("strong", c.transfer_flow?.current_stage_text || "-")]),
|
||||
h("div", [h("span", "当前位置"), h("strong", c.transfer_flow?.current_location_text || "-")]),
|
||||
h("div", [h("span", "寄入运单"), h("strong", c.logistics_info?.tracking_no || "-")]),
|
||||
h("div", [h("span", "寄回地址"), h("strong", c.return_address ? `${c.return_address.consignee} / ${c.return_address.mobile} / ${c.return_address.full_address}` : "-")]),
|
||||
]),
|
||||
c.report_info
|
||||
? h("div", { class: "report-box" }, [
|
||||
h("div", { class: "context-section-title" }, "报告信息"),
|
||||
h("div", { class: "context-grid" }, [
|
||||
h("div", [h("span", "报告编号"), h("strong", c.report_info.report_no)]),
|
||||
h("div", [h("span", "发布时间"), h("strong", c.report_info.publish_time || "-")]),
|
||||
h("div", [h("span", "中检报告编号"), h("strong", c.report_info.zhongjian_report_no || "-")]),
|
||||
h("div", [h("span", "报告录入人"), h("strong", c.report_info.report_entry_admin_name || "-")]),
|
||||
]),
|
||||
c.report_info.zhongjian_report_files?.length
|
||||
? h(
|
||||
"div",
|
||||
{ class: "file-list" },
|
||||
c.report_info.zhongjian_report_files.map((file) =>
|
||||
h("button", { class: "file-button", type: "button", onClick: () => emit("open-file", file.file_url) }, file.name || file.file_url),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
])
|
||||
: null,
|
||||
c.flow_logs?.length
|
||||
? h("div", { class: "flow-log-box" }, [
|
||||
h("div", { class: "context-section-title" }, "流转记录"),
|
||||
h(
|
||||
"div",
|
||||
{ class: "flow-log-list" },
|
||||
c.flow_logs.map((log) =>
|
||||
h("div", { class: "flow-log-item" }, [
|
||||
h("div", { class: "flow-log-item__head" }, [
|
||||
h("strong", log.action_text),
|
||||
h("span", log.created_at || "-"),
|
||||
]),
|
||||
h("div", { class: "flow-log-item__meta" }, `${log.operator_name || "系统"} / ${log.after_stage || "-"} / ${log.after_location || "-"}`),
|
||||
log.remark ? h("div", { class: "flow-log-item__remark" }, log.remark) : null,
|
||||
]),
|
||||
),
|
||||
),
|
||||
])
|
||||
: null,
|
||||
]);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function resetMode(mode: typeof activeMode.value) {
|
||||
activeMode.value = mode;
|
||||
}
|
||||
|
||||
async function lookupInbound() {
|
||||
const trackingNo = inboundTrackingNo.value.trim();
|
||||
if (!trackingNo) {
|
||||
ElMessage.warning("请扫描寄入运单号");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await adminApi.lookupWarehouseInbound(trackingNo);
|
||||
inboundContext.value = response.data;
|
||||
ElMessage.success("已匹配订单");
|
||||
await nextTick();
|
||||
inboundTagInputRef.value?.focus();
|
||||
} catch (error: any) {
|
||||
inboundContext.value = null;
|
||||
ElMessage.error(error?.message || "未匹配到订单");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function receiveInbound() {
|
||||
if (!inboundContext.value) {
|
||||
await lookupInbound();
|
||||
return;
|
||||
}
|
||||
if (!inboundTagNo.value.trim()) {
|
||||
ElMessage.warning("请扫描内部流转挂牌");
|
||||
return;
|
||||
}
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
const response = await adminApi.receiveWarehouseInbound({
|
||||
tracking_no: inboundTrackingNo.value.trim(),
|
||||
internal_tag_no: inboundTagNo.value.trim(),
|
||||
});
|
||||
inboundContext.value = response.data;
|
||||
ElMessage.success("入库完成");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "入库失败");
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function lookupZhongjian() {
|
||||
if (!zhongjianTagNo.value.trim()) {
|
||||
ElMessage.warning("请扫描内部流转码");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await adminApi.lookupZhongjianWarehouseTransfer(zhongjianTagNo.value.trim());
|
||||
zhongjianContext.value = response.data;
|
||||
ElMessage.success("已识别中检订单");
|
||||
} catch (error: any) {
|
||||
zhongjianContext.value = null;
|
||||
ElMessage.error(error?.message || "中检流转查询失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitZhongjianAction() {
|
||||
if (!zhongjianContext.value) {
|
||||
await lookupZhongjian();
|
||||
return;
|
||||
}
|
||||
const action = zhongjianContext.value.next_action;
|
||||
if (!action) {
|
||||
ElMessage.warning("当前没有可执行的送检动作");
|
||||
return;
|
||||
}
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
const response = action === "outbound"
|
||||
? await adminApi.zhongjianWarehouseOutbound(zhongjianTagNo.value.trim())
|
||||
: await adminApi.zhongjianWarehouseInbound(zhongjianTagNo.value.trim());
|
||||
zhongjianContext.value = response.data;
|
||||
ElMessage.success(action === "outbound" ? "送检出库完成" : "送检入库完成");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "中检流转操作失败");
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function lookupReturn() {
|
||||
if (!returnTagNo.value.trim()) {
|
||||
ElMessage.warning("请扫描内部流转码");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await adminApi.lookupWarehouseReturn(returnTagNo.value.trim());
|
||||
returnContext.value = response.data;
|
||||
ElMessage.success("已打开待寄回订单");
|
||||
await nextTick();
|
||||
if (response.data.order_info.service_provider === "zhongjian") {
|
||||
returnTrackingInputRef.value?.focus();
|
||||
} else {
|
||||
returnMaterialInputRef.value?.focus();
|
||||
}
|
||||
} catch (error: any) {
|
||||
returnContext.value = null;
|
||||
ElMessage.error(error?.message || "寄回查询失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmReturnReport() {
|
||||
if (!returnContext.value) {
|
||||
await lookupReturn();
|
||||
return;
|
||||
}
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
const response = currentReturnIsZhongjian.value
|
||||
? await adminApi.confirmWarehouseReturnZhongjian(returnTagNo.value.trim())
|
||||
: await adminApi.verifyWarehouseReturnMaterialTag({
|
||||
internal_tag_no: returnTagNo.value.trim(),
|
||||
qr_input: returnMaterialQr.value.trim(),
|
||||
});
|
||||
returnContext.value = response.data;
|
||||
ElMessage.success(currentReturnIsZhongjian.value ? "中检报告已确认" : "验真吊牌已确认");
|
||||
await nextTick();
|
||||
returnTrackingInputRef.value?.focus();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "报告确认失败");
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function shipReturn() {
|
||||
if (!returnContext.value) {
|
||||
await lookupReturn();
|
||||
return;
|
||||
}
|
||||
if (!returnConfirmed.value) {
|
||||
ElMessage.warning("请先确认报告信息");
|
||||
return;
|
||||
}
|
||||
if (!returnExpressCompany.value.trim() || !returnTrackingNo.value.trim()) {
|
||||
ElMessage.warning("请填写回寄快递公司和运单号");
|
||||
return;
|
||||
}
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
const response = await adminApi.shipWarehouseReturn({
|
||||
internal_tag_no: returnTagNo.value.trim(),
|
||||
express_company: returnExpressCompany.value.trim(),
|
||||
tracking_no: returnTrackingNo.value.trim(),
|
||||
});
|
||||
returnContext.value = response.data;
|
||||
ElMessage.success("回寄运单已登记");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "回寄失败");
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openFile(url: string) {
|
||||
if (!url) return;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="warehouse-workbench">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<el-segmented
|
||||
v-model="activeMode"
|
||||
:options="[
|
||||
{ label: '入库', value: 'inbound' },
|
||||
{ label: '中检流转', value: 'zhongjian' },
|
||||
{ label: '发货', value: 'return' },
|
||||
]"
|
||||
@change="resetMode"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<div v-if="activeMode === 'inbound'" class="workbench-grid">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>入库扫描</template>
|
||||
<div class="scan-stack">
|
||||
<el-input v-model="inboundTrackingNo" size="large" placeholder="扫描寄入快递运单号" clearable @keyup.enter="lookupInbound" />
|
||||
<el-input ref="inboundTagInputRef" v-model="inboundTagNo" size="large" placeholder="扫描内部流转挂牌" clearable @keyup.enter="receiveInbound" />
|
||||
<div class="actions-row">
|
||||
<el-button type="primary" :loading="loading" @click="lookupInbound">匹配订单</el-button>
|
||||
<el-button type="success" :loading="actionLoading" :disabled="!inboundContext" @click="receiveInbound">绑定挂牌并入库</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<OrderContextCard :context="inboundContext" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeMode === 'zhongjian'" class="workbench-grid">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>中检送检出入库</template>
|
||||
<div class="scan-stack">
|
||||
<el-input v-model="zhongjianTagNo" size="large" placeholder="扫描内部流转码" clearable @keyup.enter="lookupZhongjian" />
|
||||
<div class="actions-row">
|
||||
<el-button type="primary" :loading="loading" @click="lookupZhongjian">识别订单</el-button>
|
||||
<el-button type="success" :loading="actionLoading" :disabled="!zhongjianContext?.next_action" @click="submitZhongjianAction">
|
||||
{{ zhongjianContext?.next_action_text || "执行送检动作" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<OrderContextCard :context="zhongjianContext" />
|
||||
</div>
|
||||
|
||||
<div v-else class="workbench-grid">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>发货扫描</template>
|
||||
<div class="scan-stack">
|
||||
<el-input v-model="returnTagNo" size="large" placeholder="扫描内部流转码" clearable @keyup.enter="lookupReturn" />
|
||||
<el-input
|
||||
v-if="returnContext && !currentReturnIsZhongjian"
|
||||
ref="returnMaterialInputRef"
|
||||
v-model="returnMaterialQr"
|
||||
size="large"
|
||||
placeholder="扫描平台验真吊牌"
|
||||
clearable
|
||||
@keyup.enter="confirmReturnReport"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="returnContext && currentReturnIsZhongjian"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="中检订单不扫描平台验真吊牌"
|
||||
description="请核对中检报告编号和报告文件,确认无误后进入回寄物流填写。"
|
||||
/>
|
||||
<div class="actions-row">
|
||||
<el-button type="primary" :loading="loading" @click="lookupReturn">打开订单</el-button>
|
||||
<el-button type="success" :loading="actionLoading" :disabled="!returnContext" @click="confirmReturnReport">
|
||||
{{ currentReturnIsZhongjian ? "报告已确认" : "验真吊牌确认" }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="returnContext" class="return-form">
|
||||
<el-input v-model="returnExpressCompany" size="large" placeholder="回寄快递公司,例如:顺丰速运" />
|
||||
<el-input ref="returnTrackingInputRef" v-model="returnTrackingNo" size="large" placeholder="扫描或输入回寄运单号" @keyup.enter="shipReturn" />
|
||||
<el-button type="primary" size="large" :loading="actionLoading" :disabled="!returnConfirmed" @click="shipReturn">提交寄回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<OrderContextCard :context="returnContext" @open-file="openFile" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.warehouse-workbench {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.workbench-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(360px, 460px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.scan-stack {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.return-form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--admin-border);
|
||||
}
|
||||
|
||||
.empty-context {
|
||||
min-height: 260px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--admin-text-subtle);
|
||||
}
|
||||
|
||||
.context-card {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.context-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.context-title {
|
||||
color: var(--admin-text-main);
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.context-subtitle {
|
||||
margin-top: 8px;
|
||||
color: var(--admin-text-subtle);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.context-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.context-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(72, 104, 133, 0.1);
|
||||
color: var(--admin-progress);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.context-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.context-grid > div {
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 8px;
|
||||
background: #fffdfa;
|
||||
}
|
||||
|
||||
.context-grid span {
|
||||
display: block;
|
||||
color: var(--admin-text-subtle);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.context-grid strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: var(--admin-text-main);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.report-box {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.flow-log-box {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.context-section-title {
|
||||
color: var(--admin-text-main);
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.file-button {
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: var(--admin-progress);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flow-log-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.flow-log-item {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 8px;
|
||||
background: #fffdfa;
|
||||
}
|
||||
|
||||
.flow-log-item__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: var(--admin-text-main);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.flow-log-item__head span,
|
||||
.flow-log-item__meta,
|
||||
.flow-log-item__remark {
|
||||
color: var(--admin-text-subtle);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.flow-log-item__meta,
|
||||
.flow-log-item__remark {
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user