Просмотр исходного кода

电子合同接口对接及优化

chenjun 1 месяц назад
Родитель
Сommit
fc0bb533b1

+ 72 - 44
src/views/elderly/contracts/ContractManageForm.vue

@@ -335,12 +335,40 @@ const handleAttachmentsLoad = (data: Record<string, any>) => {
   if (!data) return
   Object.keys(data).forEach((key) => {
     if (attachments.hasOwnProperty(key)) {
-      console.log('AAAAAAA', data[key])
+      // console.log('AAAAAAA', data[key])
       attachments[key] = data[key]
       if (key == 'attach_1') {
         Object.assign(riskDisclosureForm, data[key])
       } else if (key == 'attach_2') {
         Object.assign(checkInRegistrationForm, data[key])
+      } else if (key == 'attach_4') {
+        Object.assign(safetyCommitmentForm, data[key])
+      } else if (key == 'attach_5') {
+        Object.assign(outingConfirmationForm, data[key])
+      } else if (key == 'attach_6') {
+        Object.assign(signatureProxyForm, data[key])
+      } else if (key == 'attach_7') {
+        Object.assign(powerOfAttorneyForm, data[key])
+      } else if (key == 'attach_8') {
+        Object.assign(delegationAgentForm, data[key])
+      } else if (key == 'attach_9') {
+        Object.assign(serviceChargingForm, data[key])
+      } else if (key == 'attach_10') {
+        Object.assign(roomFacilitiesForm, data[key])
+      } else if (key == 'attach_11') {
+        Object.assign(examinationReportForm, data[key])
+      } else if (key == 'attach_12') {
+        Object.assign(identificationPartyBForm, data[key])
+      } else if (key == 'attach_13') {
+        Object.assign(guardianPartyBForm, data[key])
+      } else if (key == 'attach_14') {
+        Object.assign(registrationCertificateForm, data[key])
+      } else if (key == 'attach_15') {
+        Object.assign(comprehensiveAssessmentForm, data[key])
+      } else if (key == 'attach_16') {
+        Object.assign(firstServiceForm, data[key])
+      } else if (key == 'attach_17') {
+        Object.assign(changeConfirmForm, data[key])
       }
     }
   })
@@ -465,56 +493,56 @@ const handleAttachmentSelect = async (row: any) => {
         const detail = await getDetail(row.id)
         if (detail && detail.content) {
           fullData = detail.content
-          const data = JSON.parse(fullData)
+          const dataJson = JSON.parse(fullData)
           // 将id添加到数据中,方便后续操作
-          data.id = row.id
-          fullData = JSON.stringify(data)
+          dataJson.id = row.id
+          fullData = JSON.stringify(dataJson)
         }
       } catch (error) {
         console.error(`获取附件详情失败: ${key}`, error)
       }
     }
-    attachments[key] = fullData
-    if (key === 'attach_1' && fullData) {
-      try {
-        const data = JSON.parse(fullData)
-        if (data) {
-          Object.assign(riskDisclosureForm, data)
-        }
-      } catch (error) {
-        console.error('解析风险披露数据失败', error)
-      }
-    }
-    if (key === 'attach_2' && fullData) {
-      try {
-        const data = JSON.parse(fullData)
-        if (data) {
-          Object.assign(checkInRegistrationForm, data)
-        }
-      } catch (error) {
-        console.error('解析入住登记表数据失败', error)
-      }
-    }
-    if (key === 'attach_4' && fullData) {
-      try {
-        const data = JSON.parse(fullData)
-        if (data) {
-          Object.assign(safetyCommitmentForm, data)
-        }
-      } catch (error) {
-        console.error('解析长者安全承诺书数据失败', error)
-      }
-    }
-    if (key === 'attach_5' && fullData) {
-      try {
-        const data = JSON.parse(fullData)
-        if (data) {
-          Object.assign(outingConfirmationForm, data)
-        }
-      } catch (error) {
-        console.error('解析长者外出情况确认书数据失败', error)
-      }
+
+    let data = undefined
+    try {
+      attachments[key] = fullData
+      data = JSON.parse(fullData)
+    } catch (error) {}
+
+    if (key === 'attach_1' && data) {
+      Object.assign(riskDisclosureForm, data)
+    } else if (key === 'attach_2') {
+      Object.assign(checkInRegistrationForm, data)
+    } else if (key === 'attach_3') {
+      Object.assign(safetyCommitmentForm, data)
+    } else if (key === 'attach_5') {
+      Object.assign(outingConfirmationForm, data)
+    } else if (key == 'attach_6') {
+      Object.assign(signatureProxyForm, data)
+    } else if (key == 'attach_7') {
+      Object.assign(powerOfAttorneyForm, data)
+    } else if (key == 'attach_8') {
+      Object.assign(delegationAgentForm, data)
+    } else if (key == 'attach_9') {
+      Object.assign(serviceChargingForm, data)
+    } else if (key == 'attach_10') {
+      Object.assign(roomFacilitiesForm, data)
+    } else if (key == 'attach_11') {
+      Object.assign(examinationReportForm, data)
+    } else if (key == 'attach_12') {
+      Object.assign(identificationPartyBForm, data)
+    } else if (key == 'attach_13') {
+      Object.assign(guardianPartyBForm, data)
+    } else if (key == 'attach_14') {
+      Object.assign(registrationCertificateForm, data)
+    } else if (key == 'attach_15') {
+      Object.assign(comprehensiveAssessmentForm, data)
+    } else if (key == 'attach_16') {
+      Object.assign(firstServiceForm, data)
+    } else if (key == 'attach_17') {
+      Object.assign(changeConfirmForm, data)
     }
+
     message.success(
       `已选择:${fullData.elderName || row[activeTitleField(key)] || attachTitle(key)}`
     )

+ 7 - 7
src/views/elderly/contracts/components/ContractBody.vue

@@ -451,7 +451,7 @@ const fetchContractData = async (row: any) => {
               config
                 .getDetail(id)
                 .then((detail) => {
-                  console.log('获取详情', detail)
+                  //console.log('获取详情', detail)
                   attachmentsMap[key] = JSON.parse(detail.content)
 
                 })
@@ -649,22 +649,22 @@ const saveContract = async () => {
     if (!contractForm.attachments) {
       contractForm.attachments = {}
     }
-    const attachmentData: Record<string, any> = {}
+   // const attachmentData: Record<string, any> = {}
     if (props.attachmentIds) {
       Object.keys(props.attachmentIds).forEach((key) => {
         const row = props.attachmentIds![key]
-        console.log('保存1', key, row)
+
         if (row) {
           try {
             const data = JSON.parse(row)
-            console.log('AAA', data)
+
             if (data.id) {
-              console.log('AAA', data.id)
+
               contractForm.attachments[key] = { id: data.id }
               const config = attachmentConfig[key]
-              console.log('config', config)
+
               contractFormUpdate.value[config.idField] = data.id
-              console.log('保存2', config, attachmentData)
+
             }
           } catch (e) {
             console.log(e)

+ 455 - 0
src/views/elderly/elder/family-appointment/time-setting/Form.vue

@@ -0,0 +1,455 @@
+<template>
+  <Dialog
+    scroll
+    @close="dialogVisible = false"
+    style="max-width: 100vw; min-width: 800px"
+    v-model="dialogVisible"
+    :title="dialogTitle"
+    class="OutwardRegistrationBak"
+    noPaddingEL="OutwardRegistrationBak"
+  >
+    <el-form ref="formRef" :model="dataForm" :label-width="labelWidth">
+      <div style="display: flex; flex-direction: column; margin-top: 20px">
+        <text>当前规则:</text>
+        <el-radio-group v-model="dataForm.type" class="ml-4">
+          <el-radio v-if="!isTodayAdd" :value="1" size="large">每天</el-radio>
+          <el-radio v-if="isTodayAdd" :value="2" size="large">单次</el-radio>
+        </el-radio-group>
+      </div>
+
+      <div style="display: flex; align-items: center; flex-direction: row" v-if="!isEdit">
+        <el-button v-if="!isEdit" @click="addTimes" type="primary" style="margin-left: 10px">
+          新增时间段
+        </el-button>
+        <template v-if="formMode === 'dailyAdd'">
+          <el-text style="margin-left: 8vw">可提前</el-text>
+          <el-input
+            :maxlength="2"
+            style="max-width: 66px; margin-left: 8px"
+            v-model="dataForm.advancedDays"
+          />
+          <el-text style="margin-left: 8px">天预约</el-text>
+        </template>
+      </div>
+
+      <div class="info-wrap">
+        <el-row
+          v-for="(item, index) in dataForm.slots"
+          :key="index"
+          style="
+            position: relative;
+            padding: 10px 0;
+            margin-bottom: 10px;
+            border: #e0e0e0 solid 1px;
+            border-radius: 6px;
+          "
+        >
+          <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+            <div style="flex-direction: row; display: flex; align-items: center; width: 100%">
+              <text
+                style="
+                  display: flex;
+                  width: 20px;
+                  height: 20px;
+                  margin-left: 10px;
+                  border: #ddd solid 1px;
+                  border-radius: 50%;
+                  align-items: center;
+                  justify-content: center;
+                "
+                >{{ index + 1 }}</text
+              >
+              <div style="display: flex; flex-direction: column; margin-left: -12px">
+                <el-form-item label="开始时间">
+                  <el-select
+                    v-model="item.hourS"
+                    class="m-2"
+                    placeholder="Select"
+                    style="width: 64px"
+                  >
+                    <el-option
+                      v-for="items in optionsH"
+                      :key="items.value"
+                      :label="items.label"
+                      :value="items.value"
+                    />
+                  </el-select>
+                  点
+                  <el-select
+                    v-model="item.minuteS"
+                    class="m-2"
+                    placeholder="Select"
+                    style="width: 64px"
+                  >
+                    <el-option
+                      v-for="items in optionsM"
+                      :key="items.value"
+                      :label="items.label"
+                      :value="items.value"
+                    />
+                  </el-select>
+                  分
+                </el-form-item>
+                <el-form-item label="结束时间">
+                  <el-select
+                    v-model="item.hourE"
+                    class="m-2"
+                    placeholder="Select"
+                    style="width: 64px"
+                  >
+                    <el-option
+                      v-for="items in optionsH"
+                      :key="items.value"
+                      :label="items.label"
+                      :value="items.value"
+                    />
+                  </el-select>
+                  点
+                  <el-select
+                    v-model="item.minuteE"
+                    class="m-2"
+                    placeholder="Select"
+                    style="width: 64px"
+                  >
+                    <el-option
+                      v-for="items in optionsM"
+                      :key="items.value"
+                      :label="items.label"
+                      :value="items.value"
+                    />
+                  </el-select>
+                  分
+                </el-form-item>
+              </div>
+              <div style="align-items: center; display: flex; flex-direction: row">
+                <text style="margin-left: 2vw">此时间段可预约次数:</text>
+                <el-input
+                  :maxlength="5"
+                  placeholder="输入次数"
+                  style="width: 78px; margin-left: 3px"
+                  v-model="item.totalQuota"
+                />
+              </div>
+
+              <el-icon
+                v-if="!isEdit"
+                @click="deleteItem(index)"
+                color="red"
+                size="large"
+                style="
+                  position: absolute;
+                  top: 0;
+                  right: 0;
+                  width: 30px;
+                  height: 30px;
+                  margin-left: auto;
+                  cursor: pointer;
+                "
+                ><Close
+              /></el-icon>
+            </div>
+          </el-col>
+        </el-row>
+      </div>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="handleClosed">关闭</el-button>
+      <el-button v-loading="formLoading" type="primary" @click="submitForm">确定</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+import { useMediaQuery } from '@vueuse/core'
+import { Close } from '@element-plus/icons-vue'
+import fetchHttp from '@/config/axios/fetchHttp'
+import { compareTimeStrings, isValidTimeFormat } from '@/utils/dateUtil'
+import { useUserStore } from '@/store/modules/user'
+
+type FormMode = 'dailyAdd' | 'dailyEdit' | 'todayAdd'
+
+type TimeslotEditItem = {
+  id?: number
+  timeslot: string
+  hourS: string
+  minuteS: string
+  hourE: string
+  minuteE: string
+  totalQuota: string | number
+}
+
+type DailyEditApiItem = {
+  id: number
+  timeQuantum: string
+  totalQuota: number
+}
+
+const { t } = useI18n()
+const message = useMessage()
+const userStore = useUserStore()
+
+const formRef = ref()
+const formLoading = ref(false)
+const dialogVisible = ref(false)
+const formMode = ref<FormMode>('dailyAdd')
+
+const currentWidth = useMediaQuery('(max-width: 800px)')
+
+const labelWidth = computed(() => (currentWidth.value ? '110px' : '120px'))
+const isEdit = computed(() => formMode.value === 'dailyEdit')
+const isTodayAdd = computed(() => formMode.value === 'todayAdd')
+const isDailyAdd = computed(() => formMode.value === 'dailyAdd')
+const dialogTitle = computed(() => {
+  if (formMode.value === 'dailyEdit') return '编辑预约时间段'
+  if (formMode.value === 'todayAdd') return '新增单日时间段'
+  return '新增预约时间段'
+})
+
+const dataForm = ref({
+  organizationId: undefined as any,
+  date: '',
+  advancedDays: '',
+  type: 1,
+  slots: [] as TimeslotEditItem[]
+})
+
+const optionsH = ref(
+  Array.from({ length: 24 }, (_, i) => {
+    const v = String(i).padStart(2, '0')
+    return { value: v, label: v }
+  })
+)
+
+const optionsM = ref([
+  { value: '00', label: '00' },
+  { value: '30', label: '30' }
+])
+
+
+const makeEmptySlot = (): TimeslotEditItem => ({
+  timeslot: '',
+  hourS: '00',
+  minuteS: '00',
+  hourE: '00',
+  minuteE: '00',
+  totalQuota: '1'
+})
+
+const buildTimeslot = (slot: TimeslotEditItem) => {
+  const sTime = `${slot.hourS}:${slot.minuteS}`
+  const eTime = `${slot.hourE}:${slot.minuteE}`
+  if (!isValidTimeFormat(sTime) || !isValidTimeFormat(eTime)) {
+    throw new Error('时间格式不正确,请使用HH:mm格式')
+  }
+  const result = compareTimeStrings(sTime, eTime)
+  if (result !== -1) {
+    throw new Error('开始时间不能大于等于结束时间!')
+  }
+  return `${sTime}-${eTime}`
+}
+
+const normalizeAndValidateSlots = () => {
+  for (const slot of dataForm.value.slots) {
+    if (slot.totalQuota === '' || slot.totalQuota === undefined || slot.totalQuota === null) {
+      throw new Error('请选择时间和输入次数')
+    }
+    const total = Number.parseInt(String(slot.totalQuota), 10)
+    if (Number.isNaN(total)) {
+      throw new Error('请输入数字!')
+    }
+    slot.totalQuota = total
+    slot.timeslot = buildTimeslot(slot) || ''
+  }
+}
+
+const addTimes = () => {
+  dataForm.value.slots.push(makeEmptySlot())
+}
+
+const deleteItem = (index: number) => {
+  dataForm.value.slots.splice(index, 1)
+}
+
+const resetForm = () => {
+  dataForm.value = {
+    organizationId: undefined,
+    date: '',
+    advancedDays: '',
+    type: 1,
+    slots: []
+  }
+  formRef.value?.resetFields()
+}
+
+const open = async (payload:{date: string, organizationId: number}) => {
+  formMode.value = 'dailyAdd'
+  resetForm()
+  dialogVisible.value = true
+  dataForm.value.date = payload.date
+  dataForm.value.type = 1
+  dataForm.value.organizationId = payload.organizationId
+}
+
+const openEdit = async (payload: { date: string; organizationId: number; list: DailyEditApiItem[] }) => {
+  formMode.value = 'dailyEdit'
+  resetForm()
+  dialogVisible.value = true
+  dataForm.value.date = payload.date
+  dataForm.value.organizationId = payload.organizationId
+  dataForm.value.type = 1
+  dataForm.value.slots = (payload.list || []).map((it) => {
+    const [start, end] = (it.timeQuantum || '').split('-')
+    const [hourS, minuteS] = (start || '').split(':')
+    const [hourE, minuteE] = (end || '').split(':')
+    return {
+      id: it.id,
+      timeslot: it.timeQuantum,
+      hourS: hourS || '00',
+      minuteS: minuteS || '00',
+      hourE: hourE || '00',
+      minuteE: minuteE || '00',
+      totalQuota: String(it.totalQuota ?? 1)
+    }
+  })
+}
+
+const openToday = async (payload: { date: string; organizationId: number }) => {
+  formMode.value = 'todayAdd'
+  resetForm()
+  dialogVisible.value = true
+  dataForm.value.date = payload.date
+  dataForm.value.organizationId = payload.organizationId
+  dataForm.value.type = 2
+  if (dataForm.value.slots.length === 0) addTimes()
+}
+
+defineExpose({ open, openEdit, openToday })
+
+const emit = defineEmits(['success'])
+
+const submitForm = async () => {
+  if (formLoading.value) return
+  formLoading.value = true
+  try {
+    try {
+      if (!formRef.value) return
+      const valid = await formRef.value.validate()
+      if (!valid) return
+
+      if (isDailyAdd.value) {
+        const num = Number.parseInt(String(dataForm.value.advancedDays), 10)
+        if (Number.isNaN(num)) {
+          message.error('天数请输入数字!')
+          return
+        }
+      }
+    } catch {
+      if (isDailyAdd.value) {
+        message.error('天数请输入数字!')
+        return
+      }
+    }
+    try {
+      normalizeAndValidateSlots()
+    } catch (e: any) {
+      message.error(e?.message || '提交失败')
+      return
+    }
+    if (formMode.value === 'dailyEdit') {
+      const submitSlots = dataForm.value.slots.map((s) => ({
+        slotId: s.id,
+        timeslot: s.timeslot,
+        totalQuota: Number(s.totalQuota)
+      }))
+      const res = await fetchHttp.post('/admin/appointment/updateTimeslotInstance', {
+        slots: submitSlots
+      })
+      if (res) {
+        dialogVisible.value = false
+        emit('success')
+      }
+      return
+    }
+    if (formMode.value === 'todayAdd') {
+      const submitSlots = dataForm.value.slots.map((s) => ({
+        timeslot: s.timeslot,
+        totalQuota: Number(s.totalQuota)
+      }))
+      if(!submitSlots.length) {
+        message.success('请添加时间段!')
+        return
+      }
+      const res = await fetchHttp.post('/admin/appointment/addTimeslotForOneDay', {
+        organizationId: dataForm.value.organizationId,
+        date: dataForm.value.date,
+        slots: submitSlots
+      })
+      if (res) {
+        dialogVisible.value = false
+        emit('success')
+      }
+      return
+    }
+    let params = {
+      ...dataForm.value,
+      advancedDays: Number(dataForm.value.advancedDays),
+      slots: dataForm.value.slots.map((s) => ({
+        timeslot: s.timeslot,
+        totalQuota: Number(s.totalQuota)
+      }))
+    }
+    const res = await fetchHttp.post('/admin/appointment/setTimeslot', params)
+    if (res) {
+      dialogVisible.value = false
+      emit('success')
+    }
+  } finally {
+    setTimeout(() => {
+      formLoading.value = false
+    }, 500)
+  }
+}
+
+const handleClosed = () => {
+  dialogVisible.value = false
+  formMode.value = 'dailyAdd'
+  resetForm()
+}
+</script>
+
+<style lang="scss" scoped>
+.OutwardRegistrationBak {
+  @media (width <= 1200) {
+    :deep(.el-form-item--default .el-form-item__label) {
+      line-height: 0 !important;
+    }
+  }
+
+  .el-form {
+    padding: 15px !important;
+  }
+
+  .info-title {
+    max-width: 180px;
+    min-width: 75px;
+    padding: 15px;
+    font-weight: bolder;
+    background-color: #f3f6fb;
+    border-top-right-radius: 50px;
+    border-bottom-right-radius: 50px;
+  }
+
+  .info-wrap {
+    margin: 15px;
+  }
+}
+
+:deep(.el-form-item--default) {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+</style>
+

+ 757 - 0
src/views/elderly/elder/family-appointment/time-setting/index.vue

@@ -0,0 +1,757 @@
+<template>
+  <ContentWrap>
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      class="setting-form"
+    >
+      <el-row :gutter="20" class="main-row">
+        <el-col :span="12" class="col-left">
+          <div class="card card-top-left">
+            <div class="card-header">
+              <el-icon class="card-icon icon-blue"><Setting /></el-icon>
+              <span class="card-title">基础设置</span>
+            </div>
+            <div class="card-body">
+              <el-form-item label="放号规则" prop="ruleType">
+                <el-radio-group v-model="formData.ruleType">
+                  <el-radio :value="1">时间循环预约</el-radio>
+                  <el-radio :value="2">固定时间段预约</el-radio>
+                </el-radio-group>
+              </el-form-item>
+
+              <template v-if="formData.ruleType === 1">
+                <el-form-item label="开始预约日" prop="startDay">
+                  <el-select v-model="formData.startDay" placeholder="请选择" style="width: 100%">
+                    <el-option label="当天" :value="0" />
+                    <el-option label="未来1天" :value="1" />
+                    <el-option label="未来2天" :value="2" />
+                    <el-option label="未来3天" :value="3" />
+                    <el-option label="未来4天" :value="4" />
+                    <el-option label="未来5天" :value="5" />
+                    <el-option label="未来6天" :value="6" />
+                    <el-option label="未来7天" :value="7" />
+                    <el-option label="未来8天" :value="8" />
+                    <el-option label="未来9天" :value="9" />
+                    <el-option label="未来10天" :value="10" />
+                    <el-option label="未来11天" :value="11" />
+                    <el-option label="未来12天" :value="12" />
+                    <el-option label="未来3天" :value="13" />
+                    <el-option label="未来14天" :value="14" />
+                    <el-option label="未来15天" :value="15" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="提前预约天数" prop="advanceDays">
+                  <el-input-number
+                    v-model="formData.advanceDays"
+                    :min="1"
+                    :max="30"
+                    style="width: 100%"
+                    ><template #suffix>
+                      <span>天</span>
+                    </template>
+                  </el-input-number>
+                </el-form-item>
+              </template>
+
+              <template v-else>
+                <el-form-item label="放号开始日期" prop="releaseStartDate">
+                  <el-date-picker
+                    v-model="formData.releaseStartDate"
+                    type="date"
+                    placeholder="选择开始日期"
+                    value-format="YYYY-MM-DD"
+                    style="width: 100%"
+                  />
+                </el-form-item>
+                <el-form-item label="放号结束日期" prop="releaseEndDate">
+                  <el-date-picker
+                    v-model="formData.releaseEndDate"
+                    type="date"
+                    placeholder="选择结束日期"
+                    value-format="YYYY-MM-DD"
+                    style="width: 100%"
+                  />
+                </el-form-item>
+              </template>
+            </div>
+          </div>
+
+          <div class="card card-bottom-left">
+            <div class="card-header">
+              <el-icon class="card-icon icon-green"><Calendar /></el-icon>
+              <span class="card-title">日期限制</span>
+            </div>
+            <div class="card-body">
+              <el-form-item label="开启日期限制" prop="dateLimitEnabled">
+                <el-switch
+                  v-model="formData.dateLimitEnabled"
+                  active-text="开启"
+                  inactive-text="关闭"
+                />
+              </el-form-item>
+
+              <template v-if="formData.dateLimitEnabled">
+                <el-form-item label="限制开始日期" prop="limitStartDate">
+                  <el-date-picker
+                    v-model="formData.limitStartDate"
+                    type="date"
+                    placeholder="选择开始日期"
+                    value-format="YYYY-MM-DD"
+                    style="width: 100%"
+                  />
+                </el-form-item>
+                <el-form-item label="限制关闭日期" prop="limitEndDate">
+                  <el-date-picker
+                    v-model="formData.limitEndDate"
+                    type="date"
+                    placeholder="选择关闭日期"
+                    value-format="YYYY-MM-DD"
+                    style="width: 100%"
+                  />
+                </el-form-item>
+                <div v-if="formData.ruleType === 2" class="tip-row" style="width: 100%">
+                  <el-alert
+                    type="warning"
+                    :closable="false"
+                    show-icon
+                    title="结束日期必须大于开始日期"
+                  />
+                </div>
+              </template>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="12" class="col-right">
+          <div class="card card-top-right">
+            <div class="card-header">
+              <el-icon class="card-icon icon-orange"><Clock /></el-icon>
+              <span class="card-title">时间配置</span>
+            </div>
+            <div class="card-body">
+              <el-form-item label="每天开始时间" prop="dailyStartTime">
+                <el-time-picker
+                  v-model="formData.dailyStartTime"
+                  format="HH:mm"
+                  value-format="HH:mm"
+                  placeholder="选择开始时间"
+                  style="width: 100%"
+                />
+              </el-form-item>
+              <el-form-item label="每天结束时间" prop="dailyEndTime">
+                <el-time-picker
+                  v-model="formData.dailyEndTime"
+                  format="HH:mm"
+                  value-format="HH:mm"
+                  placeholder="选择结束时间"
+                  style="width: 100%"
+                />
+              </el-form-item>
+
+              <div class="time-preview" style="width: 100%">
+                <div class="preview-item">
+                  <span class="preview-label">每日预约时长</span>
+                  <span class="preview-value">{{ dailyDuration }}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <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>
+              <el-button
+                type="primary"
+                size="small"
+                :disabled="totalLimits.length >= 3"
+                @click="openTotalLimitDialog"
+              >
+                <el-icon><Plus /></el-icon>
+                添加
+              </el-button>
+            </div>
+            <div class="card-body card-body-limit">
+              <div v-if="totalLimits.length === 0" class="empty-limits-inline">
+                <el-empty description="暂无限制,点击右上角添加" :image-size="40" />
+              </div>
+              <div v-else class="limit-list-inline">
+                <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-icon><Delete /></el-icon>
+                  </el-button>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <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>
+              <el-button
+                type="primary"
+                size="small"
+                :disabled="personLimits.length >= 3"
+                @click="openPersonLimitDialog"
+              >
+                <el-icon><Plus /></el-icon>
+                添加
+              </el-button>
+            </div>
+            <div class="card-body card-body-limit">
+              <div v-if="personLimits.length === 0" class="empty-limits-inline">
+                <el-empty description="暂无限制,点击右上角添加" :image-size="40" />
+              </div>
+              <div v-else class="limit-list-inline">
+                <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-icon><Delete /></el-icon>
+                  </el-button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+
+      <div class="form-footer">
+        <el-button type="primary" size="large" :loading="submitting" @click="handleSubmit">
+          保存设置
+        </el-button>
+        <el-button size="large" @click="handleReset">重置</el-button>
+      </div>
+    </el-form>
+
+    <el-dialog v-model="totalLimitDialogVisible" title="添加总名额限制" width="420px" align-center>
+      <el-form :model="totalLimitForm" label-width="100px" style="margin-bottom: 20px">
+        <el-form-item label="限制类型" required>
+          <el-select v-model="totalLimitForm.type" placeholder="请选择" style="width: 100%">
+            <el-option
+              v-for="item in availableTotalTypes"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="限制次数" required>
+          <el-input-number
+            v-model="totalLimitForm.count"
+            :min="1"
+            :max="9999"
+            style="width: 100%"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="totalLimitDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmTotalLimit">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <el-dialog
+      v-model="personLimitDialogVisible"
+      title="添加单人名额限制"
+      width="420px"
+      align-center
+    >
+      <el-form :model="personLimitForm" label-width="100px" style="margin-bottom: 20px">
+        <el-form-item label="限制类型" required>
+          <el-select v-model="personLimitForm.type" placeholder="请选择" style="width: 100%">
+            <el-option
+              v-for="item in availablePersonTypes"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="限制次数" required>
+          <el-input-number
+            v-model="personLimitForm.count"
+            :min="1"
+            :max="9999"
+            style="width: 100%"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="personLimitDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmPersonLimit">确定</el-button>
+      </template>
+    </el-dialog>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed } from 'vue'
+import { Setting, Calendar, Clock, User, Plus, Delete, Tickets } from '@element-plus/icons-vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import fetchHttp from '@/config/axios/fetchHttp'
+import { useUserStore } from '@/store/modules/user'
+
+const userStore = useUserStore()
+const message = useMessage()
+
+const formRef = ref<FormInstance>()
+const submitting = ref(false)
+const tenantIds = ref<number[]>([])
+let organizationId = 0
+
+type LimitItem = { type: number; count: number }
+
+const limitTypeOptions = [
+  { label: '每天', value: 1 },
+  { label: '每周', value: 2 },
+  { label: '每月', value: 3 }
+]
+
+const totalLimits = ref<LimitItem[]>([])
+const personLimits = ref<LimitItem[]>([])
+
+const totalLimitDialogVisible = ref(false)
+const personLimitDialogVisible = ref(false)
+const totalLimitForm = reactive<LimitItem>({ type: 1, count: 1 })
+const personLimitForm = reactive<LimitItem>({ type: 1, count: 1 })
+
+const availableTotalTypes = computed(() => {
+  const usedTypes = totalLimits.value.map((item) => item.type)
+  return limitTypeOptions.filter((item) => !usedTypes.includes(item.value))
+})
+
+const availablePersonTypes = computed(() => {
+  const usedTypes = personLimits.value.map((item) => item.type)
+  return limitTypeOptions.filter((item) => !usedTypes.includes(item.value))
+})
+
+const getLimitTypeLabel = (type: number) => {
+  const found = limitTypeOptions.find((item) => item.value === type)
+  return found ? found.label : ''
+}
+
+const defaultFormData = () => ({
+  ruleType: 1,
+  startDay: 0,
+  advanceDays: 6,
+  releaseStartDate: '',
+  releaseEndDate: '',
+  dateLimitEnabled: false,
+  limitStartDate: '',
+  limitEndDate: '',
+  holidayClosed: false,
+  dailyStartTime: '08:00',
+  dailyEndTime: '18:00'
+})
+
+const formData = reactive(defaultFormData())
+
+const openTotalLimitDialog = () => {
+  if (availableTotalTypes.value.length === 0) return
+  totalLimitForm.type = availableTotalTypes.value[0].value
+  totalLimitForm.count = 1
+  totalLimitDialogVisible.value = true
+}
+
+const confirmTotalLimit = () => {
+  if (!totalLimitForm.type || !totalLimitForm.count) {
+    message.warning('请填写完整信息')
+    return
+  }
+  totalLimits.value.push({ type: totalLimitForm.type, count: totalLimitForm.count })
+  totalLimitDialogVisible.value = false
+}
+
+const removeTotalLimit = (index: number) => {
+  totalLimits.value.splice(index, 1)
+}
+
+const openPersonLimitDialog = () => {
+  if (availablePersonTypes.value.length === 0) return
+  personLimitForm.type = availablePersonTypes.value[0].value
+  personLimitForm.count = 1
+  personLimitDialogVisible.value = true
+}
+
+const confirmPersonLimit = () => {
+  if (!personLimitForm.type || !personLimitForm.count) {
+    message.warning('请填写完整信息')
+    return
+  }
+  personLimits.value.push({ type: personLimitForm.type, count: personLimitForm.count })
+  personLimitDialogVisible.value = false
+}
+
+const removePersonLimit = (index: number) => {
+  personLimits.value.splice(index, 1)
+}
+
+const dailyDuration = computed(() => {
+  if (!formData.dailyStartTime || !formData.dailyEndTime) return '--'
+  const [sh, sm] = formData.dailyStartTime.split(':').map(Number)
+  const [eh, em] = formData.dailyEndTime.split(':').map(Number)
+  const startMin = sh * 60 + sm
+  const endMin = eh * 60 + em
+  const diff = endMin - startMin
+  if (diff <= 0) return '--'
+  const hours = Math.floor(diff / 60)
+  const mins = diff % 60
+  if (hours > 0 && mins > 0) return `${hours}小时${mins}分钟`
+  if (hours > 0) return `${hours}小时`
+  return `${mins}分钟`
+})
+
+const validateEndDate = (rule: any, value: any, callback: any) => {
+  if (formData.ruleType === 2 && formData.releaseStartDate && value) {
+    if (value <= formData.releaseStartDate) {
+      callback(new Error('放号结束日期必须大于开始日期'))
+    } else {
+      callback()
+    }
+  } else {
+    callback()
+  }
+}
+
+const validateLimitEndDate = (rule: any, value: any, callback: any) => {
+  if (formData.dateLimitEnabled && formData.limitStartDate && value) {
+    if (value <= formData.limitStartDate) {
+      callback(new Error('关闭日期必须大于开始日期'))
+    } else {
+      callback()
+    }
+  } else {
+    callback()
+  }
+}
+
+const validateDailyEndTime = (rule: any, value: any, callback: any) => {
+  if (formData.dailyStartTime && value) {
+    if (value <= formData.dailyStartTime) {
+      callback(new Error('结束预约时间必须晚于开始时间'))
+    } else {
+      callback()
+    }
+  } else {
+    callback()
+  }
+}
+
+const formRules: FormRules = reactive({
+  ruleType: [{ required: true, message: '请选择放号规则', trigger: 'change' }],
+  releaseStartDate: [{ required: true, message: '请选择放号开始日期', trigger: 'change' }],
+  releaseEndDate: [
+    { required: true, message: '请选择放号结束日期', trigger: 'change' },
+    { validator: validateEndDate, trigger: 'change' }
+  ],
+  limitStartDate: [{ required: true, message: '请选择限制开始日期', trigger: 'change' }],
+  limitEndDate: [
+    { required: true, message: '请选择限制关闭日期', trigger: 'change' },
+    { validator: validateLimitEndDate, trigger: 'change' }
+  ],
+  dailyStartTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
+  dailyEndTime: [
+    { required: true, message: '请选择结束时间', trigger: 'change' },
+    { validator: validateDailyEndTime, trigger: 'change' }
+  ]
+})
+
+const changeTen = (i: any[]) => {
+  if (i.length > 1) {
+    message.error('只能选择一个机构!')
+    if (i[0].length > 1) {
+      tenantIds.value = [i[0][1]]
+    } else {
+      tenantIds.value = []
+    }
+  }
+  if (tenantIds.value.length > 0) {
+    organizationId = tenantIds.value[0]
+    getSetting()
+  }
+}
+
+const getSetting = async () => {
+  try {
+    const res = await fetchHttp.get('/admin/appointment/getTimeslotSetting', {
+      organizationId
+    })
+    if (res) {
+      Object.assign(formData, {
+        ruleType: res.ruleType || 1,
+        startDay: res.startDay ?? 0,
+        advanceDays: res.advanceDays || 6,
+        releaseStartDate: res.releaseStartDate || '',
+        releaseEndDate: res.releaseEndDate || '',
+        dateLimitEnabled: !!res.dateLimitEnabled,
+        limitStartDate: res.limitStartDate || '',
+        limitEndDate: res.limitEndDate || '',
+        holidayClosed: !!res.holidayClosed,
+        dailyStartTime: res.dailyStartTime || '08:00',
+        dailyEndTime: res.dailyEndTime || '18:00'
+      })
+      totalLimits.value = res.totalLimits || []
+      personLimits.value = res.personLimits || []
+    }
+  } catch (e) {
+    console.log('获取设置失败', e)
+  }
+}
+
+const handleSubmit = async () => {
+  if (!formRef.value) return
+  if (!organizationId) {
+    message.error('请选择机构!')
+    return
+  }
+  try {
+    await formRef.value.validate()
+    submitting.value = true
+    const params = {
+      organizationId,
+      ...formData,
+      totalLimits: totalLimits.value,
+      personLimits: personLimits.value
+    }
+    const res = await fetchHttp.post('/admin/appointment/setTimeslotSetting', params)
+    if (res) {
+      message.success('设置成功!')
+      getSetting()
+    }
+  } catch (error: any) {
+    if (error?.message) {
+      message.error(error.message)
+    }
+  } finally {
+    submitting.value = false
+  }
+}
+
+const handleReset = () => {
+  formRef.value?.resetFields()
+  Object.assign(formData, defaultFormData())
+  totalLimits.value = []
+  personLimits.value = []
+  if (organizationId) {
+    getSetting()
+  }
+}
+
+onMounted(() => {
+  if (userStore.orgTenantId.length > 0) {
+    tenantIds.value = [userStore.orgTenantId[0]] as number[]
+    organizationId = tenantIds.value[0]
+    getSetting()
+  }
+})
+</script>
+
+<style scoped lang="scss">
+.setting-form {
+  .main-row {
+    display: flex;
+    align-items: stretch;
+  }
+
+  .col-left,
+  .col-right {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+  }
+
+  .card {
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+
+    &-top-left,
+    &-top-right,
+    &-mid-right {
+      flex: 0 0 auto;
+    }
+
+    &-bottom-left,
+    &-bottom-right {
+      flex: 1 1 auto;
+      min-height: 0;
+    }
+  }
+
+  .card-header {
+    display: flex;
+    align-items: center;
+    padding: 16px 20px;
+    border-bottom: 1px solid #f0f0f0;
+    background: linear-gradient(90deg, #fafbfc 0%, #ffffff 100%);
+
+    .card-icon {
+      width: 28px;
+      height: 28px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 6px;
+      margin-right: 10px;
+      color: #fff;
+      font-size: 16px;
+
+      &.icon-blue {
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      }
+      &.icon-green {
+        background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
+      }
+      &.icon-orange {
+        background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+      }
+      &.icon-purple {
+        background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+      }
+    }
+
+    .card-title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+      flex: 1;
+    }
+
+    .add-btn {
+      font-size: 13px;
+    }
+  }
+
+  .card-body {
+    padding: 20px;
+    flex: 1;
+
+    &-scroll {
+      overflow-y: auto;
+      max-height: 360px;
+    }
+  }
+
+  .form-suffix {
+    margin-left: 8px;
+    color: #909399;
+    font-size: 14px;
+  }
+
+  .tip-row {
+    margin-top: 4px;
+  }
+
+  .time-preview {
+    margin-top: 8px;
+    padding: 12px 16px;
+    background: linear-gradient(135deg, #f5f7fa 0%, #e4e7eb 100%);
+    border-radius: 6px;
+
+    .preview-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+
+      .preview-label {
+        font-size: 13px;
+        color: #606266;
+      }
+      .preview-value {
+        font-size: 15px;
+        font-weight: 600;
+        color: #409eff;
+      }
+    }
+  }
+
+  .empty-limits {
+    padding: 20px 0;
+  }
+
+  .card-body-limit {
+    padding: 16px 20px;
+    min-height: 120px;
+  }
+
+  .empty-limits-inline {
+    padding: 10px 0;
+  }
+
+  .limit-list-inline {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+  }
+
+  .limit-item-inline {
+    display: flex;
+    align-items: center;
+    padding: 10px 14px;
+    background-color: #f8f9fa;
+    border-radius: 6px;
+    border: 1px solid #e9ecef;
+
+    .limit-type-badge {
+      font-size: 12px;
+      font-weight: 600;
+      color: #fff;
+      padding: 3px 10px;
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      border-radius: 4px;
+      margin-right: 16px;
+    }
+
+    .limit-count-num {
+      flex: 1;
+      font-size: 16px;
+      font-weight: 600;
+      color: #f56c6c;
+
+      em {
+        font-style: normal;
+        font-size: 13px;
+        font-weight: 400;
+        color: #909399;
+        margin-left: 2px;
+      }
+    }
+  }
+
+  .form-footer {
+    display: flex;
+    justify-content: center;
+    gap: 16px;
+    margin-top: 24px;
+    padding-top: 20px;
+    border-top: 1px solid #f0f0f0;
+
+    .el-button {
+      min-width: 140px;
+    }
+  }
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 18px;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+:deep(.el-form-item__content) {
+  display: flex;
+  align-items: center;
+}
+</style>