diff --git a/admin-web/src/api/admin.ts b/admin-web/src/api/admin.ts
index 23c6349..19ea7f8 100644
--- a/admin-web/src/api/admin.ts
+++ b/admin-web/src/api/admin.ts
@@ -104,7 +104,14 @@ export interface AdminWarehouseWorkbenchContext {
operator_name: string;
remark: string;
created_at: string;
+ inbound_attachments?: AdminFileAsset[];
+ packing_attachments?: AdminFileAsset[];
}>;
+ return_verification?: {
+ verified: boolean;
+ report_id: number;
+ report_no: string;
+ };
next_action?: string;
next_action_text?: string;
}
@@ -244,6 +251,66 @@ export interface AdminOrderWarehouseOption {
supported_category_names: string[];
}
+export interface AdminManualOrderMaterialItem {
+ item_code: string;
+ item_name: string;
+ is_required: boolean;
+ files: AdminFileAsset[];
+}
+
+export interface AdminManualOrderCreatePayload {
+ service_provider: string;
+ product_info: {
+ category_id: number;
+ brand_id: number;
+ product_name: string;
+ color: string;
+ size_spec: string;
+ serial_no: string;
+ };
+ extra_info: {
+ purchase_channel: string;
+ purchase_price: number;
+ usage_status: string;
+ condition_desc: string;
+ remark: string;
+ };
+ return_address: {
+ consignee: string;
+ mobile: string;
+ province: string;
+ city: string;
+ district: string;
+ detail_address: string;
+ };
+ materials: AdminManualOrderMaterialItem[];
+}
+
+export interface AdminManualOrderCreateResponse {
+ order_id: number;
+ order_no: string;
+ appraisal_no: string;
+ user_id: number;
+ next_status: "pending_shipping";
+}
+
+export interface AdminManualOrderMeta {
+ categories: Array<{
+ id: number;
+ name: string;
+ code: string;
+ supported_service_types: string[];
+ }>;
+ brands: Array<{
+ id: number;
+ name: string;
+ en_name: string;
+ code: string;
+ category_ids: number[];
+ supported_service_types: string[];
+ }>;
+}
+
export interface CatalogOverviewCard {
title: string;
value: number;
@@ -380,6 +447,9 @@ export interface AdminReportListItem {
product_name: string;
category_name: string;
brand_name: string;
+ material_tag_bound: boolean;
+ material_tag_verify_code: string;
+ material_tag_bind_status: string;
}
export interface AdminReportDetail {
@@ -414,6 +484,7 @@ export interface AdminReportDetail {
mime_type?: string;
}>;
zhongjian_report_files: AdminFileAsset[];
+ material_tag: null | AdminMaterialTagCode;
risk_notice_text: string;
verify_info: {
verify_status: string;
@@ -705,6 +776,7 @@ export interface AdminAppraisalTaskResultPayload {
}>;
external_remark: string;
internal_remark: string;
+ qr_input?: string;
}
export interface AdminAppraisalTaskSupplementPayload {
@@ -1450,6 +1522,33 @@ export const adminApi = {
};
}>;
},
+ createManualOrder(data: AdminManualOrderCreatePayload) {
+ return request.post("/api/admin/manual-order/create", data) as Promise<{
+ code: number;
+ message: string;
+ data: AdminManualOrderCreateResponse;
+ }>;
+ },
+ getManualOrderMeta() {
+ return request.get("/api/admin/manual-order/meta") as Promise<{
+ code: number;
+ message: string;
+ data: AdminManualOrderMeta;
+ }>;
+ },
+ uploadManualOrderFile(file: File) {
+ const formData = new FormData();
+ formData.append("file", file);
+ return request.post("/api/admin/manual-order/file/upload", formData, {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ }) as Promise<{
+ code: number;
+ message: string;
+ data: AdminFileAsset;
+ }>;
+ },
getCatalogOverview() {
return request.get("/api/admin/catalog/overview") as Promise<{
code: number;
@@ -1588,13 +1687,14 @@ export const adminApi = {
data: AdminReportDetail;
}>;
},
- publishReport(id: number) {
+ publishReport(id: number, qrInput = "") {
return request.post("/api/admin/report/publish", {
id,
+ qr_input: qrInput,
}) as Promise<{
code: number;
message: string;
- data: AdminPublishReportResponse;
+ data: AdminPublishReportResponse & { material_tag?: AdminMaterialTagCode | null };
}>;
},
saveInspectionReport(data: AdminManualInspectionPayload) {
@@ -1689,7 +1789,7 @@ export const adminApi = {
};
}>;
},
- saveZhongjianAppraisalReport(data: { id: number; zhongjian_report_no: string; report_files: AdminFileAsset[] }) {
+ saveZhongjianAppraisalReport(data: { id: number; zhongjian_report_no: string; report_files: AdminFileAsset[]; qr_input: string }) {
return request.post("/api/admin/appraisal-task/zhongjian-report/save", data) as Promise<{
code: number;
message: string;
@@ -1728,16 +1828,16 @@ export const adminApi = {
data: { file_url: string };
}>;
},
- lookupWarehouseInbound(trackingNo: string) {
+ lookupWarehouseInbound(inboundNo: string) {
return request.get("/api/admin/warehouse-workbench/inbound/lookup", {
- params: { tracking_no: trackingNo },
+ params: { inbound_no: inboundNo },
}) as Promise<{
code: number;
message: string;
data: AdminWarehouseWorkbenchContext;
}>;
},
- receiveWarehouseInbound(data: { tracking_no: string; internal_tag_no: string }) {
+ receiveWarehouseInbound(data: { inbound_no?: string; tracking_no?: string; internal_tag_no: string; inbound_attachments?: AdminFileAsset[] }) {
return request.post("/api/admin/warehouse-workbench/inbound/receive", data) as Promise<{
code: number;
message: string;
@@ -1775,12 +1875,32 @@ export const adminApi = {
data: AdminWarehouseWorkbenchContext;
}>;
},
+ confirmWarehouseReturnReport(data: { internal_tag_no: string; report_id: number }) {
+ return request.post("/api/admin/warehouse-workbench/return/report/confirm", data) as Promise<{
+ code: number;
+ message: string;
+ data: AdminWarehouseWorkbenchContext;
+ }>;
+ },
confirmWarehouseReturnZhongjian(internalTagNo: string) {
return request.post("/api/admin/warehouse-workbench/return/zhongjian/confirm", {
internal_tag_no: internalTagNo,
}) as Promise<{ code: number; message: string; data: AdminWarehouseWorkbenchContext }>;
},
- shipWarehouseReturn(data: { internal_tag_no: string; express_company: string; tracking_no: string }) {
+ uploadWarehouseReturnPackingFile(file: File) {
+ const formData = new FormData();
+ formData.append("file", file);
+ return request.post("/api/admin/warehouse-workbench/return/packing/upload", formData, {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ }) as Promise<{
+ code: number;
+ message: string;
+ data: AdminFileAsset;
+ }>;
+ },
+ shipWarehouseReturn(data: { internal_tag_no: string; express_company: string; tracking_no: string; packing_attachments?: AdminFileAsset[] }) {
return request.post("/api/admin/warehouse-workbench/return/ship", data) as Promise<{
code: number;
message: string;
diff --git a/admin-web/src/pages/appraisal-tasks/index.vue b/admin-web/src/pages/appraisal-tasks/index.vue
index 4bb895b..e796d28 100644
--- a/admin-web/src/pages/appraisal-tasks/index.vue
+++ b/admin-web/src/pages/appraisal-tasks/index.vue
@@ -1,6 +1,6 @@
+
+
+
+
+ 仓管补录
+ 补录订单
+ 创建后等待入库,可用订单号或鉴定单号绑定内部流转挂牌。
+
+
+ 正在加载
+
+
+ 订单与商品
+
+ {{ pickerText(providerOptions, providerIndex, "选择服务类型") }}
+
+
+ {{ pickerText(meta.categories, categoryIndex, "选择品类") }}
+
+
+ {{ pickerText(brandOptions, brandIndex, "选择品牌") }}
+
+
+
+
+
+
+
+
+ 补充信息
+
+ 购买渠道
+
+
+
+ 购买价格
+
+
+
+ 使用情况
+
+ {{ pickerText(usageOptions, usageIndex, "请选择使用情况,可选") }}
+
+
+
+ 成色说明
+
+
+
+ 内部备注
+
+
+
+
+
+ 寄回信息
+
+
+
+
+ {{ selectedRegionText }}
+ 请选择省 / 市 / 区县
+
+
+
+
+
+
+
+
+
+ 初始资料
+ {{ materialFiles.length }} 个文件
+
+
+
+
+
+
+
+
+ {{ file.name || file.file_id }}
+ 移除
+
+
+
+
+
+
+
+
+
+
diff --git a/work-app/src/pages/report/detail.vue b/work-app/src/pages/report/detail.vue
index 119dae0..a1f535e 100644
--- a/work-app/src/pages/report/detail.vue
+++ b/work-app/src/pages/report/detail.vue
@@ -2,15 +2,18 @@
import { computed, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { adminApi, type AdminReportDetail } from "../../api/admin";
-import { showErrorToast } from "../../utils/feedback";
+import { showErrorToast, showInfoToast } from "../../utils/feedback";
const loading = ref(false);
const pageReady = ref(false);
const loadError = ref("");
const detail = ref(null);
const reportId = ref(0);
+const returnInternalTagNo = ref("");
+const returnConfirming = ref(false);
const isZhongjian = computed(() => detail.value?.report_header.service_provider === "zhongjian");
+const isReturnReview = computed(() => Boolean(returnInternalTagNo.value && reportId.value));
function previewImage(urls: string[], current: string) {
if (!urls.length) return;
@@ -79,8 +82,43 @@ async function fetchDetail() {
}
}
+function readQueryString(value: unknown) {
+ const raw = Array.isArray(value) ? value[0] : value;
+ const text = String(raw || "").trim();
+ try {
+ return decodeURIComponent(text);
+ } catch {
+ return text;
+ }
+}
+
+async function confirmReturnFromReport() {
+ const currentDetail = detail.value;
+ if (!currentDetail || !returnInternalTagNo.value) {
+ showInfoToast("缺少回寄流转码");
+ return;
+ }
+
+ returnConfirming.value = true;
+ try {
+ await adminApi.confirmWarehouseReturnReport({
+ internal_tag_no: returnInternalTagNo.value,
+ report_id: currentDetail.report_header.id || reportId.value,
+ });
+ showInfoToast("报告已确认");
+ uni.redirectTo({
+ url: `/pages/return-shipping/index?internal_tag_no=${encodeURIComponent(returnInternalTagNo.value)}`,
+ });
+ } catch (error) {
+ showErrorToast(error, "报告确认失败");
+ } finally {
+ returnConfirming.value = false;
+ }
+}
+
onLoad((options) => {
reportId.value = Number(options?.id || 0);
+ returnInternalTagNo.value = readQueryString(options?.return_internal_tag_no);
if (!reportId.value) {
loadError.value = "缺少报告编号,无法查看详情。";
}
@@ -105,6 +143,14 @@ onShow(() => {
{{ detail.report_header.report_no }}
+
+ 回寄前报告核对
+ 请核对报告编号、结论、附件和验真信息,确认无误后进入回寄信息填写。
+
+
+
@@ -238,4 +284,12 @@ onShow(() => {
display: grid;
gap: 14rpx;
}
+
+.return-review-card {
+ border-color: var(--work-accent);
+}
+
+.main-action {
+ margin-top: 18rpx;
+}
diff --git a/work-app/src/pages/return-shipping/index.vue b/work-app/src/pages/return-shipping/index.vue
new file mode 100644
index 0000000..3c84a5d
--- /dev/null
+++ b/work-app/src/pages/return-shipping/index.vue
@@ -0,0 +1,502 @@
+
+
+
+
+
+ 回寄信息
+ 确认回寄
+ 填写回寄运单,并上传打包装箱图片或视频。
+
+
+ 正在加载回寄订单
+
+
+
+
+
+ {{ context.product_info.product_name || "待完善物品信息" }}
+ {{ context.order_info.order_no }} / {{ context.order_info.appraisal_no }}
+
+
+ {{ returnConfirmed ? "报告已确认" : "待确认报告" }}
+
+
+
+
+ 内部挂牌
+ {{ context.transfer_flow?.internal_tag_no || internalTagNo || "-" }}
+
+
+ 流转阶段
+ {{ context.transfer_flow?.current_stage_text || "-" }}
+
+
+ 报告编号
+ {{ context.report_info?.report_no || "-" }}
+
+
+ 发布时间
+ {{ context.report_info?.publish_time || "-" }}
+
+
+
+ 寄回地址
+ {{ context.return_address.consignee }} / {{ context.return_address.mobile }} / {{ context.return_address.full_address }}
+
+
+
+
+ 快递单号
+ 报告确认后登记回寄物流信息。
+
+
+
+
+
+
+
+
+ 打包装箱附件
+ 支持上传打包装箱图片或视频,便于留档核对。
+
+
+
+
+
+ 文件
+ ▶
+
+
+ {{ item.name || item.file_id }}
+ 移除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ activeVideo.name || "装箱视频" }}
+ 关闭
+
+
+
+
+
+
+
+
diff --git a/work-app/src/pages/scan/index.vue b/work-app/src/pages/scan/index.vue
index 3689405..c11bfaf 100644
--- a/work-app/src/pages/scan/index.vue
+++ b/work-app/src/pages/scan/index.vue
@@ -1,7 +1,7 @@
@@ -266,22 +457,56 @@ onShow(refreshRole);
{{ primaryPlaceholder }}
-
+
- 入库绑定
+ 扫描流转码挂牌绑定
-
+
-
@@ -290,16 +515,18 @@ onShow(refreshRole);
{{ context.next_action_text || (context.order_info.service_provider === 'zhongjian' ? '确认中检报告后回寄' : '确认验真吊牌后回寄') }}
-
+
扫码
-
-
+ 报告已确认,可进入回寄信息页填写快递单号并上传打包装箱附件。
-
- {{ actionLoading ? "提交中" : "确认操作" }}
+
+ 寄回流程已完成,无需重复填写回寄信息。
+
+
+ {{ outboundActionText }}
@@ -334,6 +561,16 @@ onShow(refreshRole);
{{ context.return_address.consignee }} / {{ context.return_address.mobile }} / {{ context.return_address.full_address }}
+
+
+
+
+ {{ activeInboundVideo.name || "拆包视频" }}
+ 关闭
+
+
+
+
@@ -357,6 +594,128 @@ onShow(refreshRole);
margin-top: 18rpx;
}
+.attachment-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 14rpx;
+ margin-top: 16rpx;
+}
+
+.attachment-tile {
+ min-width: 0;
+}
+
+.attachment-preview {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 1;
+ overflow: hidden;
+ border: 1px solid var(--work-border);
+ border-radius: var(--work-radius-sm);
+ background: var(--work-card-muted);
+}
+
+.attachment-thumb {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.attachment-video-thumb {
+ background: #202124;
+}
+
+.attachment-file-thumb {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ color: var(--work-text-soft);
+ font-size: 24rpx;
+ font-weight: 800;
+}
+
+.attachment-play {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 54rpx;
+ height: 54rpx;
+ margin-left: -27rpx;
+ margin-top: -27rpx;
+ border-radius: 50%;
+ background: rgba(32, 33, 36, 0.72);
+ color: #ffffff;
+ font-size: 28rpx;
+ line-height: 54rpx;
+ text-align: center;
+}
+
+.attachment-meta {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 8rpx;
+ margin-top: 8rpx;
+}
+
+.attachment-name {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--work-text);
+ font-size: 22rpx;
+ font-weight: 700;
+ line-height: 1.35;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.attachment-remove {
+ min-height: 36rpx;
+ padding: 0 10rpx;
+ font-size: 20rpx;
+}
+
+.upload-actions {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 14rpx;
+ margin-top: 16rpx;
+}
+
+.upload-button {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 12rpx;
+ min-width: 0;
+ min-height: 82rpx;
+ padding: 0 22rpx;
+ border: 1px solid var(--work-border);
+ border-radius: var(--work-radius-sm);
+ background: var(--work-card-muted);
+ color: var(--work-text);
+ font-size: 26rpx;
+ font-weight: 800;
+}
+
+.upload-button[disabled] {
+ opacity: 0.56;
+}
+
+.upload-symbol {
+ width: 34rpx;
+ height: 34rpx;
+ border-radius: 50%;
+ background: #ffffff;
+ color: var(--work-accent-deep);
+ font-size: 28rpx;
+ font-weight: 800;
+ line-height: 32rpx;
+ text-align: center;
+}
+
.ship-fields {
display: grid;
gap: 14rpx;
@@ -369,4 +728,54 @@ onShow(refreshRole);
border-radius: var(--work-radius-sm);
background: var(--work-warning-soft);
}
+
+.video-preview-mask {
+ position: fixed;
+ inset: 0;
+ z-index: 99;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 32rpx;
+ background: rgba(0, 0, 0, 0.72);
+}
+
+.video-preview-panel {
+ width: 100%;
+ max-width: 720rpx;
+ overflow: hidden;
+ border-radius: var(--work-radius);
+ background: #111111;
+}
+
+.video-preview-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20rpx;
+ padding: 18rpx 22rpx;
+ color: #ffffff;
+}
+
+.video-preview-title {
+ min-width: 0;
+ overflow: hidden;
+ font-size: 26rpx;
+ font-weight: 800;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.video-preview-close {
+ color: #ffffff;
+ font-size: 24rpx;
+ font-weight: 800;
+}
+
+.video-preview-player {
+ width: 100%;
+ height: 420rpx;
+ display: block;
+ background: #000000;
+}
diff --git a/work-app/src/pages/task/detail.vue b/work-app/src/pages/task/detail.vue
index 464636e..15f3313 100644
--- a/work-app/src/pages/task/detail.vue
+++ b/work-app/src/pages/task/detail.vue
@@ -26,6 +26,7 @@ const internalRemark = ref("");
const zhongjianReportNo = ref("");
const zhongjianFiles = ref([]);
const evidenceFiles = ref([]);
+const activePreviewVideo = ref(null);
const supplementForm = reactive({
reason: "",
deadline: "",
@@ -33,6 +34,11 @@ const supplementForm = reactive({
});
const isZhongjian = computed(() => detail.value?.task_info.service_provider === "zhongjian");
+const isTaskReadonly = computed(() => {
+ const status = detail.value?.task_info.status || "";
+ return status === "submitted" || status === "completed";
+});
+const internalTagNo = computed(() => detail.value?.task_info.internal_tag_no || "");
const resultSummary = computed(() => detail.value?.result_info.result_text || "暂未填写");
const reportSummary = computed(() => detail.value?.report_summary?.report_no || "");
type AppraisalTemplate = NonNullable;
@@ -129,10 +135,18 @@ async function fetchDetail() {
}
function addSupplementItem() {
+ if (isTaskReadonly.value) {
+ showInfoToast("当前任务已完成,不能再编辑");
+ return;
+ }
supplementForm.items.push({ item_name: "", guide_text: "", is_required: true });
}
function removeSupplementItem(index: number) {
+ if (isTaskReadonly.value) {
+ showInfoToast("当前任务已完成,不能再编辑");
+ return;
+ }
if (supplementForm.items.length === 1) {
supplementForm.items[0].item_name = "";
supplementForm.items[0].guide_text = "";
@@ -143,8 +157,12 @@ function removeSupplementItem(index: number) {
}
async function removeEvidenceFile(fileUrl: string) {
+ if (isTaskReadonly.value || !detail.value) {
+ showInfoToast("当前任务已完成,不能再删除附件");
+ return;
+ }
try {
- await adminApi.deleteAppraisalEvidenceFile(fileUrl);
+ await adminApi.deleteAppraisalEvidenceFile(fileUrl, detail.value.task_info.id);
evidenceFiles.value = evidenceFiles.value.filter((item) => item.file_url !== fileUrl);
showInfoToast("附件已删除");
} catch (error) {
@@ -153,8 +171,12 @@ async function removeEvidenceFile(fileUrl: string) {
}
async function removeZhongjianFile(fileUrl: string) {
+ if (isTaskReadonly.value || !detail.value) {
+ showInfoToast("当前任务已完成,不能再删除文件");
+ return;
+ }
try {
- await adminApi.deleteAppraisalEvidenceFile(fileUrl);
+ await adminApi.deleteAppraisalEvidenceFile(fileUrl, detail.value.task_info.id);
zhongjianFiles.value = zhongjianFiles.value.filter((item) => item.file_url !== fileUrl);
showInfoToast("文件已删除");
} catch (error) {
@@ -162,7 +184,41 @@ async function removeZhongjianFile(fileUrl: string) {
}
}
+function isImageAsset(item: AdminFileAsset) {
+ return item.file_type === "image" || item.mime_type?.startsWith("image/");
+}
+
+function isVideoAsset(item: AdminFileAsset) {
+ return item.file_type === "video" || item.mime_type?.startsWith("video/");
+}
+
+function attachmentTypeLabel(item: AdminFileAsset) {
+ if (isImageAsset(item)) return "图片";
+ if (isVideoAsset(item)) return "视频";
+ return "附件";
+}
+
+function previewAttachment(files: AdminFileAsset[], item: AdminFileAsset) {
+ if (isImageAsset(item)) {
+ const urls = files.filter(isImageAsset).map((asset) => asset.file_url);
+ uni.previewImage({ urls, current: item.file_url });
+ return;
+ }
+
+ if (isVideoAsset(item)) {
+ activePreviewVideo.value = item;
+ return;
+ }
+
+ showInfoToast("当前附件暂不支持预览");
+}
+
+function closePreviewVideo() {
+ activePreviewVideo.value = null;
+}
+
function updateTemplatePoint(index: number, key: "point_value" | "point_remark", value: string) {
+ if (isTaskReadonly.value) return;
const template = detail.value?.appraisal_template;
if (!template) return;
const current = template.key_points[index];
@@ -195,7 +251,90 @@ function returnToWorkOrders(message: string) {
}, 700);
}
+function confirmPublishReport() {
+ return new Promise((resolve) => {
+ uni.showModal({
+ title: "提交确认",
+ content: "是否已鉴定完成并确定发布报告?",
+ cancelText: "取消",
+ confirmText: "去绑定",
+ success: (result) => resolve(Boolean(result.confirm)),
+ fail: () => resolve(false),
+ });
+ });
+}
+
+function promptMaterialTagQrInput() {
+ return new Promise((resolve, reject) => {
+ uni.showModal({
+ title: "绑定验真吊牌",
+ content: "本地预览无法直接扫码,请输入或粘贴吊牌二维码内容。",
+ editable: true,
+ placeholderText: "二维码内容 / 验真吊牌编号",
+ cancelText: "取消",
+ confirmText: "绑定",
+ success: (result) => {
+ if (!result.confirm) {
+ reject(new Error("已取消绑定验真吊牌"));
+ return;
+ }
+
+ const qrInput = String(result.content || "").trim();
+ if (!qrInput) {
+ reject(new Error("请输入验真吊牌二维码内容"));
+ return;
+ }
+
+ resolve(qrInput);
+ },
+ fail: () => reject(new Error("已取消绑定验真吊牌")),
+ });
+ });
+}
+
+function scanMaterialTagQr() {
+ return new Promise((resolve, reject) => {
+ uni.scanCode({
+ scanType: ["barCode", "qrCode"],
+ success: (result) => {
+ const qrInput = String(result.result || "").trim();
+ if (!qrInput) {
+ reject(new Error("未识别到验真吊牌二维码"));
+ return;
+ }
+ resolve(qrInput);
+ },
+ fail: () => {
+ // #ifdef H5
+ promptMaterialTagQrInput().then(resolve).catch(reject);
+ // #endif
+ // #ifndef H5
+ reject(new Error("已取消绑定验真吊牌"));
+ // #endif
+ },
+ });
+ });
+}
+
+async function confirmAndScanMaterialTag() {
+ const confirmed = await confirmPublishReport();
+ if (!confirmed) {
+ return "";
+ }
+
+ try {
+ return await scanMaterialTagQr();
+ } catch (error) {
+ showErrorToast(error, "验真吊牌扫码失败");
+ return "";
+ }
+}
+
async function chooseEvidenceImage() {
+ if (isTaskReadonly.value || !detail.value) {
+ showInfoToast("当前任务已完成,不能再上传附件");
+ return;
+ }
try {
const result = await uni.chooseImage({
count: 9,
@@ -205,7 +344,7 @@ async function chooseEvidenceImage() {
if (!result.tempFilePaths?.length) return;
uploading.value = true;
for (const filePath of result.tempFilePaths) {
- const asset = await adminApi.uploadAppraisalEvidenceFile(filePath);
+ const asset = await adminApi.uploadAppraisalEvidenceFile(filePath, detail.value.task_info.id);
evidenceFiles.value.push(asset);
}
showInfoToast("图片上传成功");
@@ -217,6 +356,10 @@ async function chooseEvidenceImage() {
}
async function chooseEvidenceVideo() {
+ if (isTaskReadonly.value || !detail.value) {
+ showInfoToast("当前任务已完成,不能再上传附件");
+ return;
+ }
try {
const result = await uni.chooseVideo({
sourceType: ["album", "camera"],
@@ -224,7 +367,7 @@ async function chooseEvidenceVideo() {
const filePath = result.tempFilePath;
if (!filePath) return;
uploading.value = true;
- const asset = await adminApi.uploadAppraisalEvidenceFile(filePath);
+ const asset = await adminApi.uploadAppraisalEvidenceFile(filePath, detail.value.task_info.id);
evidenceFiles.value.push(asset);
showInfoToast("视频上传成功");
} catch (error) {
@@ -235,6 +378,10 @@ async function chooseEvidenceVideo() {
}
async function chooseZhongjianImage() {
+ if (isTaskReadonly.value || !detail.value) {
+ showInfoToast("当前任务已完成,不能再上传文件");
+ return;
+ }
try {
const result = await uni.chooseImage({
count: 9,
@@ -244,7 +391,7 @@ async function chooseZhongjianImage() {
if (!result.tempFilePaths?.length) return;
uploading.value = true;
for (const filePath of result.tempFilePaths) {
- const asset = await adminApi.uploadAppraisalEvidenceFile(filePath);
+ const asset = await adminApi.uploadAppraisalEvidenceFile(filePath, detail.value.task_info.id);
zhongjianFiles.value.push(asset);
}
showInfoToast("图片上传成功");
@@ -256,6 +403,10 @@ async function chooseZhongjianImage() {
}
async function chooseZhongjianVideo() {
+ if (isTaskReadonly.value || !detail.value) {
+ showInfoToast("当前任务已完成,不能再上传文件");
+ return;
+ }
try {
const result = await uni.chooseVideo({
sourceType: ["album", "camera"],
@@ -263,7 +414,7 @@ async function chooseZhongjianVideo() {
const filePath = result.tempFilePath;
if (!filePath) return;
uploading.value = true;
- const asset = await adminApi.uploadAppraisalEvidenceFile(filePath);
+ const asset = await adminApi.uploadAppraisalEvidenceFile(filePath, detail.value.task_info.id);
zhongjianFiles.value.push(asset);
showInfoToast("视频上传成功");
} catch (error) {
@@ -275,6 +426,10 @@ async function chooseZhongjianVideo() {
async function submitResult(action: "save" | "submit") {
if (!detail.value) return;
+ if (isTaskReadonly.value) {
+ showInfoToast("当前任务已完成,不能再提交");
+ return;
+ }
if (isZhongjian.value) {
showInfoToast("中检订单请切换到中检报告区");
activeSection.value = "zhongjian";
@@ -286,6 +441,11 @@ async function submitResult(action: "save" | "submit") {
return;
}
+ const qrInput = action === "submit" ? await confirmAndScanMaterialTag() : "";
+ if (action === "submit" && !qrInput) {
+ return;
+ }
+
submitting.value = true;
try {
const conditionPayload = showConditionFields.value
@@ -330,10 +490,11 @@ async function submitResult(action: "save" | "submit") {
internal_remark: internalRemark.value.trim(),
attachments: evidenceFiles.value,
key_points: templateKeyPointsPayload(),
+ ...(qrInput ? { qr_input: qrInput } : {}),
}),
);
if (action === "submit") {
- returnToWorkOrders("鉴定已提交,正在返回工单");
+ returnToWorkOrders("验真吊牌已绑定,报告已发布");
return;
}
showInfoToast("鉴定已保存");
@@ -347,6 +508,10 @@ async function submitResult(action: "save" | "submit") {
async function submitSupplement() {
if (!detail.value) return;
+ if (isTaskReadonly.value) {
+ showInfoToast("当前任务已完成,不能再发起补资料");
+ return;
+ }
const items = supplementForm.items.filter((item) => item.item_name.trim());
if (!supplementForm.reason.trim()) {
showInfoToast("请先填写补资料原因");
@@ -380,6 +545,10 @@ async function submitSupplement() {
async function submitZhongjianReport() {
if (!detail.value) return;
+ if (isTaskReadonly.value) {
+ showInfoToast("当前任务已完成,不能再提交");
+ return;
+ }
if (!zhongjianReportNo.value.trim()) {
showInfoToast("请填写中检报告编号");
return;
@@ -389,14 +558,20 @@ async function submitZhongjianReport() {
return;
}
+ const qrInput = await confirmAndScanMaterialTag();
+ if (!qrInput) {
+ return;
+ }
+
submitting.value = true;
try {
await adminApi.saveZhongjianAppraisalReport({
id: detail.value.task_info.id,
zhongjian_report_no: zhongjianReportNo.value.trim(),
report_files: zhongjianFiles.value,
+ qr_input: qrInput,
});
- returnToWorkOrders("中检报告已提交,正在返回工单");
+ returnToWorkOrders("验真吊牌已绑定,报告已发布");
} catch (error) {
showErrorToast(error, "中检报告录入失败");
} finally {
@@ -418,7 +593,7 @@ onLoad((options) => {
});
onShow(() => {
- if (taskId.value) {
+ if (taskId.value && !pageReady.value) {
void fetchDetail();
}
});
@@ -461,9 +636,17 @@ onShow(() => {
报告摘要
{{ reportSummary || "-" }}
+
+ 流转码编号
+ {{ internalTagNo }}
+
+
+ 当前工单已完成,鉴定内容和附件仅可查看。
+
+
鉴定结论
@@ -475,21 +658,21 @@ onShow(() => {
鉴定结论
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
@@ -502,12 +685,14 @@ onShow(() => {
@@ -515,15 +700,34 @@ onShow(() => {
证据附件
-
-
-
- {{ item.name || item.file_id }}
- 删除
+
+
+
+
+
+ 附件
+ ▶
+
+
+ {{ item.name || item.file_id }}
+
+ {{ attachmentTypeLabel(item) }}
+ 删除
+
-
+
+
{{ uploading ? "上传中" : "添加图片" }}
@@ -534,7 +738,7 @@ onShow(() => {
-
+
保存
提交
@@ -543,19 +747,19 @@ onShow(() => {
补资料
-
-
+
+
-
-
-
+
+
+
{{ item.is_required ? "必传" : "选传" }}
删除
-
+
添加一项
发起补资料
@@ -565,16 +769,35 @@ onShow(() => {
中检报告
-
-
-
-
- {{ item.name || item.file_id }}
- 删除
+
+
+
+
+
+
+ 附件
+ ▶
+
+
+ {{ item.name || item.file_id }}
+
+ {{ attachmentTypeLabel(item) }}
+ 删除
+
-
+
+
{{ uploading ? "上传中" : "添加图片" }}
@@ -584,13 +807,23 @@ onShow(() => {
{{ uploading ? "上传中" : "添加视频" }}
-
- 提交并发布
+
+ 提交并发布
查看报告
+
+
+
+ {{ activePreviewVideo.name || "附件视频" }}
+ 关闭
+
+
+
+
+
任务信息
@@ -610,6 +843,10 @@ onShow(() => {
处理人
{{ detail.task_info.assignee_name }}
+
+ 流转码编号
+ {{ internalTagNo }}
+
@@ -622,12 +859,137 @@ onShow(() => {
gap: 14rpx;
}
+.readonly-notice {
+ margin: -6rpx 0 18rpx;
+ padding: 18rpx 22rpx;
+ border: 1px solid var(--work-border);
+ border-radius: var(--work-radius-sm);
+ background: var(--work-card-muted);
+ color: var(--work-text-soft);
+ font-size: 24rpx;
+ font-weight: 700;
+ line-height: 1.5;
+}
+
+.meta-item--wide {
+ grid-column: 1 / -1;
+}
+
+.transfer-code-value {
+ color: var(--work-warning);
+ font-weight: 900;
+ word-break: break-all;
+}
+
.evidence-title {
margin-top: 24rpx;
color: var(--work-text);
font-weight: 800;
}
+.attachment-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 14rpx;
+ margin-top: 14rpx;
+}
+
+.attachment-tile {
+ min-width: 0;
+}
+
+.attachment-preview {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 1;
+ overflow: hidden;
+ border: 1px solid var(--work-border);
+ border-radius: var(--work-radius-sm);
+ background: var(--work-card-muted);
+}
+
+.attachment-thumb {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+.attachment-video-thumb {
+ background: #202124;
+}
+
+.attachment-file-thumb {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ color: var(--work-text-soft);
+ font-size: 24rpx;
+ font-weight: 800;
+}
+
+.attachment-play {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 54rpx;
+ height: 54rpx;
+ margin-left: -27rpx;
+ margin-top: -27rpx;
+ border-radius: 50%;
+ background: rgba(32, 33, 36, 0.72);
+ color: #ffffff;
+ font-size: 28rpx;
+ line-height: 54rpx;
+ text-align: center;
+}
+
+.attachment-meta {
+ min-width: 0;
+ margin-top: 8rpx;
+}
+
+.attachment-name {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--work-text);
+ font-size: 22rpx;
+ font-weight: 700;
+ line-height: 1.35;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.attachment-actions {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+ margin-top: 6rpx;
+}
+
+.attachment-type,
+.attachment-remove {
+ flex: 0 0 auto;
+ min-height: 34rpx;
+ padding: 0 10rpx;
+ border-radius: var(--work-radius-pill);
+ font-size: 20rpx;
+ font-weight: 700;
+ line-height: 34rpx;
+ white-space: nowrap;
+}
+
+.attachment-type {
+ background: var(--work-info-soft);
+ color: var(--work-info);
+}
+
+.attachment-remove {
+ background: var(--work-danger-soft);
+ color: var(--work-danger);
+}
+
.upload-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -702,4 +1064,54 @@ onShow(() => {
background: var(--work-accent);
color: #ffffff;
}
+
+.video-preview-mask {
+ position: fixed;
+ z-index: 20;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 32rpx;
+ background: rgba(0, 0, 0, 0.58);
+}
+
+.video-preview-panel {
+ width: 100%;
+ overflow: hidden;
+ border-radius: var(--work-radius);
+ background: #ffffff;
+}
+
+.video-preview-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18rpx;
+ padding: 22rpx 24rpx;
+}
+
+.video-preview-title {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--work-text);
+ font-size: 28rpx;
+ font-weight: 800;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.video-preview-close {
+ flex: 0 0 auto;
+ color: var(--work-info);
+ font-size: 26rpx;
+ font-weight: 800;
+}
+
+.video-preview-player {
+ display: block;
+ width: 100%;
+ height: 58vh;
+ background: #000000;
+}
diff --git a/work-app/src/pages/work-order/index.vue b/work-app/src/pages/work-order/index.vue
index bb09f8b..be5a0cb 100644
--- a/work-app/src/pages/work-order/index.vue
+++ b/work-app/src/pages/work-order/index.vue
@@ -18,7 +18,7 @@ const tasks = ref([]);
const isWarehouse = computed(() => role.value === "warehouse");
const title = computed(() => (isWarehouse.value ? "订单中心" : "鉴定工单"));
-const desc = computed(() => (isWarehouse.value ? "仅展示在途、已入仓、待寄回订单。" : "处理我的鉴定待办和历史任务。"));
+const desc = computed(() => (isWarehouse.value ? "仅展示待入库、在途、已入仓、待寄回订单。" : "处理我的鉴定待办和历史任务。"));
const listCount = computed(() => (isWarehouse.value ? orders.value.length : tasks.value.length));
const hasMore = computed(() => total.value > listCount.value);
@@ -26,6 +26,7 @@ const statusOptions = computed(() =>
isWarehouse.value
? [
{ label: "全部", value: "warehouse_active" },
+ { label: "待入库", value: "warehouse_pending_inbound" },
{ label: "在途", value: "warehouse_in_transit" },
{ label: "已入仓", value: "warehouse_received" },
{ label: "待寄回", value: "warehouse_pending_return" },
@@ -112,6 +113,10 @@ function openOrder(item: AdminOrderListItem) {
uni.navigateTo({ url: `/pages/order/detail?id=${item.id}` });
}
+function openManualOrderCreate() {
+ uni.navigateTo({ url: "/pages/order/manual-create" });
+}
+
function openTask(item: AdminAppraisalTaskListItem) {
uni.navigateTo({ url: `/pages/task/detail?id=${item.id}` });
}
@@ -134,6 +139,7 @@ onReachBottom(loadMore);
+ 补录订单
@@ -159,6 +165,10 @@ onReachBottom(loadMore);
{{ item.warehouse_bucket_text || item.display_status }}
{{ item.order_no }} / {{ item.appraisal_no }}
+
+ 流转码
+ {{ item.internal_tag_no }}
+
{{ item.order_no }} / {{ item.external_order_no || item.appraisal_no }}
+
+ 流转码
+ {{ item.internal_tag_no }}
+