diff --git a/admin-web/src/api/admin.ts b/admin-web/src/api/admin.ts index 5b77655..d04b1d1 100644 --- a/admin-web/src/api/admin.ts +++ b/admin-web/src/api/admin.ts @@ -1790,7 +1790,7 @@ export const adminApi = { }; }>; }, - saveZhongjianAppraisalReport(data: { id: number; zhongjian_report_no: string; report_files: AdminFileAsset[]; qr_input: string }) { + saveZhongjianAppraisalReport(data: Record) { return request.post("/api/admin/appraisal-task/zhongjian-report/save", data) as Promise<{ code: number; message: string; diff --git a/admin-web/src/pages/appraisal-tasks/index.vue b/admin-web/src/pages/appraisal-tasks/index.vue index e796d28..f96986a 100644 --- a/admin-web/src/pages/appraisal-tasks/index.vue +++ b/admin-web/src/pages/appraisal-tasks/index.vue @@ -862,6 +862,20 @@ async function submitZhongjianReport() { ElMessage.warning("请填写中检报告编号"); 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) { ElMessage.warning("请至少上传 1 个中检报告文件"); return; @@ -876,6 +890,11 @@ async function submitZhongjianReport() { const response = await adminApi.saveZhongjianAppraisalReport({ id: detail.value.task_info.id, 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, qr_input: qrInput, }); @@ -1651,7 +1670,7 @@ onMounted(async () => {
input('zhongjian_report_no', '')); $qrInput = trim((string)$request->input('qr_input', '')); $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) { return api_error('任务 ID 不能为空', 422); } if ($reportNo === '') { return api_error('中检报告编号不能为空', 422); } + if ($resultText === '') { + return api_error('鉴定结论不能为空', 422); + } if (!$files) { return api_error('请至少上传 1 个中检报告文件', 422); } @@ -514,6 +523,15 @@ class AppraisalTasksController $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([ 'status' => 'completed', 'started_at' => $task['started_at'] ?: $now, @@ -531,14 +549,14 @@ class AppraisalTasksController 'task_id' => $id, 'order_id' => (int)$task['order_id'], 'result_status' => 'zhongjian_report', - 'result_text' => '以中检报告为准', - 'result_desc' => '中检报告已回传并由平台录入。', + 'result_text' => $resultText, + 'result_desc' => $resultDesc, 'condition_grade' => '', 'condition_desc' => '', 'valuation_min' => 0, 'valuation_max' => 0, 'valuation_desc' => '', - 'attachments_json' => json_encode($files, JSON_UNESCAPED_UNICODE), + 'attachments_json' => $attachments ? json_encode($attachments, JSON_UNESCAPED_UNICODE) : null, 'external_remark' => '', 'internal_remark' => '中检报告编号:' . $reportNo, 'updated_at' => $now, @@ -546,10 +564,12 @@ class AppraisalTasksController $resultId = Db::name('appraisal_task_results')->where('task_id', $id)->value('id'); if ($resultId) { Db::name('appraisal_task_results')->where('id', (int)$resultId)->update($resultPayload); + $savedResultId = (int)$resultId; } else { $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); $report = $this->findLatestAppraisalReport((int)$task['order_id']); @@ -578,7 +598,7 @@ class AppraisalTasksController 'order_id' => (int)$task['order_id'], 'node_code' => 'zhongjian_report_entered', 'node_text' => '中检报告已录入', - 'node_desc' => '报告录入人已录入中检报告编号并上传报告文件。', + 'node_desc' => '报告录入人已补全报告信息、录入中检报告编号并上传报告文件。', 'operator_type' => 'admin', 'operator_id' => $operatorId, 'occurred_at' => $now, @@ -1814,7 +1834,7 @@ class AppraisalTasksController 'appraisal_no' => $order['appraisal_no'] ?? '', 'report_type' => 'appraisal', '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_status' => 'pending_publish', 'publish_time' => null, @@ -1870,7 +1890,7 @@ class AppraisalTasksController 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'] ?? '') ?: $this->normalizeAssigneeName($finalReviewTask['assignee_name'] ?? ''); $reviewerName = $appraiserName; @@ -1899,6 +1919,11 @@ class AppraisalTasksController return $name; } + private function displayInstitutionName(string $serviceProvider): string + { + return $serviceProvider === 'zhongjian' ? '中检鉴定中心' : '安心检验'; + } + private function guardTaskOperator(Request $request, array $task): array { $adminId = (int)$request->header('x-admin-id', 0); diff --git a/server-api/app/controller/admin/ReportsController.php b/server-api/app/controller/admin/ReportsController.php index 6f0adf0..1d63d27 100644 --- a/server-api/app/controller/admin/ReportsController.php +++ b/server-api/app/controller/admin/ReportsController.php @@ -78,7 +78,7 @@ class ReportsController 'report_status_text' => $this->reportStatusText($item['report_status']), 'service_provider' => $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'], 'zhongjian_report_no' => (string)($item['zhongjian_report_no'] ?? ''), 'report_entry_admin_name' => (string)($item['report_entry_admin_name'] ?? ''), @@ -163,7 +163,7 @@ class ReportsController 'report_status_text' => $this->reportStatusText($report['report_status']), 'service_provider' => $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'], 'zhongjian_report_no' => (string)($report['zhongjian_report_no'] ?? ''), 'report_entry_admin_id' => (int)($report['report_entry_admin_id'] ?? 0), @@ -765,6 +765,6 @@ class ReportsController private function defaultInstitutionName(string $serviceProvider): string { - return $serviceProvider === 'zhongjian' ? '中检合作机构' : '安心验'; + return $serviceProvider === 'zhongjian' ? '中检鉴定中心' : '安心检验'; } } diff --git a/server-api/app/controller/app/ReportsController.php b/server-api/app/controller/app/ReportsController.php index 345e8ce..7f8db89 100644 --- a/server-api/app/controller/app/ReportsController.php +++ b/server-api/app/controller/app/ReportsController.php @@ -49,7 +49,7 @@ class ReportsController 'service_provider' => $item['service_provider'], 'status' => $published ? '已出报告' : '待出报告', 'result_text' => $published ? '正品' : '待出报告', - 'institution_name' => $item['institution_name'] ?: '安心验', + 'institution_name' => $this->displayInstitutionName((string)$item['service_provider']), 'publish_time' => $item['publish_time'], ]; }, $rows); @@ -94,7 +94,6 @@ class ReportsController $content = Db::name('report_contents')->where('report_id', $reportData['id'])->find(); $verify = Db::name('report_verifies')->where('report_id', $reportData['id'])->find() ?: []; $verify = $this->normalizeVerifyInfo($reportData, $verify); - $pdfUrl = $this->ensurePdfFile($request, $reportData, $content ?: [], $verify ?: []); $evidenceAttachments = $this->evidenceService()->normalize($content['evidence_attachments_json'] ?? null, $request); $zhongjianReportFiles = $this->evidenceService()->normalize($content['zhongjian_report_files_json'] ?? null, $request); $defaultRiskNotice = (new ContentService())->getReportRiskNotice((string)($reportData['report_type'] ?? 'appraisal')); @@ -105,6 +104,17 @@ class ReportsController 'valuation_snapshot' => $this->decodeJsonField($content['valuation_snapshot_json'] ?? null), '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([ 'report_header' => [ @@ -114,7 +124,7 @@ class ReportsController 'report_title' => $reportData['report_title'], 'report_status' => $reportData['report_status'], 'service_provider' => $reportData['service_provider'], - 'institution_name' => $reportData['institution_name'], + 'institution_name' => $this->displayInstitutionName((string)$reportData['service_provider']), 'publish_time' => $reportData['publish_time'], 'zhongjian_report_no' => (string)($reportData['zhongjian_report_no'] ?? ''), 'report_entry_admin_name' => (string)($reportData['report_entry_admin_name'] ?? ''), @@ -126,6 +136,9 @@ class ReportsController 'valuation_info' => $payload['valuation_snapshot'], 'evidence_attachments' => $evidenceAttachments, 'zhongjian_report_files' => $zhongjianReportFiles, + 'report_media' => $reportMedia, + 'product_display' => $productDisplay, + 'trace_info' => $traceInfo, 'risk_notice_text' => $payload['risk_notice_text'], 'verify_info' => [ '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 { if (is_array($value)) { @@ -171,49 +253,41 @@ class ReportsController 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') ->where('report_id', (int)$report['id']) ->where('file_type', 'pdf') ->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'])) { $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); } } $productInfo = $this->decodeJsonField($content['product_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')); - $relativeDir = 'uploads/reports/' . date('Ymd', strtotime($publishTime)); - $filename = $report['report_no'] . '.pdf'; - $relativePath = $relativeDir . '/' . $filename; $generator = new ReportPdfGenerator(); $pdfBinary = $generator->generate([ 'report_title' => $report['report_title'] ?? '鉴定报告', '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'] ?? '', 'publish_time' => $publishTime, 'result_text' => $resultInfo['result_text'] ?? '-', 'result_desc' => $resultInfo['result_desc'] ?? '-', 'product_name' => $productInfo['product_name'] ?? '-', - 'category_brand' => trim(($productInfo['category_name'] ?? '-') . ' / ' . ($productInfo['brand_name'] ?? '-')), - 'spec_info' => trim(($productInfo['color'] ?? '-') . ' / ' . ($productInfo['size_spec'] ?? '-')), - 'appraisers' => trim((string)($appraisalInfo['appraiser_name'] ?? '-')), - 'condition_grade' => $valuationInfo['condition_grade'] ?? '-', - 'valuation_range' => sprintf( - '¥%s - ¥%s', - $valuationInfo['valuation_min'] ?? 0, - $valuationInfo['valuation_max'] ?? 0 - ), + 'product_items' => $productDisplay['items'] ?? [], + 'hero_image' => $this->firstPdfHeroImage($reportMedia['images'] ?? []), + 'hero_image_labels' => $this->assetNameList($reportMedia['images'] ?? []), 'verify_info' => sprintf( '%s / %s', $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 : '-'), ]); - $this->storage()->putContents($relativePath, $pdfBinary); + $this->storage()->putContentsWithMimeType($relativePath, $pdfBinary, 'application/pdf'); $now = date('Y-m-d H:i:s'); $filePayload = [ @@ -243,6 +317,268 @@ class ReportsController 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 { $baseUrl = $this->normalizeH5BaseUrl($this->getSystemConfigValue('h5', 'page_base_url')); diff --git a/server-api/app/middleware/AppAuthMiddleware.php b/server-api/app/middleware/AppAuthMiddleware.php index 3f99bbd..e50c980 100644 --- a/server-api/app/middleware/AppAuthMiddleware.php +++ b/server-api/app/middleware/AppAuthMiddleware.php @@ -46,6 +46,7 @@ class AppAuthMiddleware implements MiddlewareInterface '/api/app/help-center', '/api/app/help-article/detail', '/api/app/report/detail', + '/api/app/report/anti-counterfeit/verify', '/api/app/verify', '/api/app/material-tag', '/api/app/material-tag/verify', diff --git a/server-api/app/support/ReportPdfGenerator.php b/server-api/app/support/ReportPdfGenerator.php index 7f32dad..75adf4a 100644 --- a/server-api/app/support/ReportPdfGenerator.php +++ b/server-api/app/support/ReportPdfGenerator.php @@ -6,49 +6,59 @@ class ReportPdfGenerator { 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 = [ 1 => '<< /Type /Catalog /Pages 2 0 R >>', 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), 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 >>', ]; + 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); } - private function buildContentStream(array $payload): string + private function buildContentStream(array $payload, ?array $heroImage): string { $title = $this->normalizeText((string)($payload['report_title'] ?? '鉴定报告')); $serviceProviderText = $this->normalizeText((string)($payload['service_provider_text'] ?? '-')); $institutionName = $this->normalizeText((string)($payload['institution_name'] ?? '-')); $reportNo = $this->normalizeText((string)($payload['report_no'] ?? '-')); $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'] ?? '-')); - $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'] ?? '-')); $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 = []; $y = 790; $blocks[] = $this->textBlock($title, 52, $y, 20); $y -= 32; - $blocks[] = $this->textBlock('正式报告凭证,请以编号验真结果为准。', 52, $y, 10); + $blocks[] = $this->textBlock('正式报告凭证,请以报告页防伪查询结果为准。', 52, $y, 10); $y -= 30; foreach ([ sprintf('报告编号:%s', $reportNo), - sprintf('出具机构:%s', $institutionName), + sprintf('检测机构:%s', $institutionName), sprintf('出具时间:%s', $publishTime), sprintf('服务类型:%s', $serviceProviderText), ] as $line) { @@ -56,29 +66,56 @@ class ReportPdfGenerator $y -= 22; } - $y -= 8; - $blocks[] = $this->textBlock(sprintf('鉴定结论:%s', $resultText), 52, $y, 16); - $y -= 26; - - foreach ($this->wrapText('结果说明:' . $resultDesc, 30) as $line) { - $blocks[] = $this->textBlock($line, 52, $y, 11); - $y -= 18; + if ($heroImage) { + $maxWidth = 230; + $maxHeight = 150; + $scale = min($maxWidth / $heroImage['width'], $maxHeight / $heroImage['height'], 1); + $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); + $y -= 18; + } + $y -= 8; } $y -= 8; - foreach ([ - sprintf('商品名称:%s', $productName), - sprintf('品类 / 品牌:%s', $categoryBrand), - sprintf('颜色 / 规格:%s', $specInfo), - sprintf('鉴定师:%s', $appraisers), - sprintf('成色评级:%s', $conditionGrade), - sprintf('估值区间:%s', $valuationRange), - sprintf('验真信息:%s', $verifyInfo), - ] as $line) { - $blocks[] = $this->textBlock($line, 52, $y, 11); - $y -= 20; + $blocks[] = $this->textBlock('产品信息', 52, $y, 15); + $y -= 24; + $blocks[] = $this->textBlock(sprintf('产品名称:%s', $productName), 52, $y, 12); + $y -= 22; + + foreach ($productItems as $item) { + if (!is_array($item)) { + continue; + } + $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; foreach ($this->wrapText('风险说明:' . $riskNotice, 30) as $line) { $blocks[] = $this->textBlock($line, 52, $y, 10); @@ -86,12 +123,32 @@ class ReportPdfGenerator } if ($y > 48) { - $blocks[] = $this->textBlock('安心验鉴定平台', 52, 42, 9); + $blocks[] = $this->textBlock('安心检验鉴定平台', 52, 42, 9); } 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 { $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 { $text = trim(str_replace(["\r\n", "\r", "\n", "\t"], [' ', ' ', ' ', ' '], $text)); diff --git a/server-api/config/route.php b/server-api/config/route.php index e89a65a..f72df04 100644 --- a/server-api/config/route.php +++ b/server-api/config/route.php @@ -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::get('/api/app/reports', [ReportsController::class, 'index']); 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/material-tag', [AppMaterialTagsController::class, 'show']); Route::post('/api/app/material-tag/verify', [AppMaterialTagsController::class, 'verify']); diff --git a/user-app/src/api/app.ts b/user-app/src/api/app.ts index 8ab6e44..54bcfa5 100644 --- a/user-app/src/api/app.ts +++ b/user-app/src/api/app.ts @@ -344,6 +344,27 @@ export interface ReportListItem { export interface ReportDetailData { evidence_attachments: EvidenceAttachmentAsset[]; zhongjian_report_files: EvidenceAttachmentAsset[]; + report_media: { + images: EvidenceAttachmentAsset[]; + }; + product_display: { + product_name: string; + institution_name: string; + items: Array<{ + label: string; + value: string; + remark?: string; + }>; + }; + trace_info: { + nodes: Array<{ + code: "inbound" | "appraisal" | "return"; + title: string; + occurred_at: string; + status: "completed" | "pending"; + assets: EvidenceAttachmentAsset[]; + }>; + }; report_header: { report_id: number; report_no: string; @@ -413,6 +434,12 @@ export interface MaterialTagVerifyResult { verify_count: number; } +export interface ReportAntiCounterfeitResult { + verify_passed: boolean; + verify_message: string; + verify_count: number; +} + export interface MessageSummaryData { total_count: number; unread_count: number; @@ -672,6 +699,12 @@ export const appApi = { params: { report_no: reportNo }, }); }, + verifyReportAntiCounterfeit(payload: { report_no: string; verify_code: string }) { + return request("/api/app/report/anti-counterfeit/verify", { + method: "POST", + data: payload, + }); + }, getMaterialTag(token: string) { return request("/api/app/material-tag", { params: { token }, diff --git a/user-app/src/mocks/app.ts b/user-app/src/mocks/app.ts index 0ed624f..8a2461d 100644 --- a/user-app/src/mocks/app.ts +++ b/user-app/src/mocks/app.ts @@ -368,6 +368,52 @@ export const reportsFallback: ReportListItem[] = [ export const reportDetailFallback: ReportDetailData = { evidence_attachments: [], zhongjian_report_files: [], + report_media: { + images: [ + { + file_id: "mock-report-image-1", + file_url: "https://dummyimage.com/1200x900/efe7df/8b1f2f&text=Appraisal+Image", + thumbnail_url: "https://dummyimage.com/480x360/efe7df/8b1f2f&text=Appraisal", + name: "鉴定图片", + file_type: "image", + mime_type: "image/jpeg", + }, + ], + }, + product_display: { + product_name: "Rolex 腕表", + institution_name: "中检鉴定中心", + items: [ + { label: "检测结论", value: "正品", remark: "综合当前送检资料与商品特征判断,符合正品特征。" }, + { label: "品牌", value: "Rolex" }, + { label: "主体颜色", value: "银盘" }, + ], + }, + trace_info: { + nodes: [ + { + code: "inbound", + title: "入仓", + occurred_at: "2026-04-18 12:10:00", + status: "completed", + assets: [], + }, + { + code: "appraisal", + title: "鉴定", + occurred_at: "2026-04-18 16:00:00", + status: "completed", + assets: [], + }, + { + code: "return", + title: "寄回", + occurred_at: "", + status: "pending", + assets: [], + }, + ], + }, report_header: { report_id: 1, report_no: "AXY-R-20260420-0001", @@ -375,7 +421,7 @@ export const reportDetailFallback: ReportDetailData = { report_title: "中检鉴定报告", report_status: "published", service_provider: "zhongjian", - institution_name: "中检合作机构", + institution_name: "中检鉴定中心", publish_time: "2026-04-18 18:26:00", zhongjian_report_no: "ZJ-20260418-0001", report_entry_admin_name: "王师傅", diff --git a/user-app/src/pages/report/detail.vue b/user-app/src/pages/report/detail.vue index def7fcf..6a370d9 100644 --- a/user-app/src/pages/report/detail.vue +++ b/user-app/src/pages/report/detail.vue @@ -4,42 +4,48 @@ import { onLoad } from "@dcloudio/uni-app"; import { appApi, type EvidenceAttachmentAsset, type ReportDetailData } from "../../api/app"; import { reportDetailFallback } from "../../mocks/app"; import { resolveErrorMessage } from "../../utils/feedback"; -import { resolveQrImageSource } from "../../utils/qrcode"; + +type ReportTab = "product" | "trace"; const detail = ref(reportDetailFallback); const downloading = ref(false); const loading = ref(false); const pageReady = ref(false); const loadError = ref(""); -const qrImageSource = computed(() => - resolveQrImageSource(detail.value.verify_info.verify_qrcode_url, detail.value.verify_info.verify_url), -); -const isZhongjianReport = computed(() => detail.value.report_header.service_provider === "zhongjian"); -const imageEvidenceAttachments = computed(() => - detail.value.evidence_attachments.filter((item) => item.file_type === "image"), -); -const otherEvidenceAttachments = computed(() => - detail.value.evidence_attachments.filter((item) => item.file_type !== "image"), -); -const zhongjianReportImageAttachments = computed(() => - (detail.value.zhongjian_report_files || []).filter((item) => item.file_type === "image"), -); -const zhongjianReportOtherAttachments = computed(() => - (detail.value.zhongjian_report_files || []).filter((item) => item.file_type !== "image"), -); +const activeTab = ref("product"); +const antiModalVisible = ref(false); +const antiCode = ref(""); +const antiVerifying = ref(false); +const antiResult = ref(null); -function goVerify() { - uni.navigateTo({ url: `/pages/verify/result?report_no=${detail.value.report_header.report_no}` }); -} - -function previewEvidenceImage(current: string) { - const urls = [...imageEvidenceAttachments.value, ...zhongjianReportImageAttachments.value].map((item) => item.file_url); - if (!urls.length) return; - uni.previewImage({ - urls, - current, - }); -} +const reportImages = computed(() => { + const images = detail.value.report_media?.images || []; + if (images.length) return images; + return detail.value.evidence_attachments.filter((item) => item.file_type === "image"); +}); +const productName = computed(() => + detail.value.product_display?.product_name + || detail.value.product_info.product_name + || "-", +); +const institutionName = computed(() => + detail.value.product_display?.institution_name + || detail.value.report_header.institution_name + || "-", +); +const productItems = computed(() => { + const items = detail.value.product_display?.items || []; + if (items.length) return items; + return [ + { label: "检测结论", value: detail.value.result_info.result_text || "-", remark: detail.value.result_info.result_desc || "" }, + { label: "品牌", value: detail.value.product_info.brand_name || "-" }, + ]; +}); +const traceNodes = computed(() => detail.value.trace_info?.nodes || []); +const zhongjianReportFiles = computed(() => detail.value.zhongjian_report_files || []); +const zhongjianImageFiles = computed(() => zhongjianReportFiles.value.filter((item) => item.file_type === "image")); +const zhongjianOtherFiles = computed(() => zhongjianReportFiles.value.filter((item) => item.file_type !== "image")); +const reportNo = computed(() => detail.value.report_header.report_no || ""); function evidenceTypeText(fileType: string) { if (fileType === "video") return "视频"; @@ -48,13 +54,19 @@ function evidenceTypeText(fileType: string) { return "附件"; } -function evidenceDisplayName(item: EvidenceAttachmentAsset, index: number) { +function assetDisplayName(item: EvidenceAttachmentAsset, index: number) { return item.name || `${evidenceTypeText(item.file_type)} ${index + 1}`; } -function openEvidenceAttachment(item: EvidenceAttachmentAsset) { +function previewImages(files: EvidenceAttachmentAsset[], current: string) { + const urls = files.filter((item) => item.file_type === "image").map((item) => item.file_url); + if (!urls.length) return; + uni.previewImage({ urls, current }); +} + +function openAsset(item: EvidenceAttachmentAsset, files: EvidenceAttachmentAsset[]) { if (item.file_type === "image") { - previewEvidenceImage(item.file_url); + previewImages(files, item.file_url); return; } @@ -97,13 +109,74 @@ function openEvidenceAttachment(item: EvidenceAttachmentAsset) { uni.showToast({ title: "当前附件类型暂不支持预览", icon: "none" }); } +function contactService() { + uni.navigateTo({ + url: `/pages/support/create?ticket_type=report_issue&prefill_title=${encodeURIComponent("报告咨询")}`, + }); +} + +function openAntiModal() { + antiCode.value = ""; + antiResult.value = null; + antiModalVisible.value = true; +} + +function closeAntiModal() { + if (antiVerifying.value) return; + antiModalVisible.value = false; +} + +function normalizeAntiCode(value: string) { + return value.replace(/\D/g, "").slice(0, 6); +} + +function handleAntiCodeInput(event: Event) { + const inputEvent = event as unknown as { + detail?: { value?: string }; + target?: { value?: string }; + }; + const detailValue = inputEvent.detail?.value; + const targetValue = inputEvent.target?.value; + antiCode.value = normalizeAntiCode(String(detailValue ?? targetValue ?? "")); + antiResult.value = null; +} + +async function submitAntiCounterfeit() { + const code = normalizeAntiCode(antiCode.value.trim()); + antiCode.value = code; + if (!code) { + uni.showToast({ title: "请输入防伪查询码", icon: "none" }); + return; + } + if (!reportNo.value) { + uni.showToast({ title: "报告编号为空", icon: "none" }); + return; + } + + antiVerifying.value = true; + try { + const result = await appApi.verifyReportAntiCounterfeit({ + report_no: reportNo.value, + verify_code: code, + }); + antiResult.value = { + passed: result.verify_passed, + message: result.verify_message, + }; + } catch (error) { + antiResult.value = { + passed: false, + message: resolveErrorMessage(error, "防伪查询失败,请稍后重试。"), + }; + } finally { + antiVerifying.value = false; + } +} + function downloadPdf() { const pdfUrl = detail.value.file_info?.pdf_url; if (!pdfUrl) { - uni.showToast({ - title: "报告文件暂未就绪", - icon: "none", - }); + uni.showToast({ title: "报告文件暂未就绪", icon: "none" }); return; } @@ -118,30 +191,17 @@ function downloadPdf() { url: pdfUrl, success: (response) => { if (response.statusCode !== 200 || !response.tempFilePath) { - uni.showToast({ - title: "报告下载失败", - icon: "none", - }); + uni.showToast({ title: "报告下载失败", icon: "none" }); return; } uni.openDocument({ filePath: response.tempFilePath, fileType: "pdf", showMenu: true, - fail: () => { - uni.showToast({ - title: "无法打开报告文件", - icon: "none", - }); - }, - }); - }, - fail: () => { - uni.showToast({ - title: "报告下载失败", - icon: "none", + fail: () => uni.showToast({ title: "无法打开报告文件", icon: "none" }), }); }, + fail: () => uni.showToast({ title: "报告下载失败", icon: "none" }), complete: () => { downloading.value = false; uni.hideLoading(); @@ -151,8 +211,8 @@ function downloadPdf() { onLoad(async (options) => { const id = Number(options?.id || 0); - const reportNo = String(options?.report_no || ""); - if (!id && !reportNo) { + const currentReportNo = String(options?.report_no || ""); + if (!id && !currentReportNo) { loadError.value = "缺少报告编号,无法查看详情。"; return; } @@ -161,7 +221,7 @@ onLoad(async (options) => { try { detail.value = await appApi.getReportDetail({ id: id || undefined, - report_no: reportNo || undefined, + report_no: currentReportNo || undefined, }); pageReady.value = true; } catch (error) { @@ -174,10 +234,10 @@ onLoad(async (options) => { diff --git a/work-app/src/api/admin.ts b/work-app/src/api/admin.ts index 09cf595..387de6a 100644 --- a/work-app/src/api/admin.ts +++ b/work-app/src/api/admin.ts @@ -518,7 +518,7 @@ export const adminApi = { data, }); }, - saveZhongjianAppraisalReport(data: { id: number; zhongjian_report_no: string; report_files: AdminFileAsset[]; qr_input: string }) { + saveZhongjianAppraisalReport(data: Record) { return request<{ id: number; report: Record }>("/api/admin/appraisal-task/zhongjian-report/save", { method: "POST", data, diff --git a/work-app/src/manifest.json b/work-app/src/manifest.json index b697de9..9fe13d7 100644 --- a/work-app/src/manifest.json +++ b/work-app/src/manifest.json @@ -2,8 +2,8 @@ "name": "安心验作业端", "appid": "__UNI__E0C8390", "description": "安心验仓管与鉴定作业 Android App", - "versionName": "1.0.0", - "versionCode": "101", + "versionName": "1.0.1", + "versionCode": "102", "transformPx": false, "app-plus": { "usingComponents": true, diff --git a/work-app/src/pages/task/detail.vue b/work-app/src/pages/task/detail.vue index bf75c41..019cdb4 100644 --- a/work-app/src/pages/task/detail.vue +++ b/work-app/src/pages/task/detail.vue @@ -24,6 +24,12 @@ const valuationDesc = ref(""); const externalRemark = ref(""); const internalRemark = 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([]); const evidenceFiles = ref([]); const activePreviewVideo = ref(null); @@ -86,6 +92,12 @@ function hydrate(detailData: AdminAppraisalTaskDetail) { externalRemark.value = detailData.result_info.external_remark || ""; internalRemark.value = detailData.result_info.internal_remark || ""; 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 || [])]; evidenceFiles.value = [...(detailData.result_info.attachments || [])]; @@ -553,6 +565,14 @@ async function submitZhongjianReport() { showInfoToast("请填写中检报告编号"); return; } + if (!resultText.value.trim()) { + showInfoToast("请填写鉴定结论"); + return; + } + if (!productName.value.trim() && !categoryName.value.trim() && !brandName.value.trim()) { + showInfoToast("请先完善物品信息"); + return; + } if (!zhongjianFiles.value.length) { showInfoToast("请至少上传 1 个中检报告文件"); return; @@ -568,6 +588,19 @@ async function submitZhongjianReport() { await adminApi.saveZhongjianAppraisalReport({ id: detail.value.task_info.id, 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, qr_input: qrInput, }); @@ -764,6 +797,77 @@ onShow(() => { 中检报告 + 报告展示信息 + + + + + + + + +