Procházet zdrojové kódy

更新预约功能

chenjun před 3 týdny
rodič
revize
d3bfba48cb

+ 110 - 48
src/views/elderly/elder/family-appointment/time-setting/index.vue

@@ -299,41 +299,41 @@
         </el-col>
 
         <el-col :span="12" class="col-right">
-          <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="isDisabled || 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"
-                    :disabled="isDisabled"
-                    @click="removeTotalLimit(index)"
-                  >
-                    <el-icon><Delete /></el-icon>
-                  </el-button>
-                </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="isDisabled || 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"-->
+<!--                    :disabled="isDisabled"-->
+<!--                    @click="removeTotalLimit(index)"-->
+<!--                  >-->
+<!--                    <el-icon><Delete /></el-icon>-->
+<!--                  </el-button>-->
+<!--                </div>-->
+<!--              </div>-->
+<!--            </div>-->
+<!--          </div>-->
 
           <div class="card card-bottom-right">
             <div class="card-header">
@@ -518,6 +518,7 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { Setting, Calendar, User, Plus, Delete, Tickets, Minus } from '@element-plus/icons-vue'
 import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessageBox } from 'element-plus'
 import { useUserStore } from '@/store/modules/user'
 import {
   appointmentConfigADD,
@@ -542,6 +543,8 @@ type CycleType = 'daily' | 'weekly' | 'monthly' | 'custom'
 
 type AppointmentItem = {
   id: number
+  // 后端返回的真实 id,新增的 item 没有此字段;提交时回传给后端,删除时进入 deleteIds
+  serverId?: number
   startTime: string
   endTime: string
   limitCount: number
@@ -621,6 +624,8 @@ const dateTypeToCycleType = (dateType: string): CycleType => {
 const totalLimits = ref<LimitItem[]>([])
 const personLimits = ref<LimitItem[]>([])
 const appointmentGroups = ref<AppointmentGroup[]>([])
+// 被删除的旧 item 后端 id 列表,提交时一并传给后端做物理删除
+const deleteIds = ref<number[]>([])
 
 const totalLimitDialogVisible = ref(false)
 const personLimitDialogVisible = ref(false)
@@ -729,13 +734,20 @@ const addAppointmentGroup = () => {
         id: itemIdCounter,
         startTime: '08:00',
         endTime: '09:00',
-        limitCount: 100
+        limitCount: 20
       }
     ]
   })
 }
 
 const removeGroup = (index: number) => {
+  const group = appointmentGroups.value[index]
+  // 该组下所有带 serverId 的 item 都是后端旧数据,删除时记录 id 交给后端物理删除
+  group.items.forEach((item) => {
+    if (item.serverId) {
+      deleteIds.value.push(item.serverId)
+    }
+  })
   appointmentGroups.value.splice(index, 1)
 }
 
@@ -769,11 +781,16 @@ const addItem = (group: AppointmentGroup) => {
     id: itemIdCounter,
     startTime: newStart,
     endTime: newEnd,
-    limitCount: 100
+    limitCount: 20
   })
 }
 
 const removeItem = (group: AppointmentGroup, index: number) => {
+  const item = group.items[index]
+  // 带 serverId 表示是后端旧数据,删除时记录 id 交给后端物理删除
+  if (item.serverId) {
+    deleteIds.value.push(item.serverId)
+  }
   group.items.splice(index, 1)
 }
 
@@ -891,6 +908,30 @@ const removePersonLimit = (index: number) => {
   personLimits.value.splice(index, 1)
 }
 
+const validatePersonLimits = (): string | null => {
+  const daily = personLimits.value.find((item) => item.type === 1)
+  const weekly = personLimits.value.find((item) => item.type === 2)
+  const monthly = personLimits.value.find((item) => item.type === 3)
+
+  if (daily && daily.count <= 0) {
+    return '单人每日名额限制必须大于0'
+  }
+  if (weekly && weekly.count <= 0) {
+    return '单人每周名额限制必须大于0'
+  }
+  if (monthly && monthly.count <= 0) {
+    return '单人每月名额限制必须大于0'
+  }
+  if (monthly && weekly && monthly.count <= weekly.count) {
+    return '单人每月名额限制必须大于每周名额限制'
+  }
+  if (weekly && daily && weekly.count <= daily.count) {
+    return '单人每周名额限制必须大于每日名额限制'
+  }
+
+  return null
+}
+
 const validateAdvanceDays = (_rule: any, value: any, callback: any) => {
   if (!value || value < 1) {
     callback(new Error('提前预约天数最少为1'))
@@ -948,6 +989,8 @@ const parseGroups = (groups: AppointmentGroupDO[]): AppointmentGroup[] => {
     itemIdCounter++
     group.items.push({
       id: itemIdCounter,
+      // 保留后端真实 id,用于更新时回传和删除时入 deleteIds
+      serverId: g.id,
       startTime: g.beginTime || '',
       endTime: g.endTime || '',
       limitCount: g.limitCount || 100
@@ -983,6 +1026,8 @@ const getSetting = async () => {
       })
       groupIdCounter = 0
       itemIdCounter = 0
+      // 重新拉取配置后,本地 deleteIds 已无意义,清空避免脏数据
+      deleteIds.value = []
       appointmentGroups.value = parseGroups(res.group || [])
       if (appointmentGroups.value.length > 0) {
         groupIdCounter = Math.max(...appointmentGroups.value.map((g) => g.id))
@@ -1112,23 +1157,28 @@ const validateGroups = (): string | null => {
 const handleSubmit = async () => {
   if (!formRef.value) return
   if (!organizationId) {
-    message.error('请选择机构!')
+    await ElMessageBox.alert('请选择机构!', '警告', { type: 'warning' })
     return
   }
   if (!formData.advanceDays || formData.advanceDays < 1) {
-    message.error('提前预约天数最少为1')
+    await ElMessageBox.alert('提前预约天数最少为1', '警告', { type: 'warning' })
     return
   }
   const groupError = validateGroups()
   if (groupError) {
-    message.error(groupError)
+    await ElMessageBox.alert(groupError, '警告', { type: 'warning' })
+    return
+  }
+  const personLimitError = validatePersonLimits()
+  if (personLimitError) {
+    await ElMessageBox.alert(personLimitError, '警告', { type: 'warning' })
     return
   }
   for (const group of appointmentGroups.value) {
     for (let i = 0; i < group.items.length; i++) {
       const item = group.items[i]
       if (!item.limitCount || item.limitCount <= 0) {
-        message.error('限制人数必须大于0')
+        await ElMessageBox.alert('限制人数必须大于0', '警告', { type: 'warning' })
         return
       }
       if (
@@ -1136,12 +1186,20 @@ const handleSubmit = async () => {
         item.endTime &&
         timeToMinutes(item.startTime) >= timeToMinutes(item.endTime)
       ) {
-        message.error('存在开始时间晚于或等于结束时间的预约项,请检查')
+        await ElMessageBox.alert(
+          '存在开始时间晚于或等于结束时间的预约项,请检查',
+          '警告',
+          { type: 'warning' }
+        )
         return
       }
       for (let j = i + 1; j < group.items.length; j++) {
         if (isOverlap(item, group.items[j])) {
-          message.error('同一预约组内存在时间段重叠的预约项,请检查')
+          await ElMessageBox.alert(
+            '同一预约组内存在时间段重叠的预约项,请检查',
+            '警告',
+            { type: 'warning' }
+          )
           return
         }
       }
@@ -1163,10 +1221,12 @@ const handleSubmit = async () => {
         config[field.total] = limit.count
       }
     })
-    personLimits.value.forEach((limit) => {
-      const field = limitTypeToFieldMap[limit.type]
+    // 单人名额:未设置或删除时传 -1(后端表示无限制)
+    ;[1, 2, 3].forEach((type) => {
+      const field = limitTypeToFieldMap[type]
       if (field) {
-        config[field.person] = limit.count
+        const limit = personLimits.value.find((item) => item.type === type)
+        config[field.person] = limit ? limit.count : -1
       }
     })
     const group: AppointmentGroupDO[] = []
@@ -1178,6 +1238,8 @@ const handleSubmit = async () => {
       const enable = g.enable || 0
       g.items.forEach((item) => {
         group.push({
+          // 旧数据带回后端 id 用于更新;新增数据不带 id,后端按新增处理
+          ...(item.serverId ? { id: item.serverId } : {}),
           dateType,
           weekDays,
           monthDays,
@@ -1190,7 +1252,7 @@ const handleSubmit = async () => {
         })
       })
     })
-    const params = { config, group }
+    const params = { config, group, deleteIds: deleteIds.value }
     const res = configId.value
       ? await appointmentConfigUpdate(params)
       : await appointmentConfigADD(params)
@@ -1200,7 +1262,7 @@ const handleSubmit = async () => {
     }
   } catch (error: any) {
     if (error?.message) {
-      message.error(error.message)
+      await ElMessageBox.alert(error.message, '警告', { type: 'warning' })
     }
   } finally {
     submitting.value = false

+ 75 - 24
src/views/elderly/fee/leave-settle/index.vue

@@ -31,29 +31,29 @@
         </el-select>
       </el-form-item>
 
-<!--      <el-form-item label="退住日期" prop="retreatDate">-->
-<!--        <el-date-picker-->
-<!--          v-model="queryParams.retreatDate"-->
-<!--          class="!w-240px"-->
-<!--          placeholder="请选择"-->
-<!--          end-placeholder="结束日期"-->
-<!--          start-placeholder="开始日期"-->
-<!--          type="date"-->
-<!--          value-format="YYYY-MM-DD"-->
-<!--        />-->
-<!--      </el-form-item>-->
+      <!--      <el-form-item label="退住日期" prop="retreatDate">-->
+      <!--        <el-date-picker-->
+      <!--          v-model="queryParams.retreatDate"-->
+      <!--          class="!w-240px"-->
+      <!--          placeholder="请选择"-->
+      <!--          end-placeholder="结束日期"-->
+      <!--          start-placeholder="开始日期"-->
+      <!--          type="date"-->
+      <!--          value-format="YYYY-MM-DD"-->
+      <!--        />-->
+      <!--      </el-form-item>-->
 
-<!--      <el-form-item label="费用截止日期" prop="costDeadlineDate">-->
-<!--        <el-date-picker-->
-<!--          v-model="queryParams.costDeadlineDate"-->
-<!--          class="!w-240px"-->
-<!--          placeholder="请选择"-->
-<!--          end-placeholder="结束日期"-->
-<!--          start-placeholder="开始日期"-->
-<!--          type="date"-->
-<!--          value-format="YYYY-MM-DD"-->
-<!--        />-->
-<!--      </el-form-item>-->
+      <!--      <el-form-item label="费用截止日期" prop="costDeadlineDate">-->
+      <!--        <el-date-picker-->
+      <!--          v-model="queryParams.costDeadlineDate"-->
+      <!--          class="!w-240px"-->
+      <!--          placeholder="请选择"-->
+      <!--          end-placeholder="结束日期"-->
+      <!--          start-placeholder="开始日期"-->
+      <!--          type="date"-->
+      <!--          value-format="YYYY-MM-DD"-->
+      <!--        />-->
+      <!--      </el-form-item>-->
 
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
@@ -64,10 +64,11 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-button plain type="success" @click="handleExport">
+    <el-button plain type="success" :loading="loading" @click="handleExport">
       <Icon class="mr-5px" icon="ep:download" />
       导出
     </el-button>
+
     <TabBarBtn />
 
     <Table2
@@ -117,10 +118,12 @@
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { getRefundSettlementOrderPage } from '@/api/elderly/fee/leave-settle'
 import { levelSettleColumn } from '../column'
-import Form from './Form.vue'
 import RefundSettlement from './refund-settlement.vue'
 import Print from './Print.vue'
 import { useUserStore } from '@/store/modules/user'
+import { exportWithCustomHeaders } from '@/utils/excel-export'
+import { formatToDateTime } from '@/utils/dateUtil'
+import { Message as message } from '@/layout/components/Message'
 defineOptions({ name: 'LevelSettle' })
 const userStore = useUserStore()
 const loading = ref(true) // 列表的加载中
@@ -147,6 +150,7 @@ const getList = async () => {
     const data = await getRefundSettlementOrderPage(queryParams)
     list.value = data.list
     total.value = data.total
+    return list.value
   } finally {
     loading.value = false
   }
@@ -167,6 +171,53 @@ const resetQuery = () => {
   handleQuery()
 }
 
+/** 导出按钮操作 */
+const handleExport = async () => {
+  if (loading.value) return
+  try {
+    loading.value = true
+    queryParams.pageNo = 1
+    queryParams.pageSize = 99999
+    const exportList = await getList()
+    if (!exportList || exportList.length === 0) {
+      message.error('暂无数据可以导出!')
+      return
+    }
+    const statusOptions = getStrDictOptions(DICT_TYPE.SETTLEMENT_STATUS)
+    const data = exportList.map((item) => ({
+      ...item,
+      statusLabel:
+        statusOptions.find((dict) => String(dict.value) === String(item.status))?.label ||
+        item.status,
+      settlementTimeStr: item.settlementTime ? formatToDateTime(item.settlementTime) : '',
+      createdTimeStr: item.createdTime ? formatToDateTime(item.createdTime) : ''
+    }))
+    const headers = [
+      { key: 'tenantName', title: '所属机构', width: 40 },
+      { key: 'elderName', title: '长者姓名', width: 40 },
+      { key: 'bedName', title: '床位号', width: 40 },
+      { key: 'statusLabel', title: '结算状态', width: 40 },
+      { key: 'retreatDate', title: '退住日期', width: 40 },
+      { key: 'costDeadlineDate', title: '费用截止日期', width: 40 },
+      { key: 'refundableAmount', title: '应退金额', width: 40 },
+      { key: 'receivableAmount', title: '应收金额', width: 40 },
+      { key: 'refundAmount', title: '实缴金额', width: 40 },
+      { key: 'settlementTimeStr', title: '结算时间', width: 40 },
+      { key: 'settlementPersonName', title: '结算人', width: 40 },
+      { key: 'orderNumber', title: '结算单号', width: 40 },
+      { key: 'createdBy', title: '创建人', width: 40 },
+      { key: 'createdTimeStr', title: '创建时间', width: 40 }
+    ]
+    await exportWithCustomHeaders(data, headers, `退住结算-${formatToDateTime()}.xlsx`, '退住结算')
+  } catch (e) {
+    message.error('导出失败!')
+    console.log(e)
+  } finally {
+    queryParams.pageSize = 10
+    loading.value = false
+  }
+}
+
 /** 添加/修改操作 */
 // const formRef = ref()
 const openForm = (row: any = {}, isDetail: boolean = false) => {