Files
anxinyan/admin-web/src/pages/reports/index.vue
2026-06-05 16:12:56 +08:00

1237 lines
47 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import QRCode from "qrcode";
import {
adminApi,
type AdminManualInspectionPayload,
type AdminReportDetail,
type AdminReportListItem,
} from "../../api/admin";
import OrderStatusTag from "../../components/OrderStatusTag.vue";
function createInspectionPayload(): AdminManualInspectionPayload {
return {
report_header: {
report_no: "",
report_title: "安心验检查单",
report_status: "pending_publish",
service_provider: "anxinyan",
institution_name: "安心验",
publish_time: "",
},
product_info: {
product_name: "",
category_name: "",
brand_name: "",
color: "",
size_spec: "",
serial_no: "",
},
result_info: {
result_status: "authentic",
result_text: "正品",
result_desc: "",
},
appraisal_info: {
appraiser_name: "",
reviewer_name: "",
appraisal_time: "",
},
valuation_info: {
condition_grade: "",
condition_desc: "",
valuation_min: "",
valuation_max: "",
valuation_desc: "",
},
risk_notice_text: "",
};
}
const loading = ref(false);
const detailLoading = ref(false);
const drawerVisible = ref(false);
const inspectionDrawerVisible = ref(false);
const inspectionSubmitting = ref(false);
const publishingId = ref<number | null>(null);
const rejectingId = ref<number | null>(null);
const traceVisibilitySavingId = ref<number | null>(null);
const detailQrDataUrl = ref("");
const keyword = ref("");
const serviceProvider = ref("");
const reportStatus = ref("");
const reports = ref<AdminReportListItem[]>([]);
const detail = ref<AdminReportDetail | null>(null);
const inspectionForm = ref<AdminManualInspectionPayload>(createInspectionPayload());
const route = useRoute();
const canPublishCurrentReport = computed(() => detail.value?.report_header.report_status === "pending_publish");
const canRejectCurrentReport = computed(
() => detail.value?.report_header.report_type !== "inspection" && detail.value?.report_header.report_status === "pending_publish",
);
const canEditCurrentInspection = computed(
() => detail.value?.report_header.report_type === "inspection" && detail.value?.report_header.report_status !== "published",
);
const inspectionDrawerTitle = computed(() => (inspectionForm.value.id ? "编辑补录检查单" : "补录检查单"));
const providerOptions = [
{ label: "全部服务", value: "" },
{ label: "实物鉴定", value: "anxinyan" },
{ label: "中检鉴定", value: "zhongjian" },
];
const statusOptions = [
{ label: "全部状态", value: "" },
{ label: "已发布", value: "published" },
{ label: "待发布", value: "pending_publish" },
{ label: "已驳回", value: "rejected" },
{ label: "草稿中", value: "draft" },
{ label: "已更新", value: "updated" },
{ label: "已作废", value: "invalid" },
];
const inspectionStatusOptions = [
{ label: "草稿保存", value: "draft" },
{ label: "待发布", value: "pending_publish" },
{ label: "直接发布", value: "published" },
];
const resultOptions = [
{ label: "正品", value: "authentic", text: "正品" },
{ label: "存疑", value: "uncertain", text: "存疑" },
{ label: "非正品", value: "not_authentic", text: "非正品" },
];
function applyProviderPreset(force = false) {
const provider = inspectionForm.value.report_header.service_provider;
const title = provider === "zhongjian" ? "中检检查单" : "安心验检查单";
const institution = provider === "zhongjian" ? "中检合作机构" : "安心验";
if (force || !inspectionForm.value.report_header.report_title) {
inspectionForm.value.report_header.report_title = title;
}
if (force || !inspectionForm.value.report_header.institution_name) {
inspectionForm.value.report_header.institution_name = institution;
}
}
function syncResultText() {
const matched = resultOptions.find((item) => item.value === inspectionForm.value.result_info.result_status);
if (matched && !inspectionForm.value.result_info.result_text) {
inspectionForm.value.result_info.result_text = matched.text;
}
}
function previewEvidence(url: string) {
if (!url) return;
window.open(url, "_blank", "noopener,noreferrer");
}
function evidenceTypeLabel(fileType?: string) {
return fileType === "image" ? "图片" : fileType === "video" ? "视频" : fileType === "pdf" ? "PDF" : "附件";
}
const imageEvidenceList = computed(() =>
(detail.value?.evidence_attachments || []).filter((item) => item.file_type === "image"),
);
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();
applyProviderPreset(true);
syncResultText();
inspectionDrawerVisible.value = true;
}
function openInspectionEditFromDetail() {
if (!detail.value) return;
inspectionForm.value = {
id: detail.value.report_header.id,
report_header: {
report_no: detail.value.report_header.report_no,
report_title: detail.value.report_header.report_title,
report_status: detail.value.report_header.report_status,
service_provider: detail.value.report_header.service_provider,
institution_name: detail.value.report_header.institution_name,
publish_time: detail.value.report_header.publish_time || "",
},
product_info: {
product_name: detail.value.product_info.product_name || "",
category_name: detail.value.product_info.category_name || "",
brand_name: detail.value.product_info.brand_name || "",
color: detail.value.product_info.color || "",
size_spec: detail.value.product_info.size_spec || "",
serial_no: detail.value.product_info.serial_no || "",
},
result_info: {
result_status: detail.value.result_info.result_status || "authentic",
result_text: detail.value.result_info.result_text || "",
result_desc: detail.value.result_info.result_desc || "",
},
appraisal_info: {
appraiser_name: detail.value.appraisal_info.appraiser_name || "",
reviewer_name: detail.value.appraisal_info.reviewer_name || "",
appraisal_time: detail.value.appraisal_info.appraisal_time || "",
},
valuation_info: {
condition_grade: detail.value.valuation_info.condition_grade || "",
condition_desc: detail.value.valuation_info.condition_desc || "",
valuation_min: detail.value.valuation_info.valuation_min ?? "",
valuation_max: detail.value.valuation_info.valuation_max ?? "",
valuation_desc: detail.value.valuation_info.valuation_desc || "",
},
risk_notice_text: detail.value.risk_notice_text || "",
};
inspectionDrawerVisible.value = true;
}
async function syncQrCode(url: string) {
if (!/^https?:\/\//i.test(url)) {
detailQrDataUrl.value = "";
return;
}
try {
detailQrDataUrl.value = await QRCode.toDataURL(url, {
width: 220,
margin: 1,
});
} catch (error) {
console.error(error);
detailQrDataUrl.value = "";
}
}
async function fetchReports() {
loading.value = true;
try {
const response = await adminApi.getReports({
keyword: keyword.value,
service_provider: serviceProvider.value,
status: reportStatus.value,
});
if (response.code !== 0) {
ElMessage.error(response.message || "报告列表加载失败");
return;
}
reports.value = response.data.list;
} catch (error) {
console.error(error);
ElMessage.error("报告列表加载失败");
} finally {
loading.value = false;
}
}
async function loadDetail(id: number) {
detailLoading.value = true;
detailQrDataUrl.value = "";
try {
const response = await adminApi.getReportDetail(id);
if (response.code !== 0) {
ElMessage.error(response.message || "报告详情加载失败");
return;
}
detail.value = response.data;
await syncQrCode(response.data.verify_info.verify_qrcode_url || response.data.verify_info.report_page_url || "");
} catch (error) {
console.error(error);
ElMessage.error("报告详情加载失败");
} finally {
detailLoading.value = false;
}
}
async function openDetail(row: AdminReportListItem) {
drawerVisible.value = true;
await loadDetail(row.id);
}
function parseReportId(value: unknown) {
const raw = Array.isArray(value) ? value[0] : value;
const id = Number(raw || 0);
return Number.isInteger(id) && id > 0 ? id : 0;
}
async function openDetailFromRouteQuery() {
const reportId = parseReportId(route.query.report_id);
if (!reportId) {
return;
}
if (drawerVisible.value && detail.value?.report_header.id === reportId) {
return;
}
drawerVisible.value = true;
await loadDetail(reportId);
}
type PublishReportTarget = Pick<AdminReportListItem, "id" | "report_status" | "report_type" | "material_tag_bound"> | {
id: number;
report_status: string;
report_type: string;
material_tag_bound: boolean;
};
type RejectReportTarget = Pick<AdminReportListItem, "id" | "report_status" | "report_type"> | {
id: number;
report_status: string;
report_type: string;
};
type ReportTraceVisibilityTarget = Pick<AdminReportListItem, "id" | "trace_info_visible">;
async function promptReportMaterialTagInput() {
try {
const result = await ElMessageBox.prompt("是否已鉴定完成并确定发布报告?", "绑定验真吊牌并发布报告", {
type: "warning",
inputPlaceholder: "请扫描验真吊牌二维码",
inputPattern: /\S+/,
inputErrorMessage: "请扫描验真吊牌二维码",
confirmButtonText: "是的,去绑定验真吊牌",
cancelButtonText: "取消",
closeOnClickModal: false,
});
return String(result.value || "").trim();
} catch {
return "";
}
}
async function publishReport(row: PublishReportTarget) {
if (row.report_status !== "pending_publish") {
ElMessage.warning("仅待发布报告可以执行发布");
return;
}
const needMaterialTag = row.report_type !== "inspection" && !row.material_tag_bound;
let qrInput = "";
if (needMaterialTag) {
qrInput = await promptReportMaterialTagInput();
if (!qrInput) {
return;
}
} else {
try {
await ElMessageBox.confirm("发布后用户端将可查看正式报告并进行验真,是否继续?", "发布报告", {
type: "warning",
confirmButtonText: "确认发布",
cancelButtonText: "取消",
});
} catch {
return;
}
}
publishingId.value = row.id;
try {
const response = await adminApi.publishReport(row.id, qrInput);
if (response.code !== 0) {
ElMessage.error(response.message || "报告发布失败");
return;
}
ElMessage.success(response.message || "报告已发布");
await fetchReports();
if (drawerVisible.value && detail.value?.report_header.id === row.id) {
await loadDetail(row.id);
}
} catch (error) {
console.error(error);
ElMessage.error("报告发布失败");
} finally {
publishingId.value = null;
}
}
async function rejectReport(row: RejectReportTarget) {
if (row.report_status !== "pending_publish") {
ElMessage.warning("仅待发布报告可以驳回");
return;
}
if (row.report_type === "inspection") {
ElMessage.warning("补录检查单不支持驳回复鉴");
return;
}
let reason = "";
try {
const result = await ElMessageBox.prompt("请填写驳回原因,鉴定师将据此重新鉴定。", "驳回报告", {
type: "warning",
inputType: "textarea",
inputPlaceholder: "例如:结论说明不完整、图片附件不清晰",
inputPattern: /\S+/,
inputErrorMessage: "请填写驳回原因",
confirmButtonText: "确认驳回",
cancelButtonText: "取消",
closeOnClickModal: false,
});
reason = String(result.value || "").trim();
} catch {
return;
}
rejectingId.value = row.id;
try {
const response = await adminApi.rejectReport(row.id, reason);
if (response.code !== 0) {
ElMessage.error(response.message || "报告驳回失败");
return;
}
ElMessage.success(response.message || "报告已驳回");
await fetchReports();
if (drawerVisible.value && detail.value?.report_header.id === row.id) {
await loadDetail(row.id);
}
} catch (error: any) {
console.error(error);
ElMessage.error(error?.message || "报告驳回失败");
} finally {
rejectingId.value = null;
}
}
function logSummary(data: Record<string, any>) {
const status = data.report_status || "-";
const version = data.report_version ? `v${data.report_version}` : "";
const publishTime = data.publish_time ? ` · ${data.publish_time}` : "";
const rejectReason = data.reject_reason || data.invalid_reason ? ` · ${data.reject_reason || data.invalid_reason}` : "";
return [status, version].filter(Boolean).join(" / ") + publishTime + rejectReason;
}
function switchValueToBoolean(value: unknown) {
if (typeof value === "boolean") return value;
if (typeof value === "number") return value === 1;
return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
}
async function updateReportTraceVisibility(row: ReportTraceVisibilityTarget, value: unknown) {
const visible = switchValueToBoolean(value);
traceVisibilitySavingId.value = row.id;
try {
const response = await adminApi.updateReportTraceVisibility(row.id, visible);
if (response.code !== 0) {
ElMessage.error(response.message || "追溯信息开关保存失败");
return;
}
const appliedVisible = Boolean(response.data.trace_info_visible);
row.trace_info_visible = appliedVisible;
const listItem = reports.value.find((item) => item.id === row.id);
if (listItem) {
listItem.trace_info_visible = appliedVisible;
}
if (detail.value?.report_header.id === row.id) {
detail.value.report_header.trace_info_visible = appliedVisible;
}
ElMessage.success(response.message || (appliedVisible ? "追溯信息已设为显示" : "追溯信息已隐藏"));
} catch (error) {
console.error(error);
ElMessage.error("追溯信息开关保存失败");
} finally {
traceVisibilitySavingId.value = null;
}
}
function handleTraceVisibilityChange(row: ReportTraceVisibilityTarget, value: unknown) {
void updateReportTraceVisibility(row, value);
}
function validateInspectionForm() {
const { report_header, product_info, result_info } = inspectionForm.value;
if (!report_header.report_title.trim()) {
ElMessage.warning("请填写检查单标题");
return false;
}
if (!report_header.institution_name.trim()) {
ElMessage.warning("请填写出具机构");
return false;
}
if (!product_info.product_name.trim()) {
ElMessage.warning("请填写商品名称");
return false;
}
if (!result_info.result_text.trim()) {
ElMessage.warning("请填写鉴定结论");
return false;
}
return true;
}
async function saveInspection() {
if (!validateInspectionForm()) {
return;
}
inspectionSubmitting.value = true;
try {
const response = await adminApi.saveInspectionReport(inspectionForm.value);
if (response.code !== 0) {
ElMessage.error(response.message || "检查单保存失败");
return;
}
ElMessage.success(response.message || "检查单已保存");
inspectionDrawerVisible.value = false;
await fetchReports();
drawerVisible.value = true;
await loadDetail(response.data.id);
} catch (error) {
console.error(error);
ElMessage.error("检查单保存失败");
} finally {
inspectionSubmitting.value = false;
}
}
async function copyText(value: string, label: string) {
if (!value) {
ElMessage.warning(`${label}为空`);
return;
}
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
} else {
const input = document.createElement("textarea");
input.value = value;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
}
ElMessage.success(`${label}已复制`);
} catch (error) {
console.error(error);
ElMessage.error(`${label}复制失败`);
}
}
onMounted(() => {
applyProviderPreset(true);
syncResultText();
fetchReports();
openDetailFromRouteQuery();
});
watch(
() => route.query.report_id,
() => {
openDetailFromRouteQuery();
},
);
</script>
<template>
<el-card class="panel-card" shadow="never">
<div class="filters-row" style="justify-content: space-between;">
<div class="filters-row">
<el-input v-model="keyword" placeholder="搜索报告编号 / 鉴定单号 / 订单号 / 商品名称" clearable style="width: 340px" />
<el-select v-model="serviceProvider" placeholder="服务类型" style="width: 160px">
<el-option v-for="item in providerOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="reportStatus" placeholder="报告状态" style="width: 160px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-button type="primary" @click="fetchReports">查询</el-button>
</div>
<el-button type="primary" plain @click="openInspectionCreate">补录检查单</el-button>
</div>
</el-card>
<el-card class="panel-card orders-table" shadow="never">
<el-table v-loading="loading" :data="reports" stripe>
<el-table-column prop="report_no" label="报告编号" min-width="180" />
<el-table-column prop="appraisal_no" label="鉴定单号" min-width="180" />
<el-table-column prop="report_type_text" label="类型" min-width="120" />
<el-table-column prop="report_title" label="报告标题" min-width="180" />
<el-table-column prop="product_name" label="商品名称" min-width="220" />
<el-table-column prop="service_provider_text" label="服务类型" min-width="120" />
<el-table-column label="报告状态" min-width="120">
<template #default="{ row }">
<OrderStatusTag :status="row.report_status_text" />
</template>
</el-table-column>
<el-table-column label="验真吊牌" min-width="120">
<template #default="{ row }">
<OrderStatusTag v-if="row.report_type !== 'inspection'" :status="row.material_tag_bound ? '已绑定' : '未绑定'" />
<span v-else class="detail-label">不适用</span>
</template>
</el-table-column>
<el-table-column label="追溯信息" min-width="130">
<template #default="{ row }">
<el-switch
:model-value="row.trace_info_visible"
:loading="traceVisibilitySavingId === row.id"
inline-prompt
active-text="显示"
inactive-text="隐藏"
@change="handleTraceVisibilityChange(row, $event)"
/>
</template>
</el-table-column>
<el-table-column prop="institution_name" label="出具机构" min-width="160" />
<el-table-column prop="publish_time" label="发布时间" min-width="170" />
<el-table-column label="操作" fixed="right" width="280">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">查看详情</el-button>
<el-button
v-if="row.report_type === 'inspection' && row.report_status !== 'published'"
link
type="success"
@click="openDetail(row).then(() => openInspectionEditFromDetail())"
>
编辑检查单
</el-button>
<el-button
v-if="row.report_type !== 'inspection' && row.report_status === 'pending_publish'"
link
type="warning"
:loading="publishingId === row.id"
@click="publishReport(row)"
>
发布报告
</el-button>
<el-button
v-if="row.report_type !== 'inspection' && row.report_status === 'pending_publish'"
link
type="danger"
:loading="rejectingId === row.id"
@click="rejectReport(row)"
>
驳回报告
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-drawer v-model="drawerVisible" size="62%" title="报告详情">
<div v-loading="detailLoading" v-if="detail" class="detail-grid">
<div style="grid-column: 1 / -1; display: flex; justify-content: flex-end; gap: 12px; margin-bottom: 8px">
<el-button v-if="canEditCurrentInspection" type="success" plain @click="openInspectionEditFromDetail">
编辑检查单
</el-button>
<el-button
v-if="canPublishCurrentReport"
type="primary"
:loading="publishingId === detail.report_header.id"
@click="publishReport({
id: detail.report_header.id,
report_status: detail.report_header.report_status,
report_type: detail.report_header.report_type,
material_tag_bound: Boolean(detail.material_tag),
})"
>
发布报告
</el-button>
<el-button
v-if="canRejectCurrentReport"
type="danger"
plain
:loading="rejectingId === detail.report_header.id"
@click="rejectReport({
id: detail.report_header.id,
report_status: detail.report_header.report_status,
report_type: detail.report_header.report_type,
})"
>
驳回报告
</el-button>
</div>
<div class="detail-card">
<div class="detail-card__title">报告概览</div>
<div class="detail-card__desc">
<div class="detail-label">报告编号</div>
<div class="detail-value">{{ detail.report_header.report_no }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">报告类型</div>
<div class="detail-value">{{ detail.report_header.report_type_text }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">报告标题</div>
<div class="detail-value">{{ detail.report_header.report_title }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">报告状态</div>
<div class="detail-value">
<OrderStatusTag :status="detail.report_header.report_status_text" />
</div>
</div>
<div v-if="detail.report_header.reject_reason" class="detail-card__desc">
<div class="detail-label">驳回原因</div>
<div class="detail-value">{{ detail.report_header.reject_reason }}</div>
</div>
<div v-if="detail.report_header.rejected_at" class="detail-card__desc">
<div class="detail-label">驳回信息</div>
<div class="detail-value">{{ detail.report_header.rejected_by_name || "-" }} / {{ detail.report_header.rejected_at }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">出具机构</div>
<div class="detail-value">{{ detail.report_header.institution_name }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">追溯信息</div>
<div class="detail-value report-visibility-control">
<el-switch
:model-value="detail.report_header.trace_info_visible"
:loading="traceVisibilitySavingId === detail.report_header.id"
inline-prompt
active-text="显示"
inactive-text="隐藏"
@change="handleTraceVisibilityChange(detail.report_header, $event)"
/>
<span>{{ detail.report_header.trace_info_visible ? "用户端显示追溯信息 tab" : "用户端隐藏追溯信息 tab" }}</span>
</div>
</div>
</div>
<div class="detail-card">
<div class="detail-card__title">验真吊牌</div>
<template v-if="detail.report_header.report_type === 'inspection'">
<div class="detail-card__desc">
<div class="detail-value">补录检查单不需要绑定验真吊牌</div>
</div>
</template>
<template v-else-if="detail.material_tag">
<div class="detail-card__desc">
<div class="detail-label">二维码链接</div>
<div class="detail-value" style="word-break: break-all;">{{ detail.material_tag.qr_url }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">验真编码</div>
<div class="detail-value">{{ detail.material_tag.verify_code }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">绑定时间</div>
<div class="detail-value">{{ detail.material_tag.bound_at || "-" }}</div>
</div>
</template>
<div v-else class="detail-card__desc">
<div class="detail-value">未绑定发布前需要扫描验真吊牌</div>
</div>
</div>
<div class="detail-card detail-card--wide">
<div class="detail-card__title">审核记录</div>
<div v-if="detail.audit_logs.length" class="report-log-list">
<div v-for="log in detail.audit_logs" :key="`audit-${log.id}`" class="report-log-item">
<div class="report-log-main">
<span>{{ log.action_text }}</span>
<span>{{ log.operator_name || "-" }}</span>
<span>{{ log.created_at }}</span>
</div>
<div v-if="log.remark" class="report-log-remark">{{ log.remark }}</div>
<div class="report-log-snapshot">{{ logSummary(log.after_data) }}</div>
</div>
</div>
<div v-else class="detail-card__desc">
<div class="detail-value">暂无审核记录</div>
</div>
</div>
<div class="detail-card detail-card--wide">
<div class="detail-card__title">修改记录</div>
<div v-if="detail.change_logs.length" class="report-log-list">
<div v-for="log in detail.change_logs" :key="`change-${log.id}`" class="report-log-item">
<div class="report-log-main">
<span>{{ log.action_text }}</span>
<span>{{ log.operator_name || "-" }}</span>
<span>{{ log.created_at }}</span>
</div>
<div v-if="log.remark" class="report-log-remark">{{ log.remark }}</div>
<div class="report-log-snapshot">
{{ logSummary(log.before_data) }} -> {{ logSummary(log.after_data) }}
</div>
</div>
</div>
<div v-else class="detail-card__desc">
<div class="detail-value">暂无修改记录</div>
</div>
</div>
<div class="detail-card">
<div class="detail-card__title">商品信息</div>
<div class="detail-card__desc">
<div class="detail-label">商品名称</div>
<div class="detail-value">{{ detail.product_info.product_name || "-" }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">品类 / 品牌</div>
<div class="detail-value">{{ detail.product_info.category_name || "-" }} / {{ detail.product_info.brand_name || "-" }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">颜色 / 规格</div>
<div class="detail-value">{{ detail.product_info.color || "-" }} / {{ detail.product_info.size_spec || "-" }}</div>
</div>
</div>
<div class="detail-card">
<div class="detail-card__title">鉴定结果</div>
<div class="detail-card__desc">
<div class="detail-label">结论</div>
<div class="detail-value">{{ detail.result_info.result_text || "-" }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">说明</div>
<div class="detail-value">{{ detail.result_info.result_desc || "-" }}</div>
</div>
<template v-if="detail.result_info.key_points?.length">
<div v-for="(item, index) in detail.result_info.key_points" :key="`${item.point_code || item.point_name}-${index}`" class="detail-card__desc">
<div class="detail-label">{{ item.point_name || "鉴定项" }}</div>
<div class="detail-value">{{ item.point_value || "-" }}</div>
</div>
</template>
<div v-if="detail.result_info.external_remark" class="detail-card__desc">
<div class="detail-label">对外备注</div>
<div class="detail-value">{{ detail.result_info.external_remark }}</div>
</div>
</div>
<div class="detail-card">
<div class="detail-card__title">鉴定信息</div>
<div class="detail-card__desc">
<div class="detail-label">服务类型</div>
<div class="detail-value">{{ detail.report_header.service_provider_text }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">鉴定师</div>
<div class="detail-value">{{ detail.appraisal_info.appraiser_name || "-" }}</div>
</div>
<div class="detail-card__desc">
<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">
<div class="detail-card__title">评级与估值</div>
<div class="detail-card__desc">
<div class="detail-label">成色评级</div>
<div class="detail-value">{{ detail.valuation_info.condition_grade || "-" }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">成色说明</div>
<div class="detail-value">{{ detail.valuation_info.condition_desc || "-" }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">估值区间</div>
<div class="detail-value">¥{{ detail.valuation_info.valuation_min || 0 }} - ¥{{ detail.valuation_info.valuation_max || 0 }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">估值说明</div>
<div class="detail-value">{{ detail.valuation_info.valuation_desc || "-" }}</div>
</div>
</div>
<div class="detail-card" style="grid-column: 1 / -1">
<div class="detail-card__title">证据附件</div>
<div v-if="detail.evidence_attachments.length" class="report-evidence-stack">
<div v-if="imageEvidenceList.length" class="report-evidence-section">
<div class="report-evidence-section__title">图片证据</div>
<div class="report-evidence-gallery">
<div
v-for="attachment in imageEvidenceList"
: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="fileEvidenceList.length" class="report-evidence-section">
<div class="report-evidence-section__title">视频 / 文档证据</div>
<div class="report-evidence-list">
<div v-for="attachment in fileEvidenceList" :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 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 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
style="width: 220px; height: 220px; border-radius: 16px; border: 1px dashed var(--admin-border); display: flex; align-items: center; justify-content: center; overflow: hidden; background: #fff;"
>
<el-image v-if="detailQrDataUrl" :src="detailQrDataUrl" fit="contain" style="width: 200px; height: 200px" />
<div v-else style="padding: 16px; text-align: center; color: var(--admin-text-subtle); line-height: 1.7;">
请先在系统配置中填写 H5 页面根地址再生成可扫码的公开链接
</div>
</div>
<div style="display: grid; gap: 14px;">
<div class="detail-card__desc" style="margin: 0;">
<div class="detail-label">扫码打开报告页</div>
<div class="detail-value" style="word-break: break-all;">{{ detail.verify_info.verify_qrcode_url || "-" }}</div>
<el-button size="small" style="margin-top: 8px" @click="copyText(detail.verify_info.verify_qrcode_url, '报告链接')">复制报告链接</el-button>
</div>
<div class="detail-card__desc" style="margin: 0;">
<div class="detail-label">H5 验真页</div>
<div class="detail-value" style="word-break: break-all;">{{ detail.verify_info.verify_url || "-" }}</div>
<el-button size="small" style="margin-top: 8px" @click="copyText(detail.verify_info.verify_url, '验真链接')">复制验真链接</el-button>
</div>
<div class="detail-card__desc" style="margin: 0;">
<div class="detail-label">验真状态 / 次数</div>
<div class="detail-value">{{ detail.verify_info.verify_status }} / {{ detail.verify_info.verify_count }}</div>
</div>
</div>
</div>
</div>
<div class="detail-card" style="grid-column: 1 / -1">
<div class="detail-card__title">风险说明</div>
<div class="detail-card__desc">
<div class="detail-value">{{ detail.risk_notice_text || "-" }}</div>
</div>
</div>
</div>
</el-drawer>
<el-drawer v-model="inspectionDrawerVisible" size="56%" :title="inspectionDrawerTitle">
<div style="display: grid; gap: 24px;">
<el-card shadow="never">
<template #header>基础信息</template>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="检查单编号">
<el-input v-model="inspectionForm.report_header.report_no" placeholder="可留空,系统自动生成" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="检查单标题">
<el-input v-model="inspectionForm.report_header.report_title" placeholder="请输入检查单标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="服务类型">
<el-select v-model="inspectionForm.report_header.service_provider" style="width: 100%" @change="applyProviderPreset()">
<el-option label="实物鉴定" value="anxinyan" />
<el-option label="中检鉴定" value="zhongjian" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="保存状态">
<el-select v-model="inspectionForm.report_header.report_status" style="width: 100%">
<el-option v-for="item in inspectionStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出具机构">
<el-input v-model="inspectionForm.report_header.institution_name" placeholder="请输入出具机构" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发布时间">
<el-date-picker
v-model="inspectionForm.report_header.publish_time"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="直接发布时可指定发布时间"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-card>
<el-card shadow="never">
<template #header>商品信息</template>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="商品名称"><el-input v-model="inspectionForm.product_info.product_name" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="品类"><el-input v-model="inspectionForm.product_info.category_name" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="品牌"><el-input v-model="inspectionForm.product_info.brand_name" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="颜色"><el-input v-model="inspectionForm.product_info.color" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="规格 / 尺寸"><el-input v-model="inspectionForm.product_info.size_spec" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="序列号 / 编码"><el-input v-model="inspectionForm.product_info.serial_no" /></el-form-item></el-col>
</el-row>
</el-card>
<el-card shadow="never">
<template #header>鉴定结果</template>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="结果类型">
<el-select v-model="inspectionForm.result_info.result_status" style="width: 100%" @change="syncResultText">
<el-option v-for="item in resultOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结果文案">
<el-input v-model="inspectionForm.result_info.result_text" placeholder="例如:正品 / 存疑 / 非正品" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="结果说明">
<el-input v-model="inspectionForm.result_info.result_desc" type="textarea" :rows="4" placeholder="请输入检查结论说明" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<el-card shadow="never">
<template #header>鉴定与估值信息</template>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="鉴定师"><el-input v-model="inspectionForm.appraisal_info.appraiser_name" /></el-form-item></el-col>
<el-col :span="12">
<el-form-item label="鉴定时间">
<el-date-picker
v-model="inspectionForm.appraisal_info.appraisal_time"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12"><el-form-item label="成色评级"><el-input v-model="inspectionForm.valuation_info.condition_grade" placeholder="例如 A / B+" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="最低估值"><el-input v-model="inspectionForm.valuation_info.valuation_min" type="number" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="最高估值"><el-input v-model="inspectionForm.valuation_info.valuation_max" type="number" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="成色说明"><el-input v-model="inspectionForm.valuation_info.condition_desc" type="textarea" :rows="3" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="估值说明"><el-input v-model="inspectionForm.valuation_info.valuation_desc" type="textarea" :rows="3" /></el-form-item></el-col>
</el-row>
</el-card>
<el-card shadow="never">
<template #header>风险说明</template>
<el-form-item label="页面说明文案">
<el-input v-model="inspectionForm.risk_notice_text" type="textarea" :rows="4" placeholder="请输入风险提示与适用说明" />
</el-form-item>
</el-card>
<div style="display: flex; justify-content: flex-end; gap: 12px;">
<el-button @click="inspectionDrawerVisible = false">取消</el-button>
<el-button type="primary" :loading="inspectionSubmitting" @click="saveInspection">保存检查单</el-button>
</div>
</div>
</el-drawer>
</template>
<style scoped>
.report-evidence-stack {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 14px;
}
.report-evidence-section__title {
color: var(--admin-text-main);
font-size: 14px;
font-weight: 700;
}
.report-evidence-gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
margin-top: 12px;
}
.report-evidence-gallery__item {
border-radius: 16px;
overflow: hidden;
border: 1px solid #efe8d9;
background: #fcfaf5;
cursor: pointer;
}
.report-evidence-gallery__item img {
width: 100%;
height: 180px;
object-fit: cover;
display: block;
}
.report-evidence-gallery__caption {
padding: 10px 12px;
color: var(--admin-text-main);
font-size: 13px;
font-weight: 600;
line-height: 1.5;
word-break: break-word;
}
.report-evidence-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 14px;
margin-top: 12px;
}
.report-evidence-card {
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
gap: 14px;
padding: 14px;
border-radius: 16px;
background: #fcfaf5;
border: 1px solid #efe8d9;
}
.report-evidence-card__preview {
width: 96px;
height: 96px;
border-radius: 14px;
border: 1px solid #efe8d9;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
cursor: pointer;
}
.report-evidence-card__preview img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.report-evidence-card__filetype {
color: var(--admin-progress);
font-size: 13px;
font-weight: 700;
}
.report-evidence-card__body {
min-width: 0;
}
.report-visibility-control {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.report-visibility-control span {
color: var(--admin-text-subtle);
font-size: 13px;
font-weight: 500;
}
.detail-card--wide {
grid-column: 1 / -1;
}
.report-log-list {
display: grid;
gap: 10px;
}
.report-log-item {
display: grid;
gap: 6px;
padding: 12px;
border: 1px solid var(--admin-border);
border-radius: 8px;
background: #fffdfa;
}
.report-log-main {
display: flex;
flex-wrap: wrap;
gap: 12px;
color: var(--admin-text-main);
font-size: 13px;
font-weight: 700;
}
.report-log-remark,
.report-log-snapshot {
color: var(--admin-text-subtle);
font-size: 13px;
line-height: 1.5;
word-break: break-word;
}
</style>