feat: update report detail presentation

This commit is contained in:
wushumin
2026-05-27 16:24:23 +08:00
parent 205849061d
commit 1f1f50843e
2 changed files with 599 additions and 192 deletions

View File

@@ -5,9 +5,23 @@ import { appApi, type EvidenceAttachmentAsset, type ReportDetailData } from "../
import { reportDetailFallback } from "../../mocks/app";
import { resolveErrorMessage } from "../../utils/feedback";
type ReportTab = "product" | "trace";
type ReportTab = "product" | "center" | "trace";
type ProductDisplayItem = ReportDetailData["product_display"]["items"][number];
const centerIntroParagraphs = [
"安心验(深圳)商品检验鉴定有限责任公司立足深港核心产业服务区,是一家专业从事商品检验、鉴定、测试及技术咨询的第三方服务机构。",
"公司依托粤港澳大湾区雄厚的产业基础与国际贸易枢纽优势,致力于为 C 端消费者及 B 端电商平台、商家提供网购商品真伪鉴定、成色评估、价值评估及争议仲裁等一站式解决方案。",
];
const institutionBannerTitle = "安心验(深圳)商品检验鉴定有限责任公司";
const institutionBannerSubtitle = "ANXINYAN (SHENZHEN) COMMODITY INSPECTION AND APPRAISAL CO., LTD.";
const anxinyanStatementItems = [
"本电子意见书鉴定结果仅对送检样品负责,整体任一部分损毁或丢失则意见书失效。",
"若对意见书的内容和结论持有异议需在意见书出具后15日内提出逾期不予受理本电子意见书不得用于司法用途。",
"品牌方为商品的设计及制造方,如品牌方确认该鉴定样品为品牌方制造及销售商品,以品牌方的结论为准。",
"如因我司鉴定结果失误给申请人造成损失且无法挽回的,我司予以赔偿。赔偿范围仅限于申请人的直接损失,且赔偿金额总计不超过该检验项目鉴定费用的三倍。",
"企业信息和产品信息均由委托单位提供,鉴定结论不涉及样品品质检测等信息。",
];
const detail = ref<ReportDetailData>(reportDetailFallback);
const downloading = ref(false);
const loading = ref(false);
@@ -92,6 +106,11 @@ const productSpecItems = computed(() => {
].filter((item) => item.value && item.value !== "-");
});
const traceInfoVisible = computed(() => Boolean(detail.value.trace_info?.visible || detail.value.report_header.trace_info_visible));
const centerTabVisible = computed(() => {
const serviceProvider = textValue(detail.value.report_header.service_provider).toLowerCase();
const serviceProviderTextValue = textValue(detail.value.report_header.service_provider_text);
return serviceProvider !== "zhongjian" && serviceProviderTextValue !== "中检鉴定";
});
const traceNodes = computed(() => (traceInfoVisible.value ? detail.value.trace_info?.nodes || [] : []));
const zhongjianReportFiles = computed(() => detail.value.zhongjian_report_files || []);
const zhongjianImageFiles = computed(() => zhongjianReportFiles.value.filter((item) => item.file_type === "image"));
@@ -206,12 +225,6 @@ function openAsset(item: EvidenceAttachmentAsset, files: 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;
@@ -306,8 +319,8 @@ function downloadPdf() {
});
}
watch(traceInfoVisible, (visible) => {
if (!visible && activeTab.value === "trace") {
watch([traceInfoVisible, centerTabVisible], ([traceVisible, centerVisible]) => {
if ((!traceVisible && activeTab.value === "trace") || (!centerVisible && activeTab.value === "center")) {
activeTab.value = "product";
}
});
@@ -364,6 +377,13 @@ onLoad(async (options) => {
<template v-else>
<view class="report-shell">
<view class="report-shell__watermark" aria-hidden="true"></view>
<view class="report-institution-banner">
<image class="report-institution-banner__logo" src="/static/logo.png" mode="aspectFit" />
<view class="report-institution-banner__text">
<view class="report-institution-banner__title">{{ institutionBannerTitle }}</view>
<view class="report-institution-banner__subtitle">{{ institutionBannerSubtitle }}</view>
</view>
</view>
<view class="report-cover">
<swiper v-if="reportImages.length" class="report-cover__swiper" indicator-dots circular>
<swiper-item v-for="item in reportImages" :key="item.file_url || item.file_id">
@@ -378,28 +398,29 @@ onLoad(async (options) => {
<view v-else class="report-cover__empty">暂无鉴定图片</view>
</view>
<view class="report-meta">
<view class="report-meta__row">
<text class="report-meta__label">产品名称</text>
<text class="report-meta__value">{{ productName }}</text>
<view class="report-product-summary">
<view class="report-product-summary__row">
<text class="report-product-summary__tag">产品名称</text>
<text class="report-product-summary__value">{{ productName }}</text>
</view>
<view class="report-meta__row">
<text class="report-meta__label">检测机构</text>
<text class="report-meta__value">{{ institutionName }}</text>
<view class="report-product-summary__row">
<text class="report-product-summary__tag report-product-summary__tag--muted">鉴定机构</text>
<text class="report-product-summary__value">{{ institutionName }}</text>
</view>
<view class="report-meta__row">
<text class="report-meta__label">报告编号</text>
<text class="report-meta__value">{{ reportNo || "-" }}</text>
<view class="report-product-summary__row">
<text class="report-product-summary__tag report-product-summary__tag--muted">报告编号</text>
<text class="report-product-summary__value">{{ reportNo || "-" }}</text>
</view>
<view class="report-meta__row report-meta__row--date">
<text class="report-meta__label">出具日期</text>
<text class="report-meta__download" @click="downloadPdf">{{ downloading ? "下载中" : "下载PDF" }}</text>
<text class="report-meta__value">{{ publishTime }}</text>
<view class="report-product-summary__row">
<text class="report-product-summary__tag report-product-summary__tag--muted">出具日期</text>
<text class="report-product-summary__value">{{ publishTime }}</text>
<text class="report-product-summary__download" @click="downloadPdf">{{ downloading ? "下载中" : "下载PDF" }}</text>
</view>
</view>
<view class="report-tabs">
<view :class="['report-tab', activeTab === 'product' ? 'report-tab--active' : '']" @click="activeTab = 'product'">产品信息</view>
<view v-if="centerTabVisible" :class="['report-tab', activeTab === 'center' ? 'report-tab--active' : '']" @click="activeTab = 'center'">鉴定中心</view>
<view v-if="traceInfoVisible" :class="['report-tab', activeTab === 'trace' ? 'report-tab--active' : '']" @click="activeTab = 'trace'">追溯信息</view>
</view>
@@ -446,9 +467,34 @@ onLoad(async (options) => {
</view>
</view>
</view>
<view class="report-statement">
<view class="report-statement__title">安心验声明:</view>
<view v-for="(item, index) in anxinyanStatementItems" :key="item" class="report-statement__item">
{{ index + 1 }}{{ item }}
</view>
</view>
</view>
<view v-else-if="traceInfoVisible" class="report-panel report-panel--trace">
<view v-else-if="activeTab === 'center' && centerTabVisible" class="report-panel report-panel--center">
<view class="center-profile">
<view class="center-profile__brand">
<image class="center-profile__logo" src="/static/logo.png" mode="aspectFit" />
<view class="center-profile__text">
<view class="center-profile__name">{{ institutionBannerTitle }}</view>
<view class="center-profile__subtitle">{{ institutionBannerSubtitle }}</view>
</view>
</view>
<view class="center-profile__divider"></view>
<view class="center-profile__body">
<view v-for="item in centerIntroParagraphs" :key="item" class="center-profile__paragraph">
{{ item }}
</view>
</view>
</view>
</view>
<view v-else-if="activeTab === 'trace' && traceInfoVisible" class="report-panel report-panel--trace">
<view v-if="traceNodes.length === 0" class="trace-empty">暂无追溯信息</view>
<view v-for="node in traceNodes" :key="node.code" class="trace-node">
<view class="trace-node__head">
@@ -481,7 +527,6 @@ onLoad(async (options) => {
</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>
@@ -542,23 +587,24 @@ onLoad(async (options) => {
max-width: 100vw;
box-sizing: border-box;
overflow-x: hidden;
padding: 28rpx 32rpx 170rpx;
background: #f1f3f6;
padding: 0 0 150rpx;
background: #eef1f5;
color: #3c3f45;
}
.report-shell {
position: relative;
overflow: hidden;
border-radius: 28rpx;
width: 100%;
border-radius: 0;
background: #ffffff;
box-shadow: 0 18rpx 48rpx rgba(31, 36, 48, 0.08);
box-shadow: none;
}
.report-shell__watermark {
position: absolute;
z-index: 0;
top: 374rpx;
top: 530rpx;
left: 28rpx;
right: 28rpx;
height: 560rpx;
@@ -568,18 +614,63 @@ onLoad(async (options) => {
}
.report-cover,
.report-meta,
.report-institution-banner,
.report-product-summary,
.report-tabs,
.report-panel {
position: relative;
z-index: 1;
}
.report-cover {
height: 356rpx;
margin: 28rpx 28rpx 0;
.report-institution-banner {
display: flex;
align-items: center;
gap: 14rpx;
min-height: 124rpx;
padding: 0 28rpx;
border-bottom: 1px solid rgba(229, 229, 229, 0.7);
background: #ffffff;
}
.report-institution-banner__logo {
flex: 0 0 auto;
display: block;
width: 66rpx;
height: 66rpx;
}
.report-institution-banner__text {
flex: 1;
min-width: 0;
}
.report-institution-banner__title {
color: #34363b;
font-size: 21rpx;
font-weight: 900;
line-height: 1.24;
white-space: nowrap;
overflow: hidden;
border-radius: 10rpx;
text-overflow: ellipsis;
}
.report-institution-banner__subtitle {
margin-top: 5rpx;
color: #737982;
font-size: 13rpx;
font-weight: 700;
line-height: 1.25;
letter-spacing: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.report-cover {
height: 412rpx;
margin: 0;
overflow: hidden;
border-radius: 0;
background: #e8eaee;
}
@@ -602,78 +693,89 @@ onLoad(async (options) => {
font-size: var(--font-size-sm);
}
.report-meta {
padding: 34rpx 28rpx 12rpx;
}
.report-meta__row {
display: flex;
align-items: flex-start;
gap: 18rpx;
min-width: 0;
min-height: 48rpx;
}
.report-meta__row + .report-meta__row {
margin-top: 20rpx;
}
.report-meta__label {
flex: 0 0 138rpx;
display: block;
color: #7d828a;
font-size: 28rpx;
line-height: 1.4;
}
.report-meta__value {
flex: 1;
display: block;
min-width: 0;
color: #44474d;
font-size: 28rpx;
font-weight: 700;
line-height: 1.35;
text-align: right;
white-space: normal;
word-break: break-all;
overflow-wrap: anywhere;
}
.report-meta__row--date .report-meta__value {
flex: 0 1 auto;
margin-left: auto;
word-break: normal;
}
.report-meta__download {
flex: 0 0 auto;
display: block;
min-height: 38rpx;
padding: 0 16rpx;
border: 1px solid rgba(221, 179, 47, 0.74);
border-radius: 6rpx;
background: #fff9e6;
color: var(--color-accent);
font-size: 22rpx;
font-weight: 700;
line-height: 36rpx;
}
.report-tabs {
display: flex;
align-items: center;
justify-content: center;
gap: 104rpx;
padding: 26rpx 40rpx 34rpx;
padding: 24rpx 20rpx 34rpx;
border-top: 1px solid rgba(229, 229, 229, 0.72);
border-bottom: 1px solid rgba(229, 229, 229, 0.72);
}
.report-product-summary {
display: grid;
gap: 14rpx;
padding: 20rpx 28rpx 22rpx;
background: #e7e9ef;
}
.report-product-summary__row {
display: flex;
align-items: center;
gap: 14rpx;
min-width: 0;
}
.report-product-summary__tag {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36rpx;
padding: 0 14rpx;
border: 1px solid rgba(237, 189, 0, 0.78);
border-radius: 6rpx;
color: var(--color-accent);
font-size: 22rpx;
font-weight: 800;
line-height: 1;
background: rgba(255, 255, 255, 0.62);
white-space: nowrap;
}
.report-product-summary__tag--muted {
border-color: #b8bdc6;
color: #5d636d;
}
.report-product-summary__value {
flex: 1;
min-width: 0;
color: #2f333a;
font-size: 24rpx;
font-weight: 800;
line-height: 1.45;
word-break: break-all;
overflow-wrap: anywhere;
}
.report-product-summary__download {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34rpx;
padding: 0 12rpx;
border: 1px solid rgba(237, 189, 0, 0.78);
border-radius: 6rpx;
color: var(--color-accent);
font-size: 21rpx;
font-weight: 800;
line-height: 1;
background: rgba(255, 255, 255, 0.68);
white-space: nowrap;
}
.report-tab {
position: relative;
flex: 1;
min-width: 0;
color: #8e9298;
font-size: 32rpx;
font-size: 28rpx;
font-weight: 700;
line-height: 1.6;
text-align: center;
white-space: nowrap;
}
.report-tab--active {
@@ -817,6 +919,99 @@ onLoad(async (options) => {
font-weight: 800;
}
.report-statement {
position: relative;
z-index: 1;
margin-top: 32rpx;
padding-top: 10rpx;
color: #343840;
}
.report-statement__title {
font-size: 28rpx;
font-weight: 900;
line-height: 1.7;
}
.report-statement__item {
margin-top: 16rpx;
color: #3f444c;
font-size: 26rpx;
font-weight: 700;
line-height: 2;
word-break: break-all;
overflow-wrap: anywhere;
}
.report-panel--center {
padding-top: 28rpx;
}
.center-profile {
position: relative;
z-index: 1;
}
.center-profile__brand {
display: flex;
align-items: center;
gap: 18rpx;
min-width: 0;
}
.center-profile__logo {
flex: 0 0 auto;
display: block;
width: 72rpx;
height: 72rpx;
}
.center-profile__text {
flex: 1;
min-width: 0;
}
.center-profile__name {
color: #30333a;
font-size: 26rpx;
font-weight: 900;
line-height: 1.34;
word-break: break-all;
overflow-wrap: anywhere;
}
.center-profile__subtitle {
margin-top: 6rpx;
color: #66707c;
font-size: 18rpx;
font-weight: 700;
line-height: 1.25;
letter-spacing: 0;
word-break: break-all;
overflow-wrap: anywhere;
}
.center-profile__divider {
width: 100%;
height: 1px;
margin: 26rpx 0 24rpx;
background: #e5e5e5;
}
.center-profile__body {
display: grid;
gap: 18rpx;
}
.center-profile__paragraph {
color: #767b84;
font-size: 26rpx;
line-height: 1.82;
text-align: justify;
word-break: break-all;
overflow-wrap: anywhere;
}
.report-panel--trace {
padding-top: 2rpx;
}
@@ -964,34 +1159,27 @@ onLoad(async (options) => {
}
.report-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 28rpx;
display: block;
width: 100vw;
box-sizing: border-box;
padding: 22rpx 32rpx calc(22rpx + env(safe-area-inset-bottom));
background: rgba(241, 243, 246, 0.96);
padding: 0 0 env(safe-area-inset-bottom);
background: #ffffff;
border-top: 0;
box-shadow: 0 -10rpx 34rpx rgba(31, 36, 48, 0.05);
}
.report-actions .btn {
min-width: 0;
min-height: 76rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 700;
box-shadow: none;
}
.report-actions .btn--secondary {
border: 0;
background: #ffffff;
color: #4f535a;
.report-actions .btn {
width: 100%;
min-width: 0;
min-height: 112rpx;
border-radius: 0;
font-size: 32rpx;
font-weight: 800;
box-shadow: none;
}
.report-actions .btn--primary {
background: #dfb733;
background: #edbd00;
color: #ffffff;
}