feat: update report detail and verification flow
This commit is contained in:
@@ -344,6 +344,27 @@ export interface ReportListItem {
|
||||
export interface ReportDetailData {
|
||||
evidence_attachments: EvidenceAttachmentAsset[];
|
||||
zhongjian_report_files: EvidenceAttachmentAsset[];
|
||||
report_media: {
|
||||
images: EvidenceAttachmentAsset[];
|
||||
};
|
||||
product_display: {
|
||||
product_name: string;
|
||||
institution_name: string;
|
||||
items: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
remark?: string;
|
||||
}>;
|
||||
};
|
||||
trace_info: {
|
||||
nodes: Array<{
|
||||
code: "inbound" | "appraisal" | "return";
|
||||
title: string;
|
||||
occurred_at: string;
|
||||
status: "completed" | "pending";
|
||||
assets: EvidenceAttachmentAsset[];
|
||||
}>;
|
||||
};
|
||||
report_header: {
|
||||
report_id: number;
|
||||
report_no: string;
|
||||
@@ -413,6 +434,12 @@ export interface MaterialTagVerifyResult {
|
||||
verify_count: number;
|
||||
}
|
||||
|
||||
export interface ReportAntiCounterfeitResult {
|
||||
verify_passed: boolean;
|
||||
verify_message: string;
|
||||
verify_count: number;
|
||||
}
|
||||
|
||||
export interface MessageSummaryData {
|
||||
total_count: number;
|
||||
unread_count: number;
|
||||
@@ -672,6 +699,12 @@ export const appApi = {
|
||||
params: { report_no: reportNo },
|
||||
});
|
||||
},
|
||||
verifyReportAntiCounterfeit(payload: { report_no: string; verify_code: string }) {
|
||||
return request<ReportAntiCounterfeitResult>("/api/app/report/anti-counterfeit/verify", {
|
||||
method: "POST",
|
||||
data: payload,
|
||||
});
|
||||
},
|
||||
getMaterialTag(token: string) {
|
||||
return request<MaterialTagData>("/api/app/material-tag", {
|
||||
params: { token },
|
||||
|
||||
@@ -368,6 +368,52 @@ export const reportsFallback: ReportListItem[] = [
|
||||
export const reportDetailFallback: ReportDetailData = {
|
||||
evidence_attachments: [],
|
||||
zhongjian_report_files: [],
|
||||
report_media: {
|
||||
images: [
|
||||
{
|
||||
file_id: "mock-report-image-1",
|
||||
file_url: "https://dummyimage.com/1200x900/efe7df/8b1f2f&text=Appraisal+Image",
|
||||
thumbnail_url: "https://dummyimage.com/480x360/efe7df/8b1f2f&text=Appraisal",
|
||||
name: "鉴定图片",
|
||||
file_type: "image",
|
||||
mime_type: "image/jpeg",
|
||||
},
|
||||
],
|
||||
},
|
||||
product_display: {
|
||||
product_name: "Rolex 腕表",
|
||||
institution_name: "中检鉴定中心",
|
||||
items: [
|
||||
{ label: "检测结论", value: "正品", remark: "综合当前送检资料与商品特征判断,符合正品特征。" },
|
||||
{ label: "品牌", value: "Rolex" },
|
||||
{ label: "主体颜色", value: "银盘" },
|
||||
],
|
||||
},
|
||||
trace_info: {
|
||||
nodes: [
|
||||
{
|
||||
code: "inbound",
|
||||
title: "入仓",
|
||||
occurred_at: "2026-04-18 12:10:00",
|
||||
status: "completed",
|
||||
assets: [],
|
||||
},
|
||||
{
|
||||
code: "appraisal",
|
||||
title: "鉴定",
|
||||
occurred_at: "2026-04-18 16:00:00",
|
||||
status: "completed",
|
||||
assets: [],
|
||||
},
|
||||
{
|
||||
code: "return",
|
||||
title: "寄回",
|
||||
occurred_at: "",
|
||||
status: "pending",
|
||||
assets: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
report_header: {
|
||||
report_id: 1,
|
||||
report_no: "AXY-R-20260420-0001",
|
||||
@@ -375,7 +421,7 @@ export const reportDetailFallback: ReportDetailData = {
|
||||
report_title: "中检鉴定报告",
|
||||
report_status: "published",
|
||||
service_provider: "zhongjian",
|
||||
institution_name: "中检合作机构",
|
||||
institution_name: "中检鉴定中心",
|
||||
publish_time: "2026-04-18 18:26:00",
|
||||
zhongjian_report_no: "ZJ-20260418-0001",
|
||||
report_entry_admin_name: "王师傅",
|
||||
|
||||
@@ -4,42 +4,48 @@ import { onLoad } from "@dcloudio/uni-app";
|
||||
import { appApi, type EvidenceAttachmentAsset, type ReportDetailData } from "../../api/app";
|
||||
import { reportDetailFallback } from "../../mocks/app";
|
||||
import { resolveErrorMessage } from "../../utils/feedback";
|
||||
import { resolveQrImageSource } from "../../utils/qrcode";
|
||||
|
||||
type ReportTab = "product" | "trace";
|
||||
|
||||
const detail = ref<ReportDetailData>(reportDetailFallback);
|
||||
const downloading = ref(false);
|
||||
const loading = ref(false);
|
||||
const pageReady = ref(false);
|
||||
const loadError = ref("");
|
||||
const qrImageSource = computed(() =>
|
||||
resolveQrImageSource(detail.value.verify_info.verify_qrcode_url, detail.value.verify_info.verify_url),
|
||||
);
|
||||
const isZhongjianReport = computed(() => detail.value.report_header.service_provider === "zhongjian");
|
||||
const imageEvidenceAttachments = computed(() =>
|
||||
detail.value.evidence_attachments.filter((item) => item.file_type === "image"),
|
||||
);
|
||||
const otherEvidenceAttachments = computed(() =>
|
||||
detail.value.evidence_attachments.filter((item) => item.file_type !== "image"),
|
||||
);
|
||||
const zhongjianReportImageAttachments = computed(() =>
|
||||
(detail.value.zhongjian_report_files || []).filter((item) => item.file_type === "image"),
|
||||
);
|
||||
const zhongjianReportOtherAttachments = computed(() =>
|
||||
(detail.value.zhongjian_report_files || []).filter((item) => item.file_type !== "image"),
|
||||
);
|
||||
const activeTab = ref<ReportTab>("product");
|
||||
const antiModalVisible = ref(false);
|
||||
const antiCode = ref("");
|
||||
const antiVerifying = ref(false);
|
||||
const antiResult = ref<null | { passed: boolean; message: string }>(null);
|
||||
|
||||
function goVerify() {
|
||||
uni.navigateTo({ url: `/pages/verify/result?report_no=${detail.value.report_header.report_no}` });
|
||||
}
|
||||
|
||||
function previewEvidenceImage(current: string) {
|
||||
const urls = [...imageEvidenceAttachments.value, ...zhongjianReportImageAttachments.value].map((item) => item.file_url);
|
||||
if (!urls.length) return;
|
||||
uni.previewImage({
|
||||
urls,
|
||||
current,
|
||||
});
|
||||
}
|
||||
const reportImages = computed(() => {
|
||||
const images = detail.value.report_media?.images || [];
|
||||
if (images.length) return images;
|
||||
return detail.value.evidence_attachments.filter((item) => item.file_type === "image");
|
||||
});
|
||||
const productName = computed(() =>
|
||||
detail.value.product_display?.product_name
|
||||
|| detail.value.product_info.product_name
|
||||
|| "-",
|
||||
);
|
||||
const institutionName = computed(() =>
|
||||
detail.value.product_display?.institution_name
|
||||
|| detail.value.report_header.institution_name
|
||||
|| "-",
|
||||
);
|
||||
const productItems = computed(() => {
|
||||
const items = detail.value.product_display?.items || [];
|
||||
if (items.length) return items;
|
||||
return [
|
||||
{ label: "检测结论", value: detail.value.result_info.result_text || "-", remark: detail.value.result_info.result_desc || "" },
|
||||
{ label: "品牌", value: detail.value.product_info.brand_name || "-" },
|
||||
];
|
||||
});
|
||||
const traceNodes = computed(() => detail.value.trace_info?.nodes || []);
|
||||
const zhongjianReportFiles = computed(() => detail.value.zhongjian_report_files || []);
|
||||
const zhongjianImageFiles = computed(() => zhongjianReportFiles.value.filter((item) => item.file_type === "image"));
|
||||
const zhongjianOtherFiles = computed(() => zhongjianReportFiles.value.filter((item) => item.file_type !== "image"));
|
||||
const reportNo = computed(() => detail.value.report_header.report_no || "");
|
||||
|
||||
function evidenceTypeText(fileType: string) {
|
||||
if (fileType === "video") return "视频";
|
||||
@@ -48,13 +54,19 @@ function evidenceTypeText(fileType: string) {
|
||||
return "附件";
|
||||
}
|
||||
|
||||
function evidenceDisplayName(item: EvidenceAttachmentAsset, index: number) {
|
||||
function assetDisplayName(item: EvidenceAttachmentAsset, index: number) {
|
||||
return item.name || `${evidenceTypeText(item.file_type)} ${index + 1}`;
|
||||
}
|
||||
|
||||
function openEvidenceAttachment(item: EvidenceAttachmentAsset) {
|
||||
function previewImages(files: EvidenceAttachmentAsset[], current: string) {
|
||||
const urls = files.filter((item) => item.file_type === "image").map((item) => item.file_url);
|
||||
if (!urls.length) return;
|
||||
uni.previewImage({ urls, current });
|
||||
}
|
||||
|
||||
function openAsset(item: EvidenceAttachmentAsset, files: EvidenceAttachmentAsset[]) {
|
||||
if (item.file_type === "image") {
|
||||
previewEvidenceImage(item.file_url);
|
||||
previewImages(files, item.file_url);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,13 +109,74 @@ function openEvidenceAttachment(item: EvidenceAttachmentAsset) {
|
||||
uni.showToast({ title: "当前附件类型暂不支持预览", icon: "none" });
|
||||
}
|
||||
|
||||
function contactService() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/support/create?ticket_type=report_issue&prefill_title=${encodeURIComponent("报告咨询")}`,
|
||||
});
|
||||
}
|
||||
|
||||
function openAntiModal() {
|
||||
antiCode.value = "";
|
||||
antiResult.value = null;
|
||||
antiModalVisible.value = true;
|
||||
}
|
||||
|
||||
function closeAntiModal() {
|
||||
if (antiVerifying.value) return;
|
||||
antiModalVisible.value = false;
|
||||
}
|
||||
|
||||
function normalizeAntiCode(value: string) {
|
||||
return value.replace(/\D/g, "").slice(0, 6);
|
||||
}
|
||||
|
||||
function handleAntiCodeInput(event: Event) {
|
||||
const inputEvent = event as unknown as {
|
||||
detail?: { value?: string };
|
||||
target?: { value?: string };
|
||||
};
|
||||
const detailValue = inputEvent.detail?.value;
|
||||
const targetValue = inputEvent.target?.value;
|
||||
antiCode.value = normalizeAntiCode(String(detailValue ?? targetValue ?? ""));
|
||||
antiResult.value = null;
|
||||
}
|
||||
|
||||
async function submitAntiCounterfeit() {
|
||||
const code = normalizeAntiCode(antiCode.value.trim());
|
||||
antiCode.value = code;
|
||||
if (!code) {
|
||||
uni.showToast({ title: "请输入防伪查询码", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!reportNo.value) {
|
||||
uni.showToast({ title: "报告编号为空", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
antiVerifying.value = true;
|
||||
try {
|
||||
const result = await appApi.verifyReportAntiCounterfeit({
|
||||
report_no: reportNo.value,
|
||||
verify_code: code,
|
||||
});
|
||||
antiResult.value = {
|
||||
passed: result.verify_passed,
|
||||
message: result.verify_message,
|
||||
};
|
||||
} catch (error) {
|
||||
antiResult.value = {
|
||||
passed: false,
|
||||
message: resolveErrorMessage(error, "防伪查询失败,请稍后重试。"),
|
||||
};
|
||||
} finally {
|
||||
antiVerifying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadPdf() {
|
||||
const pdfUrl = detail.value.file_info?.pdf_url;
|
||||
if (!pdfUrl) {
|
||||
uni.showToast({
|
||||
title: "报告文件暂未就绪",
|
||||
icon: "none",
|
||||
});
|
||||
uni.showToast({ title: "报告文件暂未就绪", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,30 +191,17 @@ function downloadPdf() {
|
||||
url: pdfUrl,
|
||||
success: (response) => {
|
||||
if (response.statusCode !== 200 || !response.tempFilePath) {
|
||||
uni.showToast({
|
||||
title: "报告下载失败",
|
||||
icon: "none",
|
||||
});
|
||||
uni.showToast({ title: "报告下载失败", icon: "none" });
|
||||
return;
|
||||
}
|
||||
uni.openDocument({
|
||||
filePath: response.tempFilePath,
|
||||
fileType: "pdf",
|
||||
showMenu: true,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: "无法打开报告文件",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: "报告下载失败",
|
||||
icon: "none",
|
||||
fail: () => uni.showToast({ title: "无法打开报告文件", icon: "none" }),
|
||||
});
|
||||
},
|
||||
fail: () => uni.showToast({ title: "报告下载失败", icon: "none" }),
|
||||
complete: () => {
|
||||
downloading.value = false;
|
||||
uni.hideLoading();
|
||||
@@ -151,8 +211,8 @@ function downloadPdf() {
|
||||
|
||||
onLoad(async (options) => {
|
||||
const id = Number(options?.id || 0);
|
||||
const reportNo = String(options?.report_no || "");
|
||||
if (!id && !reportNo) {
|
||||
const currentReportNo = String(options?.report_no || "");
|
||||
if (!id && !currentReportNo) {
|
||||
loadError.value = "缺少报告编号,无法查看详情。";
|
||||
return;
|
||||
}
|
||||
@@ -161,7 +221,7 @@ onLoad(async (options) => {
|
||||
try {
|
||||
detail.value = await appApi.getReportDetail({
|
||||
id: id || undefined,
|
||||
report_no: reportNo || undefined,
|
||||
report_no: currentReportNo || undefined,
|
||||
});
|
||||
pageReady.value = true;
|
||||
} catch (error) {
|
||||
@@ -174,10 +234,10 @@ onLoad(async (options) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="app-page app-page--tight">
|
||||
<view class="app-page report-page">
|
||||
<view v-if="!pageReady && loading" class="section notice-card">
|
||||
<view class="notice-card__title">正在加载报告详情</view>
|
||||
<view class="notice-card__desc">请稍候,我们正在同步报告正文、验真信息与 PDF 文件。</view>
|
||||
<view class="notice-card__desc">请稍候,我们正在同步报告正文、追溯信息与 PDF 文件。</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!pageReady && loadError" class="section notice-card">
|
||||
@@ -186,183 +246,550 @@ onLoad(async (options) => {
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="section-card certificate-header">
|
||||
<view class="certificate-header__top">
|
||||
<text class="tag tag--success">有效</text>
|
||||
<text class="certificate-meta-chip">{{ detail.report_header.institution_name }}</text>
|
||||
</view>
|
||||
<view class="page-title" style="margin-top: 20rpx; font-size: var(--font-size-2xl); color: var(--color-heading);">
|
||||
{{ detail.report_header.report_title }}
|
||||
</view>
|
||||
<view class="section__desc">{{ isZhongjianReport ? "正式结果凭证,中检报告文件可在下方查看。" : "正式结果凭证,支持编号与二维码验真。" }}</view>
|
||||
<view class="certificate-header__meta">
|
||||
<text class="certificate-meta-chip">报告编号 {{ detail.report_header.report_no }}</text>
|
||||
<text class="certificate-meta-chip">出具日期 {{ detail.report_header.publish_time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section report-result report-result--certificate">
|
||||
<text class="report-result__seal">已鉴定确认</text>
|
||||
<view class="report-result__title">鉴定结论</view>
|
||||
<view class="report-result__value" style="color: var(--color-status-success);">{{ detail.result_info.result_text }}</view>
|
||||
<view class="report-result__desc">{{ detail.result_info.result_desc }}</view>
|
||||
</view>
|
||||
|
||||
<view class="section section-card">
|
||||
<view class="section__title">商品信息</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">商品名称</text>
|
||||
<text class="report-meta__value">{{ detail.product_info.product_name }}</text>
|
||||
</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">品类 / 品牌</text>
|
||||
<text class="report-meta__value">{{ detail.product_info.category_name }} / {{ detail.product_info.brand_name }}</text>
|
||||
</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">颜色 / 规格</text>
|
||||
<text class="report-meta__value">{{ detail.product_info.color }} / {{ detail.product_info.size_spec }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section section-card">
|
||||
<view class="section__title">鉴定信息</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">服务类型</text>
|
||||
<text class="report-meta__value">{{ detail.appraisal_info.service_provider === 'zhongjian' ? '中检鉴定' : '实物鉴定' }}</text>
|
||||
</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">鉴定机构</text>
|
||||
<text class="report-meta__value">{{ detail.appraisal_info.institution_name }}</text>
|
||||
</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">鉴定师</text>
|
||||
<text class="report-meta__value">{{ detail.appraisal_info.appraiser_name }}</text>
|
||||
</view>
|
||||
<template v-if="isZhongjianReport">
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">中检报告编号</text>
|
||||
<text class="report-meta__value">{{ detail.report_header.zhongjian_report_no || "-" }}</text>
|
||||
<view class="report-shell">
|
||||
<view class="report-carousel">
|
||||
<swiper v-if="reportImages.length" class="report-carousel__swiper" indicator-dots circular>
|
||||
<swiper-item v-for="item in reportImages" :key="item.file_url || item.file_id">
|
||||
<image
|
||||
class="report-carousel__image"
|
||||
:src="item.thumbnail_url || item.file_url"
|
||||
mode="aspectFill"
|
||||
@click="previewImages(reportImages, item.file_url)"
|
||||
/>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view v-else class="report-carousel__empty">暂无鉴定图片</view>
|
||||
</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">报告录入人</text>
|
||||
<text class="report-meta__value">{{ detail.report_header.report_entry_admin_name || "-" }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="section section-card">
|
||||
<view class="section__title">评级与估值</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">成色评级</text>
|
||||
<text class="report-meta__value">{{ detail.valuation_info.condition_grade }} 级</text>
|
||||
</view>
|
||||
<view class="report-meta__row">
|
||||
<text class="report-meta__label">市场估值</text>
|
||||
<text class="report-meta__value">¥{{ detail.valuation_info.valuation_min }} - ¥{{ detail.valuation_info.valuation_max }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section section-card">
|
||||
<view class="section__title">报告凭证</view>
|
||||
<view class="credential-box">
|
||||
<view class="credential-box__qr">
|
||||
<image v-if="qrImageSource" class="credential-box__qr-image" :src="qrImageSource" mode="aspectFit" />
|
||||
<text v-else class="credential-box__qr-empty">验真二维码</text>
|
||||
<view class="report-summary">
|
||||
<view class="report-summary__row">
|
||||
<text class="report-summary__label">产品名称</text>
|
||||
<text class="report-summary__value">{{ productName }}</text>
|
||||
</view>
|
||||
<view class="report-summary__row">
|
||||
<text class="report-summary__label">检测机构</text>
|
||||
<text class="report-summary__value">{{ institutionName }}</text>
|
||||
</view>
|
||||
<view class="report-summary__tools">
|
||||
<text class="report-summary__chip">报告编号 {{ detail.report_header.report_no }}</text>
|
||||
<text class="report-summary__chip">出具日期 {{ detail.report_header.publish_time || "-" }}</text>
|
||||
<text class="report-summary__download" @click="downloadPdf">{{ downloading ? "下载中..." : "下载 PDF" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="credential-box__body">
|
||||
<text class="tag tag--accent">{{ detail.verify_info.report_no }}</text>
|
||||
<view class="section__desc">本报告支持扫码或输入编号验真,请以验真页面结果为准。</view>
|
||||
<view style="margin-top: 16rpx">
|
||||
<text class="btn btn--ghost" @click="goVerify">去验真</text>
|
||||
|
||||
<view class="report-tabs">
|
||||
<view :class="['report-tab', activeTab === 'product' ? 'report-tab--active' : '']" @click="activeTab = 'product'">产品信息</view>
|
||||
<view :class="['report-tab', activeTab === 'trace' ? 'report-tab--active' : '']" @click="activeTab = 'trace'">追溯信息</view>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab === 'product'" class="report-panel">
|
||||
<view v-for="item in productItems" :key="item.label" class="product-row">
|
||||
<view class="product-row__label">{{ item.label }}</view>
|
||||
<view class="product-row__value" :class="item.label === '检测结论' ? 'product-row__value--result' : ''">
|
||||
{{ item.value || "-" }}
|
||||
</view>
|
||||
<view v-if="item.remark" class="product-row__remark">{{ item.remark }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="zhongjianReportFiles.length" class="inline-section">
|
||||
<view class="inline-section__title">中检报告文件</view>
|
||||
<view v-if="zhongjianImageFiles.length" class="asset-grid">
|
||||
<view
|
||||
v-for="item in zhongjianImageFiles"
|
||||
:key="item.file_url || item.file_id"
|
||||
class="asset-tile"
|
||||
@click="openAsset(item, zhongjianImageFiles)"
|
||||
>
|
||||
<image class="asset-tile__image" :src="item.thumbnail_url || item.file_url" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="zhongjianOtherFiles.length" class="asset-list">
|
||||
<view
|
||||
v-for="(item, index) in zhongjianOtherFiles"
|
||||
:key="item.file_url || item.file_id"
|
||||
class="asset-list__item"
|
||||
@click="openAsset(item, zhongjianReportFiles)"
|
||||
>
|
||||
<text>{{ assetDisplayName(item, index) }}</text>
|
||||
<text class="asset-list__type">{{ evidenceTypeText(item.file_type) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="report-panel">
|
||||
<view v-for="node in traceNodes" :key="node.code" class="trace-node">
|
||||
<view class="trace-node__head">
|
||||
<view>
|
||||
<view class="trace-node__title">{{ node.title }}</view>
|
||||
<view class="trace-node__time">{{ node.occurred_at || "待完成" }}</view>
|
||||
</view>
|
||||
<text :class="['trace-node__status', node.status === 'completed' ? 'trace-node__status--done' : '']">
|
||||
{{ node.status === "completed" ? "已完成" : "待完成" }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="node.assets.length" class="asset-grid">
|
||||
<view
|
||||
v-for="item in node.assets"
|
||||
:key="item.file_url || item.file_id"
|
||||
class="asset-tile"
|
||||
@click="openAsset(item, node.assets)"
|
||||
>
|
||||
<image v-if="item.file_type === 'image'" class="asset-tile__image" :src="item.thumbnail_url || item.file_url" mode="aspectFill" />
|
||||
<view v-else class="asset-tile__file">
|
||||
<text>{{ evidenceTypeText(item.file_type) }}</text>
|
||||
</view>
|
||||
<view v-if="item.file_type === 'video'" class="asset-tile__play">▶</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="trace-node__empty">暂无影像资料</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="isZhongjianReport" class="section section-card">
|
||||
<view class="section__title">中检报告文件</view>
|
||||
<view class="section__desc">中检报告文件可在下方查看,报告验真请以报告凭证与吊牌组合验真结果为准。</view>
|
||||
<view class="fixed-action-bar report-actions">
|
||||
<view class="btn btn--secondary" @click="contactService">联系我们</view>
|
||||
<view class="btn btn--primary" @click="openAntiModal">防伪查询</view>
|
||||
</view>
|
||||
|
||||
<view v-if="zhongjianReportImageAttachments.length" class="task-files" style="margin-top: 20rpx;">
|
||||
<view
|
||||
v-for="item in zhongjianReportImageAttachments"
|
||||
:key="item.file_id"
|
||||
class="task-file"
|
||||
@click="previewEvidenceImage(item.file_url)"
|
||||
>
|
||||
<image class="task-file__img" :src="item.thumbnail_url || item.file_url" mode="aspectFill" />
|
||||
<view v-if="antiModalVisible" class="anti-modal-mask" @click="closeAntiModal">
|
||||
<view class="anti-modal" @click.stop>
|
||||
<view class="anti-modal__title">防伪查询</view>
|
||||
<view class="anti-modal__desc">请输入吊牌上的防伪查询码</view>
|
||||
<view class="field-box anti-modal__field">
|
||||
<input
|
||||
:value="antiCode"
|
||||
class="field-input"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
placeholder="请输入防伪查询码"
|
||||
confirm-type="done"
|
||||
@input="handleAntiCodeInput"
|
||||
@confirm="submitAntiCounterfeit"
|
||||
/>
|
||||
</view>
|
||||
<view v-if="antiResult" :class="['anti-result', antiResult.passed ? 'anti-result--pass' : 'anti-result--fail']">
|
||||
<view class="anti-result__title">{{ antiResult.passed ? "防伪查询通过" : "查询未通过" }}</view>
|
||||
<view class="anti-result__desc">{{ antiResult.message }}</view>
|
||||
</view>
|
||||
<view class="anti-modal__actions">
|
||||
<button class="anti-modal__button anti-modal__button--ghost" :disabled="antiVerifying" @click="closeAntiModal">取消</button>
|
||||
<button class="anti-modal__button anti-modal__button--primary" :disabled="antiVerifying" @click="submitAntiCounterfeit">
|
||||
{{ antiVerifying ? "查询中..." : "提交查询" }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="zhongjianReportOtherAttachments.length" style="margin-top: 20rpx;">
|
||||
<view
|
||||
v-for="(item, index) in zhongjianReportOtherAttachments"
|
||||
:key="item.file_id"
|
||||
class="info-list__row"
|
||||
@click="openEvidenceAttachment(item)"
|
||||
>
|
||||
<text class="info-list__label">{{ evidenceDisplayName(item, index) }}</text>
|
||||
<text class="info-list__value">{{ evidenceTypeText(item.file_type) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail.evidence_attachments.length" class="section section-card">
|
||||
<view class="section__title">证据附件</view>
|
||||
<view class="section__desc">以下附件为本次报告留存的证据材料,可点击查看原图、视频或 PDF。</view>
|
||||
|
||||
<view v-if="imageEvidenceAttachments.length" class="task-files" style="margin-top: 20rpx;">
|
||||
<view
|
||||
v-for="item in imageEvidenceAttachments"
|
||||
:key="item.file_id"
|
||||
class="task-file"
|
||||
@click="previewEvidenceImage(item.file_url)"
|
||||
>
|
||||
<image class="task-file__img" :src="item.thumbnail_url || item.file_url" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="otherEvidenceAttachments.length" style="margin-top: 20rpx;">
|
||||
<view
|
||||
v-for="(item, index) in otherEvidenceAttachments"
|
||||
:key="item.file_id"
|
||||
class="info-list__row"
|
||||
@click="openEvidenceAttachment(item)"
|
||||
>
|
||||
<text class="info-list__label">{{ evidenceDisplayName(item, index) }}</text>
|
||||
<text class="info-list__value">{{ evidenceTypeText(item.file_type) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section section-note">
|
||||
<view class="section__title">说明</view>
|
||||
<view class="section__desc">
|
||||
{{ detail.risk_notice_text }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="fixed-action-bar">
|
||||
<view class="btn btn--secondary" @click="downloadPdf">{{ downloading ? "下载中..." : "下载 PDF" }}</view>
|
||||
<view v-if="!isZhongjianReport" class="btn btn--primary" @click="goVerify">去验真</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.credential-box__qr {
|
||||
overflow: hidden;
|
||||
.report-page {
|
||||
padding-bottom: 148rpx;
|
||||
background: #eef6ff;
|
||||
}
|
||||
|
||||
.credential-box__qr-image {
|
||||
.report-shell {
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(33, 94, 160, 0.12);
|
||||
border-radius: 8rpx;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 18rpx 42rpx rgba(30, 76, 130, 0.12);
|
||||
}
|
||||
|
||||
.report-carousel {
|
||||
margin: 0 24rpx;
|
||||
height: 392rpx;
|
||||
overflow: hidden;
|
||||
background: #eaf0f6;
|
||||
}
|
||||
|
||||
.report-carousel__swiper,
|
||||
.report-carousel__image,
|
||||
.report-carousel__empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.credential-box__qr-empty {
|
||||
.report-carousel__image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.report-carousel__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.report-summary {
|
||||
padding: 34rpx 40rpx 22rpx;
|
||||
}
|
||||
|
||||
.report-summary__row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 20rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.report-summary__row + .report-summary__row {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.report-summary__label {
|
||||
flex: 0 0 128rpx;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 30rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.report-summary__value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: var(--color-heading);
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.report-summary__tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.report-summary__chip,
|
||||
.report-summary__download {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.report-summary__chip {
|
||||
background: #f3f7fb;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.report-summary__download {
|
||||
border: 1px solid var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
font-size: var(--font-size-xs);
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.report-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 112rpx;
|
||||
padding: 10rpx 40rpx 28rpx;
|
||||
}
|
||||
|
||||
.report-tab {
|
||||
position: relative;
|
||||
color: #8b929d;
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.report-tab--active {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.report-tab--active::after {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -8rpx;
|
||||
width: 42rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 999rpx;
|
||||
background: var(--color-accent);
|
||||
content: "";
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.report-panel {
|
||||
min-height: 440rpx;
|
||||
padding: 18rpx 40rpx 46rpx;
|
||||
}
|
||||
|
||||
.product-row {
|
||||
padding: 22rpx 0;
|
||||
border-bottom: 1px solid rgba(104, 121, 141, 0.14);
|
||||
}
|
||||
|
||||
.product-row__label {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 30rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.product-row__value {
|
||||
margin-top: 10rpx;
|
||||
color: var(--color-heading);
|
||||
font-size: 32rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.product-row__value--result {
|
||||
color: #d83b4c;
|
||||
}
|
||||
|
||||
.product-row__remark {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.inline-section {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.inline-section__title {
|
||||
color: var(--color-heading);
|
||||
font-size: 30rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.trace-node {
|
||||
position: relative;
|
||||
padding: 24rpx 0 26rpx 34rpx;
|
||||
}
|
||||
|
||||
.trace-node::before {
|
||||
position: absolute;
|
||||
left: 8rpx;
|
||||
top: 34rpx;
|
||||
bottom: -20rpx;
|
||||
width: 2rpx;
|
||||
background: #d9e5f2;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.trace-node:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.trace-node::after {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 34rpx;
|
||||
width: 18rpx;
|
||||
height: 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: var(--color-accent);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.trace-node__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.trace-node__title {
|
||||
color: var(--color-heading);
|
||||
font-size: 30rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.trace-node__time,
|
||||
.trace-node__empty {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.trace-node__status {
|
||||
align-self: flex-start;
|
||||
padding: 8rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #f3f5f8;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 22rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.trace-node__status--done {
|
||||
background: rgba(25, 150, 88, 0.12);
|
||||
color: #16814d;
|
||||
}
|
||||
|
||||
.asset-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.asset-tile {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(104, 121, 141, 0.16);
|
||||
border-radius: 8rpx;
|
||||
background: #f4f7fa;
|
||||
}
|
||||
|
||||
.asset-tile__image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.asset-tile__file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--color-accent);
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.asset-tile__play {
|
||||
position: absolute;
|
||||
right: 10rpx;
|
||||
bottom: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(0, 0, 0, 0.48);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.asset-list {
|
||||
display: grid;
|
||||
gap: 12rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.asset-list__item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 18rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f6f8fb;
|
||||
color: var(--color-heading);
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.asset-list__type {
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.report-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.anti-modal-mask {
|
||||
position: fixed;
|
||||
z-index: 40;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48rpx;
|
||||
background: rgba(13, 30, 48, 0.46);
|
||||
}
|
||||
|
||||
.anti-modal {
|
||||
width: 100%;
|
||||
max-width: 620rpx;
|
||||
padding: 34rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.anti-modal__title {
|
||||
color: var(--color-heading);
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.anti-modal__desc {
|
||||
margin-top: 12rpx;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.anti-modal__field {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.anti-result {
|
||||
margin-top: 20rpx;
|
||||
padding: 18rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.anti-result--pass {
|
||||
background: rgba(25, 150, 88, 0.12);
|
||||
}
|
||||
|
||||
.anti-result--fail {
|
||||
background: rgba(216, 59, 76, 0.1);
|
||||
}
|
||||
|
||||
.anti-result__title {
|
||||
color: var(--color-heading);
|
||||
font-size: 28rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.anti-result__desc {
|
||||
margin-top: 8rpx;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.anti-modal__actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18rpx;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.anti-modal__button {
|
||||
height: 82rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 900;
|
||||
line-height: 82rpx;
|
||||
}
|
||||
|
||||
.anti-modal__button--ghost {
|
||||
border: 1px solid var(--color-accent);
|
||||
background: #fff;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.anti-modal__button--primary {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user