feat: update report detail and verification flow
This commit is contained in:
@@ -1790,7 +1790,7 @@ export const adminApi = {
|
|||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
},
|
},
|
||||||
saveZhongjianAppraisalReport(data: { id: number; zhongjian_report_no: string; report_files: AdminFileAsset[]; qr_input: string }) {
|
saveZhongjianAppraisalReport(data: Record<string, unknown>) {
|
||||||
return request.post("/api/admin/appraisal-task/zhongjian-report/save", data) as Promise<{
|
return request.post("/api/admin/appraisal-task/zhongjian-report/save", data) as Promise<{
|
||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
|
|||||||
@@ -862,6 +862,20 @@ async function submitZhongjianReport() {
|
|||||||
ElMessage.warning("请填写中检报告编号");
|
ElMessage.warning("请填写中检报告编号");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!resultForm.result_text.trim()) {
|
||||||
|
ElMessage.warning("请填写鉴定结论");
|
||||||
|
activeWorkTab.value = "result";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasProductFormValue()) {
|
||||||
|
ElMessage.warning("提交前请先完善物品信息");
|
||||||
|
activeWorkTab.value = "result";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!validateRequiredKeyPoints()) {
|
||||||
|
activeWorkTab.value = "result";
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!zhongjianReportFiles.value.length) {
|
if (!zhongjianReportFiles.value.length) {
|
||||||
ElMessage.warning("请至少上传 1 个中检报告文件");
|
ElMessage.warning("请至少上传 1 个中检报告文件");
|
||||||
return;
|
return;
|
||||||
@@ -876,6 +890,11 @@ async function submitZhongjianReport() {
|
|||||||
const response = await adminApi.saveZhongjianAppraisalReport({
|
const response = await adminApi.saveZhongjianAppraisalReport({
|
||||||
id: detail.value.task_info.id,
|
id: detail.value.task_info.id,
|
||||||
zhongjian_report_no: zhongjianReportNo.value.trim(),
|
zhongjian_report_no: zhongjianReportNo.value.trim(),
|
||||||
|
product_info: normalizedProductForm(),
|
||||||
|
result_text: resultForm.result_text.trim(),
|
||||||
|
result_desc: resultForm.result_desc.trim(),
|
||||||
|
attachments: resultAttachments.value,
|
||||||
|
key_points: normalizedKeyPoints(),
|
||||||
report_files: zhongjianReportFiles.value,
|
report_files: zhongjianReportFiles.value,
|
||||||
qr_input: qrInput,
|
qr_input: qrInput,
|
||||||
});
|
});
|
||||||
@@ -1651,7 +1670,7 @@ onMounted(async () => {
|
|||||||
<el-tab-pane v-if="isZhongjianTask" label="中检报告录入" name="zhongjian">
|
<el-tab-pane v-if="isZhongjianTask" label="中检报告录入" name="zhongjian">
|
||||||
<div :key="`zhongjian-${formRenderKey}`" class="task-form-stack">
|
<div :key="`zhongjian-${formRenderKey}`" class="task-form-stack">
|
||||||
<el-alert
|
<el-alert
|
||||||
title="提交中检报告编号和文件后,需要扫描平台验真吊牌;绑定成功后才会发布报告。"
|
title="请先在“填写结论”中补全物品信息、鉴定结论和模板项,再提交中检报告编号和文件;绑定吊牌成功后才会发布报告。"
|
||||||
type="info"
|
type="info"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
|
|||||||
@@ -461,12 +461,21 @@ class AppraisalTasksController
|
|||||||
$reportNo = trim((string)$request->input('zhongjian_report_no', ''));
|
$reportNo = trim((string)$request->input('zhongjian_report_no', ''));
|
||||||
$qrInput = trim((string)$request->input('qr_input', ''));
|
$qrInput = trim((string)$request->input('qr_input', ''));
|
||||||
$files = $this->evidenceService()->normalize($request->input('report_files', []), $request, true);
|
$files = $this->evidenceService()->normalize($request->input('report_files', []), $request, true);
|
||||||
|
$productInput = $request->input('product_info', null);
|
||||||
|
$productPayload = is_array($productInput) ? $this->normalizeProductInput($productInput) : null;
|
||||||
|
$attachments = $this->evidenceService()->normalize($request->input('attachments', []), $request, true);
|
||||||
|
$keyPoints = $this->normalizeKeyPointInput($request->input('key_points', []));
|
||||||
|
$resultText = trim((string)$request->input('result_text', ''));
|
||||||
|
$resultDesc = trim((string)$request->input('result_desc', ''));
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
return api_error('任务 ID 不能为空', 422);
|
return api_error('任务 ID 不能为空', 422);
|
||||||
}
|
}
|
||||||
if ($reportNo === '') {
|
if ($reportNo === '') {
|
||||||
return api_error('中检报告编号不能为空', 422);
|
return api_error('中检报告编号不能为空', 422);
|
||||||
}
|
}
|
||||||
|
if ($resultText === '') {
|
||||||
|
return api_error('鉴定结论不能为空', 422);
|
||||||
|
}
|
||||||
if (!$files) {
|
if (!$files) {
|
||||||
return api_error('请至少上传 1 个中检报告文件', 422);
|
return api_error('请至少上传 1 个中检报告文件', 422);
|
||||||
}
|
}
|
||||||
@@ -514,6 +523,15 @@ class AppraisalTasksController
|
|||||||
$task = array_merge($task, $operatorGuard['task_update']);
|
$task = array_merge($task, $operatorGuard['task_update']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($productPayload !== null) {
|
||||||
|
$this->saveOrderProductSnapshot((int)$task['order_id'], $productPayload, $now);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hasSubmittableProductInfo((int)$task['order_id'], $productPayload)) {
|
||||||
|
Db::rollback();
|
||||||
|
return api_error('提交中检报告前请先完善物品信息', 422);
|
||||||
|
}
|
||||||
|
|
||||||
Db::name('appraisal_tasks')->where('id', $id)->update([
|
Db::name('appraisal_tasks')->where('id', $id)->update([
|
||||||
'status' => 'completed',
|
'status' => 'completed',
|
||||||
'started_at' => $task['started_at'] ?: $now,
|
'started_at' => $task['started_at'] ?: $now,
|
||||||
@@ -531,14 +549,14 @@ class AppraisalTasksController
|
|||||||
'task_id' => $id,
|
'task_id' => $id,
|
||||||
'order_id' => (int)$task['order_id'],
|
'order_id' => (int)$task['order_id'],
|
||||||
'result_status' => 'zhongjian_report',
|
'result_status' => 'zhongjian_report',
|
||||||
'result_text' => '以中检报告为准',
|
'result_text' => $resultText,
|
||||||
'result_desc' => '中检报告已回传并由平台录入。',
|
'result_desc' => $resultDesc,
|
||||||
'condition_grade' => '',
|
'condition_grade' => '',
|
||||||
'condition_desc' => '',
|
'condition_desc' => '',
|
||||||
'valuation_min' => 0,
|
'valuation_min' => 0,
|
||||||
'valuation_max' => 0,
|
'valuation_max' => 0,
|
||||||
'valuation_desc' => '',
|
'valuation_desc' => '',
|
||||||
'attachments_json' => json_encode($files, JSON_UNESCAPED_UNICODE),
|
'attachments_json' => $attachments ? json_encode($attachments, JSON_UNESCAPED_UNICODE) : null,
|
||||||
'external_remark' => '',
|
'external_remark' => '',
|
||||||
'internal_remark' => '中检报告编号:' . $reportNo,
|
'internal_remark' => '中检报告编号:' . $reportNo,
|
||||||
'updated_at' => $now,
|
'updated_at' => $now,
|
||||||
@@ -546,10 +564,12 @@ class AppraisalTasksController
|
|||||||
$resultId = Db::name('appraisal_task_results')->where('task_id', $id)->value('id');
|
$resultId = Db::name('appraisal_task_results')->where('task_id', $id)->value('id');
|
||||||
if ($resultId) {
|
if ($resultId) {
|
||||||
Db::name('appraisal_task_results')->where('id', (int)$resultId)->update($resultPayload);
|
Db::name('appraisal_task_results')->where('id', (int)$resultId)->update($resultPayload);
|
||||||
|
$savedResultId = (int)$resultId;
|
||||||
} else {
|
} else {
|
||||||
$resultPayload['created_at'] = $now;
|
$resultPayload['created_at'] = $now;
|
||||||
Db::name('appraisal_task_results')->insert($resultPayload);
|
$savedResultId = (int)Db::name('appraisal_task_results')->insertGetId($resultPayload);
|
||||||
}
|
}
|
||||||
|
$this->saveTaskKeyPoints($savedResultId, $keyPoints, $now);
|
||||||
|
|
||||||
$this->createOrUpdateReportDraft((int)$task['order_id'], $task, $resultPayload, $now);
|
$this->createOrUpdateReportDraft((int)$task['order_id'], $task, $resultPayload, $now);
|
||||||
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
||||||
@@ -578,7 +598,7 @@ class AppraisalTasksController
|
|||||||
'order_id' => (int)$task['order_id'],
|
'order_id' => (int)$task['order_id'],
|
||||||
'node_code' => 'zhongjian_report_entered',
|
'node_code' => 'zhongjian_report_entered',
|
||||||
'node_text' => '中检报告已录入',
|
'node_text' => '中检报告已录入',
|
||||||
'node_desc' => '报告录入人已录入中检报告编号并上传报告文件。',
|
'node_desc' => '报告录入人已补全报告信息、录入中检报告编号并上传报告文件。',
|
||||||
'operator_type' => 'admin',
|
'operator_type' => 'admin',
|
||||||
'operator_id' => $operatorId,
|
'operator_id' => $operatorId,
|
||||||
'occurred_at' => $now,
|
'occurred_at' => $now,
|
||||||
@@ -1814,7 +1834,7 @@ class AppraisalTasksController
|
|||||||
'appraisal_no' => $order['appraisal_no'] ?? '',
|
'appraisal_no' => $order['appraisal_no'] ?? '',
|
||||||
'report_type' => 'appraisal',
|
'report_type' => 'appraisal',
|
||||||
'service_provider' => $task['service_provider'],
|
'service_provider' => $task['service_provider'],
|
||||||
'institution_name' => $task['service_provider'] === 'zhongjian' ? '中检合作机构' : '安心验',
|
'institution_name' => $this->displayInstitutionName((string)$task['service_provider']),
|
||||||
'report_title' => $task['service_provider'] === 'zhongjian' ? '中检鉴定报告' : '安心验鉴定报告',
|
'report_title' => $task['service_provider'] === 'zhongjian' ? '中检鉴定报告' : '安心验鉴定报告',
|
||||||
'report_status' => 'pending_publish',
|
'report_status' => 'pending_publish',
|
||||||
'publish_time' => null,
|
'publish_time' => null,
|
||||||
@@ -1870,7 +1890,7 @@ class AppraisalTasksController
|
|||||||
|
|
||||||
private function buildAppraisalSnapshot(string $serviceProvider, string $fallbackTime, ?array $firstReviewTask, ?array $finalReviewTask): array
|
private function buildAppraisalSnapshot(string $serviceProvider, string $fallbackTime, ?array $firstReviewTask, ?array $finalReviewTask): array
|
||||||
{
|
{
|
||||||
$institutionName = $serviceProvider === 'zhongjian' ? '中检合作机构' : '安心验';
|
$institutionName = $this->displayInstitutionName($serviceProvider);
|
||||||
$appraiserName = $this->normalizeAssigneeName($firstReviewTask['assignee_name'] ?? '')
|
$appraiserName = $this->normalizeAssigneeName($firstReviewTask['assignee_name'] ?? '')
|
||||||
?: $this->normalizeAssigneeName($finalReviewTask['assignee_name'] ?? '');
|
?: $this->normalizeAssigneeName($finalReviewTask['assignee_name'] ?? '');
|
||||||
$reviewerName = $appraiserName;
|
$reviewerName = $appraiserName;
|
||||||
@@ -1899,6 +1919,11 @@ class AppraisalTasksController
|
|||||||
return $name;
|
return $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function displayInstitutionName(string $serviceProvider): string
|
||||||
|
{
|
||||||
|
return $serviceProvider === 'zhongjian' ? '中检鉴定中心' : '安心检验';
|
||||||
|
}
|
||||||
|
|
||||||
private function guardTaskOperator(Request $request, array $task): array
|
private function guardTaskOperator(Request $request, array $task): array
|
||||||
{
|
{
|
||||||
$adminId = (int)$request->header('x-admin-id', 0);
|
$adminId = (int)$request->header('x-admin-id', 0);
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class ReportsController
|
|||||||
'report_status_text' => $this->reportStatusText($item['report_status']),
|
'report_status_text' => $this->reportStatusText($item['report_status']),
|
||||||
'service_provider' => $item['service_provider'],
|
'service_provider' => $item['service_provider'],
|
||||||
'service_provider_text' => $this->serviceProviderText($item['service_provider']),
|
'service_provider_text' => $this->serviceProviderText($item['service_provider']),
|
||||||
'institution_name' => $item['institution_name'] ?: $this->defaultInstitutionName($item['service_provider']),
|
'institution_name' => $this->defaultInstitutionName((string)$item['service_provider']),
|
||||||
'publish_time' => $item['publish_time'],
|
'publish_time' => $item['publish_time'],
|
||||||
'zhongjian_report_no' => (string)($item['zhongjian_report_no'] ?? ''),
|
'zhongjian_report_no' => (string)($item['zhongjian_report_no'] ?? ''),
|
||||||
'report_entry_admin_name' => (string)($item['report_entry_admin_name'] ?? ''),
|
'report_entry_admin_name' => (string)($item['report_entry_admin_name'] ?? ''),
|
||||||
@@ -163,7 +163,7 @@ class ReportsController
|
|||||||
'report_status_text' => $this->reportStatusText($report['report_status']),
|
'report_status_text' => $this->reportStatusText($report['report_status']),
|
||||||
'service_provider' => $report['service_provider'],
|
'service_provider' => $report['service_provider'],
|
||||||
'service_provider_text' => $this->serviceProviderText($report['service_provider']),
|
'service_provider_text' => $this->serviceProviderText($report['service_provider']),
|
||||||
'institution_name' => $report['institution_name'] ?: $this->defaultInstitutionName($report['service_provider']),
|
'institution_name' => $this->defaultInstitutionName((string)$report['service_provider']),
|
||||||
'publish_time' => $report['publish_time'],
|
'publish_time' => $report['publish_time'],
|
||||||
'zhongjian_report_no' => (string)($report['zhongjian_report_no'] ?? ''),
|
'zhongjian_report_no' => (string)($report['zhongjian_report_no'] ?? ''),
|
||||||
'report_entry_admin_id' => (int)($report['report_entry_admin_id'] ?? 0),
|
'report_entry_admin_id' => (int)($report['report_entry_admin_id'] ?? 0),
|
||||||
@@ -765,6 +765,6 @@ class ReportsController
|
|||||||
|
|
||||||
private function defaultInstitutionName(string $serviceProvider): string
|
private function defaultInstitutionName(string $serviceProvider): string
|
||||||
{
|
{
|
||||||
return $serviceProvider === 'zhongjian' ? '中检合作机构' : '安心验';
|
return $serviceProvider === 'zhongjian' ? '中检鉴定中心' : '安心检验';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class ReportsController
|
|||||||
'service_provider' => $item['service_provider'],
|
'service_provider' => $item['service_provider'],
|
||||||
'status' => $published ? '已出报告' : '待出报告',
|
'status' => $published ? '已出报告' : '待出报告',
|
||||||
'result_text' => $published ? '正品' : '待出报告',
|
'result_text' => $published ? '正品' : '待出报告',
|
||||||
'institution_name' => $item['institution_name'] ?: '安心验',
|
'institution_name' => $this->displayInstitutionName((string)$item['service_provider']),
|
||||||
'publish_time' => $item['publish_time'],
|
'publish_time' => $item['publish_time'],
|
||||||
];
|
];
|
||||||
}, $rows);
|
}, $rows);
|
||||||
@@ -94,7 +94,6 @@ class ReportsController
|
|||||||
$content = Db::name('report_contents')->where('report_id', $reportData['id'])->find();
|
$content = Db::name('report_contents')->where('report_id', $reportData['id'])->find();
|
||||||
$verify = Db::name('report_verifies')->where('report_id', $reportData['id'])->find() ?: [];
|
$verify = Db::name('report_verifies')->where('report_id', $reportData['id'])->find() ?: [];
|
||||||
$verify = $this->normalizeVerifyInfo($reportData, $verify);
|
$verify = $this->normalizeVerifyInfo($reportData, $verify);
|
||||||
$pdfUrl = $this->ensurePdfFile($request, $reportData, $content ?: [], $verify ?: []);
|
|
||||||
$evidenceAttachments = $this->evidenceService()->normalize($content['evidence_attachments_json'] ?? null, $request);
|
$evidenceAttachments = $this->evidenceService()->normalize($content['evidence_attachments_json'] ?? null, $request);
|
||||||
$zhongjianReportFiles = $this->evidenceService()->normalize($content['zhongjian_report_files_json'] ?? null, $request);
|
$zhongjianReportFiles = $this->evidenceService()->normalize($content['zhongjian_report_files_json'] ?? null, $request);
|
||||||
$defaultRiskNotice = (new ContentService())->getReportRiskNotice((string)($reportData['report_type'] ?? 'appraisal'));
|
$defaultRiskNotice = (new ContentService())->getReportRiskNotice((string)($reportData['report_type'] ?? 'appraisal'));
|
||||||
@@ -105,6 +104,17 @@ class ReportsController
|
|||||||
'valuation_snapshot' => $this->decodeJsonField($content['valuation_snapshot_json'] ?? null),
|
'valuation_snapshot' => $this->decodeJsonField($content['valuation_snapshot_json'] ?? null),
|
||||||
'risk_notice_text' => ($content['risk_notice_text'] ?? '') !== '' ? $content['risk_notice_text'] : $defaultRiskNotice,
|
'risk_notice_text' => ($content['risk_notice_text'] ?? '') !== '' ? $content['risk_notice_text'] : $defaultRiskNotice,
|
||||||
];
|
];
|
||||||
|
$productDisplay = $this->buildProductDisplay($reportData, $payload['product_snapshot'], $payload['result_snapshot']);
|
||||||
|
$reportMedia = [
|
||||||
|
'images' => $this->filterAssetsByType($evidenceAttachments, 'image'),
|
||||||
|
];
|
||||||
|
$traceInfo = $this->buildTraceInfo(
|
||||||
|
(int)($reportData['order_id'] ?? 0),
|
||||||
|
$payload['appraisal_snapshot'],
|
||||||
|
$evidenceAttachments,
|
||||||
|
$request
|
||||||
|
);
|
||||||
|
$pdfUrl = $this->ensurePdfFile($request, $reportData, $content ?: [], $verify ?: [], $productDisplay, $reportMedia);
|
||||||
|
|
||||||
return api_success([
|
return api_success([
|
||||||
'report_header' => [
|
'report_header' => [
|
||||||
@@ -114,7 +124,7 @@ class ReportsController
|
|||||||
'report_title' => $reportData['report_title'],
|
'report_title' => $reportData['report_title'],
|
||||||
'report_status' => $reportData['report_status'],
|
'report_status' => $reportData['report_status'],
|
||||||
'service_provider' => $reportData['service_provider'],
|
'service_provider' => $reportData['service_provider'],
|
||||||
'institution_name' => $reportData['institution_name'],
|
'institution_name' => $this->displayInstitutionName((string)$reportData['service_provider']),
|
||||||
'publish_time' => $reportData['publish_time'],
|
'publish_time' => $reportData['publish_time'],
|
||||||
'zhongjian_report_no' => (string)($reportData['zhongjian_report_no'] ?? ''),
|
'zhongjian_report_no' => (string)($reportData['zhongjian_report_no'] ?? ''),
|
||||||
'report_entry_admin_name' => (string)($reportData['report_entry_admin_name'] ?? ''),
|
'report_entry_admin_name' => (string)($reportData['report_entry_admin_name'] ?? ''),
|
||||||
@@ -126,6 +136,9 @@ class ReportsController
|
|||||||
'valuation_info' => $payload['valuation_snapshot'],
|
'valuation_info' => $payload['valuation_snapshot'],
|
||||||
'evidence_attachments' => $evidenceAttachments,
|
'evidence_attachments' => $evidenceAttachments,
|
||||||
'zhongjian_report_files' => $zhongjianReportFiles,
|
'zhongjian_report_files' => $zhongjianReportFiles,
|
||||||
|
'report_media' => $reportMedia,
|
||||||
|
'product_display' => $productDisplay,
|
||||||
|
'trace_info' => $traceInfo,
|
||||||
'risk_notice_text' => $payload['risk_notice_text'],
|
'risk_notice_text' => $payload['risk_notice_text'],
|
||||||
'verify_info' => [
|
'verify_info' => [
|
||||||
'report_no' => $reportData['report_no'],
|
'report_no' => $reportData['report_no'],
|
||||||
@@ -139,6 +152,75 @@ class ReportsController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function antiCounterfeitVerify(Request $request)
|
||||||
|
{
|
||||||
|
$reportNo = trim((string)$request->input('report_no', ''));
|
||||||
|
$verifyCode = trim((string)$request->input('verify_code', ''));
|
||||||
|
if ($reportNo === '' || $verifyCode === '') {
|
||||||
|
return api_error('报告编号和防伪查询码不能为空', 422);
|
||||||
|
}
|
||||||
|
if (!preg_match('/^\d{6}$/', $verifyCode)) {
|
||||||
|
return api_error('防伪查询码应为 6 位数字', 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$report = Db::name('reports')
|
||||||
|
->where('report_no', $reportNo)
|
||||||
|
->where('report_status', 'published')
|
||||||
|
->find();
|
||||||
|
if (!$report) {
|
||||||
|
return api_success([
|
||||||
|
'verify_passed' => false,
|
||||||
|
'verify_message' => '未查询到有效报告,请核对报告编号后重试。',
|
||||||
|
'verify_count' => 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = Db::name('material_tag_codes')
|
||||||
|
->where('report_id', (int)$report['id'])
|
||||||
|
->where('bind_status', 'bound')
|
||||||
|
->find();
|
||||||
|
if (!$tag) {
|
||||||
|
return api_success([
|
||||||
|
'verify_passed' => false,
|
||||||
|
'verify_message' => '当前报告尚未绑定防伪吊牌,暂不能完成防伪查询。',
|
||||||
|
'verify_count' => 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$batch = Db::name('material_batches')->where('id', (int)$tag['batch_id'])->find();
|
||||||
|
$active = ($tag['status'] ?? 'active') !== 'invalid'
|
||||||
|
&& (!$batch || ($batch['status'] ?? 'active') !== 'invalid');
|
||||||
|
$passed = $active
|
||||||
|
&& hash_equals((string)$tag['verify_code'], $verifyCode)
|
||||||
|
&& (string)($tag['report_no'] ?: $report['report_no']) === $reportNo;
|
||||||
|
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
if ($passed) {
|
||||||
|
Db::name('material_tag_codes')->where('id', (int)$tag['id'])->update([
|
||||||
|
'verify_count' => (int)$tag['verify_count'] + 1,
|
||||||
|
'last_verified_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$this->insertMaterialTagVerifyLog($tag, $reportNo, $verifyCode, $passed, $request, $now);
|
||||||
|
|
||||||
|
if (!$active) {
|
||||||
|
return api_success([
|
||||||
|
'verify_passed' => false,
|
||||||
|
'verify_message' => '该防伪吊牌已失效,不能进行防伪查询。',
|
||||||
|
'verify_count' => (int)$tag['verify_count'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return api_success([
|
||||||
|
'verify_passed' => $passed,
|
||||||
|
'verify_message' => $passed
|
||||||
|
? '防伪查询通过,该报告编号与吊牌防伪查询码匹配。'
|
||||||
|
: '防伪查询码与当前报告不匹配,请核对吊牌后重试。',
|
||||||
|
'verify_count' => (int)$tag['verify_count'] + ($passed ? 1 : 0),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function decodeJsonField(mixed $value): array
|
private function decodeJsonField(mixed $value): array
|
||||||
{
|
{
|
||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
@@ -171,49 +253,41 @@ class ReportsController
|
|||||||
return $verify;
|
return $verify;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ensurePdfFile(Request $request, array $report, array $content, array $verify): string
|
private function ensurePdfFile(Request $request, array $report, array $content, array $verify, array $productDisplay, array $reportMedia): string
|
||||||
{
|
{
|
||||||
$existingFile = Db::name('report_files')
|
$existingFile = Db::name('report_files')
|
||||||
->where('report_id', (int)$report['id'])
|
->where('report_id', (int)$report['id'])
|
||||||
->where('file_type', 'pdf')
|
->where('file_type', 'pdf')
|
||||||
->find();
|
->find();
|
||||||
|
$publishTime = (string)($report['publish_time'] ?: date('Y-m-d H:i:s'));
|
||||||
|
$relativeDir = 'uploads/reports/' . date('Ymd', strtotime($publishTime));
|
||||||
|
$filename = $report['report_no'] . '-v2.pdf';
|
||||||
|
$relativePath = $relativeDir . '/' . $filename;
|
||||||
|
|
||||||
if ($existingFile && !empty($existingFile['file_url'])) {
|
if ($existingFile && !empty($existingFile['file_url'])) {
|
||||||
$relativeUrl = ltrim((string)$existingFile['file_url'], '/');
|
$relativeUrl = ltrim((string)$existingFile['file_url'], '/');
|
||||||
if ($this->storage()->exists($relativeUrl)) {
|
if ($relativeUrl === $relativePath && !$this->shouldRebuildPdf($existingFile, $report, $content) && $this->storage()->exists($relativeUrl)) {
|
||||||
return $this->storage()->publicUrl($request, $relativeUrl);
|
return $this->storage()->publicUrl($request, $relativeUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$productInfo = $this->decodeJsonField($content['product_snapshot_json'] ?? null);
|
$productInfo = $this->decodeJsonField($content['product_snapshot_json'] ?? null);
|
||||||
$resultInfo = $this->decodeJsonField($content['result_snapshot_json'] ?? null);
|
$resultInfo = $this->decodeJsonField($content['result_snapshot_json'] ?? null);
|
||||||
$appraisalInfo = $this->decodeJsonField($content['appraisal_snapshot_json'] ?? null);
|
|
||||||
$valuationInfo = $this->decodeJsonField($content['valuation_snapshot_json'] ?? null);
|
|
||||||
|
|
||||||
$publishTime = (string)($report['publish_time'] ?: date('Y-m-d H:i:s'));
|
|
||||||
$defaultRiskNotice = (new ContentService())->getReportRiskNotice((string)($report['report_type'] ?? 'appraisal'));
|
$defaultRiskNotice = (new ContentService())->getReportRiskNotice((string)($report['report_type'] ?? 'appraisal'));
|
||||||
$relativeDir = 'uploads/reports/' . date('Ymd', strtotime($publishTime));
|
|
||||||
$filename = $report['report_no'] . '.pdf';
|
|
||||||
$relativePath = $relativeDir . '/' . $filename;
|
|
||||||
$generator = new ReportPdfGenerator();
|
$generator = new ReportPdfGenerator();
|
||||||
$pdfBinary = $generator->generate([
|
$pdfBinary = $generator->generate([
|
||||||
'report_title' => $report['report_title'] ?? '鉴定报告',
|
'report_title' => $report['report_title'] ?? '鉴定报告',
|
||||||
'service_provider_text' => ($report['service_provider'] ?? 'anxinyan') === 'zhongjian' ? '中检鉴定' : '实物鉴定',
|
'service_provider_text' => ($report['service_provider'] ?? 'anxinyan') === 'zhongjian' ? '中检鉴定' : '实物鉴定',
|
||||||
'institution_name' => $report['institution_name'] ?? '安心验',
|
'institution_name' => $this->displayInstitutionName((string)($report['service_provider'] ?? 'anxinyan')),
|
||||||
'report_no' => $report['report_no'] ?? '',
|
'report_no' => $report['report_no'] ?? '',
|
||||||
'publish_time' => $publishTime,
|
'publish_time' => $publishTime,
|
||||||
'result_text' => $resultInfo['result_text'] ?? '-',
|
'result_text' => $resultInfo['result_text'] ?? '-',
|
||||||
'result_desc' => $resultInfo['result_desc'] ?? '-',
|
'result_desc' => $resultInfo['result_desc'] ?? '-',
|
||||||
'product_name' => $productInfo['product_name'] ?? '-',
|
'product_name' => $productInfo['product_name'] ?? '-',
|
||||||
'category_brand' => trim(($productInfo['category_name'] ?? '-') . ' / ' . ($productInfo['brand_name'] ?? '-')),
|
'product_items' => $productDisplay['items'] ?? [],
|
||||||
'spec_info' => trim(($productInfo['color'] ?? '-') . ' / ' . ($productInfo['size_spec'] ?? '-')),
|
'hero_image' => $this->firstPdfHeroImage($reportMedia['images'] ?? []),
|
||||||
'appraisers' => trim((string)($appraisalInfo['appraiser_name'] ?? '-')),
|
'hero_image_labels' => $this->assetNameList($reportMedia['images'] ?? []),
|
||||||
'condition_grade' => $valuationInfo['condition_grade'] ?? '-',
|
|
||||||
'valuation_range' => sprintf(
|
|
||||||
'¥%s - ¥%s',
|
|
||||||
$valuationInfo['valuation_min'] ?? 0,
|
|
||||||
$valuationInfo['valuation_max'] ?? 0
|
|
||||||
),
|
|
||||||
'verify_info' => sprintf(
|
'verify_info' => sprintf(
|
||||||
'%s / %s',
|
'%s / %s',
|
||||||
$verify['report_no'] ?? ($report['report_no'] ?? '-'),
|
$verify['report_no'] ?? ($report['report_no'] ?? '-'),
|
||||||
@@ -222,7 +296,7 @@ class ReportsController
|
|||||||
'risk_notice_text' => ($content['risk_notice_text'] ?? '') !== '' ? $content['risk_notice_text'] : ($defaultRiskNotice !== '' ? $defaultRiskNotice : '-'),
|
'risk_notice_text' => ($content['risk_notice_text'] ?? '') !== '' ? $content['risk_notice_text'] : ($defaultRiskNotice !== '' ? $defaultRiskNotice : '-'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->storage()->putContents($relativePath, $pdfBinary);
|
$this->storage()->putContentsWithMimeType($relativePath, $pdfBinary, 'application/pdf');
|
||||||
|
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
$filePayload = [
|
$filePayload = [
|
||||||
@@ -243,6 +317,268 @@ class ReportsController
|
|||||||
return $this->storage()->publicUrl($request, $relativePath);
|
return $this->storage()->publicUrl($request, $relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildProductDisplay(array $report, array $productInfo, array $resultInfo): array
|
||||||
|
{
|
||||||
|
$items = [
|
||||||
|
[
|
||||||
|
'label' => '检测结论',
|
||||||
|
'value' => $this->textValue($resultInfo['result_text'] ?? '') ?: '-',
|
||||||
|
'remark' => $this->textValue($resultInfo['result_desc'] ?? ''),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => '品牌',
|
||||||
|
'value' => $this->textValue($productInfo['brand_name'] ?? '') ?: '-',
|
||||||
|
'remark' => '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (($resultInfo['key_points'] ?? []) as $point) {
|
||||||
|
if (!is_array($point)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$label = $this->textValue($point['point_name'] ?? '');
|
||||||
|
if ($label === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$items[] = [
|
||||||
|
'label' => $label,
|
||||||
|
'value' => $this->textValue($point['point_value'] ?? '') ?: '-',
|
||||||
|
'remark' => $this->textValue($point['point_remark'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'product_name' => $this->textValue($productInfo['product_name'] ?? '') ?: '-',
|
||||||
|
'institution_name' => $this->displayInstitutionName((string)($report['service_provider'] ?? 'anxinyan')),
|
||||||
|
'items' => $items,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildTraceInfo(int $orderId, array $appraisalInfo, array $evidenceAttachments, Request $request): array
|
||||||
|
{
|
||||||
|
$logs = $orderId > 0
|
||||||
|
? Db::name('order_transfer_flow_logs')
|
||||||
|
->where('order_id', $orderId)
|
||||||
|
->whereIn('action_code', ['inbound_received', 'return_shipped'])
|
||||||
|
->order('id', 'asc')
|
||||||
|
->select()
|
||||||
|
->toArray()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
$inboundLog = $this->findFlowLog($logs, 'inbound_received');
|
||||||
|
$returnLog = $this->findFlowLog($logs, 'return_shipped');
|
||||||
|
$inboundPayload = $this->decodeJsonObject($inboundLog['payload_json'] ?? null);
|
||||||
|
$returnPayload = $this->decodeJsonObject($returnLog['payload_json'] ?? null);
|
||||||
|
$appraisalFinishedAt = $this->appraisalFinishedAt($orderId, $appraisalInfo);
|
||||||
|
$appraisalVideoAssets = $this->filterAssetsByType($evidenceAttachments, 'video');
|
||||||
|
|
||||||
|
$nodes = [
|
||||||
|
[
|
||||||
|
'code' => 'inbound',
|
||||||
|
'title' => '入仓',
|
||||||
|
'occurred_at' => (string)($inboundLog['created_at'] ?? ''),
|
||||||
|
'status' => $inboundLog ? 'completed' : 'pending',
|
||||||
|
'assets' => $this->evidenceService()->normalize($inboundPayload['inbound_attachments'] ?? [], $request),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'appraisal',
|
||||||
|
'title' => '鉴定',
|
||||||
|
'occurred_at' => $appraisalFinishedAt,
|
||||||
|
'status' => ($appraisalFinishedAt !== '' || $appraisalVideoAssets) ? 'completed' : 'pending',
|
||||||
|
'assets' => $appraisalVideoAssets,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'return',
|
||||||
|
'title' => '寄回',
|
||||||
|
'occurred_at' => (string)($returnLog['created_at'] ?? ''),
|
||||||
|
'status' => $returnLog ? 'completed' : 'pending',
|
||||||
|
'assets' => $this->evidenceService()->normalize($returnPayload['packing_attachments'] ?? [], $request),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return ['nodes' => $nodes];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findFlowLog(array $logs, string $actionCode): ?array
|
||||||
|
{
|
||||||
|
$matched = null;
|
||||||
|
foreach ($logs as $log) {
|
||||||
|
if (($log['action_code'] ?? '') === $actionCode) {
|
||||||
|
$matched = $log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function appraisalFinishedAt(int $orderId, array $appraisalInfo): string
|
||||||
|
{
|
||||||
|
if ($orderId > 0) {
|
||||||
|
$submittedAt = Db::name('appraisal_tasks')
|
||||||
|
->where('order_id', $orderId)
|
||||||
|
->whereRaw('submitted_at IS NOT NULL')
|
||||||
|
->order('submitted_at', 'desc')
|
||||||
|
->value('submitted_at');
|
||||||
|
if ($submittedAt) {
|
||||||
|
return (string)$submittedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->textValue($appraisalInfo['appraisal_time'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function filterAssetsByType(array $assets, string $fileType): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter($assets, fn (array $asset) => (string)($asset['file_type'] ?? '') === $fileType));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldRebuildPdf(array $existingFile, array $report, array $content): bool
|
||||||
|
{
|
||||||
|
$fileUpdatedAt = strtotime((string)($existingFile['updated_at'] ?? '')) ?: 0;
|
||||||
|
if ($fileUpdatedAt <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([$report['updated_at'] ?? '', $content['updated_at'] ?? ''] as $timestamp) {
|
||||||
|
if ((strtotime((string)$timestamp) ?: 0) > $fileUpdatedAt) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function firstPdfHeroImage(array $images): ?array
|
||||||
|
{
|
||||||
|
foreach ($images as $image) {
|
||||||
|
$binary = $this->readAssetBinary((string)($image['file_url'] ?? ''));
|
||||||
|
if ($binary === '' || strlen($binary) > 5 * 1024 * 1024) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = @getimagesizefromstring($binary);
|
||||||
|
if (!$info) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)($info[2] ?? 0) !== IMAGETYPE_JPEG) {
|
||||||
|
$converted = $this->convertImageBinaryToJpeg($binary);
|
||||||
|
if ($converted === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$binary = $converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'data' => $binary,
|
||||||
|
'width' => (int)$info[0],
|
||||||
|
'height' => (int)$info[1],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertImageBinaryToJpeg(string $binary): string
|
||||||
|
{
|
||||||
|
if (!function_exists('imagecreatefromstring')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$image = @imagecreatefromstring($binary);
|
||||||
|
if (!$image) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$width = imagesx($image);
|
||||||
|
$height = imagesy($image);
|
||||||
|
$canvas = imagecreatetruecolor($width, $height);
|
||||||
|
if (!$canvas) {
|
||||||
|
imagedestroy($image);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$background = imagecolorallocate($canvas, 255, 255, 255);
|
||||||
|
imagefill($canvas, 0, 0, $background);
|
||||||
|
imagecopy($canvas, $image, 0, 0, 0, 0, $width, $height);
|
||||||
|
ob_start();
|
||||||
|
imagejpeg($canvas, null, 86);
|
||||||
|
$jpeg = (string)ob_get_clean();
|
||||||
|
imagedestroy($canvas);
|
||||||
|
imagedestroy($image);
|
||||||
|
|
||||||
|
return $jpeg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readAssetBinary(string $fileUrl): string
|
||||||
|
{
|
||||||
|
$fileUrl = trim($fileUrl);
|
||||||
|
if ($fileUrl === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$relativePath = $this->storage()->storagePath($fileUrl);
|
||||||
|
$localPath = public_path() . '/' . $relativePath;
|
||||||
|
if (is_file($localPath)) {
|
||||||
|
return (string)file_get_contents($localPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^https?:\/\//i', $fileUrl)) {
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => ['timeout' => 3],
|
||||||
|
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false],
|
||||||
|
]);
|
||||||
|
return (string)(@file_get_contents($fileUrl, false, $context) ?: '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assetNameList(array $assets): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(array_map(function (array $asset) {
|
||||||
|
return $this->textValue($asset['name'] ?? '') ?: $this->textValue($asset['file_id'] ?? '');
|
||||||
|
}, $assets)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function insertMaterialTagVerifyLog(array $tag, string $reportNo, string $verifyCode, bool $passed, Request $request, string $now): void
|
||||||
|
{
|
||||||
|
Db::name('material_tag_scan_logs')->insert([
|
||||||
|
'tag_code_id' => (int)$tag['id'],
|
||||||
|
'batch_id' => (int)$tag['batch_id'],
|
||||||
|
'report_id' => (int)($tag['report_id'] ?? 0) ?: null,
|
||||||
|
'report_no' => $reportNo,
|
||||||
|
'verify_type' => 'report_page_code',
|
||||||
|
'verify_code_input' => mb_substr($verifyCode, 0, 16),
|
||||||
|
'verify_passed' => $passed ? 1 : 0,
|
||||||
|
'ip' => (string)$request->getRealIp(),
|
||||||
|
'user_agent' => mb_substr((string)$request->header('user-agent', ''), 0, 500),
|
||||||
|
'scanned_at' => $now,
|
||||||
|
'created_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeJsonObject(mixed $value): array
|
||||||
|
{
|
||||||
|
if (is_array($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
if (is_string($value) && $value !== '') {
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function textValue(mixed $value): string
|
||||||
|
{
|
||||||
|
return trim((string)($value ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function displayInstitutionName(string $serviceProvider): string
|
||||||
|
{
|
||||||
|
return $serviceProvider === 'zhongjian' ? '中检鉴定中心' : '安心检验';
|
||||||
|
}
|
||||||
|
|
||||||
private function buildPublicPageUrl(string $pagePath, array $query = []): string
|
private function buildPublicPageUrl(string $pagePath, array $query = []): string
|
||||||
{
|
{
|
||||||
$baseUrl = $this->normalizeH5BaseUrl($this->getSystemConfigValue('h5', 'page_base_url'));
|
$baseUrl = $this->normalizeH5BaseUrl($this->getSystemConfigValue('h5', 'page_base_url'));
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class AppAuthMiddleware implements MiddlewareInterface
|
|||||||
'/api/app/help-center',
|
'/api/app/help-center',
|
||||||
'/api/app/help-article/detail',
|
'/api/app/help-article/detail',
|
||||||
'/api/app/report/detail',
|
'/api/app/report/detail',
|
||||||
|
'/api/app/report/anti-counterfeit/verify',
|
||||||
'/api/app/verify',
|
'/api/app/verify',
|
||||||
'/api/app/material-tag',
|
'/api/app/material-tag',
|
||||||
'/api/app/material-tag/verify',
|
'/api/app/material-tag/verify',
|
||||||
|
|||||||
@@ -6,49 +6,59 @@ class ReportPdfGenerator
|
|||||||
{
|
{
|
||||||
public function generate(array $payload): string
|
public function generate(array $payload): string
|
||||||
{
|
{
|
||||||
$content = $this->buildContentStream($payload);
|
$heroImage = $this->normalizeHeroImage($payload['hero_image'] ?? null);
|
||||||
|
$content = $this->buildContentStream($payload, $heroImage);
|
||||||
|
$resources = '<< /Font << /F1 5 0 R >>';
|
||||||
|
if ($heroImage) {
|
||||||
|
$resources .= ' /XObject << /Im1 7 0 R >>';
|
||||||
|
}
|
||||||
|
$resources .= ' >>';
|
||||||
|
|
||||||
$objects = [
|
$objects = [
|
||||||
1 => '<< /Type /Catalog /Pages 2 0 R >>',
|
1 => '<< /Type /Catalog /Pages 2 0 R >>',
|
||||||
2 => '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
|
2 => '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
|
||||||
3 => '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>',
|
3 => sprintf('<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources %s /Contents 4 0 R >>', $resources),
|
||||||
4 => sprintf("<< /Length %d >>\nstream\n%s\nendstream", strlen($content), $content),
|
4 => sprintf("<< /Length %d >>\nstream\n%s\nendstream", strlen($content), $content),
|
||||||
5 => '<< /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [6 0 R] >>',
|
5 => '<< /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [6 0 R] >>',
|
||||||
6 => '<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 4 >> /DW 1000 >>',
|
6 => '<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 4 >> /DW 1000 >>',
|
||||||
];
|
];
|
||||||
|
if ($heroImage) {
|
||||||
|
$objects[7] = sprintf(
|
||||||
|
"<< /Type /XObject /Subtype /Image /Width %d /Height %d /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length %d >>\nstream\n%s\nendstream",
|
||||||
|
$heroImage['width'],
|
||||||
|
$heroImage['height'],
|
||||||
|
strlen($heroImage['data']),
|
||||||
|
$heroImage['data']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->renderPdf($objects);
|
return $this->renderPdf($objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildContentStream(array $payload): string
|
private function buildContentStream(array $payload, ?array $heroImage): string
|
||||||
{
|
{
|
||||||
$title = $this->normalizeText((string)($payload['report_title'] ?? '鉴定报告'));
|
$title = $this->normalizeText((string)($payload['report_title'] ?? '鉴定报告'));
|
||||||
$serviceProviderText = $this->normalizeText((string)($payload['service_provider_text'] ?? '-'));
|
$serviceProviderText = $this->normalizeText((string)($payload['service_provider_text'] ?? '-'));
|
||||||
$institutionName = $this->normalizeText((string)($payload['institution_name'] ?? '-'));
|
$institutionName = $this->normalizeText((string)($payload['institution_name'] ?? '-'));
|
||||||
$reportNo = $this->normalizeText((string)($payload['report_no'] ?? '-'));
|
$reportNo = $this->normalizeText((string)($payload['report_no'] ?? '-'));
|
||||||
$publishTime = $this->normalizeText((string)($payload['publish_time'] ?? '-'));
|
$publishTime = $this->normalizeText((string)($payload['publish_time'] ?? '-'));
|
||||||
$resultText = $this->normalizeText((string)($payload['result_text'] ?? '-'));
|
|
||||||
$resultDesc = $this->normalizeText((string)($payload['result_desc'] ?? '-'));
|
|
||||||
$productName = $this->normalizeText((string)($payload['product_name'] ?? '-'));
|
$productName = $this->normalizeText((string)($payload['product_name'] ?? '-'));
|
||||||
$categoryBrand = $this->normalizeText((string)($payload['category_brand'] ?? '-'));
|
|
||||||
$specInfo = $this->normalizeText((string)($payload['spec_info'] ?? '-'));
|
|
||||||
$appraisers = $this->normalizeText((string)($payload['appraisers'] ?? '-'));
|
|
||||||
$conditionGrade = $this->normalizeText((string)($payload['condition_grade'] ?? '-'));
|
|
||||||
$valuationRange = $this->normalizeText((string)($payload['valuation_range'] ?? '-'));
|
|
||||||
$verifyInfo = $this->normalizeText((string)($payload['verify_info'] ?? '-'));
|
$verifyInfo = $this->normalizeText((string)($payload['verify_info'] ?? '-'));
|
||||||
$riskNotice = $this->normalizeText((string)($payload['risk_notice_text'] ?? '-'));
|
$riskNotice = $this->normalizeText((string)($payload['risk_notice_text'] ?? '-'));
|
||||||
|
$productItems = is_array($payload['product_items'] ?? null) ? $payload['product_items'] : [];
|
||||||
|
$heroImageLabels = is_array($payload['hero_image_labels'] ?? null) ? $payload['hero_image_labels'] : [];
|
||||||
|
|
||||||
$blocks = [];
|
$blocks = [];
|
||||||
$y = 790;
|
$y = 790;
|
||||||
|
|
||||||
$blocks[] = $this->textBlock($title, 52, $y, 20);
|
$blocks[] = $this->textBlock($title, 52, $y, 20);
|
||||||
$y -= 32;
|
$y -= 32;
|
||||||
$blocks[] = $this->textBlock('正式报告凭证,请以编号验真结果为准。', 52, $y, 10);
|
$blocks[] = $this->textBlock('正式报告凭证,请以报告页防伪查询结果为准。', 52, $y, 10);
|
||||||
$y -= 30;
|
$y -= 30;
|
||||||
|
|
||||||
foreach ([
|
foreach ([
|
||||||
sprintf('报告编号:%s', $reportNo),
|
sprintf('报告编号:%s', $reportNo),
|
||||||
sprintf('出具机构:%s', $institutionName),
|
sprintf('检测机构:%s', $institutionName),
|
||||||
sprintf('出具时间:%s', $publishTime),
|
sprintf('出具时间:%s', $publishTime),
|
||||||
sprintf('服务类型:%s', $serviceProviderText),
|
sprintf('服务类型:%s', $serviceProviderText),
|
||||||
] as $line) {
|
] as $line) {
|
||||||
@@ -56,28 +66,55 @@ class ReportPdfGenerator
|
|||||||
$y -= 22;
|
$y -= 22;
|
||||||
}
|
}
|
||||||
|
|
||||||
$y -= 8;
|
if ($heroImage) {
|
||||||
$blocks[] = $this->textBlock(sprintf('鉴定结论:%s', $resultText), 52, $y, 16);
|
$maxWidth = 230;
|
||||||
$y -= 26;
|
$maxHeight = 150;
|
||||||
|
$scale = min($maxWidth / $heroImage['width'], $maxHeight / $heroImage['height'], 1);
|
||||||
foreach ($this->wrapText('结果说明:' . $resultDesc, 30) as $line) {
|
$drawWidth = (int)round($heroImage['width'] * $scale);
|
||||||
|
$drawHeight = (int)round($heroImage['height'] * $scale);
|
||||||
|
$y -= $drawHeight + 6;
|
||||||
|
$blocks[] = $this->imageBlock('Im1', 52, $y, $drawWidth, $drawHeight);
|
||||||
|
$y -= 24;
|
||||||
|
} elseif ($heroImageLabels) {
|
||||||
|
foreach ($this->wrapText('鉴定图片:' . implode('、', array_slice($heroImageLabels, 0, 3)), 34) as $line) {
|
||||||
$blocks[] = $this->textBlock($line, 52, $y, 11);
|
$blocks[] = $this->textBlock($line, 52, $y, 11);
|
||||||
$y -= 18;
|
$y -= 18;
|
||||||
}
|
}
|
||||||
|
$y -= 8;
|
||||||
|
}
|
||||||
|
|
||||||
$y -= 8;
|
$y -= 8;
|
||||||
foreach ([
|
$blocks[] = $this->textBlock('产品信息', 52, $y, 15);
|
||||||
sprintf('商品名称:%s', $productName),
|
$y -= 24;
|
||||||
sprintf('品类 / 品牌:%s', $categoryBrand),
|
$blocks[] = $this->textBlock(sprintf('产品名称:%s', $productName), 52, $y, 12);
|
||||||
sprintf('颜色 / 规格:%s', $specInfo),
|
$y -= 22;
|
||||||
sprintf('鉴定师:%s', $appraisers),
|
|
||||||
sprintf('成色评级:%s', $conditionGrade),
|
foreach ($productItems as $item) {
|
||||||
sprintf('估值区间:%s', $valuationRange),
|
if (!is_array($item)) {
|
||||||
sprintf('验真信息:%s', $verifyInfo),
|
continue;
|
||||||
] as $line) {
|
|
||||||
$blocks[] = $this->textBlock($line, 52, $y, 11);
|
|
||||||
$y -= 20;
|
|
||||||
}
|
}
|
||||||
|
$label = $this->normalizeText((string)($item['label'] ?? ''));
|
||||||
|
if ($label === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$value = $this->normalizeText((string)($item['value'] ?? '-'));
|
||||||
|
$remark = $this->normalizeText((string)($item['remark'] ?? ''));
|
||||||
|
foreach ($this->wrapText(sprintf('%s:%s', $label, $value !== '' ? $value : '-'), 34) as $line) {
|
||||||
|
$blocks[] = $this->textBlock($line, 52, $y, 11);
|
||||||
|
$y -= 18;
|
||||||
|
}
|
||||||
|
if ($remark !== '') {
|
||||||
|
foreach ($this->wrapText('说明:' . $remark, 34) as $line) {
|
||||||
|
$blocks[] = $this->textBlock($line, 72, $y, 10);
|
||||||
|
$y -= 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$y -= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$y -= 6;
|
||||||
|
$blocks[] = $this->textBlock(sprintf('验真信息:%s', $verifyInfo), 52, $y, 11);
|
||||||
|
$y -= 22;
|
||||||
|
|
||||||
$y -= 8;
|
$y -= 8;
|
||||||
foreach ($this->wrapText('风险说明:' . $riskNotice, 30) as $line) {
|
foreach ($this->wrapText('风险说明:' . $riskNotice, 30) as $line) {
|
||||||
@@ -86,12 +123,32 @@ class ReportPdfGenerator
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($y > 48) {
|
if ($y > 48) {
|
||||||
$blocks[] = $this->textBlock('安心验鉴定平台', 52, 42, 9);
|
$blocks[] = $this->textBlock('安心检验鉴定平台', 52, 42, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
return implode("\n", array_filter($blocks));
|
return implode("\n", array_filter($blocks));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeHeroImage(mixed $image): ?array
|
||||||
|
{
|
||||||
|
if (!is_array($image)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = (string)($image['data'] ?? '');
|
||||||
|
$width = (int)($image['width'] ?? 0);
|
||||||
|
$height = (int)($image['height'] ?? 0);
|
||||||
|
if ($data === '' || $width <= 0 || $height <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'data' => $data,
|
||||||
|
'width' => $width,
|
||||||
|
'height' => $height,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function wrapText(string $text, int $maxUnits): array
|
private function wrapText(string $text, int $maxUnits): array
|
||||||
{
|
{
|
||||||
$normalized = $this->normalizeText($text);
|
$normalized = $this->normalizeText($text);
|
||||||
@@ -137,6 +194,11 @@ class ReportPdfGenerator
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function imageBlock(string $name, int $x, int $y, int $width, int $height): string
|
||||||
|
{
|
||||||
|
return sprintf("q\n%d 0 0 %d %d %d cm\n/%s Do\nQ", $width, $height, $x, $y, $name);
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeText(string $text): string
|
private function normalizeText(string $text): string
|
||||||
{
|
{
|
||||||
$text = trim(str_replace(["\r\n", "\r", "\n", "\t"], [' ', ' ', ' ', ' '], $text));
|
$text = trim(str_replace(["\r\n", "\r", "\n", "\t"], [' ', ' ', ' ', ' '], $text));
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ Route::get('/api/app/order/detail', [OrdersController::class, 'detail']);
|
|||||||
Route::post('/api/app/order/return-address/save', [OrdersController::class, 'saveReturnAddress']);
|
Route::post('/api/app/order/return-address/save', [OrdersController::class, 'saveReturnAddress']);
|
||||||
Route::get('/api/app/reports', [ReportsController::class, 'index']);
|
Route::get('/api/app/reports', [ReportsController::class, 'index']);
|
||||||
Route::get('/api/app/report/detail', [ReportsController::class, 'detail']);
|
Route::get('/api/app/report/detail', [ReportsController::class, 'detail']);
|
||||||
|
Route::post('/api/app/report/anti-counterfeit/verify', [ReportsController::class, 'antiCounterfeitVerify']);
|
||||||
Route::get('/api/app/verify', [VerifyController::class, 'show']);
|
Route::get('/api/app/verify', [VerifyController::class, 'show']);
|
||||||
Route::get('/api/app/material-tag', [AppMaterialTagsController::class, 'show']);
|
Route::get('/api/app/material-tag', [AppMaterialTagsController::class, 'show']);
|
||||||
Route::post('/api/app/material-tag/verify', [AppMaterialTagsController::class, 'verify']);
|
Route::post('/api/app/material-tag/verify', [AppMaterialTagsController::class, 'verify']);
|
||||||
|
|||||||
@@ -344,6 +344,27 @@ export interface ReportListItem {
|
|||||||
export interface ReportDetailData {
|
export interface ReportDetailData {
|
||||||
evidence_attachments: EvidenceAttachmentAsset[];
|
evidence_attachments: EvidenceAttachmentAsset[];
|
||||||
zhongjian_report_files: 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_header: {
|
||||||
report_id: number;
|
report_id: number;
|
||||||
report_no: string;
|
report_no: string;
|
||||||
@@ -413,6 +434,12 @@ export interface MaterialTagVerifyResult {
|
|||||||
verify_count: number;
|
verify_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReportAntiCounterfeitResult {
|
||||||
|
verify_passed: boolean;
|
||||||
|
verify_message: string;
|
||||||
|
verify_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessageSummaryData {
|
export interface MessageSummaryData {
|
||||||
total_count: number;
|
total_count: number;
|
||||||
unread_count: number;
|
unread_count: number;
|
||||||
@@ -672,6 +699,12 @@ export const appApi = {
|
|||||||
params: { report_no: reportNo },
|
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) {
|
getMaterialTag(token: string) {
|
||||||
return request<MaterialTagData>("/api/app/material-tag", {
|
return request<MaterialTagData>("/api/app/material-tag", {
|
||||||
params: { token },
|
params: { token },
|
||||||
|
|||||||
@@ -368,6 +368,52 @@ export const reportsFallback: ReportListItem[] = [
|
|||||||
export const reportDetailFallback: ReportDetailData = {
|
export const reportDetailFallback: ReportDetailData = {
|
||||||
evidence_attachments: [],
|
evidence_attachments: [],
|
||||||
zhongjian_report_files: [],
|
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_header: {
|
||||||
report_id: 1,
|
report_id: 1,
|
||||||
report_no: "AXY-R-20260420-0001",
|
report_no: "AXY-R-20260420-0001",
|
||||||
@@ -375,7 +421,7 @@ export const reportDetailFallback: ReportDetailData = {
|
|||||||
report_title: "中检鉴定报告",
|
report_title: "中检鉴定报告",
|
||||||
report_status: "published",
|
report_status: "published",
|
||||||
service_provider: "zhongjian",
|
service_provider: "zhongjian",
|
||||||
institution_name: "中检合作机构",
|
institution_name: "中检鉴定中心",
|
||||||
publish_time: "2026-04-18 18:26:00",
|
publish_time: "2026-04-18 18:26:00",
|
||||||
zhongjian_report_no: "ZJ-20260418-0001",
|
zhongjian_report_no: "ZJ-20260418-0001",
|
||||||
report_entry_admin_name: "王师傅",
|
report_entry_admin_name: "王师傅",
|
||||||
|
|||||||
@@ -4,42 +4,48 @@ import { onLoad } from "@dcloudio/uni-app";
|
|||||||
import { appApi, type EvidenceAttachmentAsset, type ReportDetailData } from "../../api/app";
|
import { appApi, type EvidenceAttachmentAsset, type ReportDetailData } from "../../api/app";
|
||||||
import { reportDetailFallback } from "../../mocks/app";
|
import { reportDetailFallback } from "../../mocks/app";
|
||||||
import { resolveErrorMessage } from "../../utils/feedback";
|
import { resolveErrorMessage } from "../../utils/feedback";
|
||||||
import { resolveQrImageSource } from "../../utils/qrcode";
|
|
||||||
|
type ReportTab = "product" | "trace";
|
||||||
|
|
||||||
const detail = ref<ReportDetailData>(reportDetailFallback);
|
const detail = ref<ReportDetailData>(reportDetailFallback);
|
||||||
const downloading = ref(false);
|
const downloading = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const pageReady = ref(false);
|
const pageReady = ref(false);
|
||||||
const loadError = ref("");
|
const loadError = ref("");
|
||||||
const qrImageSource = computed(() =>
|
const activeTab = ref<ReportTab>("product");
|
||||||
resolveQrImageSource(detail.value.verify_info.verify_qrcode_url, detail.value.verify_info.verify_url),
|
const antiModalVisible = ref(false);
|
||||||
);
|
const antiCode = ref("");
|
||||||
const isZhongjianReport = computed(() => detail.value.report_header.service_provider === "zhongjian");
|
const antiVerifying = ref(false);
|
||||||
const imageEvidenceAttachments = computed(() =>
|
const antiResult = ref<null | { passed: boolean; message: string }>(null);
|
||||||
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"),
|
|
||||||
);
|
|
||||||
|
|
||||||
function goVerify() {
|
const reportImages = computed(() => {
|
||||||
uni.navigateTo({ url: `/pages/verify/result?report_no=${detail.value.report_header.report_no}` });
|
const images = detail.value.report_media?.images || [];
|
||||||
}
|
if (images.length) return images;
|
||||||
|
return detail.value.evidence_attachments.filter((item) => item.file_type === "image");
|
||||||
function previewEvidenceImage(current: string) {
|
});
|
||||||
const urls = [...imageEvidenceAttachments.value, ...zhongjianReportImageAttachments.value].map((item) => item.file_url);
|
const productName = computed(() =>
|
||||||
if (!urls.length) return;
|
detail.value.product_display?.product_name
|
||||||
uni.previewImage({
|
|| detail.value.product_info.product_name
|
||||||
urls,
|
|| "-",
|
||||||
current,
|
);
|
||||||
});
|
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) {
|
function evidenceTypeText(fileType: string) {
|
||||||
if (fileType === "video") return "视频";
|
if (fileType === "video") return "视频";
|
||||||
@@ -48,13 +54,19 @@ function evidenceTypeText(fileType: string) {
|
|||||||
return "附件";
|
return "附件";
|
||||||
}
|
}
|
||||||
|
|
||||||
function evidenceDisplayName(item: EvidenceAttachmentAsset, index: number) {
|
function assetDisplayName(item: EvidenceAttachmentAsset, index: number) {
|
||||||
return item.name || `${evidenceTypeText(item.file_type)} ${index + 1}`;
|
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") {
|
if (item.file_type === "image") {
|
||||||
previewEvidenceImage(item.file_url);
|
previewImages(files, item.file_url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +109,74 @@ function openEvidenceAttachment(item: EvidenceAttachmentAsset) {
|
|||||||
uni.showToast({ title: "当前附件类型暂不支持预览", icon: "none" });
|
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() {
|
function downloadPdf() {
|
||||||
const pdfUrl = detail.value.file_info?.pdf_url;
|
const pdfUrl = detail.value.file_info?.pdf_url;
|
||||||
if (!pdfUrl) {
|
if (!pdfUrl) {
|
||||||
uni.showToast({
|
uni.showToast({ title: "报告文件暂未就绪", icon: "none" });
|
||||||
title: "报告文件暂未就绪",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,30 +191,17 @@ function downloadPdf() {
|
|||||||
url: pdfUrl,
|
url: pdfUrl,
|
||||||
success: (response) => {
|
success: (response) => {
|
||||||
if (response.statusCode !== 200 || !response.tempFilePath) {
|
if (response.statusCode !== 200 || !response.tempFilePath) {
|
||||||
uni.showToast({
|
uni.showToast({ title: "报告下载失败", icon: "none" });
|
||||||
title: "报告下载失败",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
uni.openDocument({
|
uni.openDocument({
|
||||||
filePath: response.tempFilePath,
|
filePath: response.tempFilePath,
|
||||||
fileType: "pdf",
|
fileType: "pdf",
|
||||||
showMenu: true,
|
showMenu: true,
|
||||||
fail: () => {
|
fail: () => uni.showToast({ title: "无法打开报告文件", icon: "none" }),
|
||||||
uni.showToast({
|
|
||||||
title: "无法打开报告文件",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
uni.showToast({
|
|
||||||
title: "报告下载失败",
|
|
||||||
icon: "none",
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
fail: () => uni.showToast({ title: "报告下载失败", icon: "none" }),
|
||||||
complete: () => {
|
complete: () => {
|
||||||
downloading.value = false;
|
downloading.value = false;
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
@@ -151,8 +211,8 @@ function downloadPdf() {
|
|||||||
|
|
||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
const id = Number(options?.id || 0);
|
const id = Number(options?.id || 0);
|
||||||
const reportNo = String(options?.report_no || "");
|
const currentReportNo = String(options?.report_no || "");
|
||||||
if (!id && !reportNo) {
|
if (!id && !currentReportNo) {
|
||||||
loadError.value = "缺少报告编号,无法查看详情。";
|
loadError.value = "缺少报告编号,无法查看详情。";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -161,7 +221,7 @@ onLoad(async (options) => {
|
|||||||
try {
|
try {
|
||||||
detail.value = await appApi.getReportDetail({
|
detail.value = await appApi.getReportDetail({
|
||||||
id: id || undefined,
|
id: id || undefined,
|
||||||
report_no: reportNo || undefined,
|
report_no: currentReportNo || undefined,
|
||||||
});
|
});
|
||||||
pageReady.value = true;
|
pageReady.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -174,10 +234,10 @@ onLoad(async (options) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page app-page--tight">
|
<view class="app-page report-page">
|
||||||
<view v-if="!pageReady && loading" class="section notice-card">
|
<view v-if="!pageReady && loading" class="section notice-card">
|
||||||
<view class="notice-card__title">正在加载报告详情</view>
|
<view class="notice-card__title">正在加载报告详情</view>
|
||||||
<view class="notice-card__desc">请稍候,我们正在同步报告正文、验真信息与 PDF 文件。</view>
|
<view class="notice-card__desc">请稍候,我们正在同步报告正文、追溯信息与 PDF 文件。</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else-if="!pageReady && loadError" class="section notice-card">
|
<view v-else-if="!pageReady && loadError" class="section notice-card">
|
||||||
@@ -186,183 +246,550 @@ onLoad(async (options) => {
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<view class="section-card certificate-header">
|
<view class="report-shell">
|
||||||
<view class="certificate-header__top">
|
<view class="report-carousel">
|
||||||
<text class="tag tag--success">有效</text>
|
<swiper v-if="reportImages.length" class="report-carousel__swiper" indicator-dots circular>
|
||||||
<text class="certificate-meta-chip">{{ detail.report_header.institution_name }}</text>
|
<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>
|
||||||
<view class="page-title" style="margin-top: 20rpx; font-size: var(--font-size-2xl); color: var(--color-heading);">
|
|
||||||
{{ detail.report_header.report_title }}
|
<view class="report-summary">
|
||||||
|
<view class="report-summary__row">
|
||||||
|
<text class="report-summary__label">产品名称</text>
|
||||||
|
<text class="report-summary__value">{{ productName }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="section__desc">{{ isZhongjianReport ? "正式结果凭证,中检报告文件可在下方查看。" : "正式结果凭证,支持编号与二维码验真。" }}</view>
|
<view class="report-summary__row">
|
||||||
<view class="certificate-header__meta">
|
<text class="report-summary__label">检测机构</text>
|
||||||
<text class="certificate-meta-chip">报告编号 {{ detail.report_header.report_no }}</text>
|
<text class="report-summary__value">{{ institutionName }}</text>
|
||||||
<text class="certificate-meta-chip">出具日期 {{ detail.report_header.publish_time }}</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>
|
</view>
|
||||||
|
|
||||||
<view class="section report-result report-result--certificate">
|
<view class="report-tabs">
|
||||||
<text class="report-result__seal">已鉴定确认</text>
|
<view :class="['report-tab', activeTab === 'product' ? 'report-tab--active' : '']" @click="activeTab = 'product'">产品信息</view>
|
||||||
<view class="report-result__title">鉴定结论</view>
|
<view :class="['report-tab', activeTab === 'trace' ? 'report-tab--active' : '']" @click="activeTab = 'trace'">追溯信息</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>
|
||||||
|
|
||||||
<view class="section section-card">
|
<view v-if="activeTab === 'product'" class="report-panel">
|
||||||
<view class="section__title">商品信息</view>
|
<view v-for="item in productItems" :key="item.label" class="product-row">
|
||||||
<view class="report-meta__row">
|
<view class="product-row__label">{{ item.label }}</view>
|
||||||
<text class="report-meta__label">商品名称</text>
|
<view class="product-row__value" :class="item.label === '检测结论' ? 'product-row__value--result' : ''">
|
||||||
<text class="report-meta__value">{{ detail.product_info.product_name }}</text>
|
{{ item.value || "-" }}
|
||||||
</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 v-if="item.remark" class="product-row__remark">{{ item.remark }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="section section-card">
|
<view v-if="zhongjianReportFiles.length" class="inline-section">
|
||||||
<view class="section__title">鉴定信息</view>
|
<view class="inline-section__title">中检报告文件</view>
|
||||||
<view class="report-meta__row">
|
<view v-if="zhongjianImageFiles.length" class="asset-grid">
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view v-if="isZhongjianReport" class="section section-card">
|
|
||||||
<view class="section__title">中检报告文件</view>
|
|
||||||
<view class="section__desc">中检报告文件可在下方查看,报告验真请以报告凭证与吊牌组合验真结果为准。</view>
|
|
||||||
|
|
||||||
<view v-if="zhongjianReportImageAttachments.length" class="task-files" style="margin-top: 20rpx;">
|
|
||||||
<view
|
<view
|
||||||
v-for="item in zhongjianReportImageAttachments"
|
v-for="item in zhongjianImageFiles"
|
||||||
:key="item.file_id"
|
:key="item.file_url || item.file_id"
|
||||||
class="task-file"
|
class="asset-tile"
|
||||||
@click="previewEvidenceImage(item.file_url)"
|
@click="openAsset(item, zhongjianImageFiles)"
|
||||||
>
|
>
|
||||||
<image class="task-file__img" :src="item.thumbnail_url || item.file_url" mode="aspectFill" />
|
<image class="asset-tile__image" :src="item.thumbnail_url || item.file_url" mode="aspectFill" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view v-if="zhongjianOtherFiles.length" class="asset-list">
|
||||||
<view v-if="zhongjianReportOtherAttachments.length" style="margin-top: 20rpx;">
|
|
||||||
<view
|
<view
|
||||||
v-for="(item, index) in zhongjianReportOtherAttachments"
|
v-for="(item, index) in zhongjianOtherFiles"
|
||||||
:key="item.file_id"
|
:key="item.file_url || item.file_id"
|
||||||
class="info-list__row"
|
class="asset-list__item"
|
||||||
@click="openEvidenceAttachment(item)"
|
@click="openAsset(item, zhongjianReportFiles)"
|
||||||
>
|
>
|
||||||
<text class="info-list__label">{{ evidenceDisplayName(item, index) }}</text>
|
<text>{{ assetDisplayName(item, index) }}</text>
|
||||||
<text class="info-list__value">{{ evidenceTypeText(item.file_type) }}</text>
|
<text class="asset-list__type">{{ evidenceTypeText(item.file_type) }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="detail.evidence_attachments.length" class="section section-card">
|
<view v-else class="report-panel">
|
||||||
<view class="section__title">证据附件</view>
|
<view v-for="node in traceNodes" :key="node.code" class="trace-node">
|
||||||
<view class="section__desc">以下附件为本次报告留存的证据材料,可点击查看原图、视频或 PDF。</view>
|
<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="imageEvidenceAttachments.length" class="task-files" style="margin-top: 20rpx;">
|
<view v-if="node.assets.length" class="asset-grid">
|
||||||
<view
|
<view
|
||||||
v-for="item in imageEvidenceAttachments"
|
v-for="item in node.assets"
|
||||||
:key="item.file_id"
|
:key="item.file_url || item.file_id"
|
||||||
class="task-file"
|
class="asset-tile"
|
||||||
@click="previewEvidenceImage(item.file_url)"
|
@click="openAsset(item, node.assets)"
|
||||||
>
|
>
|
||||||
<image class="task-file__img" :src="item.thumbnail_url || item.file_url" mode="aspectFill" />
|
<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>
|
</view>
|
||||||
|
<view v-else class="trace-node__empty">暂无影像资料</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>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="section section-note">
|
<view class="fixed-action-bar report-actions">
|
||||||
<view class="section__title">说明</view>
|
<view class="btn btn--secondary" @click="contactService">联系我们</view>
|
||||||
<view class="section__desc">
|
<view class="btn btn--primary" @click="openAntiModal">防伪查询</view>
|
||||||
{{ detail.risk_notice_text }}
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="fixed-action-bar">
|
<view v-if="antiModalVisible" class="anti-modal-mask" @click="closeAntiModal">
|
||||||
<view class="btn btn--secondary" @click="downloadPdf">{{ downloading ? "下载中..." : "下载 PDF" }}</view>
|
<view class="anti-modal" @click.stop>
|
||||||
<view v-if="!isZhongjianReport" class="btn btn--primary" @click="goVerify">去验真</view>
|
<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>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.credential-box__qr {
|
.report-page {
|
||||||
overflow: hidden;
|
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%;
|
width: 100%;
|
||||||
height: 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);
|
color: var(--color-accent);
|
||||||
font-size: var(--font-size-xs);
|
font-weight: 800;
|
||||||
text-align: center;
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ export const adminApi = {
|
|||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveZhongjianAppraisalReport(data: { id: number; zhongjian_report_no: string; report_files: AdminFileAsset[]; qr_input: string }) {
|
saveZhongjianAppraisalReport(data: Record<string, unknown>) {
|
||||||
return request<{ id: number; report: Record<string, any> }>("/api/admin/appraisal-task/zhongjian-report/save", {
|
return request<{ id: number; report: Record<string, any> }>("/api/admin/appraisal-task/zhongjian-report/save", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"name": "安心验作业端",
|
"name": "安心验作业端",
|
||||||
"appid": "__UNI__E0C8390",
|
"appid": "__UNI__E0C8390",
|
||||||
"description": "安心验仓管与鉴定作业 Android App",
|
"description": "安心验仓管与鉴定作业 Android App",
|
||||||
"versionName": "1.0.0",
|
"versionName": "1.0.1",
|
||||||
"versionCode": "101",
|
"versionCode": "102",
|
||||||
"transformPx": false,
|
"transformPx": false,
|
||||||
"app-plus": {
|
"app-plus": {
|
||||||
"usingComponents": true,
|
"usingComponents": true,
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ const valuationDesc = ref("");
|
|||||||
const externalRemark = ref("");
|
const externalRemark = ref("");
|
||||||
const internalRemark = ref("");
|
const internalRemark = ref("");
|
||||||
const zhongjianReportNo = ref("");
|
const zhongjianReportNo = ref("");
|
||||||
|
const productName = ref("");
|
||||||
|
const categoryName = ref("");
|
||||||
|
const brandName = ref("");
|
||||||
|
const color = ref("");
|
||||||
|
const sizeSpec = ref("");
|
||||||
|
const serialNo = ref("");
|
||||||
const zhongjianFiles = ref<AdminFileAsset[]>([]);
|
const zhongjianFiles = ref<AdminFileAsset[]>([]);
|
||||||
const evidenceFiles = ref<AdminFileAsset[]>([]);
|
const evidenceFiles = ref<AdminFileAsset[]>([]);
|
||||||
const activePreviewVideo = ref<AdminFileAsset | null>(null);
|
const activePreviewVideo = ref<AdminFileAsset | null>(null);
|
||||||
@@ -86,6 +92,12 @@ function hydrate(detailData: AdminAppraisalTaskDetail) {
|
|||||||
externalRemark.value = detailData.result_info.external_remark || "";
|
externalRemark.value = detailData.result_info.external_remark || "";
|
||||||
internalRemark.value = detailData.result_info.internal_remark || "";
|
internalRemark.value = detailData.result_info.internal_remark || "";
|
||||||
zhongjianReportNo.value = detailData.zhongjian_report?.report_no || "";
|
zhongjianReportNo.value = detailData.zhongjian_report?.report_no || "";
|
||||||
|
productName.value = detailData.product_info.product_name || "";
|
||||||
|
categoryName.value = detailData.product_info.category_name || "";
|
||||||
|
brandName.value = detailData.product_info.brand_name || "";
|
||||||
|
color.value = detailData.product_info.color || "";
|
||||||
|
sizeSpec.value = detailData.product_info.size_spec || "";
|
||||||
|
serialNo.value = detailData.product_info.serial_no || "";
|
||||||
zhongjianFiles.value = [...(detailData.zhongjian_report?.files || [])];
|
zhongjianFiles.value = [...(detailData.zhongjian_report?.files || [])];
|
||||||
evidenceFiles.value = [...(detailData.result_info.attachments || [])];
|
evidenceFiles.value = [...(detailData.result_info.attachments || [])];
|
||||||
|
|
||||||
@@ -553,6 +565,14 @@ async function submitZhongjianReport() {
|
|||||||
showInfoToast("请填写中检报告编号");
|
showInfoToast("请填写中检报告编号");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!resultText.value.trim()) {
|
||||||
|
showInfoToast("请填写鉴定结论");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!productName.value.trim() && !categoryName.value.trim() && !brandName.value.trim()) {
|
||||||
|
showInfoToast("请先完善物品信息");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!zhongjianFiles.value.length) {
|
if (!zhongjianFiles.value.length) {
|
||||||
showInfoToast("请至少上传 1 个中检报告文件");
|
showInfoToast("请至少上传 1 个中检报告文件");
|
||||||
return;
|
return;
|
||||||
@@ -568,6 +588,19 @@ async function submitZhongjianReport() {
|
|||||||
await adminApi.saveZhongjianAppraisalReport({
|
await adminApi.saveZhongjianAppraisalReport({
|
||||||
id: detail.value.task_info.id,
|
id: detail.value.task_info.id,
|
||||||
zhongjian_report_no: zhongjianReportNo.value.trim(),
|
zhongjian_report_no: zhongjianReportNo.value.trim(),
|
||||||
|
product_info: {
|
||||||
|
category_id: detail.value.product_info.category_id,
|
||||||
|
product_name: productName.value.trim(),
|
||||||
|
category_name: categoryName.value.trim(),
|
||||||
|
brand_name: brandName.value.trim(),
|
||||||
|
color: color.value.trim(),
|
||||||
|
size_spec: sizeSpec.value.trim(),
|
||||||
|
serial_no: serialNo.value.trim(),
|
||||||
|
},
|
||||||
|
result_text: resultText.value.trim(),
|
||||||
|
result_desc: resultDesc.value.trim(),
|
||||||
|
attachments: evidenceFiles.value,
|
||||||
|
key_points: templateKeyPointsPayload(),
|
||||||
report_files: zhongjianFiles.value,
|
report_files: zhongjianFiles.value,
|
||||||
qr_input: qrInput,
|
qr_input: qrInput,
|
||||||
});
|
});
|
||||||
@@ -764,6 +797,77 @@ onShow(() => {
|
|||||||
<view v-else class="card">
|
<view v-else class="card">
|
||||||
<view class="card-title">中检报告</view>
|
<view class="card-title">中检报告</view>
|
||||||
<view class="stack" style="margin-top: 18rpx">
|
<view class="stack" style="margin-top: 18rpx">
|
||||||
|
<view class="card-desc">报告展示信息</view>
|
||||||
|
<input v-model="productName" class="field" :disabled="isTaskReadonly" placeholder="产品名称" />
|
||||||
|
<input v-model="categoryName" class="field" :disabled="isTaskReadonly" placeholder="品类" />
|
||||||
|
<input v-model="brandName" class="field" :disabled="isTaskReadonly" placeholder="品牌" />
|
||||||
|
<view class="meta-grid">
|
||||||
|
<input v-model="color" class="field" :disabled="isTaskReadonly" placeholder="颜色" />
|
||||||
|
<input v-model="sizeSpec" class="field" :disabled="isTaskReadonly" placeholder="规格 / 尺寸" />
|
||||||
|
</view>
|
||||||
|
<input v-model="serialNo" class="field" :disabled="isTaskReadonly" placeholder="序列号 / 编码" />
|
||||||
|
<textarea v-model="resultText" class="textarea" :disabled="isTaskReadonly" placeholder="鉴定结论" />
|
||||||
|
<textarea v-model="resultDesc" class="textarea" :disabled="isTaskReadonly" placeholder="结论说明" />
|
||||||
|
|
||||||
|
<view v-if="detail.appraisal_template?.key_points?.length" class="stack">
|
||||||
|
<view class="card-desc">模板项</view>
|
||||||
|
<view v-for="(item, index) in detail.appraisal_template.key_points" :key="`zhongjian-${item.point_code}`" class="stack">
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-label">{{ item.point_name }}</view>
|
||||||
|
<view class="meta-value">{{ item.point_type }}{{ item.is_required ? " · 必填" : "" }}</view>
|
||||||
|
</view>
|
||||||
|
<input
|
||||||
|
:value="item.point_value"
|
||||||
|
class="field"
|
||||||
|
:disabled="isTaskReadonly"
|
||||||
|
:placeholder="`${item.point_name} 值`"
|
||||||
|
@input="updateTemplatePointFromInput(index, 'point_value', $event)"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
:value="item.point_remark"
|
||||||
|
class="textarea"
|
||||||
|
:disabled="isTaskReadonly"
|
||||||
|
:placeholder="`${item.point_name} 说明`"
|
||||||
|
@input="updateTemplatePointFromInput(index, 'point_remark', $event)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-desc evidence-title">鉴定图片 / 视频</view>
|
||||||
|
<view v-if="evidenceFiles.length" class="attachment-grid">
|
||||||
|
<view v-for="item in evidenceFiles" :key="`zj-evidence-${item.file_url}`" class="attachment-tile">
|
||||||
|
<view class="attachment-preview" @click="previewAttachment(evidenceFiles, item)">
|
||||||
|
<image v-if="isImageAsset(item)" class="attachment-thumb" :src="item.thumbnail_url || item.file_url" mode="aspectFill" />
|
||||||
|
<template v-else-if="isVideoAsset(item)">
|
||||||
|
<image v-if="item.thumbnail_url" class="attachment-thumb" :src="item.thumbnail_url" mode="aspectFill" />
|
||||||
|
<view v-else class="attachment-video-thumb">
|
||||||
|
<text class="attachment-video-label">视频</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<view v-else class="attachment-file-thumb">附件</view>
|
||||||
|
<view v-if="isVideoAsset(item)" class="attachment-play" @click.stop="previewAttachment(evidenceFiles, item)">▶</view>
|
||||||
|
</view>
|
||||||
|
<view class="attachment-meta">
|
||||||
|
<view class="attachment-name">{{ item.name || item.file_id }}</view>
|
||||||
|
<view class="attachment-actions">
|
||||||
|
<text class="attachment-type">{{ attachmentTypeLabel(item) }}</text>
|
||||||
|
<text v-if="!isTaskReadonly" class="attachment-remove" @click.stop="removeEvidenceFile(item.file_url)">删除</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!isTaskReadonly" class="upload-actions">
|
||||||
|
<button class="action-button action-button--secondary" :disabled="uploading" @click="chooseEvidenceImage">
|
||||||
|
<text class="action-symbol">+</text>
|
||||||
|
<text>{{ uploading ? "上传中" : "添加鉴定图片" }}</text>
|
||||||
|
</button>
|
||||||
|
<button class="action-button action-button--secondary" :disabled="uploading" @click="chooseEvidenceVideo">
|
||||||
|
<text class="action-symbol">+</text>
|
||||||
|
<text>{{ uploading ? "上传中" : "添加鉴定视频" }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-desc">中检报告文件</view>
|
||||||
<input v-model="zhongjianReportNo" class="field" :disabled="isTaskReadonly" placeholder="中检报告编号" />
|
<input v-model="zhongjianReportNo" class="field" :disabled="isTaskReadonly" placeholder="中检报告编号" />
|
||||||
<view v-if="zhongjianFiles.length" class="attachment-grid">
|
<view v-if="zhongjianFiles.length" class="attachment-grid">
|
||||||
<view v-for="item in zhongjianFiles" :key="item.file_url" class="attachment-tile">
|
<view v-for="item in zhongjianFiles" :key="item.file_url" class="attachment-tile">
|
||||||
|
|||||||
Reference in New Issue
Block a user