Browse Source

优化电子合同

chenjun 1 month ago
parent
commit
aef547bbb4

+ 35 - 0
debug-pdf-upload-unknown-error.md

@@ -0,0 +1,35 @@
+# Debug Session: PDF Upload UnknownErrorException
+
+## Session ID
+pdf-upload-unknown-error
+
+## Status
+[OPEN]
+
+## Symptom
+上传 PDF 文件时,前端提示"PDF 解析失败 UnknownErrorException",错误位置在 DocumentAttachmentBody.vue:85(catch 块)。
+
+## Environment
+- Browser DevTools Network 显示 `pdf.worker.mjs` 请求返回 304 Not Modified
+- 请求 URL: `http://localhost/pdf.worker.mjs`
+
+## Hypotheses
+1. `pdfjs-dist` worker 路径 `/pdf.worker.mjs` 与实际部署路径不匹配,导致 worker 加载后执行失败
+2. 当前 PDF 文件本身损坏或格式特殊,触发 pdfjs 解析异常
+3. pdfjs-dist 版本与 public 目录下的 `pdf.worker.mjs` 文件版本不一致
+4. Worker 跨域或 MIME 类型问题导致 pdfjs 无法正确实例化 worker
+5. 动态 import('pdfjs-dist') 时 GlobalWorkerOptions 未正确设置,导致 worker 未生效
+
+## Evidence
+- `node_modules/pdfjs-dist/package.json` 版本为 `4.10.38`
+- `public/pdf.worker.mjs` 原文件中 `workerVersion` 与 `pdfjsVersion` 为旧版本,与安装的 `pdfjs-dist` 不匹配
+- 版本不一致会导致 worker 与主库通信协议不兼容,从而抛出 `UnknownErrorException`
+
+## Fix Applied
+将 `node_modules/pdfjs-dist/build/pdf.worker.mjs` 复制到 `public/pdf.worker.mjs`,确保 worker 版本与主库完全一致(均为 `4.10.38`)。
+
+## Verification
+请重新上传 PDF 文件验证。如仍报错,请提供新的控制台错误信息。
+
+## Created At
+2026-07-20

File diff suppressed because it is too large
+ 109 - 110
public/pdf.worker.mjs


+ 21 - 1
src/api/elderly/elder/outwardRegustration/index.ts

@@ -82,7 +82,27 @@ export const deleteASK = (id) => {
 
 export const deleteTempASK = (id) => {
   return request.delete({
-    url: '/elderly/temp-out/delete?id='+id
+    url: '/elderly/temp-out/delete?id=' + id
   })
 }
 
+export const appointmentConfigADD = (data) => {
+  return request.post({
+    url: '/appointmentConfig/add',
+    data: data
+  })
+}
+
+export const appointmentConfigUpdate = (data) => {
+  return request.post({
+    url: '/appointmentConfig/update',
+    data: data
+  })
+}
+
+export const appointmentConfigGetByTenant = (params) => {
+  return request.get({
+    url: 'appointmentConfig/getByTenant',
+    params
+  })
+}

+ 42 - 4
src/components/UploadFile/src/useUpload.ts

@@ -1,6 +1,7 @@
 import * as FileApi from '@/api/infra/file'
 import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
 import axios from 'axios'
+import pdfjsWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url'
 
 /**
  * 文件条目(通用文件数据结构)
@@ -191,6 +192,45 @@ const fileToArrayBuffer = (file: File): Promise<ArrayBuffer> => {
   })
 }
 
+/**
+ * 将 PDF 文件按页解析为图片 File 列表
+ * 返回的每张图片可单独上传
+ */
+export const pdfFileToImageFiles = async (
+  file: File,
+  options: { scale?: number; quality?: number } = {}
+): Promise<File[]> => {
+  const { scale = 2, quality = 0.92 } = options
+  const pdfjsLib = await import('pdfjs-dist')
+  const pdfjs: any = pdfjsLib
+  pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorkerUrl
+
+  const arrayBuffer = await fileToArrayBuffer(file)
+  const pdfDocument = await pdfjs.getDocument({ data: arrayBuffer }).promise
+  const imageFiles: File[] = []
+
+  for (let i = 1; i <= pdfDocument.numPages; i++) {
+    const page = await pdfDocument.getPage(i)
+    const viewport = page.getViewport({ scale })
+    const canvas = document.createElement('canvas')
+    const ctx = canvas.getContext('2d')
+    if (!ctx) continue
+    canvas.width = viewport.width
+    canvas.height = viewport.height
+    await page.render({ canvasContext: ctx, viewport }).promise
+
+    const blob = await new Promise<Blob | null>((resolve) => {
+      canvas.toBlob((b) => resolve(b), 'image/jpeg', quality)
+    })
+    if (blob) {
+      const name = `${file.name.replace(/\.pdf$/i, '')}_page_${i}.jpg`
+      imageFiles.push(new File([blob], name, { type: 'image/jpeg' }))
+    }
+  }
+
+  return imageFiles
+}
+
 /**
  * 压缩 PDF 文件(逐页渲染为图片,压缩后重新生成 PDF)
  * 依赖 pdf-lib 和 pdfjs-dist,请手动安装:pnpm add pdf-lib pdfjs-dist
@@ -202,11 +242,9 @@ const compressPdf = async (file: File, options: CompressOptions = {}): Promise<F
     import('pdfjs-dist')
   ])
 
-  // 配置 pdfjs worker,使用 public 目录下的静态 worker 文件,避免生产环境路径解析失败
+  // 配置 pdfjs worker,始终使用与 pdfjs-dist 版本匹配的 worker
   const pdfjs: any = pdfjsLib
-  if (!pdfjs.GlobalWorkerOptions.workerSrc) {
-    pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs'
-  }
+  pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorkerUrl
 
   const arrayBuffer = await fileToArrayBuffer(file)
   const pdfDocument = await pdfjs.getDocument({ data: arrayBuffer }).promise

+ 6 - 1
src/views/elderly/contracts/ContractManageForm.vue

@@ -762,7 +762,12 @@ const renderAttachmentContent = (key: string) => {
   <Teleport to="body">
     <div v-if="isPrint" class="print-container">
       <!-- 合同主体 -->
-      <ContractBody :record-row="recordRow" :is-text-mode="true" v-model="contractSignatureData" />
+      <ContractBody
+        :record-row="recordRow"
+        :is-text-mode="true"
+        v-model="contractSignatureData"
+        :attachment-ids="attachments"
+      />
 
       <!-- 已选的附件 -->
       <template v-if="selectedAttachments.length > 0">

+ 38 - 38
src/views/elderly/contracts/check-in-tab/AddForm.vue

@@ -177,15 +177,15 @@ const checkInRegistrationForm = ref({
   preResidenceHomeType: [],
   preResidenceHomeOther: '',
   checkInReason: '',
-  pastHistoryDiseaseNone: false,
+  pastHistoryDiseaseNone: null,
   pastHistoryDiseaseName1: '',
   pastHistoryDiseaseTime1: '',
   pastHistoryDiseaseName2: '',
   pastHistoryDiseaseTime2: '',
-  pastHistorySurgery: false,
+  pastHistorySurgery: null,
   pastHistorySurgeryName: '',
   pastHistorySurgeryTime: '',
-  pastHistoryTrauma: false,
+  pastHistoryTrauma: null,
   pastHistoryTraumaPart1: '',
   pastHistoryTraumaTime1: '',
   pastHistoryTraumaPart2: '',
@@ -196,41 +196,41 @@ const checkInRegistrationForm = ref({
   currentDiseaseName2: '',
   currentDiseaseTime2: '',
   currentDiseaseStatus2: '',
-  regularVisitNone: true,
+  regularVisitNone: null,
   regularVisitReason: '',
   regularVisitFreq: '',
-  hospitalizationNone: true,
+  hospitalizationNone: null,
   hospitalizationCount: '',
-  emergencyNone: true,
+  emergencyNone: null,
   emergencyCount: '',
-  discomfortSymptomsNone: true,
+  discomfortSymptomsNone: null,
   discomfortSymptomsList: [],
   discomfortSymptomsOther: '',
-  allergyDrugNone: true,
+  allergyDrugNone: null,
   allergyDrugDetail: '',
-  allergyFoodNone: true,
+  allergyFoodNone: null,
   allergyFoodDetail: '',
-  allergyEnvNone: true,
+  allergyEnvNone: null,
   allergyEnvDetail: '',
-  fallNone: true,
+  fallNone: null,
   fallDetail: '',
-  memoryDeclineNone: true,
+  memoryDeclineNone: null,
   memoryDeclineDetail: '',
-  weightLossNone: true,
+  weightLossNone: null,
   weightLossWeight: '',
-  urinaryIncontinenceNone: true,
+  urinaryIncontinenceNone: null,
   urinaryIncontinenceCount: '',
-  sleepDisorderNone: true,
+  sleepDisorderNone: null,
   sleepDisorderTypes: [],
   sleepDisorderOther: '',
-  painNone: true,
+  painNone: null,
   painPart: '',
-  visionAbnormalNone: true,
+  visionAbnormalNone: null,
   visionAbnormalTypes: [],
   visionAbnormalDegree: '',
-  hearingLossNone: true,
+  hearingLossNone: null,
   hearingLossDetail: '',
-  mentalStatusNone: true,
+  mentalStatusNone: null,
   mentalStatusList: [],
   mentalStatusOther: '',
   languageExpression: [],
@@ -375,15 +375,15 @@ const resetForm = () => {
     preResidenceHomeType: [],
     preResidenceHomeOther: '',
     checkInReason: '',
-    pastHistoryDiseaseNone: false,
+    pastHistoryDiseaseNone: null,
     pastHistoryDiseaseName1: '',
     pastHistoryDiseaseTime1: '',
     pastHistoryDiseaseName2: '',
     pastHistoryDiseaseTime2: '',
-    pastHistorySurgery: false,
+    pastHistorySurgery: null,
     pastHistorySurgeryName: '',
     pastHistorySurgeryTime: '',
-    pastHistoryTrauma: false,
+    pastHistoryTrauma: null,
     pastHistoryTraumaPart1: '',
     pastHistoryTraumaTime1: '',
     pastHistoryTraumaPart2: '',
@@ -394,41 +394,41 @@ const resetForm = () => {
     currentDiseaseName2: '',
     currentDiseaseTime2: '',
     currentDiseaseStatus2: '',
-    regularVisitNone: true,
+    regularVisitNone: null,
     regularVisitReason: '',
     regularVisitFreq: '',
-    hospitalizationNone: true,
+    hospitalizationNone: null,
     hospitalizationCount: '',
-    emergencyNone: true,
+    emergencyNone: null,
     emergencyCount: '',
-    discomfortSymptomsNone: true,
+    discomfortSymptomsNone: null,
     discomfortSymptomsList: [],
     discomfortSymptomsOther: '',
-    allergyDrugNone: true,
+    allergyDrugNone: null,
     allergyDrugDetail: '',
-    allergyFoodNone: true,
+    allergyFoodNone: null,
     allergyFoodDetail: '',
-    allergyEnvNone: true,
+    allergyEnvNone: null,
     allergyEnvDetail: '',
-    fallNone: true,
+    fallNone: null,
     fallDetail: '',
-    memoryDeclineNone: true,
+    memoryDeclineNone: null,
     memoryDeclineDetail: '',
-    weightLossNone: true,
+    weightLossNone: null,
     weightLossWeight: '',
-    urinaryIncontinenceNone: true,
+    urinaryIncontinenceNone: null,
     urinaryIncontinenceCount: '',
-    sleepDisorderNone: true,
+    sleepDisorderNone: null,
     sleepDisorderTypes: [],
     sleepDisorderOther: '',
-    painNone: true,
+    painNone: null,
     painPart: '',
-    visionAbnormalNone: true,
+    visionAbnormalNone: null,
     visionAbnormalTypes: [],
     visionAbnormalDegree: '',
-    hearingLossNone: true,
+    hearingLossNone: null,
     hearingLossDetail: '',
-    mentalStatusNone: true,
+    mentalStatusNone: null,
     mentalStatusList: [],
     mentalStatusOther: '',
     languageExpression: [],

+ 102 - 102
src/views/elderly/contracts/components/CheckInRegistrationBody.vue

@@ -39,15 +39,15 @@ export interface CheckInRegistrationForm {
   preResidenceHomeType?: string[]
   preResidenceHomeOther?: string
   checkInReason?: string
-  pastHistoryDiseaseNone?: boolean
+  pastHistoryDiseaseNone?: boolean | null
   pastHistoryDiseaseName1?: string
   pastHistoryDiseaseTime1?: string
   pastHistoryDiseaseName2?: string
   pastHistoryDiseaseTime2?: string
-  pastHistorySurgery?: boolean
+  pastHistorySurgery?: boolean | null
   pastHistorySurgeryName?: string
   pastHistorySurgeryTime?: string
-  pastHistoryTrauma?: boolean
+  pastHistoryTrauma?: boolean | null
   pastHistoryTraumaPart1?: string
   pastHistoryTraumaTime1?: string
   pastHistoryTraumaPart2?: string
@@ -58,41 +58,41 @@ export interface CheckInRegistrationForm {
   currentDiseaseName2?: string
   currentDiseaseTime2?: string
   currentDiseaseStatus2?: string
-  regularVisitNone?: boolean
+  regularVisitNone?: boolean | null
   regularVisitReason?: string
   regularVisitFreq?: string
-  hospitalizationNone?: boolean
+  hospitalizationNone?: boolean | null
   hospitalizationCount?: string
-  emergencyNone?: boolean
+  emergencyNone?: boolean | null
   emergencyCount?: string
-  discomfortSymptomsNone?: boolean
+  discomfortSymptomsNone?: boolean | null
   discomfortSymptomsList?: string[]
   discomfortSymptomsOther?: string
-  allergyDrugNone?: boolean
+  allergyDrugNone?: boolean | null
   allergyDrugDetail?: string
-  allergyFoodNone?: boolean
+  allergyFoodNone?: boolean | null
   allergyFoodDetail?: string
-  allergyEnvNone?: boolean
+  allergyEnvNone?: boolean | null
   allergyEnvDetail?: string
-  fallNone?: boolean
+  fallNone?: boolean | null
   fallDetail?: string
-  memoryDeclineNone?: boolean
+  memoryDeclineNone?: boolean | null
   memoryDeclineDetail?: string
-  weightLossNone?: boolean
+  weightLossNone?: boolean | null
   weightLossWeight?: string
-  urinaryIncontinenceNone?: boolean
+  urinaryIncontinenceNone?: boolean | null
   urinaryIncontinenceCount?: string
-  sleepDisorderNone?: boolean
+  sleepDisorderNone?: boolean | null
   sleepDisorderTypes?: string[]
   sleepDisorderOther?: string
-  painNone?: boolean
+  painNone?: boolean | null
   painPart?: string
-  visionAbnormalNone?: boolean
+  visionAbnormalNone?: boolean | null
   visionAbnormalTypes?: string[]
   visionAbnormalDegree?: string
-  hearingLossNone?: boolean
+  hearingLossNone?: boolean | null
   hearingLossDetail?: string
-  mentalStatusNone?: boolean
+  mentalStatusNone?: boolean | null
   mentalStatusList?: string[]
   mentalStatusOther?: string
   languageExpression?: string[]
@@ -155,15 +155,15 @@ const createDefaultForm = (): CheckInRegistrationForm => ({
   preResidenceHomeType: [],
   preResidenceHomeOther: '',
   checkInReason: '',
-  pastHistoryDiseaseNone: true,
+  pastHistoryDiseaseNone: null,
   pastHistoryDiseaseName1: '',
   pastHistoryDiseaseTime1: '',
   pastHistoryDiseaseName2: '',
   pastHistoryDiseaseTime2: '',
-  pastHistorySurgery: false,
+  pastHistorySurgery: null,
   pastHistorySurgeryName: '',
   pastHistorySurgeryTime: '',
-  pastHistoryTrauma: false,
+  pastHistoryTrauma: null,
   pastHistoryTraumaPart1: '',
   pastHistoryTraumaTime1: '',
   pastHistoryTraumaPart2: '',
@@ -174,41 +174,41 @@ const createDefaultForm = (): CheckInRegistrationForm => ({
   currentDiseaseName2: '',
   currentDiseaseTime2: '',
   currentDiseaseStatus2: '',
-  regularVisitNone: true,
+  regularVisitNone: null,
   regularVisitReason: '',
   regularVisitFreq: '',
-  hospitalizationNone: true,
+  hospitalizationNone: null,
   hospitalizationCount: '',
-  emergencyNone: true,
+  emergencyNone: null,
   emergencyCount: '',
-  discomfortSymptomsNone: true,
+  discomfortSymptomsNone: null,
   discomfortSymptomsList: [],
   discomfortSymptomsOther: '',
-  allergyDrugNone: true,
+  allergyDrugNone: null,
   allergyDrugDetail: '',
-  allergyFoodNone: true,
+  allergyFoodNone: null,
   allergyFoodDetail: '',
-  allergyEnvNone: true,
+  allergyEnvNone: null,
   allergyEnvDetail: '',
-  fallNone: true,
+  fallNone: null,
   fallDetail: '',
-  memoryDeclineNone: true,
+  memoryDeclineNone: null,
   memoryDeclineDetail: '',
-  weightLossNone: true,
+  weightLossNone: null,
   weightLossWeight: '',
-  urinaryIncontinenceNone: true,
+  urinaryIncontinenceNone: null,
   urinaryIncontinenceCount: '',
-  sleepDisorderNone: true,
+  sleepDisorderNone: null,
   sleepDisorderTypes: [],
   sleepDisorderOther: '',
-  painNone: true,
+  painNone: null,
   painPart: '',
-  visionAbnormalNone: true,
+  visionAbnormalNone: null,
   visionAbnormalTypes: [],
   visionAbnormalDegree: '',
-  hearingLossNone: true,
+  hearingLossNone: null,
   hearingLossDetail: '',
-  mentalStatusNone: true,
+  mentalStatusNone: null,
   mentalStatusList: [],
   mentalStatusOther: '',
   languageExpression: [],
@@ -545,20 +545,20 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
         <td class="lbl" rowspan="2">既往史</td>
         <td colspan="5" class="disease-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.pastHistoryDiseaseNone" @change="onPastHistoryNoneChange" /> 无</label>
-            <label><input type="checkbox" :checked="!form.pastHistoryDiseaseNone" @change="updateForm('pastHistoryDiseaseNone',false)" /> 有:疾病名称:</label>
+            <label><input type="checkbox" :checked="form.pastHistoryDiseaseNone === true" @change="onPastHistoryNoneChange" /> 无 </label>
+            <label><input type="checkbox" :checked="form.pastHistoryDiseaseNone === false" @change="updateForm('pastHistoryDiseaseNone',false)" /> 有:疾病名称:</label>
             <input :value="form.pastHistoryDiseaseName1" class="inline-input medium" placeholder="" @input="onInlineInput('pastHistoryDiseaseName1',$event)" />
             <span class="text-suffix">,确诊时间:</span>
             <input :value="form.pastHistoryDiseaseTime1" class="inline-input small" placeholder="" @input="onInlineInput('pastHistoryDiseaseTime1',$event)" />
           </template>
-          <template v-else>{{ printCheck(form.pastHistoryDiseaseNone) }} 无 {{ printCheck(!form.pastHistoryDiseaseNone) }} 有:疾病名称:{{ form.pastHistoryDiseaseName1 }} ,确诊时间:{{ form.pastHistoryDiseaseTime1 }}</template>
+          <template v-else>{{ printCheck(form.pastHistoryDiseaseNone === true) }} 无 {{ printCheck(form.pastHistoryDiseaseNone === false) }} 有:疾病名称:{{ form.pastHistoryDiseaseName1 }} ,确诊时间:{{ form.pastHistoryDiseaseTime1 }}</template>
         </td>
       </tr>
       <!-- 既往史第2行:(健康继续) | (既往史继续) | 内容(5) -->
       <tr>
         <td colspan="5" class="disease-row">
           <template v-if="!isTextMode">
-            <span class="text-prefix ml20"></span>
+            <span class="text-prefix"></span>
             <span class="text-suffix">疾病名称:</span>
             <input :value="form.pastHistoryDiseaseName2" class="inline-input medium" placeholder="" @input="onInlineInput('pastHistoryDiseaseName2',$event)" />
             <span class="text-suffix">,确诊时间:</span>
@@ -573,13 +573,13 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
         <td class="lbl">手术史</td>
         <td colspan="5" class="disease-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="!form.pastHistorySurgery" @change="updateForm('pastHistorySurgery',false)" /> 无</label>
-            <label><input type="checkbox" :checked="form.pastHistorySurgery" @change="updateForm('pastHistorySurgery',true)" /> 有:手术名称:</label>
+            <label><input type="checkbox" :checked="form.pastHistorySurgery === false" @change="updateForm('pastHistorySurgery',false)" /> 无 </label>
+            <label><input type="checkbox" :checked="form.pastHistorySurgery === true" @change="updateForm('pastHistorySurgery',true)" /> 有:手术名称:</label>
             <input :value="form.pastHistorySurgeryName" class="inline-input medium" placeholder="" @input="onInlineInput('pastHistorySurgeryName',$event)" />
             <span class="text-suffix">,手术时间:</span>
             <input :value="form.pastHistorySurgeryTime" class="inline-input small" placeholder="" @input="onInlineInput('pastHistorySurgeryTime',$event)" />
           </template>
-          <template v-else>{{ printCheck(!form.pastHistorySurgery) }} 无 {{ printCheck(form.pastHistorySurgery) }} 有:手术名称:{{ form.pastHistorySurgeryName }} ,手术时间:{{ form.pastHistorySurgeryTime }}</template>
+          <template v-else>{{ printCheck(form.pastHistorySurgery === false) }} 无 {{ printCheck(form.pastHistorySurgery === true) }} 有:手术名称:{{ form.pastHistorySurgeryName }} ,手术时间:{{ form.pastHistorySurgeryTime }}</template>
         </td>
       </tr>
 
@@ -588,13 +588,13 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
         <td class="lbl" rowspan="2">外伤史</td>
         <td colspan="5" class="disease-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="!form.pastHistoryTrauma" @change="updateForm('pastHistoryTrauma',false)" /> 无</label>
-            <label><input type="checkbox" :checked="form.pastHistoryTrauma" @change="updateForm('pastHistoryTrauma',true)" /> 有:外伤部位:</label>
+            <label><input type="checkbox" :checked="form.pastHistoryTrauma === false" @change="updateForm('pastHistoryTrauma',false)" /> 无 </label>
+            <label><input type="checkbox" :checked="form.pastHistoryTrauma === true" @change="updateForm('pastHistoryTrauma',true)" /> 有:外伤部位:</label>
             <input :value="form.pastHistoryTraumaPart1" class="inline-input medium" placeholder="" @input="onInlineInput('pastHistoryTraumaPart1',$event)" />
             <span class="text-suffix">,发生时间:</span>
             <input :value="form.pastHistoryTraumaTime1" class="inline-input small" placeholder="" @input="onInlineInput('pastHistoryTraumaTime1',$event)" />
           </template>
-          <template v-else>{{ printCheck(!form.pastHistoryTrauma) }} 无 {{ printCheck(form.pastHistoryTrauma) }} 有:外伤部位:{{ form.pastHistoryTraumaPart1 }} ,发生时间:{{ form.pastHistoryTraumaTime1 }}</template>
+          <template v-else>{{ printCheck(form.pastHistoryTrauma === false) }} 无 {{ printCheck(form.pastHistoryTrauma === true) }} 有:外伤部位:{{ form.pastHistoryTraumaPart1 }} ,发生时间:{{ form.pastHistoryTraumaTime1 }}</template>
         </td>
       </tr>
       <!-- 外伤史第2行:(健康继续) | (外伤史继续) | 内容(5) -->
@@ -646,32 +646,32 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
         <td colspan="5" class="disease-row">
           <template v-if="!isTextMode">
             <span class="text-suffix">固定时间就诊:</span>
-            <label><input type="checkbox" :checked="form.regularVisitNone" @change="updateForm('regularVisitNone',true)" /> 无</label>
-            <label><input type="checkbox" :checked="!form.regularVisitNone" @change="updateForm('regularVisitNone',false)" /> 有(原因:</label>
+            <label><input type="checkbox" :checked="form.regularVisitNone === true" @change="updateForm('regularVisitNone',true)" /> 无 </label>
+            <label><input type="checkbox" :checked="form.regularVisitNone === false" @change="updateForm('regularVisitNone',false)" /> 有(原因:</label>
             <input :value="form.regularVisitReason" class="inline-input small" placeholder="" @input="onInlineInput('regularVisitReason',$event)" />
             <span class="text-suffix">,频率:___次/月)</span>
           </template>
-          <template v-else>固定时间就诊:{{ printCheck(form.regularVisitNone) }} 无 {{ printCheck(!form.regularVisitNone) }} 有(原因:{{ form.regularVisitReason }} ,频率:___次/月)</template>
+          <template v-else>固定时间就诊:{{ printCheck(form.regularVisitNone === true) }} 无 {{ printCheck(form.regularVisitNone === false) }} 有(原因:{{ form.regularVisitReason }} ,频率:___次/月)</template>
         </td>
       </tr>
       <tr>
         <td colspan="5" class="disease-row">
           <template v-if="!isTextMode">
             <span class="text-suffix">近一年内住院情况:</span>
-            <label><input type="checkbox" :checked="form.hospitalizationNone" @change="updateForm('hospitalizationNone',true)" /> 未住院</label>
-            <label><input type="checkbox" :checked="!form.hospitalizationNone" @change="updateForm('hospitalizationNone',false)" /> 住过院(住院次数:<input :value="form.hospitalizationCount" class="inline-input tiny" placeholder="" @input="onInlineInput('hospitalizationCount',$event)" /> 次)</label>
+            <label><input type="checkbox" :checked="form.hospitalizationNone === true" @change="updateForm('hospitalizationNone',true)" /> 未住院 </label>
+            <label><input type="checkbox" :checked="form.hospitalizationNone === false" @change="updateForm('hospitalizationNone',false)" /> 住过院(住院次数:<input :value="form.hospitalizationCount" class="inline-input tiny" placeholder="" @input="onInlineInput('hospitalizationCount',$event)" /> 次)</label>
           </template>
-          <template v-else>近一年内住院情况:{{ printCheck(form.hospitalizationNone) }} 未住院 {{ printCheck(!form.hospitalizationNone) }} 住过院(住院次数:{{ form.hospitalizationCount }} 次)</template>
+          <template v-else>近一年内住院情况:{{ printCheck(form.hospitalizationNone === true) }} 未住院 {{ printCheck(form.hospitalizationNone === false) }} 住过院(住院次数:{{ form.hospitalizationCount }} 次)</template>
         </td>
       </tr>
       <tr>
         <td colspan="5" class="disease-row">
           <template v-if="!isTextMode">
             <span class="text-suffix">近一年内急诊情况:</span>
-            <label><input type="checkbox" :checked="form.emergencyNone" @change="updateForm('emergencyNone',true)" /> 未去过</label>
-            <label><input type="checkbox" :checked="!form.emergencyNone" @change="updateForm('emergencyNone',false)" /> 去过急诊(去急诊次数:<input :value="form.emergencyCount" class="inline-input tiny" placeholder="" @input="onInlineInput('emergencyCount',$event)" /> 次)</label>
+            <label><input type="checkbox" :checked="form.emergencyNone === true" @change="updateForm('emergencyNone',true)" /> 未去过 </label>
+            <label><input type="checkbox" :checked="form.emergencyNone === false" @change="updateForm('emergencyNone',false)" /> 去过急诊(去急诊次数:<input :value="form.emergencyCount" class="inline-input tiny" placeholder="" @input="onInlineInput('emergencyCount',$event)" /> 次)</label>
           </template>
-          <template v-else>近一年内急诊情况:{{ printCheck(form.emergencyNone) }} 未去过 {{ printCheck(!form.emergencyNone) }} 去过急诊(去急诊次数:{{ form.emergencyCount }} 次)</template>
+          <template v-else>近一年内急诊情况:{{ printCheck(form.emergencyNone === true) }} 未去过 {{ printCheck(form.emergencyNone === false) }} 去过急诊(去急诊次数:{{ form.emergencyCount }} 次)</template>
         </td>
       </tr>
 
@@ -680,10 +680,10 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
         <td class="lbl multi-line">不适症状<br/>(近一个<br/>月内情况)</td>
         <td colspan="6" class="symptom-cell">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.discomfortSymptomsNone" @change="updateForm('discomfortSymptomsNone',true);form.discomfortSymptomsList=[]" /> 无</label>
+            <label><input type="checkbox" :checked="form.discomfortSymptomsNone === true" @change="updateForm('discomfortSymptomsNone',true);form.discomfortSymptomsList=[]" /> 无</label>
             <br/>
-            <label><input type="checkbox" :checked="!form.discomfortSymptomsNone" @change="updateForm('discomfortSymptomsNone',false)" /> 有(可多选)</label>
-            <div v-if="!form.discomfortSymptomsNone" class="symptom-list">
+            <label><input type="checkbox" :checked="form.discomfortSymptomsNone === false" @change="updateForm('discomfortSymptomsNone',false)" /> 有(可多选)</label>
+            <div v-if="form.discomfortSymptomsNone === false" class="symptom-list">
               <label v-for="s in symptomOptions" :key="s" class="symptom-item">
                 <input type="checkbox" :checked="form.discomfortSymptomsList.includes(s)" @change="toggleArrayItem('discomfortSymptomsList',s)" /> {{ s }}
               </label>
@@ -691,8 +691,8 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
             </div>
           </template>
           <template v-else>
-            {{ printCheck(form.discomfortSymptomsNone) }} 无
-            <template v-if="!form.discomfortSymptomsNone">{{ printCheck(true) }} 有:{{ form.discomfortSymptomsList.join('、') }} {{ form.discomfortSymptomsOther }}</template>
+            {{ printCheck(form.discomfortSymptomsNone === true) }} 无
+            <template v-if="form.discomfortSymptomsNone === false">{{ printCheck(true) }} 有:{{ form.discomfortSymptomsList.join('、') }} {{ form.discomfortSymptomsOther }}</template>
           </template>
         </td>
       </tr>
@@ -702,31 +702,31 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
         <td class="lbl multi-line" rowspan="3">过敏史</td>
         <td colspan="6" class="allergy-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.allergyDrugNone" @change="updateForm('allergyDrugNone',true);updateForm('allergyDrugDetail','')" /> 无药物过敏</label>
-            <label><input type="checkbox" :checked="!form.allergyDrugNone" @change="updateForm('allergyDrugNone',false)" /> 药物过敏:</label>
+            <label><input type="checkbox" :checked="form.allergyDrugNone === true" @change="updateForm('allergyDrugNone',true);updateForm('allergyDrugDetail','')" /> 无药物过敏 </label>
+            <label><input type="checkbox" :checked="form.allergyDrugNone === false" @change="updateForm('allergyDrugNone',false)" /> 药物过敏:</label>
             <input :value="form.allergyDrugDetail" class="inline-input large" placeholder="" @input="onInlineInput('allergyDrugDetail',$event)" />
           </template>
-          <template v-else>{{ printCheck(form.allergyDrugNone) }} 无药物过敏 {{ printCheck(!form.allergyDrugNone) }} 药物过敏:{{ form.allergyDrugDetail }}</template>
+          <template v-else>{{ printCheck(form.allergyDrugNone === true) }} 无药物过敏 {{ printCheck(form.allergyDrugNone === false) }} 药物过敏:{{ form.allergyDrugDetail }}</template>
         </td>
       </tr>
       <tr>
         <td colspan="6" class="allergy-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.allergyFoodNone" @change="updateForm('allergyFoodNone',true);updateForm('allergyFoodDetail','')" /> 无食物过敏</label>
-            <label><input type="checkbox" :checked="!form.allergyFoodNone" @change="updateForm('allergyFoodNone',false)" /> 食物过敏:</label>
+            <label><input type="checkbox" :checked="form.allergyFoodNone === true" @change="updateForm('allergyFoodNone',true);updateForm('allergyFoodDetail','')" /> 无食物过敏 </label>
+            <label><input type="checkbox" :checked="form.allergyFoodNone === false" @change="updateForm('allergyFoodNone',false)" /> 食物过敏:</label>
             <input :value="form.allergyFoodDetail" class="inline-input large" placeholder="" @input="onInlineInput('allergyFoodDetail',$event)" />
           </template>
-          <template v-else>{{ printCheck(form.allergyFoodNone) }} 无食物过敏 {{ printCheck(!form.allergyFoodNone) }} 食物过敏:{{ form.allergyFoodDetail }}</template>
+          <template v-else>{{ printCheck(form.allergyFoodNone === true) }} 无食物过敏 {{ printCheck(form.allergyFoodNone === false) }} 食物过敏:{{ form.allergyFoodDetail }}</template>
         </td>
       </tr>
       <tr>
         <td colspan="6" class="allergy-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.allergyEnvNone" @change="updateForm('allergyEnvNone',true);updateForm('allergyEnvDetail','')" /> 无环境过敏</label>
-            <label><input type="checkbox" :checked="!form.allergyEnvNone" @change="updateForm('allergyEnvNone',false)" /> 环境过敏:</label>
+            <label><input type="checkbox" :checked="form.allergyEnvNone === true" @change="updateForm('allergyEnvNone',true);updateForm('allergyEnvDetail','')" /> 无环境过敏 </label>
+            <label><input type="checkbox" :checked="form.allergyEnvNone === false" @change="updateForm('allergyEnvNone',false)" /> 环境过敏:</label>
             <input :value="form.allergyEnvDetail" class="inline-input large" placeholder="" @input="onInlineInput('allergyEnvDetail',$event)" />
           </template>
-          <template v-else>{{ printCheck(form.allergyEnvNone) }} 无环境过敏 {{ printCheck(!form.allergyEnvNone) }} 环境过敏:{{ form.allergyEnvDetail }}</template>
+          <template v-else>{{ printCheck(form.allergyEnvNone === true) }} 无环境过敏 {{ printCheck(form.allergyEnvNone === false) }} 环境过敏:{{ form.allergyEnvDetail }}</template>
         </td>
       </tr>
 
@@ -735,53 +735,53 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
         <td class="lbl multi-line" rowspan="9">老年综合<br/>征</td>
         <td colspan="6" class="syndrome-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.fallNone" @change="updateForm('fallNone',true);updateForm('fallDetail','')" /> 无跌倒</label>
-            <label><input type="checkbox" :checked="!form.fallNone" @change="updateForm('fallNone',false)" /> 有跌倒(</label>
+            <label><input type="checkbox" :checked="form.fallNone === true" @change="updateForm('fallNone',true);updateForm('fallDetail','')" /> 无跌倒 </label>
+            <label><input type="checkbox" :checked="form.fallNone === false" @change="updateForm('fallNone',false)" /> 有跌倒(</label>
             <input :value="form.fallDetail" class="inline-input medium" placeholder="详情" @input="onInlineInput('fallDetail',$event)" />
             <span>)</span>
           </template>
-          <template v-else>{{ printCheck(form.fallNone) }} 无跌倒 {{ printCheck(!form.fallNone) }} 有跌倒({{ form.fallDetail }})</template>
+          <template v-else>{{ printCheck(form.fallNone === true) }} 无跌倒 {{ printCheck(form.fallNone === false) }} 有跌倒({{ form.fallDetail }})</template>
         </td>
       </tr>
       <tr>
         <td colspan="6" class="syndrome-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.memoryDeclineNone" @change="updateForm('memoryDeclineNone',true);updateForm('memoryDeclineDetail','')" /> 无记忆力减退</label>
-            <label><input type="checkbox" :checked="!form.memoryDeclineNone" @change="updateForm('memoryDeclineNone',false)" /> 有记忆力减退(</label>
+            <label><input type="checkbox" :checked="form.memoryDeclineNone === true" @change="updateForm('memoryDeclineNone',true);updateForm('memoryDeclineDetail','')" /> 无记忆力减退 </label>
+            <label><input type="checkbox" :checked="form.memoryDeclineNone === false" @change="updateForm('memoryDeclineNone',false)" /> 有记忆力减退(</label>
             <input :value="form.memoryDeclineDetail" class="inline-input medium" placeholder="详情" @input="onInlineInput('memoryDeclineDetail',$event)" />
             <span>)</span>
           </template>
-          <template v-else>{{ printCheck(form.memoryDeclineNone) }} 无记忆力减退 {{ printCheck(!form.memoryDeclineNone) }} 有记忆力减退({{ form.memoryDeclineDetail }})</template>
+          <template v-else>{{ printCheck(form.memoryDeclineNone === true) }} 无记忆力减退 {{ printCheck(form.memoryDeclineNone === false) }} 有记忆力减退({{ form.memoryDeclineDetail }})</template>
         </td>
       </tr>
       <tr>
         <td colspan="6" class="syndrome-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.weightLossNone" @change="updateForm('weightLossNone',true);updateForm('weightLossWeight','')" /> 无体重下降</label>
-            <label><input type="checkbox" :checked="!form.weightLossNone" @change="updateForm('weightLossNone',false)" /> 有体重下降(半年内下降</label>
+            <label><input type="checkbox" :checked="form.weightLossNone === true" @change="updateForm('weightLossNone',true);updateForm('weightLossWeight','')" /> 无体重下降 </label>
+            <label><input type="checkbox" :checked="form.weightLossNone === false" @change="updateForm('weightLossNone',false)" /> 有体重下降(半年内下降</label>
             <input :value="form.weightLossWeight" class="inline-input tiny" placeholder="kg" @input="onInlineInput('weightLossWeight',$event)" />
             <span>kg)</span>
           </template>
-          <template v-else>{{ printCheck(form.weightLossNone) }} 无体重下降 {{ printCheck(!form.weightLossNone) }} 有体重下降(半年内下降{{ form.weightLossWeight }}kg)</template>
+          <template v-else>{{ printCheck(form.weightLossNone === true) }} 无体重下降 {{ printCheck(form.weightLossNone === false) }} 有体重下降(半年内下降{{ form.weightLossWeight }}kg)</template>
         </td>
       </tr>
       <tr>
         <td colspan="6" class="syndrome-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.urinaryIncontinenceNone" @change="updateForm('urinaryIncontinenceNone',true);updateForm('urinaryIncontinenceCount','')" /> 无尿失禁</label>
-            <label><input type="checkbox" :checked="!form.urinaryIncontinenceNone" @change="updateForm('urinaryIncontinenceNone',false)" /> 有尿失禁(</label>
+            <label><input type="checkbox" :checked="form.urinaryIncontinenceNone === true" @change="updateForm('urinaryIncontinenceNone',true);updateForm('urinaryIncontinenceCount','')" /> 无尿失禁 </label>
+            <label><input type="checkbox" :checked="form.urinaryIncontinenceNone === false" @change="updateForm('urinaryIncontinenceNone',false)" /> 有尿失禁(</label>
             <input :value="form.urinaryIncontinenceCount" class="inline-input small" placeholder="次数/日" @input="onInlineInput('urinaryIncontinenceCount',$event)" />
             <span>次/日)</span>
           </template>
-          <template v-else>{{ printCheck(form.urinaryIncontinenceNone) }} 无尿失禁 {{ printCheck(!form.urinaryIncontinenceNone) }} 有尿失禁({{ form.urinaryIncontinenceCount }}次/日)</template>
+          <template v-else>{{ printCheck(form.urinaryIncontinenceNone === true) }} 无尿失禁 {{ printCheck(form.urinaryIncontinenceNone === false) }} 有尿失禁({{ form.urinaryIncontinenceCount }}次/日)</template>
         </td>
       </tr>
       <tr>
         <td colspan="6" class="syndrome-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.sleepDisorderNone" @change="updateForm('sleepDisorderNone',true);form.sleepDisorderTypes=[]" /> 无睡眠障碍</label>
-            <label><input type="checkbox" :checked="!form.sleepDisorderNone" @change="updateForm('sleepDisorderNone',false)" /> 有睡眠障碍(</label>
-            <template v-if="!form.sleepDisorderNone">
+            <label><input type="checkbox" :checked="form.sleepDisorderNone === true" @change="updateForm('sleepDisorderNone',true);form.sleepDisorderTypes=[]" /> 无睡眠障碍 </label>
+            <label><input type="checkbox" :checked="form.sleepDisorderNone === false" @change="updateForm('sleepDisorderNone',false)" /> 有睡眠障碍(</label>
+            <template v-if="form.sleepDisorderNone === false">
               <label v-for="st in ['入睡困难','早醒','夜间易醒','多梦']" :key="st" style="margin-right:4px;">
                 <input type="checkbox" :checked="form.sleepDisorderTypes.includes(st)" @change="toggleArrayItem('sleepDisorderTypes',st)" /> {{ st }}
               </label>
@@ -789,26 +789,26 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
             </template>
             <span>)</span>
           </template>
-          <template v-else>{{ printCheck(form.sleepDisorderNone) }} 无睡眠障碍 {{ printCheck(!form.sleepDisorderNone) }} 有睡眠障碍({{ [...form.sleepDisorderTypes, form.sleepDisorderOther].filter(Boolean).join('、') }})</template>
+          <template v-else>{{ printCheck(form.sleepDisorderNone === true) }} 无睡眠障碍 {{ printCheck(form.sleepDisorderNone === false) }} 有睡眠障碍({{ [...form.sleepDisorderTypes, form.sleepDisorderOther].filter(Boolean).join('、') }})</template>
         </td>
       </tr>
       <tr>
         <td colspan="6" class="syndrome-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.painNone" @change="updateForm('painNone',true);updateForm('painPart','')" /> 无疼痛</label>
-            <label><input type="checkbox" :checked="!form.painNone" @change="updateForm('painNone',false)" /> 有疼痛(部位:</label>
+            <label><input type="checkbox" :checked="form.painNone === true" @change="updateForm('painNone',true);updateForm('painPart','')" /> 无疼痛 </label>
+            <label><input type="checkbox" :checked="form.painNone === false" @change="updateForm('painNone',false)" /> 有疼痛(部位:</label>
             <input :value="form.painPart" class="inline-input medium" placeholder="" @input="onInlineInput('painPart',$event)" />
             <span>)</span>
           </template>
-          <template v-else>{{ printCheck(form.painNone) }} 无疼痛 {{ printCheck(!form.painNone) }} 有疼痛(部位:{{ form.painPart }})</template>
+          <template v-else>{{ printCheck(form.painNone === true) }} 无疼痛 {{ printCheck(form.painNone === false) }} 有疼痛(部位:{{ form.painPart }})</template>
         </td>
       </tr>
       <tr>
         <td colspan="6" class="syndrome-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.visionAbnormalNone" @change="updateForm('visionAbnormalNone',true);form.visionAbnormalTypes=[]" /> 无视力异常</label>
-            <label><input type="checkbox" :checked="!form.visionAbnormalNone" @change="updateForm('visionAbnormalNone',false)" /> 有视力异常(</label>
-            <template v-if="!form.visionAbnormalNone">
+            <label><input type="checkbox" :checked="form.visionAbnormalNone === true" @change="updateForm('visionAbnormalNone',true);form.visionAbnormalTypes=[]" /> 无视力异常 </label>
+            <label><input type="checkbox" :checked="form.visionAbnormalNone === false" @change="updateForm('visionAbnormalNone',false)" /> 有视力异常(</label>
+            <template v-if="form.visionAbnormalNone === false">
               <label v-for="vt in ['远视','近视','散光','白内障','青光眼']" :key="vt" style="margin-right:4px;">
                 <input type="checkbox" :checked="form.visionAbnormalTypes.includes(vt)" @change="toggleArrayItem('visionAbnormalTypes',vt)" /> {{ vt }}
               </label>
@@ -816,26 +816,26 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
             </template>
             <span>)</span>
           </template>
-          <template v-else>{{ printCheck(form.visionAbnormalNone) }} 无视力异常 {{ printCheck(!form.visionAbnormalNone) }} 有视力异常({{ [...form.visionAbnormalTypes, form.visionAbnormalDegree].filter(Boolean).join('、') }})</template>
+          <template v-else>{{ printCheck(form.visionAbnormalNone === true) }} 无视力异常 {{ printCheck(form.visionAbnormalNone === false) }} 有视力异常({{ [...form.visionAbnormalTypes, form.visionAbnormalDegree].filter(Boolean).join('、') }})</template>
         </td>
       </tr>
       <tr>
         <td colspan="6" class="syndrome-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.hearingLossNone" @change="updateForm('hearingLossNone',true);updateForm('hearingLossDetail','')" /> 无听力障碍</label>
-            <label><input type="checkbox" :checked="!form.hearingLossNone" @change="updateForm('hearingLossNone',false)" /> 有听力障碍(</label>
+            <label><input type="checkbox" :checked="form.hearingLossNone === true" @change="updateForm('hearingLossNone',true);updateForm('hearingLossDetail','')" /> 无听力障碍 </label>
+            <label><input type="checkbox" :checked="form.hearingLossNone === false" @change="updateForm('hearingLossNone',false)" /> 有听力障碍(</label>
             <input :value="form.hearingLossDetail" class="inline-input medium" placeholder="详情" @input="onInlineInput('hearingLossDetail',$event)" />
             <span>)</span>
           </template>
-          <template v-else>{{ printCheck(form.hearingLossNone) }} 无听力障碍 {{ printCheck(!form.hearingLossNone) }} 有听力障碍({{ form.hearingLossDetail }})</template>
+          <template v-else>{{ printCheck(form.hearingLossNone === true) }} 无听力障碍 {{ printCheck(form.hearingLossNone === false) }} 有听力障碍({{ form.hearingLossDetail }})</template>
         </td>
       </tr>
       <tr>
         <td colspan="6" class="syndrome-row">
           <template v-if="!isTextMode">
-            <label><input type="checkbox" :checked="form.mentalStatusNone" @change="updateForm('mentalStatusNone',true);form.mentalStatusList=[]" /> 无精神问题</label>
-            <label><input type="checkbox" :checked="!form.mentalStatusNone" @change="updateForm('mentalStatusNone',false)" /> 有(可多选)</label>
-            <div v-if="!form.mentalStatusNone" class="mental-list">
+            <label><input type="checkbox" :checked="form.mentalStatusNone === true" @change="updateForm('mentalStatusNone',true);form.mentalStatusList=[]" /> 无精神问题 </label>
+            <label><input type="checkbox" :checked="form.mentalStatusNone === false" @change="updateForm('mentalStatusNone',false)" /> 有(可多选)</label>
+            <div v-if="form.mentalStatusNone === false" class="mental-list">
               <label v-for="m in mentalOptions" :key="m" class="mental-item">
                 <input type="checkbox" :checked="form.mentalStatusList.includes(m)" @change="toggleArrayItem('mentalStatusList',m)" /> {{ m }}
               </label>
@@ -843,8 +843,8 @@ const onInlineInput = (key: keyof CheckInRegistrationForm, e: Event) => {
             </div>
           </template>
           <template v-else>
-            {{ printCheck(form.mentalStatusNone) }} 无精神问题
-            <template v-if="!form.mentalStatusNone">{{ printCheck(true) }} 有:{{ form.mentalStatusList.join('、') }} {{ form.mentalStatusOther }}</template>
+            {{ printCheck(form.mentalStatusNone === true) }} 无精神问题
+            <template v-if="form.mentalStatusNone === false">{{ printCheck(true) }} 有:{{ form.mentalStatusList.join('、') }} {{ form.mentalStatusOther }}</template>
           </template>
         </td>
       </tr>

+ 122 - 40
src/views/elderly/contracts/components/ContractBody.vue

@@ -641,6 +641,73 @@ const roomTypeOptions = computed(() => {
   return bedList.value.map((item) => ({ label: item.chargeName, value: item.chargeName }))
 })
 
+const isAttachmentSelected = (no: number) => !!props.attachmentIds?.[`attach_${no}`]
+
+const attachmentTitleMap: Record<number, { title: string; subTitle?: string }> = {
+  1: { title: '《知情同意书》' },
+  2: { title: '《入住登记表》' },
+  3: { title: '《入住须知》' },
+  4: { title: '《长者安全承诺书》' },
+  5: { title: '《长者外出情况确认书》' },
+  6: { title: '《签名代理申请》' },
+  7: { title: '《授权委托书》' },
+  8: { title: '《委托代理人确认表》' },
+  9: { title: '《机构服务范围及收费标准》' },
+  10: { title: '《房间设施设备清单》' },
+  11: {
+    title: '二级甲等以上医院出具的《体检报告项目说明》',
+    subTitle: '(体检时间应在签订服务合同前30日以内)'
+  },
+  12: { title: '乙方有效证件(身份证、户口本)复印件' },
+  13: { title: '乙方监护人(丙方)身份证、户口本复印件' },
+  14: { title: '甲方登记证书及民政部门备案回执' },
+  15: { title: '老年人能力综合评估结果' },
+  16: { title: '首次服务项目确认表' },
+  17: { title: '变更事项确认表' }
+}
+
+const toChineseNumber = (num: number): string => {
+  const chars = [
+    '零',
+    '一',
+    '二',
+    '三',
+    '四',
+    '五',
+    '六',
+    '七',
+    '八',
+    '九',
+    '十',
+    '十一',
+    '十二',
+    '十三',
+    '十四',
+    '十五',
+    '十六',
+    '十七'
+  ]
+  return chars[num] || String(num)
+}
+
+const selectedAttachmentItems = computed(() => {
+  const items: { no: number; title: string; subTitle?: string }[] = []
+  for (let no = 1; no <= 17; no++) {
+    if (isAttachmentSelected(no)) {
+      const config = attachmentTitleMap[no]
+      if (config) {
+        items.push({ no, ...config })
+      }
+    }
+  }
+  return items
+})
+
+const page24AttachmentItems = computed(() => selectedAttachmentItems.value.slice(0, 14))
+const page25AttachmentItems = computed(() =>
+  selectedAttachmentItems.value.slice(14)
+)
+
 const contractFormUpdate = ref({})
 
 const saveContract = async () => {
@@ -2055,18 +2122,37 @@ watch(
         </p>
 
         <h2 class="section-title">第十三条&nbsp;&nbsp;争议解决方式</h2>
-        <p class="content-text">
-          &nbsp;&nbsp;&nbsp;&nbsp;本合同在履行过程中发生争议,当事人尽量协商解决。协商不成的,当事人可以选择下列第
-          <FieldItem
-            v-model="contractForm.disputeMode"
-            :is-text-mode="isTextMode"
-            width="40px"
-            placeholder="2"
-          />
-          种方式解决:
-        </p>
-        <p class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;☑ 1.向仲裁机构申请仲裁。 </p>
-        <p class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;☑ 2.向有管辖权的人民法院提起诉讼。 </p>
+        <template v-if="!isTextMode">
+          <p class="content-text">
+            &nbsp;&nbsp;&nbsp;&nbsp;本合同在履行过程中发生争议,当事人尽量协商解决。协商不成的,当事人可以选择下列第
+            <el-radio-group
+              v-model="contractForm.disputeMode"
+              size="small"
+              style="display: inline-flex; vertical-align: middle; margin: 0 4px"
+            >
+              <el-radio label="1">1</el-radio>
+              <el-radio label="2">2</el-radio>
+            </el-radio-group>
+            种方式解决:
+          </p>
+          <p class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;1.向仲裁机构申请仲裁。 </p>
+          <p class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;2.向有管辖权的人民法院提起诉讼。 </p>
+        </template>
+        <template v-else>
+          <p class="content-text">
+            &nbsp;&nbsp;&nbsp;&nbsp;本合同在履行过程中发生争议,当事人尽量协商解决。协商不成的,当事人可以选择下列第
+            <span style="display: inline-block; min-width: 30px; text-align: center; border-bottom: 1px solid #333">
+              {{ contractForm.disputeMode || '2' }}
+            </span>
+            种方式解决:
+          </p>
+          <p class="content-text">
+            &nbsp;&nbsp;&nbsp;&nbsp;{{ contractForm.disputeMode === '1' ? '☑' : '□' }} 1.向仲裁机构申请仲裁。
+          </p>
+          <p class="content-text">
+            &nbsp;&nbsp;&nbsp;&nbsp;{{ contractForm.disputeMode === '2' ? '☑' : '□' }} 2.向有管辖权的人民法院提起诉讼。
+          </p>
+        </template>
       </div>
     </div>
 
@@ -2116,31 +2202,18 @@ watch(
         <p class="content-text" style="margin-top: 16px">
           &nbsp;&nbsp;&nbsp;&nbsp;1.下列文件为本合同附件,与本合同具有同等法律效力:
         </p>
-        <p class="content-text" style="padding-left: 2em"> 附件一:《知情同意书》 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件二:《入住登记表》 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件三:《入住须知》 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件四:《长者安全承诺书》 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件五:《长者外出情况确认书》 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件六:《签名代理申请》 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件七:《授权委托书》 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件八:《委托代理人确认表》 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件九:《机构服务范围及收费标准》 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件十:《房间设施设备清单》 </p>
-        <p class="content-text" style="padding-left: 2em">
-          附件十一:二级甲等以上医院出具的《体检报告项目说明》
-        </p>
-        <p class="content-text" style="padding-left: 3em; font-size: 13px">
-          (体检时间应在签订服务合同前30日以内)
-        </p>
-        <p class="content-text" style="padding-left: 2em">
-          附件十二:乙方有效证件(身份证、户口本)复印件
-        </p>
-        <p class="content-text" style="padding-left: 2em">
-          附件十三:乙方监护人(丙方)身份证、户口本复印件
-        </p>
-        <p class="content-text" style="padding-left: 2em">
-          附件十四:甲方登记证书及民政部门备案回执
-        </p>
+        <template v-for="(item, index) in page24AttachmentItems" :key="item.no">
+          <p class="content-text" style="padding-left: 2em">
+            附件{{ toChineseNumber(index + 1) }}:{{ item.title }}
+          </p>
+          <p
+            v-if="item.subTitle"
+            class="content-text"
+            style="padding-left: 3em; font-size: 13px"
+          >
+            {{ item.subTitle }}
+          </p>
+        </template>
       </div>
     </div>
 
@@ -2154,9 +2227,18 @@ watch(
         <span class="page-no-right">24</span>
       </div>
       <div class="page-content">
-        <p class="content-text" style="padding-left: 2em"> 附件十五:老年人能力综合评估结果 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件十六:首次服务项目确认表 </p>
-        <p class="content-text" style="padding-left: 2em"> 附件十七:变更事项确认表 </p>
+        <template v-for="(item, index) in page25AttachmentItems" :key="item.no">
+          <p class="content-text" style="padding-left: 2em">
+            附件{{ toChineseNumber(page24AttachmentItems.length + index + 1) }}:{{ item.title }}
+          </p>
+          <p
+            v-if="item.subTitle"
+            class="content-text"
+            style="padding-left: 3em; font-size: 13px"
+          >
+            {{ item.subTitle }}
+          </p>
+        </template>
 
         <p class="content-text" style="margin-top: 12px">
           &nbsp;&nbsp;&nbsp;&nbsp;2.本合同附件系本合同不可分割的组成部分,与本合同具有同等法律效力。

+ 32 - 14
src/views/elderly/contracts/components/DocumentAttachmentBody.vue

@@ -1,6 +1,8 @@
 <script setup lang="ts">
 import { reactive, watch, ref, computed } from 'vue'
-import { useUpload, FileItem } from '@/components/UploadFile/src/useUpload'
+import { useUpload, pdfFileToImageFiles } from '@/components/UploadFile/src/useUpload'
+
+const message = useMessage()
 
 const props = withDefaults(defineProps<{
   isTextMode?: boolean
@@ -52,24 +54,40 @@ const { httpRequest: uploadFn } = useUpload(props.funName, { elderId: props.elde
 const uploadingCount = ref(0)
 const uploading = computed(() => uploadingCount.value > 0)
 
-const handleFileChange = (e: Event) => {
+const uploadSingleFile = (file: File) => {
+  uploadingCount.value++
+  return uploadFn({ file } as any)
+    .then((res: any) => {
+      if (res.code === 0) {
+        form.images.push(res.data)
+        updateForm()
+      }
+    })
+    .finally(() => {
+      uploadingCount.value--
+    })
+}
+
+const handleFileChange = async (e: Event) => {
   const target = e.target as HTMLInputElement
   const files = target.files
   if (!files || files.length === 0) return
 
   for (let i = 0; i < files.length; i++) {
     const file = files[i]
-    uploadingCount.value++
-    uploadFn({ file } as any)
-      .then((res: any) => {
-        if (res.code === 0) {
-          form.images.push(res.data)
-          updateForm()
+    if (file.type === 'application/pdf') {
+      try {
+        const imageFiles = await pdfFileToImageFiles(file)
+        for (const imgFile of imageFiles) {
+          await uploadSingleFile(imgFile)
         }
-      })
-      .finally(() => {
-        uploadingCount.value--
-      })
+      } catch (error) {
+        console.error('PDF 解析失败', error)
+        message.error(`PDF 解析失败:${file.name}`)
+      }
+    } else {
+      await uploadSingleFile(file)
+    }
   }
 
   target.value = ''
@@ -98,12 +116,12 @@ defineExpose({ form })
       <div class="upload-box" @click="triggerUpload">
         <div class="upload-placeholder">
           <span class="upload-icon">+</span>
-          <span class="upload-text">{{ uploading ? '上传中...' : '点击上传图片(支持多张)' }}</span>
+          <span class="upload-text">{{ uploading ? '上传中...' : '点击上传图片或 PDF(支持多张)' }}</span>
         </div>
         <input
           ref="fileInputRef"
           type="file"
-          accept="image/*"
+          accept="image/*,application/pdf"
           multiple
           style="display: none"
           @change="handleFileChange"

+ 32 - 14
src/views/elderly/contracts/components/ExplanationExaminationReportBody.vue

@@ -1,6 +1,8 @@
 <script setup lang="ts">
 import { reactive, watch, ref, computed } from 'vue'
-import { useUpload } from '@/components/UploadFile/src/useUpload'
+import { useUpload, pdfFileToImageFiles } from '@/components/UploadFile/src/useUpload'
+
+const message = useMessage()
 
 const props = withDefaults(defineProps<{
   isTextMode?: boolean
@@ -46,24 +48,40 @@ const { httpRequest: uploadFn } = useUpload('体检报告', { elderId: props.eld
 const uploadingCount = ref(0)
 const uploading = computed(() => uploadingCount.value > 0)
 
-const handleFileChange = (e: Event) => {
+const uploadSingleFile = (file: File) => {
+  uploadingCount.value++
+  return uploadFn({ file } as any)
+    .then((res: any) => {
+      if (res.code === 0) {
+        form.images.push(res.data)
+        updateForm()
+      }
+    })
+    .finally(() => {
+      uploadingCount.value--
+    })
+}
+
+const handleFileChange = async (e: Event) => {
   const target = e.target as HTMLInputElement
   const files = target.files
   if (!files || files.length === 0) return
 
   for (let i = 0; i < files.length; i++) {
     const file = files[i]
-    uploadingCount.value++
-    uploadFn({ file } as any)
-      .then((res: any) => {
-        if (res.code === 0) {
-          form.images.push(res.data)
-          updateForm()
+    if (file.type === 'application/pdf') {
+      try {
+        const imageFiles = await pdfFileToImageFiles(file)
+        for (const imgFile of imageFiles) {
+          await uploadSingleFile(imgFile)
         }
-      })
-      .finally(() => {
-        uploadingCount.value--
-      })
+      } catch (error) {
+        console.error('PDF 解析失败', error)
+        message.error(`PDF 解析失败:${file.name}`)
+      }
+    } else {
+      await uploadSingleFile(file)
+    }
   }
 
   target.value = ''
@@ -93,12 +111,12 @@ defineExpose({ form })
       <div class="upload-box" @click="triggerUpload">
         <div class="upload-placeholder">
           <span class="upload-icon">+</span>
-          <span class="upload-text">{{ uploading ? '上传中...' : '点击上传体检报告图片(支持多张)' }}</span>
+          <span class="upload-text">{{ uploading ? '上传中...' : '点击上传体检报告图片或 PDF(支持多张)' }}</span>
         </div>
         <input
           ref="fileInputRef"
           type="file"
-          accept="image/*"
+          accept="image/*,application/pdf"
           multiple
           style="display: none"
           @change="handleFileChange"

+ 245 - 52
src/views/elderly/elder/family-appointment/time-setting/index.vue

@@ -30,13 +30,18 @@
             </div>
             <div class="card-body">
               <el-form-item label="放号规则">
-                <el-radio-group v-model="formData.ruleType">
+                <el-radio-group v-model="formData.ruleType" :disabled="isDisabled">
                   <el-radio :value="1">时间循环预约</el-radio>
                 </el-radio-group>
               </el-form-item>
 
               <el-form-item label="开始预约日" prop="startDay">
-                <el-select v-model="formData.startDay" placeholder="请选择" style="width: 100%">
+                <el-select
+                  v-model="formData.startDay"
+                  placeholder="请选择"
+                  style="width: 100%"
+                  :disabled="isDisabled"
+                >
                   <el-option label="当天" :value="0" />
                   <el-option label="未来1天" :value="1" />
                   <el-option label="未来2天" :value="2" />
@@ -58,9 +63,10 @@
               <el-form-item label="提前预约天数" prop="advanceDays">
                 <el-input-number
                   v-model="formData.advanceDays"
-                  :min="1"
+                  :min="0"
                   :max="30"
                   style="width: 100%"
+                  :disabled="isDisabled"
                 >
                   <template #suffix>
                     <span>天</span>
@@ -74,7 +80,12 @@
             <div class="card-header">
               <el-icon class="card-icon icon-green"><Calendar /></el-icon>
               <span class="card-title">预约组</span>
-              <el-button type="primary" size="small" @click="addAppointmentGroup">
+              <el-button
+                type="primary"
+                size="small"
+                :disabled="isDisabled"
+                @click="addAppointmentGroup"
+              >
                 <el-icon><Plus /></el-icon>
                 添加预约组
               </el-button>
@@ -84,13 +95,18 @@
                 <el-empty description="暂无预约组,点击右上角添加" :image-size="60" />
               </div>
               <div v-else class="group-list">
-                <div v-for="(group, groupIndex) in appointmentGroups" :key="group.id" class="group-card">
+                <div
+                  v-for="(group, groupIndex) in appointmentGroups"
+                  :key="group.id"
+                  class="group-card"
+                >
                   <div class="group-header">
                     <span class="group-label">预约日期:</span>
                     <div class="cycle-buttons">
                       <el-button
                         :type="group.cycleType === 'daily' ? 'success' : 'default'"
                         size="small"
+                        :disabled="isDisabled"
                         @click="setCycleType(group, 'daily')"
                       >
                         每天
@@ -98,6 +114,7 @@
                       <el-button
                         :type="group.cycleType === 'weekly' ? 'success' : 'default'"
                         size="small"
+                        :disabled="isDisabled"
                         @click="openWeeklyDialog(group)"
                       >
                         每周
@@ -105,6 +122,7 @@
                       <el-button
                         :type="group.cycleType === 'monthly' ? 'success' : 'default'"
                         size="small"
+                        :disabled="isDisabled"
                         @click="openMonthlyDialog(group)"
                       >
                         每月
@@ -112,12 +130,19 @@
                       <el-button
                         :type="group.cycleType === 'custom' ? 'success' : 'default'"
                         size="small"
+                        :disabled="isDisabled"
                         @click="openCustomDialog(group)"
                       >
                         自定义
                       </el-button>
                     </div>
-                    <el-button type="danger" link size="small" @click="removeGroup(groupIndex)">
+                    <el-button
+                      type="danger"
+                      link
+                      size="small"
+                      :disabled="isDisabled"
+                      @click="removeGroup(groupIndex)"
+                    >
                       <el-icon><Delete /></el-icon>
                       删除组
                     </el-button>
@@ -129,7 +154,9 @@
                     </template>
                     <template v-else-if="group.cycleType === 'weekly'">
                       <span class="dates-label">每周可预约:</span>
-                      <el-tag v-if="group.weeklyDays.length === 0" type="info" size="small">未设置</el-tag>
+                      <el-tag v-if="group.weeklyDays.length === 0" type="info" size="small"
+                        >未设置</el-tag
+                      >
                       <el-tag
                         v-for="day in group.weeklyDays"
                         :key="day"
@@ -142,7 +169,9 @@
                     </template>
                     <template v-else-if="group.cycleType === 'monthly'">
                       <span class="dates-label">每月可预约:</span>
-                      <el-tag v-if="group.monthlyDays.length === 0" type="info" size="small">未设置</el-tag>
+                      <el-tag v-if="group.monthlyDays.length === 0" type="info" size="small"
+                        >未设置</el-tag
+                      >
                       <el-tag
                         v-for="day in group.monthlyDays"
                         :key="day"
@@ -155,7 +184,9 @@
                     </template>
                     <template v-else-if="group.cycleType === 'custom'">
                       <span class="dates-label">自定义日期:</span>
-                      <el-tag v-if="group.customDates.length === 0" type="info" size="small">未设置</el-tag>
+                      <el-tag v-if="group.customDates.length === 0" type="info" size="small"
+                        >未设置</el-tag
+                      >
                       <el-tag
                         v-for="date in group.customDates.slice(0, 5)"
                         :key="date"
@@ -187,12 +218,12 @@
                       暂无预约项,点击下方按钮添加
                     </div>
                     <div v-else class="items-list">
-                      <div
-                        v-for="(item, itemIndex) in group.items"
-                        :key="item.id"
-                        class="item-row"
-                      >
-                        <div class="item-delete-btn" @click="removeItem(group, itemIndex)">
+                      <div v-for="(item, itemIndex) in group.items" :key="item.id" class="item-row">
+                        <div
+                          class="item-delete-btn"
+                          :class="{ disabled: isDisabled }"
+                          @click="removeItem(group, itemIndex)"
+                        >
                           <el-icon><Minus /></el-icon>
                         </div>
                         <div class="item-fields">
@@ -205,6 +236,7 @@
                               placeholder="开始时间"
                               size="small"
                               style="width: 110px"
+                              :disabled="isDisabled"
                               @change="checkTimeOverlap(group, itemIndex)"
                             />
                             <span class="time-sep">至</span>
@@ -215,6 +247,7 @@
                               placeholder="结束时间"
                               size="small"
                               style="width: 110px"
+                              :disabled="isDisabled"
                               @change="checkTimeOverlap(group, itemIndex)"
                             />
                           </div>
@@ -226,6 +259,7 @@
                               :max="9999"
                               size="small"
                               style="width: 110px"
+                              :disabled="isDisabled"
                             />
                           </div>
                         </div>
@@ -233,7 +267,12 @@
                     </div>
 
                     <div class="add-item-row">
-                      <el-button type="success" size="small" @click="addItem(group)">
+                      <el-button
+                        type="success"
+                        size="small"
+                        :disabled="isDisabled"
+                        @click="addItem(group)"
+                      >
                         <el-icon><Plus /></el-icon>
                         添加预约项
                       </el-button>
@@ -249,11 +288,11 @@
           <div class="card card-mid-right">
             <div class="card-header">
               <el-icon class="card-icon icon-green"><Tickets /></el-icon>
-              <span class="card-title">总名额限制</span>
+              <span class="card-title">总名额限制(按自然周和月)</span>
               <el-button
                 type="primary"
                 size="small"
-                :disabled="totalLimits.length >= 3"
+                :disabled="isDisabled || totalLimits.length >= 3"
                 @click="openTotalLimitDialog"
               >
                 <el-icon><Plus /></el-icon>
@@ -268,7 +307,13 @@
                 <div v-for="(limit, index) in totalLimits" :key="index" class="limit-item-inline">
                   <span class="limit-type-badge">{{ getLimitTypeLabel(limit.type) }}</span>
                   <span class="limit-count-num">{{ limit.count }} <em>次</em></span>
-                  <el-button type="danger" link size="small" @click="removeTotalLimit(index)">
+                  <el-button
+                    type="danger"
+                    link
+                    size="small"
+                    :disabled="isDisabled"
+                    @click="removeTotalLimit(index)"
+                  >
                     <el-icon><Delete /></el-icon>
                   </el-button>
                 </div>
@@ -279,11 +324,11 @@
           <div class="card card-bottom-right">
             <div class="card-header">
               <el-icon class="card-icon icon-purple"><User /></el-icon>
-              <span class="card-title">单人名额限制</span>
+              <span class="card-title">单人名额限制(按自然周和月)</span>
               <el-button
                 type="primary"
                 size="small"
-                :disabled="personLimits.length >= 3"
+                :disabled="isDisabled || personLimits.length >= 3"
                 @click="openPersonLimitDialog"
               >
                 <el-icon><Plus /></el-icon>
@@ -298,7 +343,13 @@
                 <div v-for="(limit, index) in personLimits" :key="index" class="limit-item-inline">
                   <span class="limit-type-badge">{{ getLimitTypeLabel(limit.type) }}</span>
                   <span class="limit-count-num">{{ limit.count }} <em>次</em></span>
-                  <el-button type="danger" link size="small" @click="removePersonLimit(index)">
+                  <el-button
+                    type="danger"
+                    link
+                    size="small"
+                    :disabled="isDisabled"
+                    @click="removePersonLimit(index)"
+                  >
                     <el-icon><Delete /></el-icon>
                   </el-button>
                 </div>
@@ -455,6 +506,11 @@ import { Setting, Calendar, User, Plus, Delete, Tickets, Minus } from '@element-
 import type { FormInstance, FormRules } from 'element-plus'
 import fetchHttp from '@/config/axios/fetchHttp'
 import { useUserStore } from '@/store/modules/user'
+import {
+  appointmentConfigADD,
+  appointmentConfigGetByTenant,
+  appointmentConfigUpdate
+} from '@/api/elderly/elder/outwardRegustration'
 
 const userStore = useUserStore()
 const message = useMessage()
@@ -465,6 +521,7 @@ const tenantIds = ref<number[]>([])
 let organizationId = 0
 let groupIdCounter = 0
 let itemIdCounter = 0
+const configId = ref<number | undefined>(undefined)
 
 type LimitItem = { type: number; count: number }
 
@@ -486,6 +543,33 @@ type AppointmentGroup = {
   items: AppointmentItem[]
 }
 
+type AppointmentConfigDO = {
+  id?: number
+  advanceDays?: number
+  beginBookDays?: number
+  enabled?: number
+  personDailyLimit?: number
+  personMonthlyLimit?: number
+  personWeeklyLimit?: number
+  tenantId?: number
+  totalDailyLimit?: number
+  totalMonthlyLimit?: number
+  totalWeeklyLimit?: number
+  [property: string]: any
+}
+
+type AppointmentGroupDO = {
+  id?: number
+  beginTime?: string
+  customDates?: string
+  dateType?: string
+  endTime?: string
+  limitCount?: number
+  monthDays?: string
+  tenantId?: number
+  weekDays?: string
+}
+
 const limitTypeOptions = [
   { label: '每天', value: 1 },
   { label: '每周', value: 2 },
@@ -502,6 +586,23 @@ const weekDays = [
   { label: '周日', value: 7 }
 ]
 
+const limitTypeToFieldMap: Record<number, { total: string; person: string }> = {
+  1: { total: 'totalDailyLimit', person: 'personDailyLimit' },
+  2: { total: 'totalWeeklyLimit', person: 'personWeeklyLimit' },
+  3: { total: 'totalMonthlyLimit', person: 'personMonthlyLimit' }
+}
+
+const cycleTypeToDateType = (cycleType: CycleType): string => cycleType.toUpperCase()
+const dateTypeToCycleType = (dateType: string): CycleType => {
+  const map: Record<string, CycleType> = {
+    DAILY: 'daily',
+    WEEKLY: 'weekly',
+    MONTHLY: 'monthly',
+    CUSTOM: 'custom'
+  }
+  return map[dateType] || 'daily'
+}
+
 const totalLimits = ref<LimitItem[]>([])
 const personLimits = ref<LimitItem[]>([])
 const appointmentGroups = ref<AppointmentGroup[]>([])
@@ -530,6 +631,8 @@ const availablePersonTypes = computed(() => {
   return limitTypeOptions.filter((item) => !usedTypes.includes(item.value))
 })
 
+const isDisabled = computed(() => !formData.appointmentEnabled)
+
 const getLimitTypeLabel = (type: number) => {
   const found = limitTypeOptions.find((item) => item.value === type)
   return found ? found.label : ''
@@ -620,9 +723,13 @@ const addItem = (group: AppointmentGroup) => {
   if (lastEndMin >= 0) {
     const newStartMin = Math.min(lastEndMin + 60, 23 * 60)
     const newEndMin = Math.min(newStartMin + 60, 24 * 60)
-    const sh = Math.floor(newStartMin / 60).toString().padStart(2, '0')
+    const sh = Math.floor(newStartMin / 60)
+      .toString()
+      .padStart(2, '0')
     const sm = (newStartMin % 60).toString().padStart(2, '0')
-    const eh = Math.floor(newEndMin / 60).toString().padStart(2, '0')
+    const eh = Math.floor(newEndMin / 60)
+      .toString()
+      .padStart(2, '0')
     const em = (newEndMin % 60).toString().padStart(2, '0')
     newStart = `${sh}:${sm}`
     newEnd = `${eh}:${em}`
@@ -775,29 +882,65 @@ const changeTen = (i: any[]) => {
   }
 }
 
+const parseGroups = (groups: AppointmentGroupDO[]): AppointmentGroup[] => {
+  const map = new Map<string, AppointmentGroup>()
+  groups.forEach((g) => {
+    const cycleType = dateTypeToCycleType(g.dateType || 'DAILY')
+    const weekDays = g.weekDays || ''
+    const monthDays = g.monthDays || ''
+    const customDates = g.customDates || ''
+    const key = `${cycleType}_${weekDays}_${monthDays}_${customDates}`
+    if (!map.has(key)) {
+      groupIdCounter++
+      map.set(key, {
+        id: groupIdCounter,
+        cycleType,
+        weeklyDays: weekDays ? weekDays.split(',').map(Number) : [],
+        monthlyDays: monthDays ? monthDays.split(',').map(Number) : [],
+        customDates: customDates ? customDates.split(',') : [],
+        items: []
+      })
+    }
+    const group = map.get(key)!
+    itemIdCounter++
+    group.items.push({
+      id: itemIdCounter,
+      startTime: g.beginTime || '',
+      endTime: g.endTime || '',
+      limitCount: g.limitCount || 100
+    })
+  })
+  return Array.from(map.values())
+}
+
 const getSetting = async () => {
   try {
-    const res = await fetchHttp.get('/admin/appointment/getTimeslotSetting', {
-      organizationId
-    })
-    if (res) {
+    const res = await appointmentConfigGetByTenant({ tenantId: organizationId })
+    if (res && res.config) {
+      const config = res.config
+      configId.value = config.id
       Object.assign(formData, {
-        ruleType: res.ruleType || 1,
-        startDay: res.startDay ?? 0,
-        advanceDays: res.advanceDays || 6,
-        appointmentEnabled: res.appointmentEnabled !== false
+        ruleType: 1,
+        startDay: config.beginBookDays ?? 0,
+        advanceDays: config.advanceDays ?? 0,
+        appointmentEnabled: config.enabled === 1
       })
-      totalLimits.value = res.totalLimits || []
-      personLimits.value = res.personLimits || []
-      appointmentGroups.value = (res.appointmentGroups || []).map((g: AppointmentGroup) => ({
-        ...g,
-        items: (g.items || []).map((it) => ({
-          id: it.id,
-          startTime: it.startTime || '',
-          endTime: it.endTime || '',
-          limitCount: it.limitCount || 100
-        }))
-      }))
+      totalLimits.value = []
+      personLimits.value = []
+      ;[1, 2, 3].forEach((type) => {
+        const field = limitTypeToFieldMap[type]
+        const totalVal = config[field.total]
+        if (totalVal !== undefined && totalVal !== null && totalVal > 0) {
+          totalLimits.value.push({ type, count: totalVal })
+        }
+        const personVal = config[field.person]
+        if (personVal !== undefined && personVal !== null && personVal > 0) {
+          personLimits.value.push({ type, count: personVal })
+        }
+      })
+      groupIdCounter = 0
+      itemIdCounter = 0
+      appointmentGroups.value = parseGroups(res.group || [])
       if (appointmentGroups.value.length > 0) {
         groupIdCounter = Math.max(...appointmentGroups.value.map((g) => g.id))
         let maxItemId = 0
@@ -808,6 +951,12 @@ const getSetting = async () => {
         })
         itemIdCounter = maxItemId
       }
+    } else {
+      configId.value = undefined
+      Object.assign(formData, defaultFormData())
+      totalLimits.value = []
+      personLimits.value = []
+      appointmentGroups.value = []
     }
   } catch (e) {
     console.log('获取设置失败', e)
@@ -823,7 +972,11 @@ const handleSubmit = async () => {
   for (const group of appointmentGroups.value) {
     for (let i = 0; i < group.items.length; i++) {
       const item = group.items[i]
-      if (item.startTime && item.endTime && timeToMinutes(item.startTime) >= timeToMinutes(item.endTime)) {
+      if (
+        item.startTime &&
+        item.endTime &&
+        timeToMinutes(item.startTime) >= timeToMinutes(item.endTime)
+      ) {
         message.error('存在开始时间晚于或等于结束时间的预约项,请检查')
         return
       }
@@ -838,17 +991,51 @@ const handleSubmit = async () => {
   try {
     await formRef.value.validate()
     submitting.value = true
-    const params = {
-      organizationId,
-      ...formData,
-      totalLimits: totalLimits.value,
-      personLimits: personLimits.value,
-      appointmentGroups: appointmentGroups.value
+    const config: AppointmentConfigDO = {
+      id: configId.value,
+      tenantId: organizationId,
+      advanceDays: formData.advanceDays,
+      beginBookDays: formData.startDay,
+      enabled: formData.appointmentEnabled ? 1 : 0
     }
-    const res = await fetchHttp.post('/admin/appointment/setTimeslotSetting', params)
+    totalLimits.value.forEach((limit) => {
+      const field = limitTypeToFieldMap[limit.type]
+      if (field) {
+        config[field.total] = limit.count
+      }
+    })
+    personLimits.value.forEach((limit) => {
+      const field = limitTypeToFieldMap[limit.type]
+      if (field) {
+        config[field.person] = limit.count
+      }
+    })
+    const group: AppointmentGroupDO[] = []
+    appointmentGroups.value.forEach((g) => {
+      const dateType = cycleTypeToDateType(g.cycleType)
+      const weekDays = g.weeklyDays.join(',') || undefined
+      const monthDays = g.monthlyDays.join(',') || undefined
+      const customDates = g.customDates.join(',') || undefined
+      g.items.forEach((item) => {
+        group.push({
+          dateType,
+          weekDays,
+          monthDays,
+          customDates,
+          beginTime: item.startTime,
+          endTime: item.endTime,
+          limitCount: item.limitCount,
+          tenantId: organizationId
+        })
+      })
+    })
+    const params = { config, group }
+    const res = configId.value
+      ? await appointmentConfigUpdate(params)
+      : await appointmentConfigADD(params)
     if (res) {
       message.success('设置成功!')
-      getSetting()
+      await getSetting()
     }
   } catch (error: any) {
     if (error?.message) {
@@ -1090,6 +1277,12 @@ onMounted(() => {
     &:hover {
       background-color: #fef0f0;
     }
+
+    &.disabled {
+      pointer-events: none;
+      opacity: 0.4;
+      cursor: not-allowed;
+    }
   }
 
   .item-fields {

Some files were not shown because too many files changed in this diff