Forráskód Böngészése

feat(risk-factoryVisit): 品控访厂(3.1.2)PC 端前端完整实现

- 列表页 src/views/risk/factoryVisit/index.vue:11 列展示 + 按状态动态操作按钮
- 报告弹框 src/views/risk/factoryVisit/components/ReportDialog.vue:7 模块评分
- API 封装 src/api/risk/factoryVisit.ts:8 个接口(list/getInfo/add/edit/remove/export/submitReport/audit)
- 类型扩展 src/api/risk/types.ts:4 个新接口(RiskFactoryVisitBo/Vo/ReportBo/ReportVo)
- 路由 src/router/index.ts:/risk/factoryVisit 挂在风控平台下
yangjingjing 2 napja
szülő
commit
c18176dac8

+ 73 - 0
src/api/risk/factoryVisit.ts

@@ -0,0 +1,73 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { RiskFactoryVisitBo, RiskFactoryVisitVo } from '@/api/risk/types';
+
+// 查询访厂任务列表
+export const listFactoryVisit = (query: RiskFactoryVisitBo): AxiosPromise<RiskFactoryVisitVo[]> => {
+  return request({
+    url: '/risk/factoryVisit/list',
+    method: 'get',
+    params: query
+  });
+};
+
+// 查询访厂任务详情(含报告子表)
+export const getFactoryVisit = (id: string | number): AxiosPromise<RiskFactoryVisitVo> => {
+  return request({
+    url: '/risk/factoryVisit/' + id,
+    method: 'get'
+  });
+};
+
+// 新增访厂任务
+export const addFactoryVisit = (data: RiskFactoryVisitBo): AxiosPromise => {
+  return request({
+    url: '/risk/factoryVisit',
+    method: 'post',
+    data: data
+  });
+};
+
+// 修改访厂任务
+export const updateFactoryVisit = (data: RiskFactoryVisitBo): AxiosPromise => {
+  return request({
+    url: '/risk/factoryVisit',
+    method: 'put',
+    data: data
+  });
+};
+
+// 删除访厂任务
+export const delFactoryVisit = (ids: string | number | Array<string | number>): AxiosPromise => {
+  return request({
+    url: '/risk/factoryVisit/' + ids,
+    method: 'delete'
+  });
+};
+
+// 导出访厂任务
+export const exportFactoryVisit = (query: RiskFactoryVisitBo): AxiosPromise => {
+  return request({
+    url: '/risk/factoryVisit/export',
+    method: 'post',
+    params: query
+  });
+};
+
+// 提交访厂报告(7 个模块评分)
+export const submitFactoryVisitReport = (data: any): AxiosPromise<{ reportId: number }> => {
+  return request({
+    url: '/risk/factoryVisit/submitReport',
+    method: 'post',
+    data: data
+  });
+};
+
+// 审核访厂任务
+export const auditFactoryVisit = (data: RiskFactoryVisitBo): AxiosPromise => {
+  return request({
+    url: '/risk/factoryVisit/audit',
+    method: 'put',
+    data: data
+  });
+};

+ 85 - 0
src/api/risk/types.ts

@@ -830,3 +830,88 @@ export interface RiskInspectionRecordVo {
   updateTime: string;
 }
 
+// ==================== 访厂管理 ====================
+export interface RiskFactoryVisitBo {
+  id?: string | number;
+  visitCode?: string;
+  companyName?: string;
+  companyAddress?: string;
+  visitDate?: string;
+  visitorId?: string | number;
+  visitorName?: string;
+  auditDept?: string;
+  productScope?: string;
+  reviewerId?: string | number;
+  reviewerName?: string;
+  totalScore?: number;
+  status?: string;
+  remark?: string;
+  keyword?: string;
+  /** 审核结论(通过/不通过/待定) - 仅审核接口使用 */
+  auditResult?: string;
+  /** 审核意见 */
+  auditRemark?: string;
+  /** 审核人ID */
+  auditorId?: string | number;
+  /** 审核人姓名 */
+  auditorName?: string;
+  params?: any;
+}
+
+export interface RiskFactoryVisitVo {
+  id: string | number;
+  visitCode: string;
+  companyName: string;
+  companyAddress: string;
+  visitDate: string;
+  visitorId: string | number;
+  visitorName: string;
+  auditDept: string;
+  productScope: string;
+  reviewerId: string | number;
+  reviewerName: string;
+  totalScore: number;
+  auditResult: string;
+  auditRemark: string;
+  auditorId: string | number;
+  auditorName: string;
+  auditTime: string;
+  status: string;
+  remark: string;
+  createBy: string;
+  createTime: string;
+  updateBy: string;
+  updateTime: string;
+  report?: RiskFactoryVisitReportVo;
+}
+
+export interface RiskFactoryVisitReportVo {
+  id: string | number;
+  visitId: string | number;
+  basicEvaluate: any;
+  esgScore: any;
+  gmpScore: any;
+  iso14001Score: any;
+  iso45001Score: any;
+  iso9001Score: any;
+  allergenEvaluate: any;
+  summary: string;
+}
+
+export interface RiskFactoryVisitReportBo {
+  id?: string | number;
+  visitId: string | number;
+  basicEvaluate?: any;
+  esgScore?: any;
+  gmpScore?: any;
+  iso14001Score?: any;
+  iso45001Score?: any;
+  iso9001Score?: any;
+  allergenEvaluate?: any;
+  summary?: string;
+  /** 总分(可由前端传入,否则后端按各模块分数自动求和) */
+  totalScore?: number;
+  /** 访厂日期(写入主表) */
+  visitDate?: string;
+}
+

+ 6 - 0
src/router/index.ts

@@ -325,6 +325,12 @@ export const dynamicRoutes: RouteRecordRaw[] = [
         component: () => import('@/views/risk/qualityInspectionTask/index.vue'),
         name: 'RiskQualityInspectionTask',
         meta: { title: '品控巡检', icon: 'list', permissions: ['risk:qualityInspectionTask:list'] }
+      },
+      {
+        path: 'factoryVisit',
+        component: () => import('@/views/risk/factoryVisit/index.vue'),
+        name: 'RiskFactoryVisit',
+        meta: { title: '品控访厂', icon: 'guide', permissions: ['risk:factoryVisit:list'] }
       }
     ]
   }

+ 601 - 0
src/views/risk/factoryVisit/components/ReportDialog.vue

@@ -0,0 +1,601 @@
+<template>
+  <el-dialog
+    :model-value="visible"
+    title="填写访厂报告"
+    width="1000px"
+    append-to-body
+    :close-on-click-modal="false"
+    :before-close="handleClose"
+    top="3vh"
+    class="factory-report-dialog"
+  >
+    <template v-if="visit">
+      <!-- 顶部基础信息 -->
+      <el-descriptions :column="3" border size="small" class="mb-3">
+        <el-descriptions-item label="访厂ID">{{ visit.visitCode }}</el-descriptions-item>
+        <el-descriptions-item label="公司名称">{{ visit.companyName }}</el-descriptions-item>
+        <el-descriptions-item label="审核部门">{{ visit.auditDept || '-' }}</el-descriptions-item>
+      </el-descriptions>
+
+      <el-tabs v-model="activeTab" type="border-card">
+        <!-- 模块 1:基础信息核查 -->
+        <el-tab-pane label="1.基础信息核查" name="basic">
+          <el-form label-width="280px">
+            <h4 class="section-title">1.1 资质合法</h4>
+            <el-form-item label="营业执照齐全有效">
+              <el-select v-model="form.basicEvaluate.businessLicense" placeholder="请选择" style="width: 160px">
+                <el-option label="是" value="是" />
+                <el-option label="否" value="否" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="食品经营许可证/生产许可证齐全">
+              <el-select v-model="form.basicEvaluate.foodLicense" placeholder="请选择" style="width: 160px">
+                <el-option label="是" value="是" />
+                <el-option label="否" value="否" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="检验检疫证明齐全(生鲜肉/畜/禽)">
+              <el-select v-model="form.basicEvaluate.inspectionCert" placeholder="请选择" style="width: 160px">
+                <el-option label="是" value="是" />
+                <el-option label="否" value="否" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="进口食品许可及检验检疫证明">
+              <el-select v-model="form.basicEvaluate.importCert" placeholder="请选择" style="width: 160px">
+                <el-option label="是" value="是" />
+                <el-option label="否" value="否" />
+              </el-select>
+            </el-form-item>
+            <h4 class="section-title">1.2 经营合法</h4>
+            <el-form-item label="生产经营活动及员工合法合规">
+              <el-select v-model="form.basicEvaluate.operationLegal" placeholder="请选择" style="width: 160px">
+                <el-option label="是" value="是" />
+                <el-option label="否" value="否" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="产品无虚假标注(批号/日期)">
+              <el-select v-model="form.basicEvaluate.productLabel" placeholder="请选择" style="width: 160px">
+                <el-option label="是" value="是" />
+                <el-option label="否" value="否" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="存储/运输/分包活动合法合规">
+              <el-select v-model="form.basicEvaluate.logisticsLegal" placeholder="请选择" style="width: 160px">
+                <el-option label="是" value="是" />
+                <el-option label="否" value="否" />
+              </el-select>
+            </el-form-item>
+            <h4 class="section-title">1.5 GMP 标准</h4>
+            <el-form-item label="GMP 认证齐全有效,覆盖生产范围">
+              <el-select v-model="form.basicEvaluate.gmpCert" placeholder="请选择" style="width: 160px">
+                <el-option label="是" value="是" />
+                <el-option label="否" value="否" />
+              </el-select>
+            </el-form-item>
+            <h4 class="section-title">1.6 三大 ISO 标准</h4>
+            <el-form-item label="ISO9001/14001/45001 证书齐全有效">
+              <el-select v-model="form.basicEvaluate.isoCert" placeholder="请选择" style="width: 160px">
+                <el-option label="是" value="是" />
+                <el-option label="否" value="否" />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+
+        <!-- 模块 2:ESG 审查(总分 30:E12+S12+G6) -->
+        <el-tab-pane label="2.ESG 审查" name="esg">
+          <el-form label-width="380px">
+            <h4 class="section-title">环境 E(12 分)</h4>
+            <el-form-item :label="`碳排放管理(0~${SCORE.esg.carbon})`">
+              <el-input-number v-model="form.esgScore.carbon" :min="0" :max="SCORE.esg.carbon" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`废弃物管理(0~${SCORE.esg.waste})`">
+              <el-input-number v-model="form.esgScore.waste" :min="0" :max="SCORE.esg.waste" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`三废排放(0~${SCORE.esg.emission})`">
+              <el-input-number v-model="form.esgScore.emission" :min="0" :max="SCORE.esg.emission" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`节能降耗(0~${SCORE.esg.energy})`">
+              <el-input-number v-model="form.esgScore.energy" :min="0" :max="SCORE.esg.energy" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <h4 class="section-title">社会 S(12 分)</h4>
+            <el-form-item :label="`员工权益(0~${SCORE.esg.labor})`">
+              <el-input-number v-model="form.esgScore.labor" :min="0" :max="SCORE.esg.labor" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`职业健康(0~${SCORE.esg.health})`">
+              <el-input-number v-model="form.esgScore.health" :min="0" :max="SCORE.esg.health" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`供应链责任(0~${SCORE.esg.supply})`">
+              <el-input-number v-model="form.esgScore.supply" :min="0" :max="SCORE.esg.supply" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`社区责任(0~${SCORE.esg.community})`">
+              <el-input-number v-model="form.esgScore.community" :min="0" :max="SCORE.esg.community" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <h4 class="section-title">治理 G(6 分)</h4>
+            <el-form-item :label="`ESG管理制度(0~${SCORE.esg.management})`">
+              <el-input-number v-model="form.esgScore.management" :min="0" :max="SCORE.esg.management" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`反腐败/反商业贿赂(0~${SCORE.esg.antibribery})`">
+              <el-input-number v-model="form.esgScore.antibribery" :min="0" :max="SCORE.esg.antibribery" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <div class="score-summary">当前 ESG 小计: {{ esgTotal }} / 30</div>
+          </el-form>
+        </el-tab-pane>
+
+        <!-- 模块 3:GMP 审查(总分 20) -->
+        <el-tab-pane label="3.GMP 审查" name="gmp">
+          <el-form label-width="380px">
+            <el-form-item :label="`车间布局/洁净区(0~${SCORE.gmp.layout})`">
+              <el-input-number v-model="form.gmpScore.layout" :min="0" :max="SCORE.gmp.layout" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`生产工艺/批记录(0~${SCORE.gmp.process})`">
+              <el-input-number v-model="form.gmpScore.process" :min="0" :max="SCORE.gmp.process" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`成品检验/召回机制(0~${SCORE.gmp.release})`">
+              <el-input-number v-model="form.gmpScore.release" :min="0" :max="SCORE.gmp.release" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`原材料供应商管控(0~${SCORE.gmp.material})`">
+              <el-input-number v-model="form.gmpScore.material" :min="0" :max="SCORE.gmp.material" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`设备清洁/校准/验证(0~${SCORE.gmp.equipment})`">
+              <el-input-number v-model="form.gmpScore.equipment" :min="0" :max="SCORE.gmp.equipment" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <div class="score-summary">当前 GMP 小计: {{ gmpTotal }} / 20</div>
+          </el-form>
+        </el-tab-pane>
+
+        <!-- 模块 4:ISO14001(总分 15) -->
+        <el-tab-pane label="4.ISO14001" name="iso14001">
+          <el-form label-width="380px">
+            <el-form-item :label="`环境管理手册/程序文件(0~${SCORE.iso14001.manual})`">
+              <el-input-number v-model="form.iso14001Score.manual" :min="0" :max="SCORE.iso14001.manual" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`环保设施/监测记录(0~${SCORE.iso14001.facility})`">
+              <el-input-number v-model="form.iso14001Score.facility" :min="0" :max="SCORE.iso14001.facility" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`环境因素识别/风险管控(0~${SCORE.iso14001.factor})`">
+              <el-input-number v-model="form.iso14001Score.factor" :min="0" :max="SCORE.iso14001.factor" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`内审/管理评审/持续改进(0~${SCORE.iso14001.review})`">
+              <el-input-number v-model="form.iso14001Score.review" :min="0" :max="SCORE.iso14001.review" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <div class="score-summary">当前 ISO14001 小计: {{ iso14001Total }} / 15</div>
+          </el-form>
+        </el-tab-pane>
+
+        <!-- 模块 5:ISO45001(总分 15) -->
+        <el-tab-pane label="5.ISO45001" name="iso45001">
+          <el-form label-width="380px">
+            <el-form-item :label="`职业健康手册/程序文件(0~${SCORE.iso45001.manual})`">
+              <el-input-number v-model="form.iso45001Score.manual" :min="0" :max="SCORE.iso45001.manual" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`安全培训/应急演练(0~${SCORE.iso45001.training})`">
+              <el-input-number v-model="form.iso45001Score.training" :min="0" :max="SCORE.iso45001.training" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`职业危害/防控措施(0~${SCORE.iso45001.hazard})`">
+              <el-input-number v-model="form.iso45001Score.hazard" :min="0" :max="SCORE.iso45001.hazard" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`工伤/职业病记录与整改(0~${SCORE.iso45001.incident})`">
+              <el-input-number v-model="form.iso45001Score.incident" :min="0" :max="SCORE.iso45001.incident" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <div class="score-summary">当前 ISO45001 小计: {{ iso45001Total }} / 15</div>
+          </el-form>
+        </el-tab-pane>
+
+        <!-- 模块 6:ISO9001(总分 20) -->
+        <el-tab-pane label="6.ISO9001" name="iso9001">
+          <el-form label-width="380px">
+            <el-form-item :label="`质量管理手册/目标(0~${SCORE.iso9001.manual})`">
+              <el-input-number v-model="form.iso9001Score.manual" :min="0" :max="SCORE.iso9001.manual" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`过程质量管控/记录(0~${SCORE.iso9001.process})`">
+              <el-input-number v-model="form.iso9001Score.process" :min="0" :max="SCORE.iso9001.process" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`内审/客户投诉/改进(0~${SCORE.iso9001.review})`">
+              <el-input-number v-model="form.iso9001Score.review" :min="0" :max="SCORE.iso9001.review" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`供应商评价/采购追溯(0~${SCORE.iso9001.supplier})`">
+              <el-input-number v-model="form.iso9001Score.supplier" :min="0" :max="SCORE.iso9001.supplier" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <el-form-item :label="`设备校准/检测仪器(0~${SCORE.iso9001.equipment})`">
+              <el-input-number v-model="form.iso9001Score.equipment" :min="0" :max="SCORE.iso9001.equipment" :step="1" controls-position="right" />
+              <span class="score-tip">分</span>
+            </el-form-item>
+            <div class="score-summary">当前 ISO9001 小计: {{ iso9001Total }} / 20</div>
+          </el-form>
+        </el-tab-pane>
+
+        <!-- 模块 7:致敏原管控 -->
+        <el-tab-pane label="7.致敏原管控" name="allergen">
+          <el-form label-width="320px">
+            <h4 class="section-title">7.1 访问前准备核查</h4>
+            <el-form-item label="提前获取工厂致敏原清单">
+              <el-select v-model="form.allergenEvaluate.prep1.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.prep1.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="访问人员已完成致敏原认知培训">
+              <el-select v-model="form.allergenEvaluate.prep2.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.prep2.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="按特性准备防护装备(N95/防护服)">
+              <el-select v-model="form.allergenEvaluate.prep3.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.prep3.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="访问人员无急性过敏症状">
+              <el-select v-model="form.allergenEvaluate.prep4.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.prep4.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+
+            <h4 class="section-title">7.2 现场区域管控核查</h4>
+            <el-form-item label="致敏原区物理隔离到位">
+              <el-select v-model="form.allergenEvaluate.area1.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.area1.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="致敏原区域有醒目标识">
+              <el-select v-model="form.allergenEvaluate.area2.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.area2.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="区域入口有准入要求提示">
+              <el-select v-model="form.allergenEvaluate.area3.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.area3.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="致敏原仓储区分区存放">
+              <el-select v-model="form.allergenEvaluate.area4.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.area4.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+
+            <h4 class="section-title">7.3 人员行为与防护管控核查</h4>
+            <el-form-item label="穿戴/脱卸防护装备合规">
+              <el-select v-model="form.allergenEvaluate.person1.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.person1.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="禁止跨致敏原/非致敏原区流动">
+              <el-select v-model="form.allergenEvaluate.person2.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.person2.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="防护装备按规定集中处理">
+              <el-select v-model="form.allergenEvaluate.person3.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.person3.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="员工已接受致敏原专项培训">
+              <el-select v-model="form.allergenEvaluate.person4.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.person4.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+
+            <h4 class="section-title">7.4 设备与清洁管控核查</h4>
+            <el-form-item label="致敏原专用设备无混用">
+              <el-select v-model="form.allergenEvaluate.equip1.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.equip1.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="设备清洁流程规范">
+              <el-select v-model="form.allergenEvaluate.equip2.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.equip2.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="清洁验证记录完整可追溯">
+              <el-select v-model="form.allergenEvaluate.equip3.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.equip3.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="清洁工具分区专用">
+              <el-select v-model="form.allergenEvaluate.equip4.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.equip4.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+
+            <h4 class="section-title">7.5 台账与追溯管控核查</h4>
+            <el-form-item label="致敏原台账完整可追溯">
+              <el-select v-model="form.allergenEvaluate.log1.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.log1.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="定期巡检与整改闭环">
+              <el-select v-model="form.allergenEvaluate.log2.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.log2.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+            <el-form-item label="变更(原料/工序)有评估记录">
+              <el-select v-model="form.allergenEvaluate.log3.result" placeholder="请选择" style="width: 160px">
+                <el-option label="合格" value="合格" />
+                <el-option label="不合格" value="不合格" />
+                <el-option label="不适用" value="不适用" />
+              </el-select>
+              <el-input v-model="form.allergenEvaluate.log3.remark" placeholder="问题描述/说明" style="width: 320px; margin-left: 8px" />
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+
+        <!-- 7.6 问题汇总 -->
+        <el-tab-pane label="7.6 问题汇总" name="summary">
+          <el-form label-width="auto">
+            <el-form-item label="问题描述及整改措施">
+              <el-input
+                v-model="form.summary"
+                type="textarea"
+                :rows="12"
+                placeholder="请填写整体问题描述及整改要求(无预设选项)"
+                maxlength="2000"
+                show-word-limit
+              />
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+
+      <!-- 总分显示 -->
+      <el-alert
+        class="mt-3"
+        :title="`访查实际得分: ${totalScore} / 100`"
+        type="info"
+        show-icon
+        :closable="false"
+      />
+    </template>
+
+    <template #footer>
+      <el-button @click="handleClose">取 消</el-button>
+      <el-button type="primary" :loading="submitLoading" @click="onSubmit">提 交</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, watch } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { submitFactoryVisitReport } from '@/api/risk/factoryVisit';
+
+const props = defineProps<{
+  visible: boolean;
+  visit: any;
+}>();
+const emit = defineEmits<{
+  'update:visible': [v: boolean];
+  success: [];
+}>();
+
+const activeTab = ref('basic');
+const submitLoading = ref(false);
+
+// 评分上限常量(参考需求文档)
+const SCORE = {
+  esg: { carbon: 3, waste: 3, emission: 3, energy: 3, labor: 3, health: 3, supply: 3, community: 3, management: 3, antibribery: 3 },
+  gmp: { layout: 4, process: 4, release: 4, material: 4, equipment: 4 },
+  iso14001: { manual: 3, facility: 4, factor: 4, review: 4 },
+  iso45001: { manual: 3, training: 4, hazard: 4, incident: 4 },
+  iso9001: { manual: 4, process: 4, review: 4, supplier: 4, equipment: 4 }
+};
+
+// 初始化表单默认值
+const makeEmptyBasic = () => ({
+  businessLicense: '',
+  foodLicense: '',
+  inspectionCert: '',
+  importCert: '',
+  operationLegal: '',
+  productLabel: '',
+  logisticsLegal: '',
+  gmpCert: '',
+  isoCert: ''
+});
+const makeEmptyEsg = () => ({
+  carbon: 0, waste: 0, emission: 0, energy: 0,
+  labor: 0, health: 0, supply: 0, community: 0,
+  management: 0, antibribery: 0
+});
+const makeEmptyGmp = () => ({ layout: 0, process: 0, release: 0, material: 0, equipment: 0 });
+const makeEmptyIso = () => ({ manual: 0, facility: 0, factor: 0, review: 0 });
+const makeEmptyIso2 = () => ({ manual: 0, training: 0, hazard: 0, incident: 0 });
+const makeEmptyIso3 = () => ({ manual: 0, process: 0, review: 0, supplier: 0, equipment: 0 });
+const makeEmptyAllergen = () => ({
+  prep1: { result: '', remark: '' }, prep2: { result: '', remark: '' }, prep3: { result: '', remark: '' }, prep4: { result: '', remark: '' },
+  area1: { result: '', remark: '' }, area2: { result: '', remark: '' }, area3: { result: '', remark: '' }, area4: { result: '', remark: '' },
+  person1: { result: '', remark: '' }, person2: { result: '', remark: '' }, person3: { result: '', remark: '' }, person4: { result: '', remark: '' },
+  equip1: { result: '', remark: '' }, equip2: { result: '', remark: '' }, equip3: { result: '', remark: '' }, equip4: { result: '', remark: '' },
+  log1: { result: '', remark: '' }, log2: { result: '', remark: '' }, log3: { result: '', remark: '' }
+});
+
+const form = reactive({
+  basicEvaluate: makeEmptyBasic(),
+  esgScore: makeEmptyEsg(),
+  gmpScore: makeEmptyGmp(),
+  iso14001Score: makeEmptyIso(),
+  iso45001Score: makeEmptyIso2(),
+  iso9001Score: makeEmptyIso3(),
+  allergenEvaluate: makeEmptyAllergen(),
+  summary: ''
+});
+
+// 监听 visit 变化:加载已有报告数据(仅在有 report 时)
+watch(
+  () => props.visit,
+  (v) => {
+    if (!v) return;
+    const r = v.report;
+    if (r) {
+      // 已有报告数据,合并
+      Object.assign(form.basicEvaluate, r.basicEvaluate || {});
+      Object.assign(form.esgScore, r.esgScore || {});
+      Object.assign(form.gmpScore, r.gmpScore || {});
+      Object.assign(form.iso14001Score, r.iso14001Score || {});
+      Object.assign(form.iso45001Score, r.iso45001Score || {});
+      Object.assign(form.iso9001Score, r.iso9001Score || {});
+      if (r.allergenEvaluate) {
+        for (const k of Object.keys(r.allergenEvaluate)) {
+          if (form.allergenEvaluate[k]) {
+            form.allergenEvaluate[k] = { ...form.allergenEvaluate[k], ...(r.allergenEvaluate[k] || {}) };
+          }
+        }
+      }
+      form.summary = r.summary || '';
+    }
+  },
+  { immediate: true }
+);
+
+// 计算各模块小计
+const sumObj = (o: Record<string, number>) => Object.values(o).reduce((a, b) => a + (Number(b) || 0), 0);
+const esgTotal = computed(() => sumObj(form.esgScore));
+const gmpTotal = computed(() => sumObj(form.gmpScore));
+const iso14001Total = computed(() => sumObj(form.iso14001Score));
+const iso45001Total = computed(() => sumObj(form.iso45001Score));
+const iso9001Total = computed(() => sumObj(form.iso9001Score));
+const totalScore = computed(
+  () => esgTotal.value + gmpTotal.value + iso14001Total.value + iso45001Total.value + iso9001Total.value
+);
+
+const handleClose = async () => {
+  try {
+    await ElMessageBox.confirm('报告尚未提交,确认关闭?', '提示', { type: 'warning' });
+    emit('update:visible', false);
+  } catch {
+    // 用户取消
+  }
+};
+
+const onSubmit = async () => {
+  submitLoading.value = true;
+  try {
+    const payload = {
+      visitId: props.visit.id,
+      basicEvaluate: form.basicEvaluate,
+      esgScore: form.esgScore,
+      gmpScore: form.gmpScore,
+      iso14001Score: form.iso14001Score,
+      iso45001Score: form.iso45001Score,
+      iso9001Score: form.iso9001Score,
+      allergenEvaluate: form.allergenEvaluate,
+      summary: form.summary,
+      totalScore: totalScore.value,
+      visitDate: props.visit.visitDate
+    };
+    const res: any = await submitFactoryVisitReport(payload);
+    ElMessage.success(`报告提交成功,得分 ${totalScore.value} 分,已转待审核`);
+    emit('update:visible', false);
+    emit('success');
+  } finally {
+    submitLoading.value = false;
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.factory-report-dialog :deep(.el-dialog__body) {
+  max-height: 75vh;
+  overflow-y: auto;
+}
+.section-title {
+  font-weight: 600;
+  font-size: 14px;
+  color: #303133;
+  border-left: 3px solid #409eff;
+  padding-left: 10px;
+  margin: 16px 0 12px 0;
+}
+.score-tip {
+  margin-left: 8px;
+  color: #909399;
+}
+.score-summary {
+  margin-top: 16px;
+  padding: 8px 16px;
+  background: #ecf5ff;
+  border-radius: 4px;
+  color: #409eff;
+  font-weight: 600;
+  text-align: right;
+}
+</style>

+ 774 - 0
src/views/risk/factoryVisit/index.vue

@@ -0,0 +1,774 @@
+<template>
+  <div class="p-2 factory-visit-page">
+    <!-- 搜索区域 -->
+    <transition
+      :enter-active-class="proxy?.animate.searchAnimate.enter"
+      :leave-active-class="proxy?.animate.searchAnimate.leave"
+    >
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
+            <el-form-item label="访厂ID" prop="visitCode">
+              <el-input
+                v-model="queryParams.visitCode"
+                placeholder="请输入访厂ID"
+                clearable
+                style="width: 180px"
+                @keyup.enter="handleQuery"
+              />
+            </el-form-item>
+            <el-form-item label="公司名称" prop="companyName">
+              <el-input
+                v-model="queryParams.companyName"
+                placeholder="请输入公司名称"
+                clearable
+                style="width: 180px"
+                @keyup.enter="handleQuery"
+              />
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 160px">
+                <el-option
+                  v-for="dict in visit_status_dict"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="审核结论" prop="auditResult">
+              <el-select
+                v-model="queryParams.auditResult"
+                placeholder="全部"
+                clearable
+                style="width: 160px"
+              >
+                <el-option
+                  v-for="dict in visit_audit_result_dict"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="访厂日期" prop="dateRange">
+              <el-date-picker
+                v-model="dateRange"
+                value-format="YYYY-MM-DD"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                style="width: 240px"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <!-- 数据表格区域 -->
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10" class="mb-2">
+          <el-col :span="1.5">
+            <el-button
+              v-hasPermi="['risk:factoryVisit:add']"
+              type="primary"
+              plain
+              icon="Plus"
+              @click="handleAdd"
+              >新增访厂任务</el-button
+            >
+          </el-col>
+          <el-col :span="1.5">
+            <el-button
+              v-hasPermi="['risk:factoryVisit:export']"
+              type="warning"
+              plain
+              icon="Download"
+              @click="handleExport"
+              >导出</el-button
+            >
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" @query-table="getList" />
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" :data="visitList" border>
+        <el-table-column label="访厂ID" align="center" prop="visitCode" width="150" />
+        <el-table-column
+          label="公司名称"
+          align="center"
+          prop="companyName"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column
+          label="地址"
+          align="center"
+          prop="companyAddress"
+          width="180"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column label="访厂日期" align="center" prop="visitDate" width="120">
+          <template #default="scope">
+            <span>{{ parseDate(scope.row.visitDate) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="访查实际得分" align="center" prop="totalScore" width="120">
+          <template #default="scope">
+            <span v-if="scope.row.totalScore != null">{{ scope.row.totalScore }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="审核结论" align="center" prop="auditResult" width="100">
+          <template #default="scope">
+            <el-tag v-if="scope.row.auditResult" :type="getAuditResultTag(scope.row.auditResult)">{{
+              scope.row.auditResult
+            }}</el-tag>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="访查人" align="center" prop="visitorName" width="100" />
+        <el-table-column
+          label="审核产品范围"
+          align="center"
+          prop="productScope"
+          width="160"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column label="审核部门" align="center" prop="auditDept" width="120" />
+        <el-table-column label="评审人" align="center" prop="reviewerName" width="100" />
+        <el-table-column label="状态" align="center" prop="status" width="100">
+          <template #default="scope">
+            <el-tag :type="getStatusTag(scope.row.status)">{{ scope.row.status }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="操作"
+          fixed="right"
+          align="center"
+          width="240"
+          class-name="small-padding fixed-width"
+        >
+          <template #default="scope">
+            <el-button
+              v-hasPermi="['risk:factoryVisit:list']"
+              link
+              type="primary"
+              icon="Document"
+              @click="handleView(scope.row)"
+              >查看详情</el-button
+            >
+            <el-button
+              v-if="scope.row.status === '待访厂'"
+              v-hasPermi="['risk:factoryVisit:report']"
+              link
+              type="primary"
+              icon="EditPen"
+              @click="handleFillReport(scope.row)"
+              >填写报告</el-button
+            >
+            <el-button
+              v-if="scope.row.status === '待审核'"
+              v-hasPermi="['risk:factoryVisit:audit']"
+              link
+              type="primary"
+              icon="CircleCheck"
+              @click="handleAudit(scope.row)"
+              >审核</el-button
+            >
+            <el-button
+              v-if="scope.row.status === '待访厂'"
+              v-hasPermi="['risk:factoryVisit:edit']"
+              link
+              type="primary"
+              icon="Edit"
+              @click="handleUpdate(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              v-if="scope.row.status === '待访厂'"
+              v-hasPermi="['risk:factoryVisit:remove']"
+              link
+              type="danger"
+              icon="Delete"
+              @click="handleDelete(scope.row)"
+              >删除</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="total > 0"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        :total="total"
+        @pagination="getList"
+      />
+    </el-card>
+
+    <!-- 新增/编辑对话框 -->
+    <el-dialog
+      v-model="dialog.visible"
+      :title="dialog.title"
+      width="780px"
+      append-to-body
+      @close="closeDialog"
+    >
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="公司名称" prop="companyName">
+              <el-input v-model="form.companyName" placeholder="请输入被访厂商名称" maxlength="200" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="审核部门" prop="auditDept">
+              <el-input v-model="form.auditDept" placeholder="请输入审核部门" maxlength="100" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="厂商地址" prop="companyAddress">
+              <el-input
+                v-model="form.companyAddress"
+                placeholder="请输入厂商地址"
+                maxlength="500"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="访厂日期" prop="visitDate">
+              <el-date-picker
+                v-model="form.visitDate"
+                type="date"
+                value-format="YYYY-MM-DD"
+                placeholder="请选择访厂日期"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="访查人" prop="visitorName">
+              <el-input
+                v-model="form.visitorName"
+                placeholder="请输入访查人"
+                maxlength="50"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="评审人" prop="reviewerName">
+              <el-input
+                v-model="form.reviewerName"
+                placeholder="请输入评审人"
+                maxlength="50"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="审核产品范围" prop="productScope">
+              <el-input
+                v-model="form.productScope"
+                placeholder="如:烘焙类、冷链食品等"
+                maxlength="200"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="备注" prop="remark">
+          <el-input
+            v-model="form.remark"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入备注"
+            maxlength="1000"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="cancel">取 消</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="submitForm">确 定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 查看详情对话框 -->
+    <el-dialog v-model="viewDialog.visible" title="访厂详情" width="900px" append-to-body>
+      <el-descriptions v-if="viewDialog.data" :column="2" border>
+        <el-descriptions-item label="访厂ID">{{
+          viewDialog.data.visitCode
+        }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="getStatusTag(viewDialog.data.status)">{{ viewDialog.data.status }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="公司名称">{{
+          viewDialog.data.companyName
+        }}</el-descriptions-item>
+        <el-descriptions-item label="审核部门">{{
+          viewDialog.data.auditDept || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="地址" :span="2">{{
+          viewDialog.data.companyAddress || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="访厂日期">{{
+          parseDate(viewDialog.data.visitDate)
+        }}</el-descriptions-item>
+        <el-descriptions-item label="访查人">{{
+          viewDialog.data.visitorName || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="评审人">{{
+          viewDialog.data.reviewerName || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="审核产品范围">{{
+          viewDialog.data.productScope || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="访查实际得分">{{
+          viewDialog.data.totalScore ?? '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="审核结论">
+          <el-tag
+            v-if="viewDialog.data.auditResult"
+            :type="getAuditResultTag(viewDialog.data.auditResult)"
+            >{{ viewDialog.data.auditResult }}</el-tag
+          >
+          <span v-else>-</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="审核人">{{
+          viewDialog.data.auditorName || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="审核时间">{{
+          parseDateTime(viewDialog.data.auditTime)
+        }}</el-descriptions-item>
+        <el-descriptions-item label="审核意见" :span="2">{{
+          viewDialog.data.auditRemark || '-'
+        }}</el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{
+          viewDialog.data.remark || '-'
+        }}</el-descriptions-item>
+      </el-descriptions>
+
+      <!-- 报告子表(已审核时也可查看) -->
+      <div v-if="viewDialog.data?.report" class="mt-4">
+        <el-divider content-position="left">访厂报告</el-divider>
+        <el-tabs v-model="activeReportTab" type="border-card">
+          <el-tab-pane label="基础信息" name="basic">
+            <pre class="report-json">{{
+              formatJson(viewDialog.data.report.basicEvaluate)
+            }}</pre>
+          </el-tab-pane>
+          <el-tab-pane label="ESG 评分" name="esg">
+            <pre class="report-json">{{ formatJson(viewDialog.data.report.esgScore) }}</pre>
+          </el-tab-pane>
+          <el-tab-pane label="GMP 评分" name="gmp">
+            <pre class="report-json">{{ formatJson(viewDialog.data.report.gmpScore) }}</pre>
+          </el-tab-pane>
+          <el-tab-pane label="ISO14001" name="iso14001">
+            <pre class="report-json">{{
+              formatJson(viewDialog.data.report.iso14001Score)
+            }}</pre>
+          </el-tab-pane>
+          <el-tab-pane label="ISO45001" name="iso45001">
+            <pre class="report-json">{{
+              formatJson(viewDialog.data.report.iso45001Score)
+            }}</pre>
+          </el-tab-pane>
+          <el-tab-pane label="ISO9001" name="iso9001">
+            <pre class="report-json">{{
+              formatJson(viewDialog.data.report.iso9001Score)
+            }}</pre>
+          </el-tab-pane>
+          <el-tab-pane label="致敏原管控" name="allergen">
+            <pre class="report-json">{{
+              formatJson(viewDialog.data.report.allergenEvaluate)
+            }}</pre>
+          </el-tab-pane>
+          <el-tab-pane label="问题汇总" name="summary">
+            <div class="report-summary">{{ viewDialog.data.report.summary || '(空)' }}</div>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+
+      <template #footer>
+        <el-button @click="viewDialog.visible = false">关 闭</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 填写报告弹框 -->
+    <ReportDialog
+      v-if="reportDialog.visible"
+      v-model:visible="reportDialog.visible"
+      :visit="reportDialog.data"
+      @success="onReportSuccess"
+    />
+
+    <!-- 审核弹框 -->
+    <el-dialog
+      v-model="auditDialog.visible"
+      title="审核访厂任务"
+      width="560px"
+      append-to-body
+      @close="closeAuditDialog"
+    >
+      <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="100px">
+        <el-form-item label="访厂ID">
+          <span>{{ auditDialog.data?.visitCode }}</span>
+        </el-form-item>
+        <el-form-item label="公司名称">
+          <span>{{ auditDialog.data?.companyName }}</span>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-tag :type="getStatusTag(auditDialog.data?.status)">{{
+            auditDialog.data?.status
+          }}</el-tag>
+        </el-form-item>
+        <el-form-item label="访查得分">
+          <span>{{ auditDialog.data?.totalScore ?? '-' }}</span>
+        </el-form-item>
+        <el-form-item label="审核结论" prop="auditResult">
+          <el-select
+            v-model="auditForm.auditResult"
+            placeholder="请选择审核结论"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="dict in visit_audit_result_dict"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="审核意见" prop="auditRemark">
+          <el-input
+            v-model="auditForm.auditRemark"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入审核意见"
+            maxlength="1000"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="auditDialog.visible = false">取 消</el-button>
+        <el-button type="primary" :loading="auditLoading" @click="submitAudit">确 定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="FactoryVisit">
+import { ref, reactive, computed, getCurrentInstance, watch } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import {
+  listFactoryVisit,
+  getFactoryVisit,
+  addFactoryVisit,
+  updateFactoryVisit,
+  delFactoryVisit,
+  exportFactoryVisit,
+  auditFactoryVisit
+} from '@/api/risk/factoryVisit';
+import ReportDialog from './components/ReportDialog.vue';
+
+const { proxy } = getCurrentInstance() as any;
+
+// 字典
+const { visit_status_dict, visit_audit_result_dict } = proxy.useDict(
+  'visit_status',
+  'visit_audit_result'
+);
+
+const visitList = ref<any[]>([]);
+const loading = ref(false);
+const submitLoading = ref(false);
+const auditLoading = ref(false);
+const total = ref(0);
+const showSearch = ref(true);
+const dateRange = ref<[string, string] | null>(null);
+const activeReportTab = ref('basic');
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  visitCode: undefined,
+  companyName: undefined,
+  status: undefined,
+  auditResult: undefined,
+  params: {}
+});
+
+const initialForm = {
+  id: undefined,
+  visitCode: undefined,
+  companyName: undefined,
+  companyAddress: undefined,
+  visitDate: undefined,
+  visitorName: undefined,
+  auditDept: undefined,
+  productScope: undefined,
+  reviewerName: undefined,
+  remark: undefined
+};
+const form = reactive({ ...initialForm });
+const rules = {
+  companyName: [{ required: true, message: '公司名称不能为空', trigger: 'blur' }]
+};
+
+const dialog = reactive({
+  visible: false,
+  title: ''
+});
+
+const viewDialog = reactive({
+  visible: false,
+  data: null as any
+});
+
+const reportDialog = reactive({
+  visible: false,
+  data: null as any
+});
+
+const auditDialog = reactive({
+  visible: false,
+  data: null as any
+});
+
+const initialAuditForm = {
+  id: undefined,
+  auditResult: undefined,
+  auditRemark: undefined
+};
+const auditForm = reactive({ ...initialAuditForm });
+const auditRules = {
+  auditResult: [{ required: true, message: '请选择审核结论', trigger: 'change' }],
+  auditRemark: [{ required: true, message: '请填写审核意见', trigger: 'blur' }]
+};
+
+watch(dateRange, (val) => {
+  if (val && val.length === 2) {
+    queryParams.params = { ...(queryParams.params || {}), beginVisitDate: val[0], endVisitDate: val[1] };
+  } else {
+    queryParams.params = { ...(queryParams.params || {}) };
+    delete (queryParams.params as any).beginVisitDate;
+    delete (queryParams.params as any).endVisitDate;
+  }
+});
+
+// ============ 列表 ============
+
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res: any = await listFactoryVisit(queryParams);
+    visitList.value = res.rows || [];
+    total.value = res.total || 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  Object.assign(queryParams, {
+    pageNum: 1,
+    pageSize: 10,
+    visitCode: undefined,
+    companyName: undefined,
+    status: undefined,
+    auditResult: undefined,
+    params: {}
+  });
+  dateRange.value = null;
+  getList();
+};
+
+// ============ 新增/编辑 ============
+
+const handleAdd = () => {
+  Object.assign(form, initialForm);
+  dialog.title = '新增访厂任务';
+  dialog.visible = true;
+};
+
+const handleUpdate = async (row: any) => {
+  Object.assign(form, initialForm);
+  const res: any = await getFactoryVisit(row.id);
+  Object.assign(form, res.data || res);
+  dialog.title = '编辑访厂任务';
+  dialog.visible = true;
+};
+
+const submitForm = async () => {
+  const formRef = proxy.$refs.formRef;
+  await formRef.validate();
+  submitLoading.value = true;
+  try {
+    if (form.id) {
+      await updateFactoryVisit(form);
+      ElMessage.success('修改成功');
+    } else {
+      await addFactoryVisit(form);
+      ElMessage.success('新增成功');
+    }
+    dialog.visible = false;
+    getList();
+  } finally {
+    submitLoading.value = false;
+  }
+};
+
+const cancel = () => {
+  dialog.visible = false;
+  Object.assign(form, initialForm);
+};
+
+const closeDialog = () => {
+  Object.assign(form, initialForm);
+};
+
+const handleDelete = async (row: any) => {
+  await ElMessageBox.confirm(`确认删除访厂任务 "${row.visitCode}" ?`, '提示', {
+    type: 'warning'
+  });
+  await delFactoryVisit(row.id);
+  ElMessage.success('删除成功');
+  getList();
+};
+
+// ============ 查看详情 ============
+
+const handleView = async (row: any) => {
+  const res: any = await getFactoryVisit(row.id);
+  viewDialog.data = res.data || res;
+  viewDialog.visible = true;
+};
+
+// ============ 填写报告 ============
+
+const handleFillReport = async (row: any) => {
+  const res: any = await getFactoryVisit(row.id);
+  reportDialog.data = res.data || res;
+  reportDialog.visible = true;
+};
+
+const onReportSuccess = () => {
+  reportDialog.visible = false;
+  getList();
+};
+
+// ============ 审核 ============
+
+const handleAudit = (row: any) => {
+  Object.assign(auditForm, initialAuditForm);
+  auditDialog.data = row;
+  auditDialog.visible = true;
+};
+
+const submitAudit = async () => {
+  const ref: any = proxy.$refs.auditFormRef;
+  await ref.validate();
+  auditLoading.value = true;
+  try {
+    await auditFactoryVisit({
+      id: auditDialog.data.id,
+      auditResult: auditForm.auditResult,
+      auditRemark: auditForm.auditRemark
+    });
+    ElMessage.success('审核成功');
+    auditDialog.visible = false;
+    getList();
+  } finally {
+    auditLoading.value = false;
+  }
+};
+
+const closeAuditDialog = () => {
+  Object.assign(auditForm, initialAuditForm);
+};
+
+// ============ 导出 ============
+
+const handleExport = async () => {
+  const res: any = await exportFactoryVisit(queryParams);
+  proxy.download(res);
+};
+
+// ============ 辅助 ============
+
+const getAuditResultTag = (result?: string): 'success' | 'danger' | 'info' => {
+  if (result === '通过') return 'success';
+  if (result === '不通过') return 'danger';
+  if (result === '待定') return 'info';
+  return 'info';
+};
+
+const getStatusTag = (status?: string): 'warning' | 'primary' | 'success' | 'info' => {
+  if (status === '待访厂') return 'warning';
+  if (status === '待审核') return 'primary';
+  if (status === '已审核') return 'success';
+  return 'info';
+};
+
+const parseDate = (d?: string | Date) => {
+  if (!d) return '-';
+  const s = typeof d === 'string' ? d : d.toISOString();
+  return s.substring(0, 10);
+};
+
+const parseDateTime = (d?: string | Date) => {
+  if (!d) return '-';
+  const s = typeof d === 'string' ? d : d.toISOString();
+  return s.substring(0, 19).replace('T', ' ');
+};
+
+const formatJson = (data: any) => {
+  if (!data) return '(空)';
+  try {
+    return JSON.stringify(data, null, 2);
+  } catch {
+    return String(data);
+  }
+};
+
+getList();
+</script>
+
+<style scoped lang="scss">
+.factory-visit-page {
+  .report-json {
+    background: #f5f7fa;
+    padding: 12px;
+    border-radius: 4px;
+    font-size: 13px;
+    line-height: 1.6;
+    max-height: 400px;
+    overflow: auto;
+    white-space: pre-wrap;
+    word-break: break-all;
+  }
+  .report-summary {
+    background: #f5f7fa;
+    padding: 16px;
+    border-radius: 4px;
+    line-height: 1.8;
+    min-height: 100px;
+    white-space: pre-wrap;
+  }
+}
+</style>