|
|
@@ -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">
|
|
|
+ 添加项目 >
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" size="small" @click="addToLeft" :disabled="rightSelected.length === 0">
|
|
|
+ < 移除项目
|
|
|
+ </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>
|