| | |
| | | |
| | | 背景:该表原先每天自动生成,现已改为手工维护,此前没有维护界面。 |
| | | --> |
| | | <Dialog :title="dialogTitle" v-model="dialogVisible" width="1000px"> |
| | | <Dialog :title="dialogTitle" v-model="dialogVisible" width="65%" > |
| | | <!-- 顶部说明:这些字段的含义与"每日重置"的关系 --> |
| | | <el-alert type="info" :closable="false" class="mb-10px"> |
| | | <template #title> |
| | | <div class="text-13px leading-6"> |
| | | 正在维护检查项目:<b>{{ checkTypeName }}</b> |
| | | <span class="text-gray-500">(检查项目值 {{ checkTypeValue }},共 {{ list.length }} 个时段)</span> |
| | | <span class="text-gray-500">(检查项目值 {{ checkTypeValue }},共 {{ slotList.length }} 个时段)</span> |
| | | </div> |
| | | </template> |
| | | <div class="text-12px leading-5"> |
| | | · <b>起始序号</b>:每天自动重置时回到该值;<b>上限</b>:到达后该时段视为「已满」,患者签到会顺延到后续时段。<br /> |
| | | · <b>当前序号</b>:已发到几号,一般无需手工改(每日重置会自动回到"起始序号")。<br /> |
| | | · <b>序号上限必须大于起始序号</b>,否则该时段永远无法取号。<br /> |
| | | · 同一检查项目下<b>同一时段只能有一条规则</b>,重复添加会被拒绝。 |
| | | · <b>号段规则</b>:每个时段按顺序占用一段连续号 —— 正常预约号在前,预留号紧跟其后(号更大), |
| | | 下一段从上一段的最大号继续,因此<b>号随时段递增、段内连续</b>。<br /> |
| | | · <b>起始序号</b>:由系统自动计算(上一段的最大号),<b>不可手工修改</b>。<br /> |
| | | · <b>上限</b>:该号池「号满的最大序号」;<b>当前序号</b>:已发到几号(每日重置回起始序号)。<br /> |
| | | · <b>取号顺序</b>:普通患者 → 本时段正常预约号;满了 → 本时段<b>预留号</b>(给临时加号); |
| | | 再满 → <b>总预留池</b>(无预约时段的患者也直接走总预留池)。<b>总预留池不设上限,保证一定能签到。</b><br /> |
| | | · <b>预定额度</b> = 上限 − 起始序号(只读)。上限 = 起始序号 表示该号池无名额(允许保存,显示 0 并标红)。<br /> |
| | | · 上限 <b>不能小于</b>起始序号;同一检查项目下<b>同一时段只能有一条规则</b>。 |
| | | </div> |
| | | </el-alert> |
| | | |
| | |
| | | </span> |
| | | </div> |
| | | |
| | | <el-table v-loading="loading" :data="list" :stripe="true" border height="420px"> |
| | | <el-table-column label="时段" align="center" min-width="130px"> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="slotList" |
| | | :stripe="true" |
| | | border |
| | | class="qs-table" |
| | | height="calc(100vh - 300px)" |
| | | > |
| | | <!-- ============ ① 时段 ============ --> |
| | | <el-table-column label="时段" align="center" width="96"> |
| | | <template #default="scope"> |
| | | <b>{{ formatTimeSlot(scope.row.timeSlot) }}</b> |
| | | <div class="text-11px text-gray-400">{{ scope.row.timeSlot }}</div> |
| | | <b class="text-12px" :title="String(scope.row.timeSlot ?? '')"> |
| | | {{ formatTimeSlot(scope.row.timeSlot) }} |
| | | </b> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="普通-起始序号" align="center" min-width="120px"> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.queueSet" |
| | | :min="0" |
| | | :controls="false" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="markChanged(scope.row)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="普通-上限" align="center" min-width="110px"> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.queueFull" |
| | | :min="1" |
| | | :controls="false" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="markChanged(scope.row)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="普通-当前序号" align="center" min-width="115px"> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.queueNo" |
| | | :min="0" |
| | | :controls="false" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="markChanged(scope.row)" |
| | | /> |
| | | </template> |
| | | <!-- ============ ② 正常预约 ============ --> |
| | | <el-table-column label="正常预约" align="center"> |
| | | <el-table-column label="起始序号" align="center" width="90"> |
| | | <template #default="scope"> |
| | | <!-- 【本需求】起始序号 = 号段起点,由系统按顺序自动计算,不允许手工改 --> |
| | | <span class="qs-fixed" :title="'系统自动计算(上一段的最大号)'"> |
| | | {{ scope.row.queueSet ?? 0 }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="上限" align="center" width="80"> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.queueFull" |
| | | :min="0" |
| | | :controls="false" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="markChanged(scope.row)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="预定额度" align="center" width="90"> |
| | | <template #default="scope"> |
| | | <span |
| | | class="qs-quota" |
| | | :class="{ 'qs-quota--zero': quotaOf(scope.row.queueSet, scope.row.queueFull) <= 0 }" |
| | | :title="`上限 ${scope.row.queueFull ?? 0} − 起始 ${scope.row.queueSet ?? 0}`" |
| | | > |
| | | {{ quotaOf(scope.row.queueSet, scope.row.queueFull) }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="当前序号" align="center" width="90"> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.queueNo" |
| | | :min="0" |
| | | :controls="false" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="markChanged(scope.row)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" align="center" width="90"> |
| | | <template #default="scope"> |
| | | <el-tag v-if="isFull(scope.row)" type="danger" size="small">已满</el-tag> |
| | | <el-tag v-else type="success" size="small">可取号</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="VIP-起始序号" align="center" min-width="115px"> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.queueVipSet" |
| | | :min="0" |
| | | :controls="false" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="markChanged(scope.row)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="VIP-上限" align="center" min-width="105px"> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.queueVipFull" |
| | | :min="1" |
| | | :controls="false" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="markChanged(scope.row)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="VIP-当前序号" align="center" min-width="110px"> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.queueVipNo" |
| | | :min="0" |
| | | :controls="false" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="markChanged(scope.row)" |
| | | /> |
| | | </template> |
| | | <!-- ============ ③ 预留 ============ --> |
| | | <el-table-column label="预留" align="center"> |
| | | <el-table-column label="起始序号" align="center" width="90"> |
| | | <template #default="scope"> |
| | | <!-- 【本需求】预留号段起点,同样由系统自动计算 --> |
| | | <span class="qs-fixed" :title="'系统自动计算(正常预约的上限)'"> |
| | | {{ scope.row.queueVipSet ?? 0 }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="上限" align="center" width="80"> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.queueVipFull" |
| | | :min="0" |
| | | :controls="false" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="markChanged(scope.row)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="预定额度" align="center" width="90"> |
| | | <template #default="scope"> |
| | | <span |
| | | class="qs-quota" |
| | | :class="{ 'qs-quota--zero': quotaOf(scope.row.queueVipSet, scope.row.queueVipFull) <= 0 }" |
| | | :title="`上限 ${scope.row.queueVipFull ?? 0} − 起始 ${scope.row.queueVipSet ?? 0}`" |
| | | > |
| | | {{ quotaOf(scope.row.queueVipSet, scope.row.queueVipFull) }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="当前序号" align="center" width="90"> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.queueVipNo" |
| | | :min="0" |
| | | :controls="false" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="markChanged(scope.row)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" align="center" width="90"> |
| | | <template #default="scope"> |
| | | <el-tag v-if="isFullVip(scope.row)" type="danger" size="small">已满</el-tag> |
| | | <el-tag v-else type="success" size="small">可取号</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="状态" align="center" width="90px"> |
| | | <template #default="scope"> |
| | | <el-tag v-if="isFull(scope.row)" type="danger" size="small">已满</el-tag> |
| | | <el-tag v-else type="success" size="small">可取号</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="操作" align="center" width="160px" fixed="right"> |
| | | <!-- ============ ④ 操作 ============ --> |
| | | <el-table-column label="操作" align="center" width="160" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | :loading="savingId === scope.row.id" |
| | | |
| | | @click="handleRowSave(scope.row)" |
| | | > |
| | | 保存 |
| | | </el-button> |
| | | <el-button link type="primary" @click="openForm('update', scope.row)">编辑</el-button> |
| | | <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button> |
| | | <el-button link type="primary" @click="openForm('update', scope.row)"> |
| | | 编辑 |
| | | </el-button> |
| | | <el-button link type="danger" @click="handleDelete(scope.row)"> |
| | | 删除 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- |
| | | 【本需求】总预留池(time_slot = 0) |
| | | · 本段普通号 + 本段预留号都发完,或患者没有预约时段时,从总预留池取号; |
| | | · 号段放在当天所有时段之后(号最大 ⇒ 排全天最后),且**不设上限**(永远不会满)。 |
| | | --> |
| | | <el-alert type="warning" :closable="false" class="mt-10px"> |
| | | <template #title> |
| | | <div class="text-13px"> |
| | | 总预留池(无时段 / 所有号源用尽时的兜底): |
| | | <template v-if="totalPool"> |
| | | 起始 <b>{{ totalPool.queueVipSet ?? 0 }}</b> · |
| | | 已发 <b>{{ totalPool.queueVipNo ?? 0 }}</b> · |
| | | 上限 <b>{{ totalPool.queueVipFull ?? 0 }}</b> |
| | | <span class="text-gray-500">(不设上限,满后自动继续发号,保证一定能签到)</span> |
| | | </template> |
| | | <template v-else> |
| | | <span class="text-gray-500">尚未初始化(首次签到时系统会自动创建,号段接在当天最后一段之后)</span> |
| | | </template> |
| | | </div> |
| | | </template> |
| | | </el-alert> |
| | | |
| | | <!-- 新增 / 编辑表单 --> |
| | | <Dialog v-model="formVisible" :title="formTitle" width="520px" append-to-body> |
| | |
| | | 选项来自字典 <code>ecg_book_timeslot</code>;也支持直接输入起始/结束时间后回车新建。 |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="普通-起始序号" prop="queueSet"> |
| | | <el-input-number v-model="formData.queueSet" :min="0" class="w-full" /> |
| | | </el-form-item> |
| | | <el-form-item label="普通-上限" prop="queueFull"> |
| | | <el-input-number v-model="formData.queueFull" :min="1" class="w-full" /> |
| | | </el-form-item> |
| | | <el-form-item label="VIP-起始序号" prop="queueVipSet"> |
| | | <el-input-number v-model="formData.queueVipSet" :min="0" class="w-full" /> |
| | | </el-form-item> |
| | | <el-form-item label="VIP-上限" prop="queueVipFull"> |
| | | <el-input-number v-model="formData.queueVipFull" :min="1" class="w-full" /> |
| | | </el-form-item> |
| | | <el-form-item label="普通-当前序号"> |
| | | <el-input-number v-model="formData.queueNo" :min="0" class="w-full" /> |
| | | <el-form-item label="正常预约-起始序号" prop="queueSet"> |
| | | <!-- 【本需求】起始序号 = 号段起点,由系统按「检查类型 → 时段顺序」自动计算,不可手工改 --> |
| | | <el-input-number v-model="formData.queueSet" :min="0" class="w-full" disabled /> |
| | | <div class="text-12px text-gray-500 leading-5 mt-1"> |
| | | 新增时段时建议与「普通-起始序号」一致。 |
| | | 系统自动计算(= 上一段的最大号),不需要填写。 |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="VIP-当前序号"> |
| | | <el-form-item label="正常预约-上限" prop="queueFull"> |
| | | <!-- 【本次需求】上限允许等于起始序号(=该时段无名额),故 min 改为 0 --> |
| | | <el-input-number v-model="formData.queueFull" :min="0" class="w-full" /> |
| | | </el-form-item> |
| | | <el-form-item label="预留-起始序号" prop="queueVipSet"> |
| | | <!-- 【本需求】预留号段紧跟正常预约号段之后,由系统自动计算 --> |
| | | <el-input-number v-model="formData.queueVipSet" :min="0" class="w-full" disabled /> |
| | | <div class="text-12px text-gray-500 leading-5 mt-1"> |
| | | 系统自动计算(= 正常预约的上限),不需要填写。 |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="预留-上限" prop="queueVipFull"> |
| | | <!-- 【本次需求】同普通上限:允许等于起始序号 --> |
| | | <el-input-number v-model="formData.queueVipFull" :min="0" class="w-full" /> |
| | | </el-form-item> |
| | | <el-form-item label="正常预约-当前序号"> |
| | | <el-input-number v-model="formData.queueNo" :min="0" class="w-full" /> |
| | | <div class="text-12px text-gray-500 leading-5 mt-1"> |
| | | 新增时段时建议与「正常预约-起始序号」一致。 |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="预留-当前序号"> |
| | | <el-input-number v-model="formData.queueVipNo" :min="0" class="w-full" /> |
| | | </el-form-item> |
| | | </el-form> |
| | |
| | | ) |
| | | const changedCount = computed(() => changedRows.value.size) |
| | | |
| | | /** |
| | | * 【本需求】表格只显示"真实时段"(time_slot <> 0); |
| | | * time_slot = 0 的那一行是「总预留池」(无时段 / 全满时的兜底号池),单独在表格下方展示。 |
| | | */ |
| | | const slotList = computed(() => |
| | | (list.value || []).filter((r: QueueSequenceVO) => Number(r.timeSlot ?? 0) !== 0) |
| | | ) |
| | | |
| | | /** 总预留池行(time_slot = 0);未初始化时为 undefined */ |
| | | const totalPool = computed(() => |
| | | (list.value || []).find((r: QueueSequenceVO) => Number(r.timeSlot ?? 0) === 0) |
| | | ) |
| | | |
| | | /** 时段下拉选项:取自字典 ecg_book_timeslot(value 为 HHMMHHMM 数字字符串,label 形如 08:00-08:30) */ |
| | | const timeSlotOptions = computed(() => { |
| | | const opts = getIntDictOptions(DICT_TYPE.ECG_BOOK_TIMESLOT) || [] |
| | |
| | | { |
| | | validator: (_rule: any, value: any, callback: any) => { |
| | | const set = formData.value.queueSet |
| | | if (value !== undefined && value !== null && set !== undefined && set !== null && value <= set) { |
| | | callback(new Error('上限必须大于起始序号,否则该时段无法取号')) |
| | | // 【本次需求】上限 = 起始序号 是**合法**的:表示该时段没有名额(预定额度 0),允许保存。 |
| | | // 只有「上限 < 起始序号」才是配置错误(该时段永远无法取号)。 |
| | | if (value !== undefined && value !== null && set !== undefined && set !== null && value < set) { |
| | | callback(new Error('上限不能小于起始序号(等于表示该时段没有名额)')) |
| | | } else { |
| | | callback() |
| | | } |
| | | }, |
| | | trigger: 'blur' |
| | | } |
| | | ], |
| | | queueVipFull: [ |
| | | { |
| | | validator: (_rule: any, value: any, callback: any) => { |
| | | const set = formData.value.queueVipSet |
| | | if (value !== undefined && value !== null && set !== undefined && set !== null && value < set) { |
| | | callback(new Error('预留上限不能小于预留起始序号(等于表示预留没有名额)')) |
| | | } else { |
| | | callback() |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | /** 该时段是否已满(当前序号 >= 上限) */ |
| | | const isFull = (row: QueueSequenceVO) => |
| | | (row.queueNo ?? 0) >= (row.queueFull ?? 0) && (row.queueFull ?? 0) > 0 |
| | | /** |
| | | * 预定额度 = 上限 − 起始序号(小于 0 按 0 显示)。 |
| | | * |
| | | * 【本次需求】该列只读,值为 0 时单元格浅红底 —— 表示这个时段**没有名额**。 |
| | | * 「上限 = 起始序号」是合法配置(无名额时段),不再当作错误。 |
| | | */ |
| | | const quotaOf = (set?: number | null, full?: number | null) => |
| | | Math.max(0, (full ?? 0) - (set ?? 0)) |
| | | |
| | | /** |
| | | * 该时段是否已满(正常预约)。 |
| | | * |
| | | * 满的两种情形: |
| | | * ① 预定额度 <= 0(上限 <= 起始序号)→ 本来就没有名额; |
| | | * ② 当前序号已达到上限。 |
| | | */ |
| | | const isFull = (row: QueueSequenceVO) => { |
| | | if (quotaOf(row.queueSet, row.queueFull) <= 0) return true |
| | | return (row.queueNo ?? 0) >= (row.queueFull ?? 0) |
| | | } |
| | | |
| | | /** 该时段是否已满(预留组),口径同上 */ |
| | | const isFullVip = (row: QueueSequenceVO) => { |
| | | if (quotaOf(row.queueVipSet, row.queueVipFull) <= 0) return true |
| | | return (row.queueVipNo ?? 0) >= (row.queueVipFull ?? 0) |
| | | } |
| | | |
| | | /** 行内保存(只改数值,不动时段) */ |
| | | const handleRowSave = async (row: QueueSequenceVO) => { |
| | | if (!row.id) return |
| | | if ((row.queueFull ?? 0) <= (row.queueSet ?? 0)) { |
| | | message.error(`时段 ${formatTimeSlot(row.timeSlot)}:上限必须大于起始序号`) |
| | | // 【本次需求】只有「上限 < 起始序号」才是错误;相等表示该时段没有名额,允许保存。 |
| | | if ((row.queueFull ?? 0) < (row.queueSet ?? 0)) { |
| | | message.error( |
| | | `时段 ${formatTimeSlot(row.timeSlot)}:上限不能小于起始序号(等于表示该时段没有名额)` |
| | | ) |
| | | return |
| | | } |
| | | if ((row.queueVipFull ?? 0) < (row.queueVipSet ?? 0)) { |
| | | message.error( |
| | | `时段 ${formatTimeSlot(row.timeSlot)}:预留上限不能小于预留起始序号(等于表示预留没有名额)` |
| | | ) |
| | | return |
| | | } |
| | | savingId.value = row.id |
| | |
| | | await getList() |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | /* |
| | | 【本次调整】字号恢复为系统默认,**允许横向滚动**: |
| | | · 不再压缩字号与内边距 —— 表体恢复默认字号(14px)、输入框默认字号、状态标签用 small 默认样式; |
| | | · 列宽由模板固定(时段 96 + 正常预约 5 列 + 预留 5 列 + 操作 160); |
| | | 总宽超过弹窗宽度时表格出现**横向滚动条**,操作列 fixed="right" 始终可见; |
| | | · 表格高度 calc(100vh - 300px):竖向多显示几行,页面本身不出现滚动条。 |
| | | */ |
| | | .qs-table { |
| | | /* 行内数字输入占满单元格(不缩小字号,只控制宽度) */ |
| | | :deep(.el-input-number) { |
| | | width: 100%; |
| | | } |
| | | } |
| | | |
| | | /* 预定额度(只读):值为 0 时浅红底,表示该时段没有名额 */ |
| | | /* 【本需求】号段起点(系统自动计算,只读展示) */ |
| | | .qs-fixed { |
| | | display: inline-block; |
| | | min-width: 34px; |
| | | padding: 1px 4px; |
| | | border-radius: 3px; |
| | | color: var(--el-text-color-secondary); |
| | | background-color: var(--el-fill-color-light); |
| | | font-variant-numeric: tabular-nums; |
| | | } |
| | | .qs-quota { |
| | | display: inline-block; |
| | | padding: 1px 6px; |
| | | border-radius: 3px; |
| | | font-weight: 600; |
| | | font-variant-numeric: tabular-nums; |
| | | color: var(--el-text-color-primary); |
| | | } |
| | | |
| | | .qs-quota--zero { |
| | | color: var(--el-color-danger); |
| | | background-color: var(--el-color-danger-light-9); |
| | | box-shadow: inset 0 0 0 1px var(--el-color-danger-light-5); |
| | | } |
| | | </style> |