Parcourir la source

增加居家上门服务

unknown il y a 3 semaines
Parent
commit
0360e4f70a

+ 72 - 0
src/api/living-home/elderly/index.ts

@@ -488,4 +488,76 @@ export const deleteServiceCombo = (ids: string) => {
   })
 }
 
+// ==================== 预约服务工单接口 ====================
+
+// 预约服务工单 VO
+export interface AppointmentVO {
+  id?: number
+  elderName?: string
+  gender?: string
+  age?: number
+  phone?: string
+  address?: string
+  orderType?: string
+  status?: string
+  servicePersonName?: string
+  orderTime?: string
+  visitTime?: string
+  isExpired?: string
+}
+
+// 查询预约服务工单列表(分页)
+export const getAppointmentPage = (params: any) => {
+  return request.get({
+    url: '/home/appointment/page',
+    params
+  })
+}
+
+// 查询预约服务工单详情
+export const getAppointment = (id: number) => {
+  return request.get({
+    url: `/home/appointment/get?id=${id}`
+  })
+}
+
+// 新增预约服务工单
+export const createAppointment = (data: any) => {
+  return request.post({
+    url: '/home/appointment/create',
+    data
+  })
+}
+
+// 修改预约服务工单
+export const updateAppointment = (data: any) => {
+  return request.put({
+    url: '/home/appointment/update',
+    data
+  })
+}
+
+// 删除预约服务工单
+export const deleteAppointment = (id: number) => {
+  return request.delete({
+    url: `/home/appointment/delete?id=${id}`
+  })
+}
+
+// 重新派单
+export const reassignAppointment = (data: { id: number; servicePersonId: number; reason: string }) => {
+  return request.post({
+    url: '/home/appointment/reassign',
+    data
+  })
+}
+
+// 修改服务人员
+export const changeAppointmentServicePerson = (data: { id: number; servicePersonId: number; reason: string }) => {
+  return request.post({
+    url: '/home/appointment/change-service-person',
+    data
+  })
+}
+
 

+ 82 - 14
src/views/elderly/elder/contract/Renewal.vue

@@ -35,14 +35,28 @@
               </TgSelect>
             </el-form-item>
           </el-col>
-          <el-col :span="24">
-            <el-alert
-              v-if="deathTime"
-              type="warning"
-              :closable="false"
-              :title="'合同截止时间为: ' + deathTime"
-            />
+          <el-col :lg="12" :md="12" :sm="24" :xs="24">
+            <el-form-item
+              label="合同结束日期"
+              prop="checkOutTime"
+            >
+              <TgDatePicker type="date" @change="changeOutTime" v-model="dataForm.checkOutTime" placeholder="选择日期时间"   :toggle-type="isDetail"/>
+            </el-form-item>
           </el-col>
+          <el-alert
+            v-show="false"
+            type="warning"
+            :closable="false"
+            :title="'合同截止时间为: ' + deathTime"
+          />
+<!--          <el-col :span="24">-->
+<!--            <el-alert-->
+<!--              v-if="deathTime"-->
+<!--              type="warning"-->
+<!--              :closable="false"-->
+<!--              :title="'合同截止时间为: ' + deathTime"-->
+<!--            />-->
+<!--          </el-col>-->
           <el-col :span="24">
             <el-form-item label="合同编号" prop="contractNumber">
               <TgInput v-model="dataForm.contractNumber" />
@@ -503,6 +517,7 @@ const state = reactive({
     contractNumber: '',
     checkInTime: '',
     expireTime: '',
+    checkOutTime: '',
     status: '',
     times: '',
     remark: '',
@@ -522,13 +537,36 @@ const state = reactive({
         trigger: ['blur', 'change']
       }
     ],
-    times: [
+    checkOutTime: [
       {
         required: true,
-        message: '续签年限不能为空',
-        trigger: ['blur', 'change']
-      }
-    ],
+        message: '合同结束日期不能为空',
+        trigger: 'change'
+      },
+      {
+        validator: (rule, value, callback) => {
+          if (value && dataForm.value.checkInTime) {
+            const checkInTime = new Date(dataForm.value.checkInTime).getTime()
+            const checkOutTime = new Date(value).getTime()
+
+            if (checkOutTime <= checkInTime) {
+              callback(new Error('合同结束日期必须大于开始日期'))
+            } else {
+              callback()
+            }
+          } else {
+            callback()
+          }
+        },
+        trigger: 'change'
+      }],
+    // times: [
+    //   {
+    //     required: true,
+    //     message: '续签年限不能为空',
+    //     trigger: ['blur', 'change']
+    //   }
+    // ],
   }
 })
 const { dataForm, dataRule } = toRefs(state)
@@ -538,6 +576,11 @@ const titleItem = ref('') // 弹窗标题
 const type = ref(1) // 1: 续签 2: 变更
 let oldContractId = null
 
+const changeOutTime = ()=>{
+  dataForm.value.times = ''
+
+}
+
 /** 打开弹窗 */
 const open = async (row, pageType) => {
   dialogVisible.value = true
@@ -573,13 +616,38 @@ const open = async (row, pageType) => {
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
+// const deathTime = computed(() => {
+//   if (dataForm.value.checkInTime && dataForm.value.times) {
+//     // 要比原来的天数少1天
+//     return dayjs(dataForm.value.checkInTime)
+//       .add(Number(dataForm.value.times), 'month')
+//       .add(-1, 'day')
+//       .format('YYYY-MM-DD')
+//   }
+//   return ''
+// })
+
 const deathTime = computed(() => {
   if (dataForm.value.checkInTime && dataForm.value.times) {
     // 要比原来的天数少1天
-    return dayjs(dataForm.value.checkInTime)
+    const data = dayjs(dataForm.value.checkInTime)
       .add(Number(dataForm.value.times), 'month')
       .add(-1, 'day')
       .format('YYYY-MM-DD')
+    dataForm.value.checkOutTime = data
+    return data
+
+  } else if (dataForm.value.checkInTime && dataForm.value.checkInDeadlineTime) {
+
+    const data = (
+      dataForm.value.checkInDeadlineTime
+      // +
+      // '(' +
+      // dayjs(dataForm.value.checkInDeadlineTime).add(1, 'day').diff(dataForm.value.checkInTime, 'day') +
+      // '天)'
+    )
+    dataForm.value.checkOutTime = data
+    return data
   }
   return ''
 })
@@ -645,7 +713,7 @@ const submitForm = async () => {
       elderName: dataForm.value.elderName,
       beginTime: dataForm.value.checkInTime,
       contractTerm: dataForm.value.times,
-      expireTime: deathTime.value.substring(0, 10),
+      expireTime: dataForm.value.checkOutTime,
       contractNumber: dataForm.value.contractNumber,
       details: dataForm.value.details,
       specialCareNotes: dataForm.value.specialCareNotes,

+ 1 - 1
src/views/elderly/elder/nurse-change/ProcessForm.vue

@@ -346,7 +346,7 @@ const init = async (id, detail, status) => {
  await getOverheadList()
   if(id){
     handleChange()
-    console.log("AAAA",dataForm.value)
+
   }
 
 }

+ 122 - 0
src/views/living-home/visiting-service/appointment-list/ChangeServicePersonDialog.vue

@@ -0,0 +1,122 @@
+<template>
+  <Dialog v-model="dialogVisible" title="修改服务人员" width="500px">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+    >
+      <el-form-item label="长者姓名">
+        <el-input v-model="currentElderName" disabled />
+      </el-form-item>
+      <el-form-item label="当前服务人员">
+        <el-input v-model="currentServicePerson" disabled />
+      </el-form-item>
+      <el-form-item label="新服务人员" prop="newServicePersonId">
+        <el-select
+          v-model="formData.newServicePersonId"
+          placeholder="请选择新服务人员"
+          clearable
+          class="w-full"
+          filterable
+        >
+          <el-option
+            v-for="item in servicePersonList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="修改原因" prop="reason">
+        <el-input
+          v-model="formData.reason"
+          type="textarea"
+          rows="3"
+          placeholder="请输入修改原因"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="dialogVisible = false">取消</el-button>
+      <el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import * as ElderApi from '@/api/living-home/elderly'
+
+const emit = defineEmits(['success'])
+
+const dialogVisible = ref(false)
+const submitLoading = ref(false)
+const formRef = ref()
+const servicePersonList = ref<any[]>([])
+const currentServicePerson = ref('')
+const currentElderName = ref('')
+const currentRow = ref<any>({})
+
+const formData = reactive({
+  newServicePersonId: undefined as number | undefined,
+  reason: ''
+})
+
+const formRules = {
+  newServicePersonId: [{ required: true, message: '请选择新服务人员', trigger: 'change' }],
+ // reason: [{ required: true, message: '请输入修改原因', trigger: 'blur' }]
+}
+
+const open = async (row: any) => {
+  currentRow.value = row
+  currentElderName.value = row.elderName
+  currentServicePerson.value = row.servicePersonName
+  dialogVisible.value = true
+  resetForm()
+  await getServicePersonList()
+}
+
+const resetForm = () => {
+  formData.newServicePersonId = undefined
+  formData.reason = ''
+  nextTick(() => {
+    formRef.value?.resetFields()
+  })
+}
+
+const getServicePersonList = async () => {
+  try {
+    const params = { pageNo: 1, pageSize: 1000 }
+    const data = await ElderApi.getServicePersonList(params)
+    // 过滤掉当前服务人员
+    servicePersonList.value = (data.list || []).filter(
+      (item: any) => item.name !== currentServicePerson.value
+    )
+  } catch (error) {
+    console.log('获取服务人员列表失败', error)
+  }
+}
+
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
+
+  submitLoading.value = true
+  try {
+    await AppointmentApi.changeAppointmentServicePerson({
+      id: currentRow.value.id,
+      servicePersonId: formData.newServicePersonId,
+      reason: formData.reason
+    })
+    useMessage().success('修改服务人员成功')
+    dialogVisible.value = false
+    emit('success')
+  } catch (error) {
+    console.log('修改服务人员失败', error)
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+defineExpose({ open })
+</script>

+ 1319 - 0
src/views/living-home/visiting-service/appointment-list/OrderByElderDialog.vue

@@ -0,0 +1,1319 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="72vw">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <!-- 长者信息 -->
+      <div class="section-title">长者信息</div>
+      
+      <el-form-item label="长者姓名" prop="elderId">
+        <el-select
+          v-model="formData.elderId"
+          placeholder="请输入长者姓名搜索"
+          class="w-full"
+          filterable
+          remote
+          clearable
+          :disabled="isDetailMode"
+          :remote-method="searchElderRemote"
+          :loading="elderSearchLoading"
+          @change="handleElderChange"
+        >
+          <el-option
+            v-for="item in elderSearchOptions"
+            :key="item.id"
+            :label="`${item.elderName} ${item.phone || ''} ${item.currentLiveAddress || ''}`"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      
+      <!-- 循环模式 -->
+      <el-form-item label="循环模式" prop="cycleMode" required>
+        <el-row :gutter="10">
+          <el-col :span="12">
+            <el-select v-model="formData.cycleMode" placeholder="请选择" class="w-full" style="width: 20vw;" @change="handleCycleModeChange" :disabled="isDetailMode" >
+              <el-option label="单次工单,不循环" value="single" />
+              <el-option label="多次循环" value="multiple" />
+            </el-select>
+          </el-col>
+          <el-col  :span="12" v-if="formData.cycleMode === 'multiple'">
+            <el-select v-model="formData.cycleType" placeholder="请选择" class="w-full" @change="handleCycleTypeChange" style="margin-left: 10px" :disabled="isDetailMode">
+              <el-option label="每天" value="daily" />
+              <el-option label="每周" value="weekly" />
+              <el-option label="每月" value="monthly" />
+            </el-select>
+          </el-col>
+        </el-row>
+      </el-form-item>
+
+      
+      <!-- 循环时每 - 每周 -->
+      <el-form-item label="循环时每" prop="cycleValue" required v-if="formData.cycleMode === 'multiple' && formData.cycleType === 'weekly'">
+        <el-select
+          v-model="formData.cycleValue"
+          multiple
+          :disabled="isDetailMode"
+          collapse-tags-tooltip
+          placeholder="请选择"
+          class="w-full"
+        >
+          <el-option label="周一" value="1" />
+          <el-option label="周二" value="2" />
+          <el-option label="周三" value="3" />
+          <el-option label="周四" value="4" />
+          <el-option label="周五" value="5" />
+          <el-option label="周六" value="6" />
+          <el-option label="周日" value="7" />
+        </el-select>
+      </el-form-item>
+      
+      <!-- 循环时每 - 每月 -->
+      <el-form-item label="循环时每" prop="cycleValue" required v-if="formData.cycleMode === 'multiple' && formData.cycleType === 'monthly'">
+        <el-date-picker
+          v-model="formData.cycleValue"
+          type="dates"
+          placeholder="请选择日期"
+          style="width: 100%;"
+          value-format="D"
+          :disabled="isDetailMode"
+        />
+      </el-form-item>
+      
+      <!-- 预约日期/预约起止日期 -->
+      <el-form-item :label="formData.cycleMode === 'single' ? '预约日期' : '预约起止日期'" prop="appointmentDate" required>
+        <el-date-picker
+          v-if="formData.cycleMode === 'single'"
+          v-model="formData.appointmentDate"
+          type="date"
+          placeholder="选择日期"
+          class="w-full"
+          value-format="YYYY-MM-DD"
+          :disabled="isDetailMode"
+        />
+        <el-row :gutter="10" v-else>
+          <el-col :span="11">
+            <el-date-picker
+              v-model="formData.appointmentStartDate"
+              type="date"
+              placeholder="开始日期"
+              class="w-full"
+              value-format="YYYY-MM-DD"
+              :disabled="isDetailMode"
+            />
+          </el-col>
+          <el-col :span="2" class="text-center">-</el-col>
+          <el-col :span="11">
+            <el-date-picker
+              v-model="formData.appointmentEndDate"
+              type="date"
+              placeholder="结束日期"
+              class="w-full"
+              value-format="YYYY-MM-DD"
+              :disabled="isDetailMode"
+            />
+          </el-col>
+        </el-row>
+      </el-form-item>
+      
+      <!-- 时间段选择(多次循环时显示) -->
+      <el-form-item label="时间段" prop="timeRange" v-if="formData.cycleMode === 'multiple'" required>
+        <el-select v-model="formData.timeRange" placeholder="请选择" class="w-full" :disabled="isDetailMode">
+          <el-option label="上午" value="morning" />
+          <el-option label="下午" value="afternoon" />
+          <el-option label="晚上" value="evening" />
+        </el-select>
+      </el-form-item>
+      
+      <!-- 工单有效时间 -->
+      <el-form-item label="工单有效时间" prop="validityType" required>
+        <el-radio-group v-model="formData.validityType" :disabled="isDetailMode">
+          <el-radio value="same_day" label="当天有效"/>
+          <el-radio value="always" label="一直有效"/>
+        </el-radio-group>
+      </el-form-item>
+      
+      <el-form-item >
+        <div class="tip-text">
+          说明:工单有效时间选择"当天有效"时,当超过预约日期后,工单视为过期,服务人员将不能接单。
+        </div>
+      </el-form-item>
+      
+      <!-- 消费券选择 -->
+      <el-form-item label="消费券" prop="consumerVoucherId">
+        <el-select
+          v-model="formData.consumerVoucherId"
+          placeholder="请选择消费券"
+          class="w-full"
+          clearable
+
+          @change="handleVoucherChange"
+        >
+          <el-option
+            v-for="item in consumerVoucherList"
+            :key="item.id"
+            :label="`${item.voucherType === 'percentage' ? '百分比' : '固定金额'} - ${item.amount}${item.voucherType === 'percentage' ? '%' : '元'}`"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      
+      <!-- 消费券金额/百分比编辑 -->
+      <el-form-item 
+        v-if="selectedVoucher" 
+        :label="selectedVoucher.voucherType === 'percentage' ? '百分比值' : '使用金额'" 
+        prop="voucherValue"
+        :rules="voucherValueRules"
+      >
+        <el-input
+          v-model="formData.voucherValue"
+          :placeholder="selectedVoucher.voucherType === 'percentage' ? '请输入1-100之间的百分比' : '请输入使用金额'"
+          :disabled="isDetailMode"
+          class="w-full"
+        >
+          <template #append>{{ selectedVoucher.voucherType === 'percentage' ? '%' : '元' }}</template>
+        </el-input>
+        <div class="tip-text" v-if="selectedVoucher">
+          消费券总额:{{ selectedVoucher.amount }}{{ selectedVoucher.voucherType === 'percentage' ? '%' : '元' }}
+        </div>
+      </el-form-item>
+      
+
+      
+      <!-- 工单备注 -->
+      <el-form-item label="工单备注" prop="remark">
+        <el-input
+          v-model="formData.remark"
+          type="textarea"
+          :rows="4"
+          placeholder="请输入"
+          maxlength="100"
+          show-word-limit
+          :disabled="isDetailMode"
+        />
+      </el-form-item>
+      
+      <!-- 服务人员选择 -->
+      <div class="section-title">服务信息</div>
+      
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="服务区域">
+            <el-select v-model="serviceAreaFilter" placeholder="请选择" clearable class="w-full" @change="handleServiceAreaChange" :disabled="isDetailMode">
+              <el-option
+                v-for="item in serviceAreaList"
+                :key="item.id"
+                :label="getAreaNameByCode(item.area)"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="服务人员">
+            <el-input
+              v-model="servicePersonKeyword"
+              placeholder="请输入"
+              class="w-full"
+              clearable
+              :disabled="isDetailMode"
+              @input="handleServicePersonSearch"
+            >
+              <template #append>
+                <el-button @click="searchServicePerson">
+                  <Icon icon="ep:search" />
+                </el-button>
+              </template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      
+      <!-- 服务人员卡片列表 -->
+      <el-form-item>
+        <div class="service-person-list" :class="{ 'detail-mode': isDetailMode }">
+          <div
+            v-for="person in filteredServicePersonList"
+            :key="person.id"
+            class="service-person-card"
+            :class="{ selected: selectedServicePerson?.id === person.id }"
+            @click="!isDetailMode && selectServicePerson(person)"
+          >
+            <div class="card-header">
+              <el-checkbox :model-value="selectedServicePerson?.id === person.id" />
+            </div>
+            <div class="card-body">
+              <div class="avatar-wrapper">
+                <el-avatar v-if="person.avatar" :size="60" :src="person.avatar" />
+                <el-avatar v-else :size="60">
+                  <el-icon :size="30"><UserFilled /></el-icon>
+                </el-avatar>
+              </div>
+              <div class="person-type">{{ person.isElderServicePerson ? '(该长者的服务人员)' : '(其他服务人员)' }}</div>
+              <div class="person-name">{{ person.name }}</div>
+              <div class="person-name">{{ formatPhone(person.phone)}}</div>
+            </div>
+          </div>
+        </div>
+      </el-form-item>
+      
+      <!-- 需求明细 - 使用 Transfer 穿梭框 -->
+      <el-form-item label="需求明细" prop="serviceItems" required style="margin-top: 40px">
+        <div class="transfer-wrapper">
+          <div class="transfer-header" v-if="!isDetailMode">
+            <el-button type="primary" size="small" @click="addToRight" :disabled="leftSelected.length === 0">
+              添加项目 &gt;
+            </el-button>
+            <el-button type="primary" size="small" @click="addToLeft" :disabled="rightSelected.length === 0">
+              &lt; 移除项目
+            </el-button>
+          </div>
+          <div class="transfer-header" v-else>
+            <span style="color: #909399; font-size: 14px;">已选服务项目</span>
+          </div>
+          <div class="transfer-content">
+            <div class="transfer-panel">
+              <div class="transfer-panel__header">
+                <el-checkbox v-model="leftCheckAll" :indeterminate="leftIsIndeterminate" @change="handleLeftCheckAllChange" label="可选服务项目"/>
+                <span>{{ leftSelected.length }}/{{ serviceItemTransferData.length }}</span>
+              </div>
+              <div class="transfer-panel__filter">
+                <el-input v-model="leftFilter" placeholder="请输入搜索内容" clearable prefix-icon="Search" />
+              </div>
+              <div class="transfer-panel__list">
+                <el-checkbox-group v-model="leftSelected">
+                  <el-checkbox 
+                    v-for="item in filteredLeftItems" 
+                    :key="item.key" 
+                    :label="item.key"
+                    class="transfer-item"
+                  >
+                    {{ item.label }}
+                  </el-checkbox>
+                </el-checkbox-group>
+              </div>
+            </div>
+            <div style="width: 20px;background-color: #e4e7ed"></div>
+            <div class="transfer-panel">
+              <div class="transfer-panel__header">
+                <el-checkbox v-model="rightCheckAll" :indeterminate="rightIsIndeterminate" @change="handleRightCheckAllChange" label="已选服务项目"/>
+                <span>{{ rightSelected.length }}/{{ rightItems.length }}</span>
+              </div>
+              <div class="transfer-panel__filter">
+                <el-input v-model="rightFilter" placeholder="请输入搜索内容" clearable prefix-icon="Search" />
+              </div>
+              <div class="transfer-panel__list">
+                <el-checkbox-group v-model="rightSelected">
+                  <el-checkbox 
+                    v-for="item in filteredRightItems" 
+                    :key="item.key" 
+                    :label="item.key"
+                    class="transfer-item"
+                  >
+                    {{ item.label }}
+                  </el-checkbox>
+                </el-checkbox-group>
+              </div>
+            </div>
+          </div>
+        </div>
+      </el-form-item>
+      <!-- 此处为金额板块 -->
+      <el-divider content-position="left">金额明细</el-divider>
+      <el-row :gutter="20">
+        <el-col :span="8" :xs="24">
+          <el-form-item label="总金额">
+            <el-input v-model="totalAmount" disabled class="w-full">
+              <template #append>元</template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8" :xs="24">
+          <el-form-item label="优惠金额">
+            <el-input v-model="discountAmount" disabled class="w-full">
+              <template #append>元</template>
+            </el-input>
+            <div class="tip-text" v-if="selectedVoucher">
+              消费券类型:{{ selectedVoucher.voucherType === 'percentage' ? '百分比' : '固定金额' }}
+              <span v-if="selectedVoucher.voucherType === 'percentage'">
+                ({{ formData.voucherValue }}%)
+              </span>
+            </div>
+          </el-form-item>
+        </el-col>
+        <el-col :span="8" :xs="24">
+          <el-form-item label="自费金额">
+            <el-input v-model="selfPayAmount" disabled class="w-full" type="number">
+              <template #append>元</template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    
+    <!-- 数据库字段说明 - 用于后台开发参考 -->
+    <el-divider content-position="left">数据库字段说明(供后台开发参考)</el-divider>
+    <el-descriptions :column="3" border size="small" class="field-description">
+      <el-descriptions-item label="elder_id" :span="1">长者ID(关联长者表)</el-descriptions-item>
+      <el-descriptions-item label="elder_name" :span="2">长者姓名</el-descriptions-item>
+      
+      <el-descriptions-item label="cycle_mode" :span="1">循环模式(single=单次,multiple=多次)</el-descriptions-item>
+      <el-descriptions-item label="cycle_type" :span="1">循环类型(daily=每天,weekly=每周,monthly=每月)</el-descriptions-item>
+      <el-descriptions-item label="cycle_value" :span="1">
+        <div>循环值(JSON数组)</div>
+        <div style="margin-top: 5px; color: #606266; font-size: 12px;">
+          <div>当 cycle_type = weekly 时:</div>
+          <div>值范围:["1", "2", "3", "4", "5", "6", "7"]</div>
+          <div>1=周一, 2=周二, 3=周三, 4=周四, 5=周五, 6=周六, 7=周日</div>
+          <div>示例:["1", "3", "5"] 表示每周一、三、五</div>
+          <div style="margin-top: 5px;">当 cycle_type = monthly 时:</div>
+          <div>值范围:["1", "2", ..., "31"]</div>
+          <div>示例:["10", "20"] 表示每月10日、20日</div>
+          <div style="margin-top: 5px;">当 cycle_type = daily 时:为空数组 []</div>
+        </div>
+      </el-descriptions-item>
+      
+      <el-descriptions-item label="appointment_start_date" :span="1">预约开始日期(单次/多次循环共用)</el-descriptions-item>
+      <el-descriptions-item label="appointment_end_date" :span="1">预约结束日期(多次循环使用,单次为空)</el-descriptions-item>
+      <el-descriptions-item label="-" :span="1">-</el-descriptions-item>
+      
+      <el-descriptions-item label="time_range" :span="1">时间段(morning=上午,afternoon=下午,evening=晚上)</el-descriptions-item>
+      <el-descriptions-item label="validity_type" :span="2">工单有效时间(same_day=当天有效,always=一直有效)</el-descriptions-item>
+      
+      <el-descriptions-item label="service_person_id" :span="1">服务人员ID</el-descriptions-item>
+      <el-descriptions-item label="service_person_name" :span="2">服务人员姓名</el-descriptions-item>
+      
+      <el-descriptions-item label="service_items" :span="3">
+        <div>需求明细/服务项目ID列表(JSON数组)</div>
+        <div style="margin-top: 5px; color: #909399; font-size: 12px;">
+          示例:[1, 2, 3] - 存储选中的服务项目ID数组
+        </div>
+        <div style="margin-top: 5px; color: #606266; font-size: 12px;">
+          <div>关联表:service_item(服务项目表)</div>
+          <div>关联字段:id</div>
+          <div style="margin-top: 3px;">服务项目包含字段:</div>
+          <div>- id: 服务项目ID</div>
+          <div>- service_name: 服务名称</div>
+          <div>- service_type_id: 服务类别ID</div>
+          <div>- service_type_name: 服务类别名称</div>
+          <div>- service_price: 服务价格(元)</div>
+          <div>- billing_method: 计费方式(按次/按时)</div>
+          <div>- service_points: 服务积分</div>
+        </div>
+      </el-descriptions-item>
+      
+      <el-descriptions-item label="remark" :span="3">工单备注</el-descriptions-item>
+      
+      <el-descriptions-item label="consumer_voucher_id" :span="1">消费券ID(关联消费券表)</el-descriptions-item>
+      <el-descriptions-item label="voucher_type" :span="1">消费券类型(fixed=固定金额,percentage=百分比)</el-descriptions-item>
+      <el-descriptions-item label="voucher_value" :span="1">消费券使用值</el-descriptions-item>
+      
+      <el-descriptions-item label="voucher_amount" :span="3">
+        <div>消费券总额</div>
+        <div style="margin-top: 5px; color: #606266; font-size: 12px;">
+          <div>当 voucher_type = fixed 时:</div>
+          <div>表示固定金额,单位:元</div>
+          <div>voucher_value 必须小于等于 voucher_amount</div>
+          <div style="margin-top: 5px;">当 voucher_type = percentage 时:</div>
+          <div>表示百分比上限,单位:%</div>
+          <div>voucher_value 必须在 1-100 之间,且小于等于 voucher_amount</div>
+          <div style="margin-top: 5px; color: #909399;">示例:</div>
+          <div>voucher_type=fixed, voucher_amount=100, voucher_value=80</div>
+          <div>表示使用80元,剩余20元</div>
+          <div>voucher_type=percentage, voucher_amount=50, voucher_value=30</div>
+          <div>表示按30%折扣计算</div>
+        </div>
+      </el-descriptions-item>
+      
+      <el-descriptions-item label="total_amount" :span="1">总金额(所有服务项目金额总和)</el-descriptions-item>
+      <el-descriptions-item label="discount_amount" :span="1">优惠金额(消费券抵扣金额)</el-descriptions-item>
+      <el-descriptions-item label="self_pay_amount" :span="1">自费金额(总金额 - 优惠金额)</el-descriptions-item>
+      
+      <el-descriptions-item label="amount_calculation" :span="3">
+        <div>金额计算规则</div>
+        <div style="margin-top: 5px; color: #606266; font-size: 12px;">
+          <div><strong>总金额(total_amount)</strong> = 所有选中的服务项目价格总和</div>
+          <div style="margin-top: 3px;"><strong>优惠金额(discount_amount)</strong> 计算方式:</div>
+          <div style="margin-left: 10px;">- 当 voucher_type = fixed 时:discount_amount = voucher_value</div>
+          <div style="margin-left: 10px;">- 当 voucher_type = percentage 时:discount_amount = total_amount × (voucher_value / 100)</div>
+          <div style="margin-top: 3px;"><strong>自费金额(self_pay_amount)</strong> = total_amount - discount_amount</div>
+          <div style="margin-top: 5px; color: #909399;">示例1(固定金额):</div>
+          <div>服务项目:A项目100元 + B项目200元 = 总金额300元</div>
+          <div>消费券:固定金额,voucher_value=50元</div>
+          <div>优惠金额=50元,自费金额=300-50=250元</div>
+          <div style="margin-top: 5px; color: #909399;">示例2(百分比):</div>
+          <div>服务项目:A项目100元 + B项目200元 = 总金额300元</div>
+          <div>消费券:百分比,voucher_value=20%</div>
+          <div>优惠金额=300×20%=60元,自费金额=300-60=240元</div>
+        </div>
+      </el-descriptions-item>
+      
+      <el-descriptions-item label="status" :span="1">工单状态</el-descriptions-item>
+      <el-descriptions-item label="create_time" :span="1">创建时间</el-descriptions-item>
+      <el-descriptions-item label="update_time" :span="1">更新时间</el-descriptions-item>
+    </el-descriptions>
+    
+    <template #footer>
+      <el-button @click="dialogVisible = false">{{ isDetailMode ? '关闭' : '取消' }}</el-button>
+      <el-button v-if="!isDetailMode" type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import * as AppointmentApi from '@/api/living-home/elderly'
+import * as ElderApi from '@/api/living-home/elderly'
+import * as AreaApi from '@/api/system/area'
+import { getConsumerVouchersPage } from '@/api/elderly/fee/consumpotion-coupon'
+import { UserFilled, Search } from '@element-plus/icons-vue'
+
+const emit = defineEmits(['success'])
+
+const dialogVisible = ref(false)
+const dialogMode = ref<'create' | 'edit' | 'detail'>('create')
+const currentRow = ref<any>({})
+const submitLoading = ref(false)
+
+/** 是否为详情模式(只读) */
+const isDetailMode = computed(() => dialogMode.value === 'detail')
+
+/** 弹窗标题 */
+const dialogTitle = computed(() => {
+  const titles = { create: '按长者预约', edit: '编辑工单', detail: '工单详情' }
+  return titles[dialogMode.value]
+})
+const formRef = ref()
+const elderList = ref<any[]>([])
+const serviceItemList = ref<any[]>([])
+const serviceAreaList = ref<any[]>([])
+const servicePersonList = ref<any[]>([])
+const deviceList = ref<any[]>([])
+const areaTree = ref<any[]>([]) // 区域树数据
+
+// 长者搜索相关
+const elderSearchLoading = ref(false)
+const elderSearchOptions = ref<any[]>([])
+
+// 服务人员筛选
+const serviceAreaFilter = ref('')
+const servicePersonKeyword = ref('')
+const selectedServicePerson = ref<any>(null)
+
+// 自定义穿梭框相关
+const leftSelected = ref<number[]>([])
+const rightSelected = ref<number[]>([])
+const leftFilter = ref('')
+const rightFilter = ref('')
+const leftCheckAll = ref(false)
+const rightCheckAll = ref(false)
+
+const formData = reactive({
+  elderId: undefined as number | undefined,
+  cycleMode: 'single',
+  cycleType: 'daily',
+  cycleValue: [] as string[],
+  appointmentDate: '',
+  appointmentStartDate: '',
+  appointmentEndDate: '',
+  timeRange: '',
+  validityType: 'always',
+  servicePersonId: undefined as number | undefined,
+  serviceItems: [] as number[],
+  remark: '',
+  consumerVoucherId: undefined as number | undefined,
+  voucherValue: ''
+})
+
+// 消费券列表
+const consumerVoucherList = ref<any[]>([])
+// 当前选中的消费券
+const selectedVoucher = computed(() => {
+  return consumerVoucherList.value.find(item => item.id === formData.consumerVoucherId)
+})
+
+// 总金额(所有服务项目金额总和)
+const totalAmount = computed(() => {
+  if (!formData.serviceItems || formData.serviceItems.length === 0) return '0.00'
+  const total = formData.serviceItems.reduce((sum, itemId) => {
+    const item = serviceItemList.value.find(i => i.id === itemId)
+    return sum + (item?.amount || item?.servicePrice || 0)
+  }, 0)
+  return total.toFixed(2)
+})
+
+// 优惠金额(消费券抵扣金额)
+const discountAmount = computed(() => {
+  if (!selectedVoucher.value || !formData.voucherValue) return '0.00'
+  const total = parseFloat(totalAmount.value)
+  if (selectedVoucher.value.voucherType === 'percentage') {
+    // 百分比类型:总金额 × 百分比
+    const percentage = parseFloat(formData.voucherValue) || 0
+    return (total * percentage / 100).toFixed(2)
+  } else {
+    // 固定金额类型:直接使用 voucherValue
+    const voucherVal = parseFloat(formData.voucherValue) || 0
+    // 优惠金额不能超过总金额
+    return Math.min(voucherVal, total).toFixed(2)
+  }
+})
+
+// 自费金额(总金额 - 优惠金额)
+const selfPayAmount = computed(() => {
+  const total = parseFloat(totalAmount.value)
+  const discount = parseFloat(discountAmount.value)
+  return (total - discount).toFixed(2)
+})
+
+// 消费券金额/百分比校验规则
+const voucherValueRules = computed(() => {
+  if (!selectedVoucher.value) return []
+  
+  if (selectedVoucher.value.voucherType === 'percentage') {
+    return [
+      { required: true, message: '请输入百分比值', trigger: 'blur' },
+      { 
+        validator: (rule: any, value: any, callback: any) => {
+          const num = Number(value)
+          if (isNaN(num) || num < 1 || num > 100) {
+            callback(new Error('百分比值必须在1-100之间'))
+          } else {
+            callback()
+          }
+        },
+        trigger: 'blur'
+      }
+    ]
+  } else {
+    return [
+      { required: true, message: '请输入使用金额', trigger: 'blur' },
+      {
+        validator: (rule: any, value: any, callback: any) => {
+          const num = Number(value)
+          if (isNaN(num) || num <= 0) {
+            callback(new Error('金额必须大于0'))
+          } else if (num > Number(selectedVoucher.value.amount)) {
+            callback(new Error(`金额不能大于消费券总额 ${selectedVoucher.value.amount} 元`))
+          } else {
+            callback()
+          }
+        },
+        trigger: 'blur'
+      }
+    ]
+  }
+})
+
+const formRules = {
+  elderId: [{ required: true, message: '请选择长者', trigger: 'change' }],
+  cycleMode: [{ required: true, message: '请选择循环模式', trigger: 'change' }],
+  appointmentDate: [{ 
+    required: true, 
+    message: '请选择预约日期', 
+    trigger: 'change',
+    validator: (rule: any, value: any, callback: any) => {
+      if (formData.cycleMode === 'single') {
+        if (!value || value === '' || (Array.isArray(value) && value.length === 0)) {
+          callback(new Error('请选择预约日期'))
+        } else {
+          callback()
+        }
+      } else {
+        // 多次循环模式,校验起止日期
+        if (!formData.appointmentStartDate || !formData.appointmentEndDate) {
+          callback(new Error('请选择预约起止日期'))
+        } else {
+          callback()
+        }
+      }
+    }
+  }],
+  validityType: [{ required: true, message: '请选择工单有效时间', trigger: 'change' }],
+  serviceItems: [{ required: true, message: '请选择服务项目', trigger: 'change', type: 'array' }]
+}
+
+// 服务项目穿梭框数据
+const serviceItemTransferData = computed(() => {
+  return serviceItemList.value.map(item => ({
+    key: item.id,
+    label: `${item.serviceName || item.itemName || item.name} - (${item.amount}元)`,
+    disabled: false
+  }))
+})
+
+// 右侧已选项(已选服务项目)
+const rightItems = computed(() => {
+  return serviceItemTransferData.value.filter(item => formData.serviceItems.includes(item.key))
+})
+
+// 左侧可选项目(过滤掉已选的)
+const leftItems = computed(() => {
+  return serviceItemTransferData.value.filter(item => !formData.serviceItems.includes(item.key))
+})
+
+// 过滤后的左侧项目
+const filteredLeftItems = computed(() => {
+  if (!leftFilter.value) return leftItems.value
+  return leftItems.value.filter(item => 
+    item.label.toLowerCase().includes(leftFilter.value.toLowerCase())
+  )
+})
+
+// 过滤后的右侧项目
+const filteredRightItems = computed(() => {
+  if (!rightFilter.value) return rightItems.value
+  return rightItems.value.filter(item => 
+    item.label.toLowerCase().includes(rightFilter.value.toLowerCase())
+  )
+})
+
+// 左侧全选状态
+const leftIsIndeterminate = computed(() => {
+  return leftSelected.value.length > 0 && leftSelected.value.length < filteredLeftItems.value.length
+})
+
+// 右侧全选状态
+const rightIsIndeterminate = computed(() => {
+  return rightSelected.value.length > 0 && rightSelected.value.length < filteredRightItems.value.length
+})
+
+// 监听左侧选择变化
+watch(leftSelected, (val) => {
+  leftCheckAll.value = val.length === filteredLeftItems.value.length && val.length > 0
+})
+
+// 监听右侧选择变化
+watch(rightSelected, (val) => {
+  rightCheckAll.value = val.length === filteredRightItems.value.length && val.length > 0
+})
+
+// 监听左侧过滤变化,清空选择
+watch(leftFilter, () => {
+  leftSelected.value = []
+  leftCheckAll.value = false
+})
+
+// 监听右侧过滤变化,清空选择
+watch(rightFilter, () => {
+  rightSelected.value = []
+  rightCheckAll.value = false
+})
+
+// 左侧全选
+const handleLeftCheckAllChange = (val: boolean) => {
+  leftSelected.value = val ? filteredLeftItems.value.map(item => item.key) : []
+}
+
+// 右侧全选
+const handleRightCheckAllChange = (val: boolean) => {
+  rightSelected.value = val ? filteredRightItems.value.map(item => item.key) : []
+}
+
+// 添加到右侧
+const addToRight = () => {
+  formData.serviceItems.push(...leftSelected.value)
+  leftSelected.value = []
+  leftCheckAll.value = false
+}
+
+// 添加到左侧(移除)
+const addToLeft = () => {
+  formData.serviceItems = formData.serviceItems.filter(key => !rightSelected.value.includes(key))
+  rightSelected.value = []
+  rightCheckAll.value = false
+}
+
+// 过滤后的服务人员列表
+const filteredServicePersonList = computed(() => {
+  let list = servicePersonList.value
+  if (serviceAreaFilter.value) {
+    list = list.filter(p => p.serviceAreaId === serviceAreaFilter.value)
+  }
+  if (servicePersonKeyword.value) {
+    const keyword = servicePersonKeyword.value.toLowerCase()
+    list = list.filter(p => 
+      p.name?.toLowerCase().includes(keyword) || 
+      p.phone?.includes(keyword)
+    )
+  }
+  return list
+})
+
+const open = async (row?: any, mode: 'create' | 'edit' | 'detail' = 'create') => {
+  dialogMode.value = mode
+  currentRow.value = row || {}
+  dialogVisible.value = true
+  resetForm()
+
+  if (row && (mode === 'edit' || mode === 'detail')) {
+    // 回填数据
+    await fillFormData(row)
+  } else {
+    // 新建模式
+    await getElderList()
+    await getServiceItemList()
+    await getServiceAreaList()
+    await getServicePersonList()
+  }
+}
+
+/** 回填表单数据 */
+const fillFormData = async (row: any) => {
+  // 设置长者信息(可能需要搜索)
+  formData.elderId = row.elderId
+  elderSearchOptions.value = [{
+    id: row.elderId,
+    elderName: row.elderName,
+    phone: row.phone,
+    currentLiveAddress: row.address
+  }]
+
+  // 设置服务人员
+  formData.servicePersonId = row.servicePersonId
+  selectedServicePerson.value = {
+    id: row.servicePersonId,
+    name: row.servicePersonName
+  }
+
+  // 加载列表数据
+  await getServiceItemList()
+  await getServiceAreaList()
+  await getServicePersonList()
+
+  // 设置服务项目(如果有)
+  if (row.serviceItems && row.serviceItems.length > 0) {
+    formData.serviceItems = row.serviceItems.map((item: any) =>
+      typeof item === 'object' ? item.id : item
+    )
+  }
+
+  // 设置其他字段
+  formData.remark = row.remark || ''
+  formData.validityType = row.validityType || 'always'
+  formData.appointmentDate = row.visitTime ? row.visitTime.split(' ')[0] : ''
+}
+
+const resetForm = () => {
+  formData.elderId = undefined
+  formData.cycleMode = 'single'
+  formData.cycleType = 'daily'
+  formData.cycleValue = []
+  formData.appointmentDate = ''
+  formData.appointmentStartDate = ''
+  formData.appointmentEndDate = ''
+  formData.timeRange = ''
+  formData.validityType = 'always'
+  formData.servicePersonId = undefined
+  formData.serviceItems = []
+  formData.remark = ''
+  formData.consumerVoucherId = undefined
+  formData.voucherValue = ''
+  selectedServicePerson.value = null
+  serviceAreaFilter.value = ''
+  servicePersonKeyword.value = ''
+  elderSearchOptions.value = []
+  consumerVoucherList.value = []
+  nextTick(() => {
+    formRef.value?.resetFields()
+  })
+}
+
+const getElderList = async () => {
+  try {
+    const params = { pageNo: 1, pageSize: 1000 }
+    const data = await ElderApi.getHomeElderlyList(params)
+    elderList.value = data.list || []
+  } catch (error) {
+    console.log('获取长者列表失败', error)
+  }
+}
+
+const getServiceItemList = async () => {
+  try {
+    const params = { pageNo: 1, pageSize: 1000 }
+    const data = await ElderApi.getServiceItemList(params)
+    serviceItemList.value = data.list || []
+  } catch (error) {
+    console.log('获取服务项目列表失败', error)
+  }
+}
+
+const getServiceAreaList = async () => {
+  try {
+    // 先加载区域树数据(用于转换代码为名称)
+    if (areaTree.value.length === 0) {
+      areaTree.value = await AreaApi.getAreaTree()
+    }
+    const data = await ElderApi.getServiceAreaList()
+    serviceAreaList.value = data.list || []
+  } catch (error) {
+    console.log('获取服务区域列表失败', error)
+  }
+}
+
+/** 根据区域代码字符串获取区域名称 */
+const getAreaNameByCode = (areaCodeStr: string): string => {
+  if (!areaCodeStr) return ''
+  
+  // 可能是单个代码 "130304" 或多个代码 "120000,120100,120102"
+  const codes = areaCodeStr.split(',').map(Number)
+  
+  // 如果只有一个代码,需要查找完整路径
+  if (codes.length === 1) {
+    const fullPath = findFullPath(areaTree.value, codes[0])
+    if (fullPath.length > 0) {
+      return getNamesFromPath(areaTree.value, fullPath)
+    }
+  }
+  
+  // 多个代码,直接按路径查找
+  return getNamesFromPath(areaTree.value, codes)
+}
+
+/** 根据最后一个code查找完整路径 */
+const findFullPath = (tree: any[], targetCode: number): number[] => {
+  for (const node of tree) {
+    const nodeId = node.id || node.value
+    if (nodeId === targetCode) {
+      return [targetCode]
+    }
+    if (node.children && node.children.length > 0) {
+      const path = findFullPath(node.children, targetCode)
+      if (path.length > 0) {
+        return [nodeId, ...path]
+      }
+    }
+  }
+  return []
+}
+
+/** 根据路径获取名称 */
+const getNamesFromPath = (tree: any[], codes: number[]): string => {
+  const names: string[] = []
+  let currentTree = tree
+  
+  for (const code of codes) {
+    const node = currentTree.find((item: any) => (item.id || item.value) === code)
+    if (node) {
+      names.push(node.name || node.label)
+      currentTree = node.children || []
+    } else {
+      break
+    }
+  }
+  
+  return names.join('/')
+}
+
+const getServicePersonList = async () => {
+  try {
+    const params = { pageNo: 1, pageSize: 1000 }
+    const data = await ElderApi.getServicePersonList(params)
+    servicePersonList.value = data.list || []
+  } catch (error) {
+    console.log('获取服务人员列表失败', error)
+  }
+}
+
+// 循环模式变化
+const handleCycleModeChange = (val: string) => {
+  formData.cycleValue = []
+  formData.appointmentDate = ''
+  formData.appointmentStartDate = ''
+  formData.appointmentEndDate = ''
+  formData.timeRange = ''
+  // 清除表单校验
+  nextTick(() => {
+    formRef.value?.clearValidate('appointmentDate')
+  })
+}
+
+// 循环类型变化
+const handleCycleTypeChange = (val: string) => {
+  formData.cycleValue = []
+}
+
+// 远程搜索长者
+const searchElderRemote = async (query: string) => {
+  if (!query || query.trim() === '') {
+    elderSearchOptions.value = []
+    return
+  }
+  elderSearchLoading.value = true
+  try {
+    const params = {
+      pageNo: 1,
+      pageSize: 20,
+      elderName: query.trim()
+    }
+    const data = await ElderApi.getHomeElderlyList(params)
+    elderSearchOptions.value = data.list || []
+  } catch (error) {
+    console.log('搜索长者失败', error)
+    elderSearchOptions.value = []
+  } finally {
+    elderSearchLoading.value = false
+  }
+}
+
+// 长者选择变化
+const handleElderChange = (val: number) => {
+  // 加载长者设备信息
+  loadElderDevices(val)
+  // 加载长者消费券列表
+  loadConsumerVouchers(val)
+  // 清空已选消费券
+  formData.consumerVoucherId = undefined
+  formData.voucherValue = ''
+}
+
+// 加载长者消费券列表
+const loadConsumerVouchers = async (elderId: number) => {
+  if (!elderId) {
+    consumerVoucherList.value = []
+    return
+  }
+  try {
+    const params = {
+      pageNo: 1,
+      pageSize: 100,
+      elderId: elderId
+    }
+    const data = await getConsumerVouchersPage(params)
+    consumerVoucherList.value = data.list || []
+  } catch (error) {
+    console.log('获取消费券列表失败', error)
+    consumerVoucherList.value = []
+  }
+}
+
+// 消费券选择变化
+const handleVoucherChange = (val: number) => {
+  const voucher = consumerVoucherList.value.find(item => item.id === val)
+  if (voucher) {
+    // 默认填充最大可用金额/百分比
+    formData.voucherValue = voucher.amount
+  } else {
+    formData.voucherValue = ''
+  }
+}
+
+// 加载长者设备
+const loadElderDevices = async (elderId: number) => {
+  // 这里可以根据实际需求调用接口获取设备信息
+  deviceList.value = []
+}
+
+// 打开长者搜索(已废弃,保留兼容)
+const openElderSearch = () => {
+  console.log('请直接在输入框中搜索长者')
+}
+
+// 选择服务人员
+const selectServicePerson = (person: any) => {
+  selectedServicePerson.value = person
+  formData.servicePersonId = person.id
+}
+
+// 服务人员区域筛选变化
+const handleServiceAreaChange = () => {
+  // 筛选逻辑在 computed 中处理
+}
+
+// 服务人员搜索
+const handleServicePersonSearch = () => {
+  // 搜索逻辑在 computed 中处理
+}
+
+const searchServicePerson = () => {
+  // 搜索逻辑在 computed 中处理
+}
+
+// 格式化手机号
+const formatPhone = (phone: string) => {
+  if (!phone) return ''
+  if (phone.length === 11) {
+    return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
+  }
+  return phone
+}
+
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
+
+  submitLoading.value = true
+  try {
+    const submitData = {
+      elderId: formData.elderId,
+      cycleMode: formData.cycleMode,
+      cycleType: formData.cycleType,
+      cycleValue: formData.cycleValue,
+      appointmentDate: formData.appointmentDate,
+      appointmentStartDate: formData.appointmentStartDate,
+      appointmentEndDate: formData.appointmentEndDate,
+      timeRange: formData.timeRange,
+      validityType: formData.validityType,
+      servicePersonId: formData.servicePersonId,
+      serviceItems: formData.serviceItems,
+      remark: formData.remark
+    }
+
+    if (dialogMode.value === 'edit' && currentRow.value.id) {
+      // 编辑模式:调用更新接口
+      await AppointmentApi.updateAppointment({
+        id: currentRow.value.id,
+        ...submitData
+      })
+      useMessage().success('修改成功')
+    } else {
+      // 新建模式
+      await AppointmentApi.createAppointment(submitData)
+      useMessage().success('预约成功')
+    }
+
+    dialogVisible.value = false
+    emit('success')
+  } catch (error) {
+    console.log('提交失败', error)
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+defineExpose({ open })
+</script>
+
+<style scoped lang="scss">
+.section-title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #333;
+  margin: 20px 0 15px 0;
+  padding-left: 10px;
+  border-left: 4px solid #409eff;
+}
+
+.tip-text {
+  color: #f56c6c;
+  font-size: 12px;
+  line-height: 1.5;
+}
+
+.empty-text {
+  color: #909399;
+  padding: 20px 0;
+}
+
+.service-person-list {
+  display: flex;
+  flex-wrap: nowrap;
+  gap: 15px;
+  width: 100%;
+  min-height: 200px;
+  max-height: 280px;
+  overflow-x: auto;
+  overflow-y: hidden;
+  padding: 15px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  align-items: flex-start;
+
+  &::-webkit-scrollbar {
+    height: 8px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background-color: #c0c4cc;
+    border-radius: 4px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background-color: #e4e7ed;
+    border-radius: 4px;
+  }
+}
+
+.service-person-card {
+  width: 140px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.3s;
+  overflow: hidden;
+
+  &:hover {
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  }
+
+  &.selected {
+    border-color: #409eff;
+    box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+
+    .card-header {
+      background-color: #409eff;
+
+      :deep(.el-checkbox__inner) {
+        background-color: #fff;
+        border-color: #fff;
+      }
+
+      :deep(.el-checkbox__inner::after) {
+        border-color: #409eff;
+      }
+    }
+  }
+
+  .card-header {
+    padding: 3px 8px 3px 3px;
+    background-color: #f5f7fa;
+    text-align: right;
+  }
+
+  .card-body {
+    padding: 15px;
+    text-align: center;
+
+    .person-type {
+      font-size: 12px;
+      color: #909399;
+      margin-top: 2px;
+    }
+
+    .person-name {
+      font-size: 13px;
+      color: #606266;
+      margin-top: 0px;
+      word-break: break-all;
+    }
+  }
+}
+
+.avatar-wrapper {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.transfer-wrapper {
+  width: 100%;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+
+  .transfer-header {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      min-height: 40px;
+    }
+
+    // 详情模式下服务人员列表不可点击
+    .service-person-list.detail-mode {
+      .service-person-card {
+        cursor: default;
+
+        &:hover {
+          box-shadow: none;
+        }
+
+        &.selected {
+          border-color: #409eff;
+          box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+        }
+      }
+    };
+    gap: 20px;
+    padding: 10px;
+    background-color: #f5f7fa;
+    border-bottom: 1px solid #e4e7ed;
+  }
+
+  .transfer-content {
+    display: flex;
+    height: 400px;
+  }
+
+  .transfer-panel {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+
+    &:first-child {
+      border-right: 1px solid #e4e7ed;
+    }
+
+    &__header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 10px 15px;
+      background-color: #f5f7fa;
+      border-bottom: 1px solid #e4e7ed;
+      font-size: 14px;
+      color: #606266;
+
+      span {
+        color: #909399;
+        font-size: 12px;
+      }
+    }
+
+    &__filter {
+      padding: 10px 15px;
+      border-bottom: 1px solid #e4e7ed;
+    }
+
+    &__list {
+      flex: 1;
+      overflow-y: auto;
+      padding: 10px 15px;
+
+      &::-webkit-scrollbar {
+        width: 6px;
+      }
+
+      &::-webkit-scrollbar-thumb {
+        background-color: #c0c4cc;
+        border-radius: 3px;
+      }
+    }
+  }
+
+  .transfer-item {
+    display: flex;
+    align-items: center;
+    padding: 8px 0;
+    width: 100%;
+
+    :deep(.el-checkbox__label) {
+      flex: 1;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
+
+// 数据库字段说明样式
+.field-description {
+  margin: 10px 0 20px 0;
+  background-color: #f5f7fa;
+  padding: 15px;
+  border-radius: 4px;
+
+  :deep(.el-descriptions__label) {
+    background-color: #e4e7ed;
+    font-weight: bold;
+    color: #606266;
+    font-family: 'Courier New', monospace;
+  }
+
+  :deep(.el-descriptions__content) {
+    color: #303133;
+  }
+}
+</style>

+ 126 - 0
src/views/living-home/visiting-service/appointment-list/QuickSignInDialog.vue

@@ -0,0 +1,126 @@
+<template>
+  <Dialog v-model="dialogVisible" title="一键签到" width="40vw">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+    >
+      <el-form-item label="长者姓名">
+        <el-input v-model="currentElderName" disabled />
+      </el-form-item>
+      <el-form-item label="服务人员">
+        <el-input v-model="currentServicePerson" disabled />
+      </el-form-item>
+      <el-form-item label="签到位置" prop="location">
+        <el-input
+          v-model="formData.location"
+          placeholder="请输入签到位置"
+          clearable
+        />
+      </el-form-item>
+      <el-form-item label="签到备注" prop="remark">
+        <el-input
+          v-model="formData.remark"
+          type="textarea"
+          rows="3"
+          placeholder="请输入签到备注(选填)"
+        />
+      </el-form-item>
+
+        <div class="tip-text">
+
+          如果有多条工单,比如按月的工单,会有多条工单,会一键自动完成服务工单。主要针对不会使用智能手机的护理员,帮她完成服务工单。
+        </div>
+
+    </el-form>
+    <template #footer>
+      <el-button @click="dialogVisible = false">取消</el-button>
+      <el-button type="primary" @click="handleSubmit" :loading="submitLoading">确认签到</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import * as AppointmentApi from '@/api/living-home/elderly'
+
+const emit = defineEmits(['success'])
+
+const dialogVisible = ref(false)
+const submitLoading = ref(false)
+const formRef = ref()
+const currentServicePerson = ref('')
+const currentElderName = ref('')
+const currentRow = ref<any>({})
+
+const formData = reactive({
+  location: '',
+  remark: ''
+})
+
+const formRules = {
+ // location: [{ required: true, message: '请输入签到位置', trigger: 'blur' }]
+}
+
+const open = async (row: any) => {
+  currentRow.value = row
+  currentElderName.value = row.elderName
+  currentServicePerson.value = row.servicePersonName
+  dialogVisible.value = true
+  resetForm()
+  // 自动获取当前位置
+  await getCurrentLocation()
+}
+
+const resetForm = () => {
+  formData.location = ''
+  formData.remark = ''
+  nextTick(() => {
+    formRef.value?.resetFields()
+  })
+}
+
+// 获取当前位置
+const getCurrentLocation = async () => {
+  try {
+    // 这里可以调用地图API获取当前位置
+    // 暂时使用模拟数据
+    formData.location = '河源市源城区'
+  } catch (error) {
+    console.log('获取位置失败', error)
+  }
+}
+
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
+
+  submitLoading.value = true
+  try {
+    await AppointmentApi.quickSignIn({
+      appointmentId: currentRow.value.id,
+      location: formData.location,
+      remark: formData.remark,
+      signInTime: new Date().toISOString()
+    })
+    useMessage().success('签到成功')
+    dialogVisible.value = false
+    emit('success')
+  } catch (error) {
+    console.log('签到失败', error)
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+defineExpose({ open })
+</script>
+
+<style scoped>
+.tip-text {
+  padding: 10px;
+  color: #fb093d;
+  font-size: 12px;
+
+}
+</style>

+ 74 - 0
src/views/living-home/visiting-service/appointment-list/SignInRecordDialog.vue

@@ -0,0 +1,74 @@
+<template>
+  <Dialog v-model="dialogVisible" title="服务签到记录" width="60vw">
+    <el-table :data="signInList" v-loading="loading" border>
+      <el-table-column type="index" label="序号" width="60" align="center" />
+      <el-table-column prop="signInTime" label="服务签到时间" align="center" min-width="160" />
+      <el-table-column prop="signInType" label="服务签到项目" align="center" width="120">
+        <template #default="scope">
+          <el-tag :type="scope.row.signInType === 'manual' ? 'warning' : 'success'">
+            {{ scope.row.signInType === 'manual' ? '项目1' : '项目2' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="servicePersonName" label="服务签到人员" align="center" width="120" />
+      <el-table-column prop="location" label="服务签到位置" align="center" min-width="200" show-overflow-tooltip />
+      <el-table-column prop="remark" label="备注" align="center" min-width="150" show-overflow-tooltip />
+    </el-table>
+    <template #footer>
+      <el-button @click="dialogVisible = false">关闭</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import * as AppointmentApi from '@/api/living-home/elderly'
+
+const emit = defineEmits(['success'])
+
+const dialogVisible = ref(false)
+const loading = ref(false)
+const signInList = ref<any[]>([])
+const currentRow = ref<any>({})
+
+const open = async (row: any) => {
+  currentRow.value = row
+  dialogVisible.value = true
+  await getSignInList()
+}
+
+const getSignInList = async () => {
+  loading.value = true
+  try {
+    // 调用接口获取签到记录
+    const data = await AppointmentApi.getSignInRecordList({
+      appointmentId: currentRow.value.id
+    })
+    signInList.value = data.list || []
+  } catch (error) {
+    console.log('获取签到记录失败', error)
+    // 使用模拟数据
+    signInList.value = [
+      {
+        id: 1,
+        signInTime: '2026-04-27 10:30:00',
+        signInType: 'manual',
+        servicePersonName: '张磊',
+        location: '河源市源城区xxx街道',
+        remark: '正常签到'
+      },
+      {
+        id: 2,
+        signInTime: '2026-04-28 14:20:00',
+        signInType: 'quick',
+        servicePersonName: '张磊',
+        location: '河源市源城区yyy小区',
+        remark: '一键签到'
+      }
+    ]
+  } finally {
+    loading.value = false
+  }
+}
+
+defineExpose({ open })
+</script>

+ 333 - 0
src/views/living-home/visiting-service/appointment-list/index.vue

@@ -0,0 +1,333 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="120px"
+    >
+      <el-form-item label="长者姓名:" prop="elderName">
+        <el-input
+          v-model="queryParams.elderName"
+          placeholder="请输入"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-180px"
+        />
+      </el-form-item>
+      <el-form-item label="服务人员姓名:" prop="servicePersonName">
+        <el-input
+          v-model="queryParams.servicePersonName"
+          placeholder="请输入"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-180px"
+        />
+      </el-form-item>
+      <el-form-item label="联系电话:" prop="phone">
+        <el-input
+          v-model="queryParams.phone"
+          placeholder="请输入"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-180px"
+        />
+      </el-form-item>
+      <el-form-item label="工单状态:" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="全部"
+          clearable
+          class="!w-180px"
+        >
+          <el-option label="全部" value="" />
+          <el-option label="待派单" value="待派单" />
+          <el-option label="待接单" value="待接单" />
+          <el-option label="已接单" value="已接单" />
+          <el-option label="已签到" value="已签到" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="下单方式:" prop="orderType">
+        <el-select
+          v-model="queryParams.orderType"
+          placeholder="全部"
+          clearable
+          class="!w-180px"
+        >
+          <el-option label="全部" value="" />
+          <el-option label="报警" value="报警" />
+          <el-option label="电话咨询" value="电话咨询" />
+          <el-option label="自主下单" value="自主下单" />
+          <el-option label="上门关怀" value="上门关怀" />
+          <el-option label="工单详情下单" value="工单详情下单" />
+          <el-option label="长者小程序下单" value="长者小程序下单" />
+          <el-option label="守护中心下单" value="守护中心下单" />
+        </el-select>
+      </el-form-item>
+      <el-form-item class="!ml-auto">
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> 查询
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" /> 重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <div class="flex items-center justify-between mb-15px">
+      <span class="font-bold text-base">预约服务工单</span>
+      <div class="flex gap-10px">
+        <el-button type="primary" @click="handleOrderByElder">
+          <Icon icon="ep:user" class="mr-5px" /> 按长者预约
+        </el-button>
+
+      </div>
+    </div>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      row-key="id"
+    >
+      <el-table-column label="序号" type="index" width="80" align="center" />
+      <el-table-column prop="elderName" label="姓名" align="center" min-width="100" />
+      <el-table-column prop="gender" label="性别" align="center" width="80" />
+      <el-table-column prop="age" label="年龄" align="center" width="80" />
+      <el-table-column prop="phone" label="手机号码" align="center" min-width="120" />
+      <el-table-column prop="address" label="居住地址" align="center" min-width="200" show-overflow-tooltip />
+      <el-table-column prop="orderType" label="下单方式" align="center" min-width="120" />
+      <el-table-column prop="servicePersonName" label="服务人员姓名" align="center" min-width="120" />
+      <el-table-column prop="orderTime" label="下单时间" align="center" min-width="160" />
+      <el-table-column prop="visitTime" label="上门时间" align="center" min-width="120" />
+      <el-table-column prop="isExpired" label="是否过期" align="center" width="100" />
+      <el-table-column label="操作" align="center" width="350" fixed="right">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleDetail(scope.row)">
+            <Icon icon="ep:view" class="mr-5px" /> 详情
+          </el-button>
+          <el-button  link type="warning" @click="handleEdit(scope.row)">
+            <Icon icon="ep:edit" class="mr-5px" /> 编辑
+          </el-button>
+          <el-button link type="danger" @click="handleDelete(scope.row)">
+            <Icon icon="ep:delete" class="mr-5px" /> 删除
+          </el-button>
+
+          <el-dropdown @command="(command) => handleSignInCommand(command, scope.row)" trigger="hover">
+            <el-button style="margin-left: 10px">
+              其他操作<Icon icon="ep:arrow-down" class="ml-5px" />
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="record">
+                  <Icon icon="ep:list" class="mr-5px" /> 签到记录
+                </el-dropdown-item>
+                <el-dropdown-item command="quick">
+                  <Icon icon="ep:check" class="mr-5px" /> 一键签到
+                </el-dropdown-item>
+                <el-dropdown-item>
+                  <el-button link type="success" @click="handleChangeServicePerson(scope.row)">
+                    <Icon icon="ep:edit" class="mr-5px" /> 修改服务人员
+                  </el-button>
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <Pagination
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      :total="total"
+      @pagination="getList"
+      class="mt-15px"
+    />
+  </ContentWrap>
+
+  <!-- 按长者预约弹窗(同时用于详情/编辑) -->
+  <OrderByElderDialog ref="orderByElderRef" @success="getList" />
+
+  <!-- 修改服务人员弹窗 -->
+  <ChangeServicePersonDialog ref="changeServicePersonRef" @success="getList" />
+
+  <!-- 签到记录弹窗 -->
+  <SignInRecordDialog ref="signInRecordRef" @success="getList" />
+
+  <!-- 一键签到弹窗 -->
+  <QuickSignInDialog ref="quickSignInRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import * as AppointmentApi from '@/api/living-home/elderly'
+import OrderByElderDialog from './OrderByElderDialog.vue'
+import ChangeServicePersonDialog from './ChangeServicePersonDialog.vue'
+import SignInRecordDialog from './SignInRecordDialog.vue'
+import QuickSignInDialog from './QuickSignInDialog.vue'
+
+defineOptions({ name: 'AppointmentList' })
+
+const message = useMessage()
+const { t } = useI18n()
+
+const loading = ref(false)
+const list = ref<any[]>([])
+const total = ref(0)
+const queryFormRef = ref()
+
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  elderName: '',
+  servicePersonName: '',
+  phone: '',
+  status: '',
+  orderType: ''
+})
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await AppointmentApi.getAppointmentPage(queryParams)
+    list.value = data.list || []
+    total.value = data.total || 0
+  } catch (error) {
+    console.log('获取预约列表失败', error)
+    // 使用模拟数据
+    list.value = [
+      {
+        id: 1,
+        elderName: '张三',
+        gender: '女',
+        age: 43,
+        phone: '156****5555',
+        address: '河源市/源城区/城东街道aaa',
+        orderType: '坐席下单',
+        servicePersonName: '张磊',
+        orderTime: '2026-04-27 15:00:04',
+        visitTime: '2026-04-27 上午',
+        isExpired: '否'
+      },
+      {
+        id: 2,
+        elderName: '张三',
+        gender: '女',
+        age: 43,
+        phone: '156****5555',
+        address: '河源市/源城区/城东街道aaa',
+        orderType: '坐席下单',
+        servicePersonName: '张磊',
+        orderTime: '2026-04-27 15:00:04',
+        visitTime: '2026-04-28 上午',
+        isExpired: '否'
+      },
+      {
+        id: 3,
+        elderName: '张三',
+        gender: '女',
+        age: 43,
+        phone: '156****5555',
+        address: '河源市/源城区/城东街道aaa',
+        orderType: '坐席下单',
+        servicePersonName: '张磊',
+        orderTime: '2026-04-27 15:00:04',
+        visitTime: '2026-04-29 上午',
+        isExpired: '否'
+    }
+    ]
+    total.value = list.value.length
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.elderName = ''
+  queryParams.servicePersonName = ''
+  queryParams.phone = ''
+  queryParams.status = ''
+  queryParams.orderType = ''
+  handleQuery()
+}
+
+/** 判断是否可编辑 */
+const isEditable = (status: string) => {
+  return status === '待派单' || status === '待接单'
+}
+
+/** 详情(只读) */
+const handleDetail = (row: any) => {
+  orderByElderRef.value.open(row, 'detail')
+}
+
+/** 编辑 */
+const handleEdit = (row: any) => {
+  orderByElderRef.value.open(row, 'edit')
+}
+
+/** 删除 */
+const handleDelete = async (row: any) => {
+  try {
+    await message.delConfirm()
+    await AppointmentApi.deleteAppointment(row.id)
+    message.success(t('common.delSuccess'))
+    getList()
+  } catch {}
+}
+
+/** 签到下拉菜单命令处理 */
+const handleSignInCommand = (command: string, row: any) => {
+  if (command === 'record') {
+    signInRecordRef.value.open(row)
+  } else if (command === 'quick') {
+    quickSignInRef.value.open(row)
+  }
+}
+
+/** 修改服务人员 */
+const changeServicePersonRef = ref()
+const handleChangeServicePerson = (row: any) => {
+  changeServicePersonRef.value.open(row)
+}
+
+/** 签到记录 */
+const signInRecordRef = ref()
+
+/** 一键签到 */
+const quickSignInRef = ref()
+
+/** 按长者预约 */
+const orderByElderRef = ref()
+const handleOrderByElder = () => {
+  orderByElderRef.value.open()
+}
+
+/** 按服务人员预约 */
+const orderByServicePersonRef = ref()
+const handleOrderByServicePerson = () => {
+  orderByServicePersonRef.value.open()
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped lang="scss">
+</style>

+ 225 - 0
src/views/living-home/visiting-service/consumer-list/Form.vue

@@ -0,0 +1,225 @@
+<template>
+  <Dialog
+    v-model="dialogVisible"
+    :title="itemTitle"
+    class="expense-allowance-form"
+    width="60%"
+    @close="handleClosed"
+  >
+    <el-form
+      class="-mb-15px"
+      :model="dataForm"
+      ref="formRef"
+      label-width="120px"
+      :rules="dataRule"
+      :toggleType="isDetail"
+    >
+      <el-row :gutter="20">
+        <el-col :span="12" :xs="24">
+          <el-form-item label="长者姓名" prop="elderId">
+            <el-select
+              v-model="dataForm.elderId"
+              placeholder="请输入长者姓名搜索"
+              class="!w-full"
+              filterable
+              remote
+              clearable
+              :disabled="isDetail"
+              :remote-method="searchElderRemote"
+              :loading="elderSearchLoading"
+              @change="handleElderChange"
+            >
+              <el-option
+                v-for="item in elderSearchOptions"
+                :key="item.id"
+                :label="`${item.elderName} ${item.phone || ''} ${item.currentLiveAddress || ''}`"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" :xs="24">
+          <el-form-item label="身份证号码" prop="idCard">
+            <TgInput v-model="dataForm.idCard" :toggleType="true" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" :xs="24">
+          <el-form-item label="消费券类型" prop="voucherType">
+            <el-select v-model="dataForm.voucherType" placeholder="请选择" class="!w-full">
+              <el-option label="按实际金额" value="fixed" />
+              <el-option label="按百分比" value="percentage" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" :xs="24">
+          <el-form-item :label="dataForm.voucherType === 'percentage' ? '百分比值' : '消费券金额'" prop="amount">
+            <TgInput v-model="dataForm.amount" :appendText="dataForm.voucherType === 'percentage' ? '%' : '¥'" input-type="number"/>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remarks">
+            <TgTextarea v-model="dataForm.remarks" show-word-limit :maxlength="255" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button @click="handleClosed">关闭</el-button>
+      <el-button v-loading="formLoading" type="primary" @click="submitForm" v-if="!isDetail"
+        >确定</el-button
+      >
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { FormRules } from 'element-plus'
+import {
+  addConsumerVouchers,
+  editConsumerVouchers,
+  getConsumerVouchersDetail
+} from "@/api/elderly/fee/consumpotion-coupon"
+import { getHomeElderlyList } from "@/api/living-home/elderly"
+defineOptions({ name: 'ConsumerVoucherForm' })
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const dialogVisible = ref(false) // 弹窗
+const formRef = ref() // 表单 Ref
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const isDetail = ref(false)
+const elderSearchLoading = ref(false) // 长者搜索加载状态
+const elderSearchOptions = ref<any[]>([]) // 长者搜索选项
+const state = reactive({
+  dataForm: {
+    // 表单字段
+    id: '',
+    elderId: '',
+    idCard: '',
+    voucherType: 'fixed',
+    amount: '',
+    remarks: '',
+    tenantId: undefined
+  },
+  dataRule: {
+    // 表单规则
+    elderId: [
+      {
+        required: true,
+        message: '姓名不能为空',
+        trigger: 'blur'
+      }
+    ],
+    voucherType: [
+      {
+        required: true,
+        message: '消费券类型不能为空',
+        trigger: ['blur', 'change']
+      }
+    ],
+    amount: [
+      {
+        required: true,
+        message: '不能为空',
+        trigger: ['blur']
+      }
+    ],
+
+  }
+})
+const { dataForm, dataRule } = toRefs(state)
+
+// 弹窗标题
+const itemTitle = computed(() => {
+  return isDetail.value ? '详情' : !dataForm.value.id ? '新增' : '修改'
+})
+
+/** 打开弹窗 */
+const open = async (tId, id, detail) => {
+  dialogVisible.value = true
+  isDetail.value = detail
+  dataForm.value.tenantId = tId
+  if (id) {
+    const res = await getConsumerVouchersDetail(id)
+    dataForm.value = res
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+// 远程搜索长者
+const searchElderRemote = async (query: string) => {
+  if (!query || query.trim() === '') {
+    elderSearchOptions.value = []
+    return
+  }
+  elderSearchLoading.value = true
+  try {
+    const params = {
+      pageNo: 1,
+      pageSize: 20,
+      elderName: query.trim()
+    }
+    const data = await getHomeElderlyList(params)
+    elderSearchOptions.value = data.list || []
+  } catch (error) {
+    console.log('搜索长者失败', error)
+    elderSearchOptions.value = []
+  } finally {
+    elderSearchLoading.value = false
+  }
+}
+
+// 长者选择变化
+const handleElderChange = (val: number) => {
+  const selectedElder = elderSearchOptions.value.find(item => item.id === val)
+  if (selectedElder) {
+    dataForm.value.idCard = selectedElder.idCard || ''
+  }
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  if(formLoading.value){
+    return
+  }
+  // 提交请求
+  formLoading.value = true
+  try {
+    // 校验表单
+    if (!formRef.value) return
+    const valid = await formRef.value.validate()
+    if (!valid) return
+    const res = dataForm.value.id
+      ? await editConsumerVouchers(dataForm.value)
+      : await addConsumerVouchers(dataForm.value)
+    if (res) {
+      message.success(t('common.updateSuccess'))
+      dialogVisible.value = false
+      // 发送操作成功的事件
+      emit('success')
+    }
+  } finally {
+    setTimeout(()=>{
+      formLoading.value = false
+    },500)
+  }
+}
+
+// 关闭表单
+const handleClosed = () => {
+  dataForm.value = {
+    // 表单字段
+    id: '',
+    elderId: '',
+    idCard: '',
+    voucherType: 'fixed',
+    amount: '',
+    remarks: '',
+    tenantId: undefined
+  }
+  formRef.value?.resetFields()
+  dialogVisible.value = false
+}
+
+
+</script>

+ 378 - 0
src/views/living-home/visiting-service/consumer-list/column.ts

@@ -0,0 +1,378 @@
+import { DICT_TYPE } from '@/utils/dict'
+// =================收费分类=====================
+export const ChargeCategoryColumns = reactive([
+  {
+    label: '所属机构',
+    field: 'tenantName',
+    type: '99'
+  },
+  {
+    label: '类别名称',
+    field: 'name'
+  },
+  {
+    label: '级别',
+    field: 'parentId',
+    dictArr: DICT_TYPE.LEVEL_ARR,
+    type: '1'
+  },
+  {
+    label: '状态',
+    field: 'status',
+    type: '2',
+    dictArr: DICT_TYPE.COMMON_STATUS2
+  },
+  {
+    label: '备注',
+    field: 'remark'
+  }
+])
+// =================收费项目=====================
+export const OverheadChargeColumns = reactive([
+  {
+    label: '所属机构',
+    field: 'tenantName',
+    type: '99'
+  },
+  {
+    label: '项目名称',
+    field: 'chargeName',
+    align: 'left'
+  },
+  {
+    label: '价格(元)',
+    field: 'price'
+  },
+  {
+    label: '单位',
+    field: 'chargeType',
+    type: '1',
+    dictArr: DICT_TYPE.FEE_CHARGE_TYPE
+  },
+  {
+    label: '状态',
+    field: 'status',
+    type: '2',
+    dictArr: DICT_TYPE.COMMON_STATUS2
+  },
+  {
+    label: '金蝶编码',
+    field: 'kingdeeCostid',
+  },
+])
+
+// ======================账单缴费=====================
+export const BillPayColumns = reactive([
+  {
+    label: '所属机构',
+    field: 'tenantName',
+    type: '99',
+    fixed: true,
+    width: 180
+  },
+  {
+    label: '长者姓名',
+    field: 'elderName',
+    width: 120,
+    fixed: true
+  },
+  {
+    label: '床位号',
+    field: 'bedName',
+    width: 180,
+    fixed: true
+  },
+  {
+    label: '账单月',
+    field: 'billingMonth',
+    width: 120,
+    fixed: true
+  },
+  // {
+  //   label: '推送状态',
+  //   field: 'pushStatus',
+  //   dictArr: DICT_TYPE.PUSH_STATUS,
+  //   type: '1',
+  //   width: 120
+  // },
+  {
+    label: '缴费状态',
+    field: 'payStatus',
+    dictArr: DICT_TYPE.PAY_TYPE,
+    type: '2',
+    width: 100
+  },
+  {
+    label: '缴费时间',
+    field: 'payTime',
+    type: '9',
+    format: true,
+    width: 180
+  },
+  {
+    label: '出纳',
+    field: 'payeeName',
+    width: 120
+  },
+  // {
+  //   label: '已缴费用',
+  //   field: 'actualAmount',
+  //   type: '14',
+  //   width: 120
+  // },
+  {
+    label: '本月账单应收',
+    field: 'actualAmount',
+    type: '14',
+    width: 120
+  },
+  {
+    label: '床位费',
+    field: 'bedAmount',
+    type: '15',
+    width: 120
+  },
+  {
+    label: '护理费',
+    field: 'nurseAmount',
+    type: '15',
+    width: 120
+  },
+  {
+    label: '餐饮费',
+    field: 'mealAmount',
+    type: '15',
+    width: 120
+  },
+  {
+    label: '其他',
+    field: 'otherAmount',
+    otherField: 'otherRoundAmount',
+    type: '13',
+    width: 120
+  },
+  {
+    label: '医保',
+    field: 'insuranceAmount',
+    type: '3',
+    width: 120
+  },
+  {
+    label: '已缴费用',
+    field: 'payableAmount',
+    type: '17',
+    width: 120
+  }
+])
+
+// ======================日常费用=====================
+export const DailyFeeColumns = reactive([
+  {
+    label: '所属机构',
+    field: 'tenantName',
+    type: '99',
+  },
+  {
+    label: '长者姓名',
+    field: 'elderName',
+    width: 120
+  },
+  {
+    label: '床位号',
+    field: 'bedName',
+    width: 180
+  },
+  {
+    label: '账单归属月',
+    field: 'attributionBillTime',
+  },
+  {
+    label: '费用来源',
+    field: 'type',
+    type: '1',
+    dictArr: DICT_TYPE.COST_FROM_TYPE,
+  },
+  {
+    label: '收费项目',
+    field: 'itemName',
+    width: 180
+  },
+  {
+    label: '费用(元)',
+    field: 'amount',
+    type: '3'
+  },
+  {
+    label: '缴费状态',
+    field: 'status',
+    dictArr: DICT_TYPE.PAYMENT_STATUS,
+    type: '1'
+  },
+  {
+    label: '费用产生时间',
+    field: 'createdTime',
+    format: true,
+    type: '9',
+    width: 180
+  },
+  {
+    label: '缴费单号',
+    field: 'expenseBillCode'
+  },
+  {
+    label: '记账人',
+    field: 'createdBy'
+  },
+  {
+    label: '备注',
+    field: 'remarks'
+  }
+])
+
+// ==============入住押金====================
+export const depositColumn = [
+  {
+    label: '所属机构',
+    field: 'tenantName',
+    type: '99',
+  },
+  {
+    label: '长者姓名',
+    field: 'elderName'
+  },
+  {
+    label: '床位号',
+    field: 'bedName'
+  },
+  {
+    label: '长者状态',
+    field: 'inStatus',
+    type: '1',
+    dictArr: DICT_TYPE.IN_STATUS_ARR
+  },
+  {
+    label: '押金金额(元)',
+    field: 'amount',
+    type: '3'
+  }
+]
+
+// ==============退住结算====================
+export const levelSettleColumn = [
+  {
+    label: '所属机构',
+    field: 'tenantName',
+    type: '99',
+  },
+  {
+    label: '长者姓名',
+    field: 'elderName'
+  },
+  {
+    label: '床位号',
+    field: 'bedName'
+  },
+  {
+    label: '结算状态',
+    field: 'status',
+    dictArr: DICT_TYPE.SETTLEMENT_STATUS,
+    type: '1'
+  },
+  {
+    label: '结算时间',
+    field: 'settlementTime',
+    format: true
+  },
+  {
+    label: '结算人',
+    field: 'settlementPersonName'
+  }
+]
+
+// =================长护险===================
+export const expenseAllowanceColumn = [
+  {
+    label: '所属机构',
+    field: 'tenantName',
+    type: '99',
+  },
+  {
+    label: '长者姓名',
+    field: 'elderName'
+  },
+  {
+    label: '账单归属月',
+    field: 'billingMonth'
+  },
+  {
+    label: '消费券金额',
+    field: 'amount',
+    type: '3'
+  },
+  {
+    label: '缴费单号',
+    field: 'orderNumber'
+  },
+  {
+    label: '备注',
+    field: 'remarks'
+  }
+]
+
+// ==================余额预存===================
+export const balanceColumn = [
+  {
+    label: '长者姓名',
+    field: 'elderName'
+  },
+  {
+    label: '床位号',
+    field: 'bedName'
+  },
+  {
+    label: '入住状态',
+    field: 'inStatus',
+    dictArr: DICT_TYPE.IN_STATUS_ARR,
+    type: '1'
+  },
+  {
+    label: '预存金额(元)',
+    field: 'amount',
+    type: '3'
+  }
+]
+
+// ===================外出费用配置=========================
+export const outRefundColumn = [
+  {
+    label: '所属机构',
+    field: 'tenantName',
+    type: '99'
+  },
+  {
+    label: '类型名称',
+    field: 'name'
+  },
+  {
+    label: '是否退费',
+    field: 'isRefund',
+    dictArr: DICT_TYPE.COMMON_STATUS6,
+    type: '1'
+  },
+  {
+    label: '返院当日退费',
+    field: 'isSameDayRefund',
+    dictArr: DICT_TYPE.REFUND_TERMS_TYPE,
+    type: '1'
+  },
+  {
+    label: '状态',
+    field: 'status',
+    dictArr: DICT_TYPE.COMMON_STATUS2,
+    type: '1'
+  },
+  {
+    label: '备注',
+    field: 'remarks'
+  }
+]

+ 202 - 0
src/views/living-home/visiting-service/consumer-list/index.vue

@@ -0,0 +1,202 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="长者姓名:" prop="elderName">
+        <el-input
+          v-model="queryParams.elderName"
+          placeholder="请输入长者姓名"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-180px"
+        />
+      </el-form-item>
+      <el-form-item label="缴费单号:" prop="paymentOrderNo">
+        <el-input
+          v-model="queryParams.paymentOrderNo"
+          placeholder="请输入缴费单号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-180px"
+        />
+      </el-form-item>
+      <el-form-item label="使用状态:" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择"
+          clearable
+          class="!w-180px"
+        >
+          <el-option label="全部" value="" />
+          <el-option label="未使用" value="未使用" />
+          <el-option label="已使用" value="已使用" />
+          <el-option label="已过期" value="已过期" />
+        </el-select>
+      </el-form-item>
+      <el-form-item class="!ml-auto">
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> 搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" /> 重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <div class="flex items-center justify-between mb-15px">
+      <span class="font-bold text-base">消费券列表</span>
+      <div class="flex gap-10px">
+        <el-button type="primary" @click="handleAdd">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button type="success" @click="handleImport">
+          <Icon icon="ep:upload" class="mr-5px" /> 导入
+        </el-button>
+      </div>
+    </div>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      row-key="id"
+    >
+      <el-table-column label="编号" type="index" width="80" align="center" />
+      <el-table-column prop="elderName" label="长者姓名" align="center" min-width="120" />
+      <el-table-column prop="amount" label="消费券金额" align="center" min-width="120">
+        <template #default="scope">
+          <span>¥{{ scope.row.amount }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="paymentOrderNo" label="缴费单号" align="center" min-width="150" />
+      <el-table-column prop="remarks" label="备注" align="center" min-width="200" show-overflow-tooltip />
+      <el-table-column label="操作" align="center" width="200" fixed="right">
+        <template #default="scope">
+          <el-button link type="primary" @click="handleDetail(scope.row)">
+            <Icon icon="ep:view" class="mr-5px" /> 详情
+          </el-button>
+          <el-button link type="warning" @click="handleEdit(scope.row)">
+            <Icon icon="ep:edit" class="mr-5px" /> 编辑
+          </el-button>
+          <el-button link type="danger" @click="handleDelete(scope.row)">
+            <Icon icon="ep:delete" class="mr-5px" /> 删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <Pagination
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      :total="total"
+      @pagination="getList"
+      class="mt-15px"
+    />
+  </ContentWrap>
+
+  <!-- 新增/编辑/详情弹窗 -->
+  <Form ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import * as ConsumerVouchersApi from '@/api/elderly/fee/consumpotion-coupon'
+import Form from './Form.vue'
+
+defineOptions({ name: 'ConsumerList' })
+
+const message = useMessage()
+const { t } = useI18n()
+
+const loading = ref(false)
+const list = ref<any[]>([])
+const total = ref(0)
+const queryFormRef = ref()
+const formRef = ref()
+
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  elderName: '',
+  paymentOrderNo: '',
+  status: ''
+})
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ConsumerVouchersApi.getConsumerVouchersPage(queryParams)
+    list.value = data.list || []
+    total.value = data.total || 0
+  } catch (error) {
+    console.log('获取消费券列表失败', error)
+    // 使用模拟数据
+    list.value = []
+    total.value = 0
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.elderName = ''
+  queryParams.paymentOrderNo = ''
+  queryParams.status = ''
+  handleQuery()
+}
+
+/** 新增 */
+const handleAdd = () => {
+  formRef.value?.open(undefined, undefined, false)
+}
+
+/** 导入 */
+const handleImport = () => {
+  message.info('导入功能开发中...')
+}
+
+/** 详情 */
+const handleDetail = (row: any) => {
+  formRef.value?.open(undefined, row.id, true)
+}
+
+/** 编辑 */
+const handleEdit = (row: any) => {
+  formRef.value?.open(undefined, row.id, false)
+}
+
+/** 删除 */
+const handleDelete = async (row: any) => {
+  try {
+    await message.delConfirm()
+    await ConsumerVouchersApi.consumerVouchersDelete(row.id)
+    message.success(t('common.delSuccess'))
+    getList()
+  } catch {}
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped lang="scss">
+</style>