|
|
@@ -0,0 +1,906 @@
|
|
|
+<template>
|
|
|
+ <Dialog v-model="dialogVisible" scroll :title="dialogTitle" width="66vw">
|
|
|
+ <el-form
|
|
|
+ ref="formRef"
|
|
|
+ :model="formData"
|
|
|
+ :rules="formRules"
|
|
|
+ label-width="110px"
|
|
|
+ >
|
|
|
+ <!-- 长者信息选择 -->
|
|
|
+ <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-row :gutter="20" v-if="selectedElder">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="姓名">
|
|
|
+ <el-input v-model="selectedElder.elderName" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="性别">
|
|
|
+ <el-input :value="selectedElder.sex === '1' ? '男' : '女'" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="年龄">
|
|
|
+ <el-input :value="selectedElder.age || '-'" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-row :gutter="20" v-if="selectedElder">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="手机号码">
|
|
|
+ <el-input v-model="selectedElder.phone" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="居住地址">
|
|
|
+ <el-input v-model="selectedElder.currentLiveAddress" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 工单信息 -->
|
|
|
+ <div class="section-title">工单信息</div>
|
|
|
+
|
|
|
+ <!-- 服务项目选择 -->
|
|
|
+ <el-form-item label="服务项目" prop="serviceItems">
|
|
|
+ <!-- 搜索框 -->
|
|
|
+ <el-input
|
|
|
+ v-model="serviceItemSearchKeyword"
|
|
|
+ placeholder="请输入项目名称搜索"
|
|
|
+ clearable
|
|
|
+ :disabled="isDetailMode"
|
|
|
+ @input="handleServiceItemSearch"
|
|
|
+ >
|
|
|
+ <template #prefix>
|
|
|
+ <Icon icon="ep:search" />
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 已选择展示 -->
|
|
|
+ <el-form-item v-if="selectedServiceItems.length > 0">
|
|
|
+ <div class="selected-info">
|
|
|
+ <span class="selected-label">已选:</span>
|
|
|
+ <span class="selected-names">{{ selectedServiceItemNames }}</span>
|
|
|
+ <span class="selected-count">({{ selectedServiceItems.length }}项,合计¥{{ totalServiceAmount }})</span>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 服务项目卡片列表 -->
|
|
|
+ <el-form-item>
|
|
|
+ <div class="service-item-list" :class="{ 'detail-mode': isDetailMode }">
|
|
|
+ <div
|
|
|
+ v-for="item in displayedServiceItemList"
|
|
|
+ :key="item.id"
|
|
|
+ class="service-item-card"
|
|
|
+ :class="{ selected: selectedServiceItems.some(s => s.id === item.id) }"
|
|
|
+ @click="!isDetailMode && toggleServiceItem(item)"
|
|
|
+ >
|
|
|
+ <div class="card-header">
|
|
|
+ <el-checkbox
|
|
|
+ :model-value="selectedServiceItems.some(s => s.id === item.id)"
|
|
|
+ @click.stop
|
|
|
+ @change="!isDetailMode && toggleServiceItem(item)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="item-name" :title="item.itemName">{{ item.itemName }}</div>
|
|
|
+ <div class="item-amount">¥{{ item.amount }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="displayedServiceItemList.length === 0" class="empty-text">
|
|
|
+ 暂无匹配的服务项目
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="下单方式" prop="orderType">
|
|
|
+ <el-select
|
|
|
+ v-model="formData.orderType"
|
|
|
+ placeholder="请选择下单方式"
|
|
|
+ class="w-full"
|
|
|
+ :disabled="isDetailMode"
|
|
|
+ >
|
|
|
+ <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 label="服务人员" prop="servicePersonId">
|
|
|
+ <el-select
|
|
|
+ v-model="formData.servicePersonId"
|
|
|
+ placeholder="请选择服务人员"
|
|
|
+ clearable
|
|
|
+ class="w-full"
|
|
|
+ filterable
|
|
|
+ :disabled="isDetailMode"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in servicePersonList"
|
|
|
+ :key="item.id"
|
|
|
+ :label="`${item.name} ${item.phone ? '(' + item.phone + ')' : ''}`"
|
|
|
+ :value="item.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="下单时间" prop="orderTime">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="formData.orderTime"
|
|
|
+ type="datetime"
|
|
|
+ placeholder="选择下单时间"
|
|
|
+ class="w-full"
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ :disabled="isDetailMode"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="上门开始时间" prop="visitStartTime">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="formData.visitStartTime"
|
|
|
+ type="datetime"
|
|
|
+ placeholder="选择上门开始时间"
|
|
|
+ class="w-full"
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ :disabled="isDetailMode"
|
|
|
+ @change="handleVisitStartTimeChange"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="上门结束时间" prop="visitEndTime">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="formData.visitEndTime"
|
|
|
+ type="datetime"
|
|
|
+ placeholder="选择上门结束时间"
|
|
|
+ class="w-full"
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ :disabled="isDetailMode"
|
|
|
+ @change="handleVisitEndTimeChange"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-form-item label="服务时长" prop="serviceDuration">
|
|
|
+ <el-row :gutter="10" class="w-full">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-input-number
|
|
|
+ v-model="formData.serviceDuration"
|
|
|
+ :min="0"
|
|
|
+ :step="15"
|
|
|
+ placeholder="请输入服务时长(分钟)"
|
|
|
+ class="w-full"
|
|
|
+ :disabled="isDetailMode"
|
|
|
+ @change="handleDurationChange"
|
|
|
+ />
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12" class="flex items-center">
|
|
|
+ <span class="duration-display">{{ formattedDuration }}</span>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="工单备注" prop="remark">
|
|
|
+ <el-input
|
|
|
+ v-model="formData.remark"
|
|
|
+ type="textarea"
|
|
|
+ :rows="3"
|
|
|
+ placeholder="请输入工单备注"
|
|
|
+ maxlength="200"
|
|
|
+ show-word-limit
|
|
|
+ :disabled="isDetailMode"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 金额信息 -->
|
|
|
+ <div class="section-title">金额信息</div>
|
|
|
+
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="项目金额" prop="projectAmount">
|
|
|
+ <el-input-number
|
|
|
+ v-model="formData.projectAmount"
|
|
|
+ :min="0"
|
|
|
+ :precision="2"
|
|
|
+ placeholder="请输入项目金额"
|
|
|
+ class="w-full"
|
|
|
+ :disabled="isDetailMode"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="补贴金额" prop="subsidyAmount">
|
|
|
+ <el-input-number
|
|
|
+ v-model="formData.subsidyAmount"
|
|
|
+ :min="0"
|
|
|
+ :precision="2"
|
|
|
+ placeholder="补贴金额"
|
|
|
+ class="w-full"
|
|
|
+ disabled
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-form-item label="自费金额" prop="selfPayAmount">
|
|
|
+ <el-input-number
|
|
|
+ v-model="formData.selfPayAmount"
|
|
|
+ :min="0"
|
|
|
+ :precision="2"
|
|
|
+ placeholder="自费金额"
|
|
|
+ class="w-full"
|
|
|
+ disabled
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-form-item label="补贴到账日期" prop="subsidyArrivalDate">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="formData.subsidyArrivalDate"
|
|
|
+ type="date"
|
|
|
+ placeholder="选择补贴到账日期"
|
|
|
+ class="w-full"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ :disabled="isDetailMode"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 详情模式下显示额外信息 -->
|
|
|
+ <template v-if="isDetailMode && detailData">
|
|
|
+ <div class="section-title">其他信息</div>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="下单时间">
|
|
|
+ <el-input v-model="detailData.orderTime" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="是否过期">
|
|
|
+ <el-input :value="detailData.isExpired === '是' ? '是' : '否'" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="工单状态">
|
|
|
+ <el-input v-model="detailData.status" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </template>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- 后端提交参数说明 -->
|
|
|
+ <el-divider content-position="left">后端接口参数说明</el-divider>
|
|
|
+ <el-descriptions :column="2" border size="small" class="field-description">
|
|
|
+ <el-descriptions-item label="elderId" :span="1">长者ID(number,必填)</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="elderName" :span="1">长者姓名(string,必填)</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="gender" :span="1">性别(string,男/女)</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="age" :span="1">年龄(number)</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="phone" :span="1">手机号码(string)</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="address" :span="1">居住地址(string)</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="orderType" :span="1">下单方式(string,必填)</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="servicePersonId" :span="1">服务人员ID(number,必填)</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="servicePersonName" :span="1">服务人员姓名(string,必填)</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="orderTime" :span="1">下单时间(string,YYYY-MM-DD HH:mm:ss)</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="visitStartTime" :span="1">上门开始时间(string,YYYY-MM-DD HH:mm:ss)</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="visitEndTime" :span="1">上门结束时间(string,YYYY-MM-DD HH:mm:ss)</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="serviceDuration" :span="1">服务时长(number,分钟)</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="projectAmount" :span="1">项目金额(number,元)</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="subsidyAmount" :span="1">补贴金额(number,元,默认为项目金额的50%,上限800元)</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="selfPayAmount" :span="1">自费金额(number,元,项目金额-补贴金额)</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="subsidyArrivalDate" :span="2">补贴到账日期(string,YYYY-MM-DD,非必填)</el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="serviceItems" :span="2">
|
|
|
+ <div>服务项目列表(array,必填)</div>
|
|
|
+ <div style="margin-top: 5px; color: #606266; font-size: 12px;">
|
|
|
+ 每项包含:id(项目ID)、itemName(项目名称)、amount(金额)
|
|
|
+ </div>
|
|
|
+ </el-descriptions-item>
|
|
|
+
|
|
|
+ <el-descriptions-item label="remark" :span="2">工单备注(string)</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 ElderApi from '@/api/living-home/elderly'
|
|
|
+
|
|
|
+const emit = defineEmits(['success'])
|
|
|
+
|
|
|
+const dialogVisible = ref(false)
|
|
|
+const dialogMode = ref<'create' | 'edit' | 'detail'>('create')
|
|
|
+const submitLoading = ref(false)
|
|
|
+const formRef = ref()
|
|
|
+const servicePersonList = ref<any[]>([])
|
|
|
+const serviceItemList = ref<any[]>([])
|
|
|
+const selectedServiceItems = ref<any[]>([])
|
|
|
+const serviceItemSearchKeyword = ref('')
|
|
|
+const detailData = ref<any>({})
|
|
|
+
|
|
|
+// 长者搜索相关
|
|
|
+const elderSearchLoading = ref(false)
|
|
|
+const elderSearchOptions = ref<any[]>([])
|
|
|
+const selectedElder = ref<any>(null)
|
|
|
+
|
|
|
+const formData = reactive({
|
|
|
+ id: undefined as number | undefined,
|
|
|
+ elderId: undefined as number | undefined,
|
|
|
+ elderName: '',
|
|
|
+ gender: '',
|
|
|
+ age: undefined as number | undefined,
|
|
|
+ phone: '',
|
|
|
+ address: '',
|
|
|
+ orderType: '电话咨询', // 默认使用电话咨询
|
|
|
+ servicePersonId: undefined as number | undefined,
|
|
|
+ servicePersonName: '',
|
|
|
+ serviceItemIds: [] as number[], // 服务项目ID
|
|
|
+ orderTime: '', // 下单时间
|
|
|
+ visitStartTime: '', // 上门开始时间
|
|
|
+ visitEndTime: '', // 上门结束时间
|
|
|
+ serviceDuration: undefined as number | undefined, // 服务时长(分钟)
|
|
|
+ serviceItems: [] as number[], // 服务项目ID列表
|
|
|
+ projectAmount: 0, // 项目金额,默认为0
|
|
|
+ subsidyAmount: 0, // 补贴金额,默认为0
|
|
|
+ selfPayAmount: 0, // 自费金额,默认为0
|
|
|
+ subsidyArrivalDate: '', // 补贴到账日期
|
|
|
+ remark: ''
|
|
|
+})
|
|
|
+
|
|
|
+const formRules = {
|
|
|
+ elderId: [{ required: true, message: '请选择长者', trigger: 'change' }],
|
|
|
+ serviceItems: [{ required: true, message: '请选择服务项目', trigger: 'change', type: 'array' }],
|
|
|
+ orderType: [{ required: true, message: '请选择下单方式', trigger: 'change' }],
|
|
|
+ servicePersonId: [{ required: true, message: '请选择服务人员', trigger: 'change' }],
|
|
|
+ orderTime: [{ required: true, message: '请选择下单时间', trigger: 'change' }],
|
|
|
+ visitStartTime: [{ required: true, message: '请选择上门开始时间', trigger: 'change' }],
|
|
|
+ visitEndTime: [{ required: true, message: '请选择上门结束时间', trigger: 'change' }],
|
|
|
+ serviceDuration: [{ required: true, message: '请输入服务时长', trigger: 'change' }],
|
|
|
+ projectAmount: [{ required: true, message: '请输入项目金额', trigger: 'change' }]
|
|
|
+}
|
|
|
+
|
|
|
+/** 是否为详情模式(只读) */
|
|
|
+const isDetailMode = computed(() => dialogMode.value === 'detail')
|
|
|
+
|
|
|
+/** 弹窗标题 */
|
|
|
+const dialogTitle = computed(() => {
|
|
|
+ const titles = { create: '新增预约', edit: '编辑预约', detail: '预约详情' }
|
|
|
+ return titles[dialogMode.value]
|
|
|
+})
|
|
|
+
|
|
|
+// 远程搜索长者
|
|
|
+const searchElderRemote = async (query: string) => {
|
|
|
+ if (query) {
|
|
|
+ elderSearchLoading.value = true
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 50,
|
|
|
+ elderName: query
|
|
|
+ }
|
|
|
+ const data = await ElderApi.getHomeElderlyList(params)
|
|
|
+ elderSearchOptions.value = data.list || []
|
|
|
+ } catch (error) {
|
|
|
+ console.log('搜索长者失败', error)
|
|
|
+ } finally {
|
|
|
+ elderSearchLoading.value = false
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ elderSearchOptions.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 长者选择变化
|
|
|
+const handleElderChange = (val: number) => {
|
|
|
+ if (val) {
|
|
|
+ const elder = elderSearchOptions.value.find(item => item.id === val)
|
|
|
+ if (elder) {
|
|
|
+ selectedElder.value = elder
|
|
|
+ formData.elderName = elder.elderName
|
|
|
+ formData.gender = elder.sex === '1' ? '男' : '女'
|
|
|
+ formData.age = elder.age
|
|
|
+ formData.phone = elder.phone
|
|
|
+ formData.address = elder.currentLiveAddress
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ selectedElder.value = null
|
|
|
+ formData.elderName = ''
|
|
|
+ formData.gender = ''
|
|
|
+ formData.age = undefined
|
|
|
+ formData.phone = ''
|
|
|
+ formData.address = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化服务时长显示
|
|
|
+const formattedDuration = computed(() => {
|
|
|
+ const minutes = formData.serviceDuration
|
|
|
+ if (!minutes || minutes <= 0) return ''
|
|
|
+ const hours = Math.floor(minutes / 60)
|
|
|
+ const mins = minutes % 60
|
|
|
+ if (hours > 0 && mins > 0) {
|
|
|
+ return `${hours}小时${mins}分钟`
|
|
|
+ } else if (hours > 0) {
|
|
|
+ return `${hours}小时`
|
|
|
+ } else {
|
|
|
+ return `${mins}分钟`
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 计算时间差(分钟)
|
|
|
+const calculateDuration = (startTime: string, endTime: string): number => {
|
|
|
+ if (!startTime || !endTime) return 0
|
|
|
+ const start = new Date(startTime).getTime()
|
|
|
+ const end = new Date(endTime).getTime()
|
|
|
+ if (isNaN(start) || isNaN(end) || end <= start) return 0
|
|
|
+ return Math.round((end - start) / (1000 * 60))
|
|
|
+}
|
|
|
+
|
|
|
+// 计算结束时间
|
|
|
+const calculateEndTime = (startTime: string, durationMinutes: number): string => {
|
|
|
+ if (!startTime || !durationMinutes || durationMinutes <= 0) return ''
|
|
|
+ const start = new Date(startTime)
|
|
|
+ if (isNaN(start.getTime())) return ''
|
|
|
+ const end = new Date(start.getTime() + durationMinutes * 60 * 1000)
|
|
|
+ const year = end.getFullYear()
|
|
|
+ const month = String(end.getMonth() + 1).padStart(2, '0')
|
|
|
+ const day = String(end.getDate()).padStart(2, '0')
|
|
|
+ const hours = String(end.getHours()).padStart(2, '0')
|
|
|
+ const minutes = String(end.getMinutes()).padStart(2, '0')
|
|
|
+ const seconds = String(end.getSeconds()).padStart(2, '0')
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
|
|
+}
|
|
|
+
|
|
|
+// 上门开始时间变化
|
|
|
+const handleVisitStartTimeChange = () => {
|
|
|
+ if (formData.visitStartTime && formData.serviceDuration) {
|
|
|
+ formData.visitEndTime = calculateEndTime(formData.visitStartTime, formData.serviceDuration)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 上门结束时间变化
|
|
|
+const handleVisitEndTimeChange = () => {
|
|
|
+ if (formData.visitStartTime && formData.visitEndTime) {
|
|
|
+ const duration = calculateDuration(formData.visitStartTime, formData.visitEndTime)
|
|
|
+ if (duration > 0) {
|
|
|
+ formData.serviceDuration = duration
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 服务时长变化
|
|
|
+const handleDurationChange = () => {
|
|
|
+ if (formData.visitStartTime && formData.serviceDuration) {
|
|
|
+ formData.visitEndTime = calculateEndTime(formData.visitStartTime, formData.serviceDuration)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 补贴金额上限
|
|
|
+const SUBSIDY_MAX_AMOUNT = 800
|
|
|
+
|
|
|
+// 监听项目金额变化,自动计算补贴金额和自费金额
|
|
|
+watch(() => formData.projectAmount, (newVal) => {
|
|
|
+ const projectAmount = newVal || 0
|
|
|
+ // 补贴金额默认为项目金额的50%,但不超过上限
|
|
|
+ let subsidyAmount = projectAmount * 0.5
|
|
|
+ if (subsidyAmount > SUBSIDY_MAX_AMOUNT) {
|
|
|
+ subsidyAmount = SUBSIDY_MAX_AMOUNT
|
|
|
+ }
|
|
|
+ formData.subsidyAmount = Math.round(subsidyAmount * 100) / 100
|
|
|
+ // 自费金额 = 项目金额 - 补贴金额
|
|
|
+ formData.selfPayAmount = Math.round((projectAmount - formData.subsidyAmount) * 100) / 100
|
|
|
+}, { immediate: true })
|
|
|
+
|
|
|
+// 获取服务人员列表
|
|
|
+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 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 filteredServiceItemList = computed(() => {
|
|
|
+ if (!serviceItemSearchKeyword.value) {
|
|
|
+ return serviceItemList.value
|
|
|
+ }
|
|
|
+ const keyword = serviceItemSearchKeyword.value.toLowerCase()
|
|
|
+ return serviceItemList.value.filter(item =>
|
|
|
+ item.itemName?.toLowerCase().includes(keyword)
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+// 展示的服务项目列表(最多10条)
|
|
|
+const displayedServiceItemList = computed(() => {
|
|
|
+ return filteredServiceItemList.value.slice(0, 10)
|
|
|
+})
|
|
|
+
|
|
|
+// 已选择项目名称(用于显示)
|
|
|
+const selectedServiceItemNames = computed(() => {
|
|
|
+ return selectedServiceItems.value.map(item => item.itemName).join('、')
|
|
|
+})
|
|
|
+
|
|
|
+// 服务项目搜索
|
|
|
+const handleServiceItemSearch = () => {
|
|
|
+ // 搜索时只过滤列表,不影响已选择的项目
|
|
|
+}
|
|
|
+
|
|
|
+// 服务项目总金额计算
|
|
|
+const totalServiceAmount = computed(() => {
|
|
|
+ return selectedServiceItems.value.reduce((sum, item) => sum + (item.amount || 0), 0)
|
|
|
+})
|
|
|
+
|
|
|
+// 切换服务项目选择
|
|
|
+const toggleServiceItem = (item: any) => {
|
|
|
+ const index = selectedServiceItems.value.findIndex(s => s.id === item.id)
|
|
|
+ if (index > -1) {
|
|
|
+ selectedServiceItems.value.splice(index, 1)
|
|
|
+ } else {
|
|
|
+ selectedServiceItems.value.push(item)
|
|
|
+ }
|
|
|
+ // 更新表单数据
|
|
|
+ formData.serviceItems = selectedServiceItems.value.map(s => s.id)
|
|
|
+ formData.serviceItemIds = formData.serviceItems
|
|
|
+ // 自动更新项目金额
|
|
|
+ formData.projectAmount = totalServiceAmount.value
|
|
|
+ // 更新补贴金额(等于项目金额)
|
|
|
+ formData.subsidyAmount = formData.projectAmount
|
|
|
+ formData.selfPayAmount = 0
|
|
|
+}
|
|
|
+
|
|
|
+// 打开弹窗
|
|
|
+const open = async (row?: any, mode: 'create' | 'edit' | 'detail' = 'create') => {
|
|
|
+ dialogMode.value = mode
|
|
|
+ dialogVisible.value = true
|
|
|
+ resetForm()
|
|
|
+
|
|
|
+ // 加载服务人员列表和服务项目列表
|
|
|
+ await getServicePersonList()
|
|
|
+ await getServiceItemList()
|
|
|
+
|
|
|
+ if (row && (mode === 'edit' || mode === 'detail')) {
|
|
|
+ // 回填数据
|
|
|
+ await fillFormData(row)
|
|
|
+ detailData.value = row
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 回填表单数据
|
|
|
+const fillFormData = async (row: any) => {
|
|
|
+ formData.id = row.id
|
|
|
+ formData.elderId = row.elderId
|
|
|
+ formData.elderName = row.elderName
|
|
|
+ formData.gender = row.gender
|
|
|
+ formData.age = row.age
|
|
|
+ formData.phone = row.phone
|
|
|
+ formData.address = row.address
|
|
|
+ formData.orderType = row.orderType
|
|
|
+ formData.servicePersonId = row.servicePersonId
|
|
|
+ formData.servicePersonName = row.servicePersonName
|
|
|
+ formData.orderTime = row.orderTime
|
|
|
+ formData.visitStartTime = row.visitStartTime || row.visitTime
|
|
|
+ formData.visitEndTime = row.visitEndTime
|
|
|
+ formData.serviceDuration = row.serviceDuration
|
|
|
+ formData.projectAmount = row.projectAmount || 0
|
|
|
+ formData.subsidyAmount = row.subsidyAmount || row.projectAmount || 0
|
|
|
+ formData.selfPayAmount = row.selfPayAmount || 0
|
|
|
+ formData.subsidyArrivalDate = row.subsidyArrivalDate || ''
|
|
|
+ formData.remark = row.remark || ''
|
|
|
+
|
|
|
+ // 回填服务项目
|
|
|
+ if (row.serviceItems && row.serviceItems.length > 0) {
|
|
|
+ const itemIds = row.serviceItems.map((item: any) => typeof item === 'object' ? item.id : item)
|
|
|
+ formData.serviceItems = itemIds
|
|
|
+ formData.serviceItemIds = itemIds
|
|
|
+ // 从列表中找到对应的服务项目
|
|
|
+ selectedServiceItems.value = serviceItemList.value.filter(item => itemIds.includes(item.id))
|
|
|
+ } else {
|
|
|
+ formData.serviceItems = []
|
|
|
+ formData.serviceItemIds = []
|
|
|
+ selectedServiceItems.value = []
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置选中的长者信息
|
|
|
+ selectedElder.value = {
|
|
|
+ id: row.elderId,
|
|
|
+ elderName: row.elderName,
|
|
|
+ sex: row.gender === '男' ? '1' : '2',
|
|
|
+ age: row.age,
|
|
|
+ phone: row.phone,
|
|
|
+ currentLiveAddress: row.address
|
|
|
+ }
|
|
|
+ elderSearchOptions.value = [selectedElder.value]
|
|
|
+}
|
|
|
+
|
|
|
+// 获取当前时间格式化
|
|
|
+const getCurrentDateTime = (): string => {
|
|
|
+ const now = new Date()
|
|
|
+ const year = now.getFullYear()
|
|
|
+ const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
|
+ const day = String(now.getDate()).padStart(2, '0')
|
|
|
+ const hours = String(now.getHours()).padStart(2, '0')
|
|
|
+ const minutes = String(now.getMinutes()).padStart(2, '0')
|
|
|
+ const seconds = String(now.getSeconds()).padStart(2, '0')
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
|
|
+}
|
|
|
+
|
|
|
+// 重置表单
|
|
|
+const resetForm = () => {
|
|
|
+ formData.id = undefined
|
|
|
+ formData.elderId = undefined
|
|
|
+ formData.elderName = ''
|
|
|
+ formData.gender = ''
|
|
|
+ formData.age = undefined
|
|
|
+ formData.phone = ''
|
|
|
+ formData.address = ''
|
|
|
+ formData.orderType = '电话咨询'
|
|
|
+ formData.servicePersonId = undefined
|
|
|
+ formData.servicePersonName = ''
|
|
|
+ formData.orderTime = getCurrentDateTime() // 默认当前时间
|
|
|
+ formData.visitStartTime = ''
|
|
|
+ formData.visitEndTime = ''
|
|
|
+ formData.serviceDuration = undefined
|
|
|
+ formData.serviceItems = []
|
|
|
+ formData.serviceItemIds = []
|
|
|
+ selectedServiceItems.value = []
|
|
|
+ serviceItemSearchKeyword.value = ''
|
|
|
+ formData.projectAmount = 0
|
|
|
+ formData.subsidyAmount = 0
|
|
|
+ formData.selfPayAmount = 0
|
|
|
+ formData.remark = ''
|
|
|
+
|
|
|
+ selectedElder.value = null
|
|
|
+ elderSearchOptions.value = []
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ formRef.value?.resetFields()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 提交表单
|
|
|
+const handleSubmit = async () => {
|
|
|
+ const valid = await formRef.value?.validate().catch(() => false)
|
|
|
+ if (!valid) return
|
|
|
+
|
|
|
+ submitLoading.value = true
|
|
|
+ try {
|
|
|
+ // 获取选中的服务人员名称
|
|
|
+ const servicePerson = servicePersonList.value.find(item => item.id === formData.servicePersonId)
|
|
|
+ const servicePersonName = servicePerson?.name || ''
|
|
|
+
|
|
|
+ const data = {
|
|
|
+ ...formData,
|
|
|
+ servicePersonName,
|
|
|
+ serviceItems: selectedServiceItems.value.map(item => ({
|
|
|
+ id: item.id,
|
|
|
+ itemName: item.itemName,
|
|
|
+ amount: item.amount
|
|
|
+ }))
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dialogMode.value === 'edit' && formData.id) {
|
|
|
+ await ElderApi.updateAppointment(data)
|
|
|
+ ElMessage.success('编辑成功')
|
|
|
+ } else {
|
|
|
+ await ElderApi.createAppointment(data)
|
|
|
+ ElMessage.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: #303133;
|
|
|
+ margin: 20px 0 15px 0;
|
|
|
+ padding-left: 10px;
|
|
|
+ border-left: 4px solid #409eff;
|
|
|
+
|
|
|
+ &:first-child {
|
|
|
+ margin-top: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.w-full {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.duration-display {
|
|
|
+ color: #606266;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-left: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.mt-10px {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.mt-15px {
|
|
|
+ margin-top: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.mr-5px {
|
|
|
+ margin-right: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+ margin: 20px 0 15px 0;
|
|
|
+ padding-left: 10px;
|
|
|
+ border-left: 4px solid #409eff;
|
|
|
+
|
|
|
+ &:first-child {
|
|
|
+ margin-top: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.selected-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 5px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 13px;
|
|
|
+
|
|
|
+ .selected-label {
|
|
|
+ color: #606266;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .selected-names {
|
|
|
+ color: #303133;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+
|
|
|
+ .selected-count {
|
|
|
+ color: #409eff;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.service-item-list {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: nowrap;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 10px 0;
|
|
|
+ overflow-x: auto;
|
|
|
+ overflow-y: hidden;
|
|
|
+
|
|
|
+ &.detail-mode {
|
|
|
+ .service-item-card {
|
|
|
+ cursor: default;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .empty-text {
|
|
|
+ width: 100%;
|
|
|
+ text-align: center;
|
|
|
+ color: #909399;
|
|
|
+ padding: 20px 0;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.service-item-card {
|
|
|
+ width: calc(20% - 8px);
|
|
|
+ min-width: 120px;
|
|
|
+ max-width: 150px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 8px 10px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ background-color: #fff;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+ box-shadow: 0 2px 8px 0 rgba(64, 158, 255, 0.15);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.selected {
|
|
|
+ border-color: #409eff;
|
|
|
+ background-color: #ecf5ff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-body {
|
|
|
+ text-align: center;
|
|
|
+
|
|
|
+ .item-name {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .item-amount {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #f56c6c;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|