This commit is contained in:
Your Name
2026-03-23 18:02:16 +08:00
parent 18fee2e8f8
commit 250d173c2f
525 changed files with 10659 additions and 793 deletions
+598
View File
@@ -0,0 +1,598 @@
<template>
<div class="paiban-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>医生排班管理</span>
</div>
</template>
<el-form :model="form" label-width="100px" class="paiban-form">
<!-- 选择医生 -->
<el-form-item label="选择医生:">
<div class="doctor-list">
<el-radio-group v-model="selectedDoctorId" @change="handleDoctorChange">
<el-radio
v-for="doctor in doctorList"
:key="doctor.id"
:value="doctor.id"
class="doctor-radio"
>
{{ doctor.name }}
</el-radio>
</el-radio-group>
</div>
</el-form-item>
<!-- 排班时间 -->
<el-form-item label="排班时间:">
<!-- 未选择医生提示 -->
<el-empty
v-if="!selectedDoctorId"
description="请先选择医生"
:image-size="80"
/>
<!-- 医生无排班提示 -->
<el-empty
v-else-if="selectedDoctorId && doctorRosterDates.length === 0"
description="该医生暂无排班"
:image-size="80"
/>
<!-- 有排班时显示日期和时间段 -->
<div v-else class="paiban-time-container">
<!-- 日期选择 -->
<div class="date-selector">
<el-button
v-for="dateOption in dateOptions"
:key="dateOption.date"
:type="form.date === dateOption.date ? 'primary' : ''"
@click="selectDate(dateOption.date)"
class="date-button"
>
{{ dateOption.label }}
</el-button>
</div>
<!-- 时间段区域 -->
<div v-if="form.date" class="time-slots-container">
<!-- 时间段标题和刷新按钮 -->
<!-- <div class="time-slots-header">
<span class="header-title">排班时段</span>
<el-button
text
type="primary"
size="small"
:loading="refreshing"
@click="handleRefreshSlots"
>
<template #icon>
<Refresh />
</template>
刷新
</el-button>
</div> -->
<!-- 时间段网格 -->
<div class="time-slots-grid">
<div
v-for="slot in filteredTimeSlots"
:key="slot.time"
class="time-slot-item"
:class="{
'available': slot.available,
'unavailable': !slot.available,
'selected': form.selectedTime === slot.time
}"
@click="selectTimeSlot(slot)"
>
<div class="slot-time">{{ slot.time }}</div>
<div class="slot-status" :class="{ 'status-available': slot.available }">
{{ slot.available ? '可约' : '已约' }}
</div>
</div>
</div>
</div>
</div>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import isoWeek from 'dayjs/plugin/isoWeek'
import { getDoctors } from '@/api/tcm'
import { getAvailableSlots, rosterLists } from '@/api/doctor'
import feedback from '@/utils/feedback'
dayjs.extend(isoWeek)
interface TimeSlot {
time: string
available: boolean
quota: number
}
interface DateOption {
date: string
label: string
}
const loading = ref(false)
const refreshing = ref(false)
const doctorList = ref<any[]>([])
const timeSlots = ref<TimeSlot[]>([])
const selectedDoctorId = ref(0)
const doctorRosterDates = ref<string[]>([])
let autoRefreshTimer: number | null = null
let isLoadingSlots = false // 防止并发请求
const form = reactive({
date: '',
selectedTime: ''
})
// 生成未来7天的日期选项(只显示有排班的日期,且大于等于今天)
const dateOptions = computed<DateOption[]>(() => {
if (!selectedDoctorId.value || doctorRosterDates.value.length === 0) {
return []
}
const options: DateOption[] = []
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
const today = dayjs().startOf('day')
// 只显示有排班的日期,且日期大于等于今天
for (const date of doctorRosterDates.value) {
const dateObj = dayjs(date)
// 过滤掉今天之前的日期
if (dateObj.isBefore(today)) {
continue
}
const label = `${dateObj.format('MM月DD日')} (${weekDays[dateObj.day()]})`
options.push({
date: date,
label
})
}
return options
})
// 过滤时间段:如果是今天,只显示当前时间之后的时间段
const filteredTimeSlots = computed(() => {
if (!form.date || timeSlots.value.length === 0) {
return []
}
const today = dayjs().format('YYYY-MM-DD')
const isToday = form.date === today
if (!isToday) {
// 不是今天,显示所有时间段
return timeSlots.value
}
// 是今天,只显示当前时间之后的时间段
const now = dayjs()
return timeSlots.value.map(slot => {
const slotDateTime = dayjs(`${form.date} ${slot.time}`)
const isPast = slotDateTime.isBefore(now) || slotDateTime.isSame(now, 'minute')
// 如果时间已过,标记为不可用
if (isPast) {
return {
...slot,
available: false
}
}
return slot
})
})
// 加载医生列表
const loadDoctors = async () => {
try {
loading.value = true
const res = await getDoctors()
doctorList.value = res || []
// 默认选中第一个医生
if (doctorList.value.length > 0 && !selectedDoctorId.value) {
selectedDoctorId.value = doctorList.value[0].id
await handleDoctorChange()
}
} catch (error) {
console.error('加载医生列表失败:', error)
feedback.msgError('加载医生列表失败')
} finally {
loading.value = false
}
}
// 医生改变
const handleDoctorChange = async () => {
form.selectedTime = ''
form.date = ''
timeSlots.value = []
doctorRosterDates.value = []
if (selectedDoctorId.value) {
await loadDoctorRoster()
}
}
// 加载医生排班信息
const loadDoctorRoster = async () => {
if (!selectedDoctorId.value) {
return
}
try {
loading.value = true
// 获取未来7天的日期范围
const startDate = dayjs().format('YYYY-MM-DD')
const endDate = dayjs().add(6, 'day').format('YYYY-MM-DD')
// 查询医生在这个日期范围内的排班
const res = await rosterLists({
doctor_id: selectedDoctorId.value,
start_date: startDate,
end_date: endDate,
status: 1 // 只查询出诊状态
})
// 提取有排班的日期
if (res?.lists && res.lists.length > 0) {
doctorRosterDates.value = [...new Set(res.lists.map((item: any) => item.date))] as string[]
doctorRosterDates.value.sort()
// 自动选择日期:优先选择今天,如果今天没有排班则选择第一个有排班的日期
await nextTick()
if (doctorRosterDates.value.length > 0) {
const today = dayjs().format('YYYY-MM-DD')
const defaultDate = doctorRosterDates.value.includes(today)
? today
: doctorRosterDates.value[0]
selectDate(defaultDate)
}
} else {
doctorRosterDates.value = []
feedback.msgWarning('该医生暂无排班')
}
} catch (error) {
console.error('加载医生排班失败:', error)
feedback.msgError('加载医生排班失败')
doctorRosterDates.value = []
} finally {
loading.value = false
}
}
// 选择日期
const selectDate = (date: string) => {
form.date = date
form.selectedTime = ''
if (selectedDoctorId.value) {
loadTimeSlots()
}
}
// 加载时间段
const loadTimeSlots = async (silent = false) => {
if (!selectedDoctorId.value || !form.date) {
return
}
// 如果正在加载,跳过本次请求
if (isLoadingSlots) {
console.log('正在加载中,跳过本次请求')
return
}
try {
isLoadingSlots = true
if (!silent) {
loading.value = true
}
// 加载全天时段(9:00-18:00,每15分钟一个)
const response = await getAvailableSlots({
doctor_id: selectedDoctorId.value,
appointment_date: form.date,
period: 'all' // 获取全天时段
})
timeSlots.value = (response?.slots || []).map((slot: any) => ({
time: slot.time,
available: slot.available,
quota: slot.available ? 1 : 0
}))
} catch (error) {
console.error('加载时间段失败:', error)
if (!silent) {
feedback.msgError('加载时间段失败')
}
timeSlots.value = []
} finally {
isLoadingSlots = false
if (!silent) {
loading.value = false
}
}
}
// 选择时间段
const selectTimeSlot = (slot: TimeSlot) => {
if (!slot.available) {
feedback.msgWarning('该时间段不可预约')
return
}
form.selectedTime = slot.time
}
// 刷新时间段
const handleRefreshSlots = async () => {
if (!selectedDoctorId.value || !form.date) {
return
}
// 如果正在加载,提示用户
if (isLoadingSlots) {
feedback.msgWarning('正在刷新中,请稍候')
return
}
try {
refreshing.value = true
const previousSelection = form.selectedTime
form.selectedTime = ''
await loadTimeSlots()
if (previousSelection) {
const slot = timeSlots.value.find(s => s.time === previousSelection)
if (slot && slot.available) {
form.selectedTime = previousSelection
}
}
feedback.msgSuccess('刷新成功')
} catch (error) {
console.error('刷新失败:', error)
feedback.msgError('刷新失败,请重试')
} finally {
refreshing.value = false
}
}
// 启动自动刷新定时器
const startAutoRefresh = () => {
// 清除已存在的定时器
stopAutoRefresh()
// 每5秒自动刷新时间段
autoRefreshTimer = window.setInterval(() => {
if (selectedDoctorId.value && form.date) {
loadTimeSlots(true) // silent模式,不显示loading
}
}, 5000)
}
// 停止自动刷新定时器
const stopAutoRefresh = () => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
}
onMounted(() => {
loadDoctors()
startAutoRefresh()
})
onUnmounted(() => {
stopAutoRefresh()
})
</script>
<style scoped lang="scss">
.paiban-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
}
.paiban-form {
:deep(.el-form-item__label) {
font-weight: 500;
}
}
.doctor-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
.doctor-radio {
margin-right: 0;
:deep(.el-radio__label) {
display: flex;
align-items: center;
}
}
}
.paiban-time-container {
width: 100%;
}
.date-selector {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
.date-button {
min-width: 130px;
height: 40px;
font-size: 14px;
border-radius: 8px;
transition: all 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
}
.time-slots-container {
background-color: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.time-slots-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.header-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
}
.time-slots-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 10px;
max-height: 450px;
overflow-y: auto;
padding: 2px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: #dcdfe6;
border-radius: 3px;
&:hover {
background-color: #c0c4cc;
}
}
.time-slot-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0px 8px;
border: 2px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background-color: #fff;
min-height: 70px;
.slot-time {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 6px;
}
.slot-status {
font-size: 12px;
color: #909399;
padding: 0px 8px;
border-radius: 4px;
background-color: #f4f4f5;
&.status-available {
color: #67c23a;
background-color: #f0f9ff;
}
}
&.available {
border-color: #e4e7ed;
&:hover {
border-color: #409eff;
background-color: #ecf5ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
}
&.unavailable {
background-color: #f5f7fa;
border-color: #e4e7ed;
cursor: not-allowed;
opacity: 0.6;
.slot-time {
color: #c0c4cc;
}
.slot-status {
color: #c0c4cc;
background-color: #f5f7fa;
}
&:hover {
transform: none;
box-shadow: none;
}
}
&.selected {
border-color: #409eff;
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
.slot-time {
color: #fff;
}
.slot-status {
color: #fff;
background-color: rgba(255, 255, 255, 0.2);
}
}
}
}
:deep(.el-radio-group) {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
:deep(.el-radio) {
margin-right: 0;
}
</style>
@@ -0,0 +1,663 @@
<!-- 处方管理 -->
<template>
<div class="prescription-list">
<el-card class="!border-none" shadow="never">
<el-form class="mb-[-16px]" :model="formData" inline>
<el-form-item class="w-[280px]" label="处方名称">
<el-input
v-model="formData.prescription_name"
placeholder="请输入处方名称"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item class="w-[280px]" label="患者姓名">
<el-input
v-model="formData.patient_name"
placeholder="请输入患者姓名"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item class="w-[200px]" label="是否共享">
<el-select v-model="formData.is_shared" placeholder="全部" clearable>
<el-option label="全部" :value="''" />
<el-option label="仅自己可见" :value="0" />
<el-option label="所有人可见" :value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card v-loading="pager.loading" class="mt-4 !border-none" shadow="never">
<el-button type="primary" @click="handleAdd" v-perms="['cf.prescription/add']">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增处方
</el-button>
<div class="mt-4">
<el-table :data="pager.lists" size="large">
<el-table-column label="ID" prop="id" min-width="60" />
<el-table-column label="处方编号" prop="sn" min-width="140" />
<el-table-column label="处方名称" prop="prescription_name" min-width="150" show-overflow-tooltip />
<el-table-column label="类型" prop="prescription_type" min-width="100" />
<el-table-column label="患者姓名" prop="patient_name" min-width="100" />
<el-table-column label="性别" min-width="60">
<template #default="{ row }">
{{ row.gender === 1 ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column label="年龄" prop="age" min-width="60" />
<el-table-column label="药材数量" min-width="90">
<template #default="{ row }">
{{ row.herbs?.length || 0 }}
</template>
</el-table-column>
<el-table-column label="剂数" min-width="80">
<template #default="{ row }">
{{ row.dose_count }}{{ row.dose_unit }}
</template>
</el-table-column>
<el-table-column label="服用天数" min-width="90">
<template #default="{ row }">
{{ row.usage_days }}
</template>
</el-table-column>
<el-table-column label="是否共享" min-width="100">
<template #default="{ row }">
<el-tag :type="row.is_shared ? 'success' : 'info'">
{{ row.is_shared ? '所有人可见' : '仅自己可见' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="医师" prop="doctor_name" min-width="100" />
<el-table-column label="处方日期" prop="prescription_date" min-width="120" />
<el-table-column label="创建时间" prop="create_time" min-width="160" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleView(row)" v-perms="['cf.prescription/read']">
查看
</el-button>
<el-button type="primary" link @click="handleEdit(row)" v-perms="['cf.prescription/edit']">
编辑
</el-button>
<el-button type="danger" link @click="handleDelete(row.id)" v-perms="['cf.prescription/del']">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex mt-4 justify-end">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="showEdit"
:title="editMode === 'add' ? '新增处方' : editMode === 'edit' ? '编辑处方' : '查看处方'"
width="900px"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="editForm"
:rules="rules"
label-width="120px"
:disabled="editMode === 'view'"
>
<el-form-item label="处方名称" prop="prescription_name">
<el-input
v-model="editForm.prescription_name"
placeholder="请输入处方名称"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="患者姓名" prop="patient_name">
<el-input v-model="editForm.patient_name" placeholder="请输入患者姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="门诊号" prop="visit_no">
<el-input v-model="editForm.visit_no" placeholder="自动生成或手动输入" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="editForm.gender">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="年龄" prop="age">
<el-input-number
v-model="editForm.age"
:min="0"
:max="150"
placeholder="请输入年龄"
class="w-full"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="处方日期" prop="prescription_date">
<el-date-picker
v-model="editForm.prescription_date"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
class="w-full"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="面象" prop="tongue">
<el-input v-model="editForm.tongue" placeholder="请输入面象" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="舌象" prop="tongue_image">
<el-input v-model="editForm.tongue_image" placeholder="请输入舌象" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="脉象" prop="pulse">
<el-input v-model="editForm.pulse" placeholder="请输入脉象" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="脉象详情" prop="pulse_condition">
<el-input v-model="editForm.pulse_condition" placeholder="请输入脉象详情" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="临床诊断" prop="clinical_diagnosis">
<el-input
v-model="editForm.clinical_diagnosis"
type="textarea"
:rows="3"
placeholder="请输入临床诊断"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="药材配方" prop="herbs" required>
<div class="w-full">
<el-button
type="primary"
size="small"
@click="addHerb"
:disabled="editMode === 'view'"
>
添加药材
</el-button>
<el-table :data="editForm.herbs" class="mt-2" border>
<el-table-column label="序号" type="index" width="60" />
<el-table-column label="药材名称" min-width="150">
<template #default="{ row, $index }">
<el-input
v-model="row.name"
placeholder="请输入药材名称"
:disabled="editMode === 'view'"
/>
</template>
</el-table-column>
<el-table-column label="剂量(克)" min-width="120">
<template #default="{ row, $index }">
<el-input-number
v-model="row.dosage"
:min="0"
:precision="1"
:step="0.5"
placeholder="剂量"
class="w-full"
:disabled="editMode === 'view'"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" v-if="editMode !== 'view'">
<template #default="{ $index }">
<el-button
type="danger"
link
@click="removeHerb($index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="剂数" prop="dose_count">
<el-input-number
v-model="editForm.dose_count"
:min="1"
placeholder="剂数"
class="w-full"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="剂量单位" prop="dose_unit">
<el-select v-model="editForm.dose_unit" placeholder="请选择" class="w-full">
<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-col>
<el-col :span="8">
<el-form-item label="处方类型" prop="prescription_type">
<el-select v-model="editForm.prescription_type" placeholder="请选择处方类型" class="w-full">
<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-col>
<el-col :span="8">
<el-form-item label="服用天数" prop="usage_days">
<el-input-number
v-model="editForm.usage_days"
:min="1"
:max="365"
placeholder="服用天数"
class="w-full"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="用法" prop="usage_instruction">
<el-input
v-model="editForm.usage_instruction"
placeholder="例如:水煎服,一日二次"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="服用时间" prop="usage_time">
<el-select v-model="editForm.usage_time" placeholder="请选择服用时间" class="w-full">
<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-col>
<el-col :span="12">
<el-form-item label="服用方式" prop="usage_way">
<el-select v-model="editForm.usage_way" placeholder="请选择服用方式" class="w-full">
<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-col>
</el-row>
<el-form-item label="忌口" prop="dietary_taboo">
<el-select
v-model="editForm.dietary_taboo"
multiple
placeholder="请选择忌口内容(可多选)"
class="w-full"
>
<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-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="usage_notes">
<el-input
v-model="editForm.usage_notes"
type="textarea"
:rows="2"
placeholder="其他服用注意事项"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item label="医师姓名" prop="doctor_name">
<el-input v-model="editForm.doctor_name" placeholder="请输入医师姓名" />
</el-form-item>
<el-form-item label="是否共享" prop="is_shared">
<el-switch
v-model="editForm.is_shared"
:active-value="1"
:inactive-value="0"
active-text="所有人可见"
inactive-text="仅自己可见"
/>
<div class="text-xs text-gray-400 mt-1">
勾选后所有医生都可以查看和使用此处方模板
</div>
</el-form-item>
</el-form>
<template #footer v-if="editMode !== 'view'">
<el-button @click="showEdit = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup name="prescriptionList">
import { prescriptionLists, prescriptionAdd, prescriptionEdit, prescriptionDelete } from '@/api/tcm'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import type { FormInstance, FormRules } from 'element-plus'
// 表单数据
const formData = reactive({
prescription_name: '',
patient_name: '',
is_shared: ''
})
const showEdit = ref(false)
const editMode = ref<'add' | 'edit' | 'view'>('add')
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
// 编辑表单
const editForm = reactive({
id: 0,
prescription_name: '',
prescription_type: '浓缩水丸',
patient_name: '',
gender: 1,
age: 0,
visit_no: '',
prescription_date: '',
tongue: '',
tongue_image: '',
pulse: '',
pulse_condition: '',
clinical_diagnosis: '',
herbs: [] as Array<{ name: string; dosage: number }>,
dose_count: 7,
dose_unit: '剂',
usage_days: 7,
usage_instruction: '水煎服,一日二次',
usage_time: '饭前',
usage_way: '温水送服',
dietary_taboo: [] as string[],
usage_notes: '',
doctor_name: '',
is_shared: 0,
diagnosis_id: 0
})
// 表单验证规则
const rules: FormRules = {
prescription_name: [
{ required: true, message: '请输入处方名称', trigger: 'blur' }
],
patient_name: [
{ required: true, message: '请输入患者姓名', trigger: 'blur' }
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' }
],
prescription_date: [
{ required: true, message: '请选择处方日期', trigger: 'change' }
],
clinical_diagnosis: [
{ required: true, message: '请输入临床诊断', trigger: 'blur' }
],
dose_count: [
{ required: true, message: '请输入剂数', trigger: 'blur' }
],
doctor_name: [
{ required: true, message: '请输入医师姓名', trigger: 'blur' }
]
}
const { pager, getLists, resetParams, resetPage } = usePaging({
fetchFun: prescriptionLists,
params: formData
})
// 添加药材
const addHerb = () => {
editForm.herbs.push({
name: '',
dosage: 0
})
}
// 删除药材
const removeHerb = (index: number) => {
editForm.herbs.splice(index, 1)
}
// 重置表单
const resetForm = () => {
editForm.id = 0
editForm.prescription_name = ''
editForm.prescription_type = '浓缩水丸'
editForm.patient_name = ''
editForm.gender = 1
editForm.age = 0
editForm.visit_no = ''
editForm.prescription_date = new Date().toISOString().split('T')[0]
editForm.tongue = ''
editForm.tongue_image = ''
editForm.pulse = ''
editForm.pulse_condition = ''
editForm.clinical_diagnosis = ''
editForm.herbs = []
editForm.dose_count = 7
editForm.dose_unit = '剂'
editForm.usage_days = 7
editForm.usage_instruction = '水煎服,一日二次'
editForm.usage_time = '饭前'
editForm.usage_way = '温水送服'
editForm.dietary_taboo = []
editForm.usage_notes = ''
editForm.doctor_name = ''
editForm.is_shared = 0
editForm.diagnosis_id = 0
}
// 新增处方
const handleAdd = () => {
resetForm()
editMode.value = 'add'
showEdit.value = true
}
// 查看处方
const handleView = (row: any) => {
editMode.value = 'view'
// 深拷贝数据到表单
editForm.id = row.id
editForm.prescription_name = row.prescription_name || ''
editForm.prescription_type = row.prescription_type || '浓缩水丸'
editForm.patient_name = row.patient_name || ''
editForm.gender = row.gender ?? 1
editForm.age = row.age ?? 0
editForm.visit_no = row.visit_no || ''
editForm.prescription_date = row.prescription_date || ''
editForm.tongue = row.tongue || ''
editForm.tongue_image = row.tongue_image || ''
editForm.pulse = row.pulse || ''
editForm.pulse_condition = row.pulse_condition || ''
editForm.clinical_diagnosis = row.clinical_diagnosis || ''
editForm.herbs = row.herbs ? JSON.parse(JSON.stringify(row.herbs)) : []
editForm.dose_count = row.dose_count ?? 7
editForm.dose_unit = row.dose_unit || '剂'
editForm.usage_days = row.usage_days ?? 7
editForm.usage_instruction = row.usage_instruction || '水煎服,一日二次'
editForm.usage_time = row.usage_time || '饭前'
editForm.usage_way = row.usage_way || '温水送服'
editForm.dietary_taboo = row.dietary_taboo ? (Array.isArray(row.dietary_taboo) ? row.dietary_taboo : row.dietary_taboo.split(',').filter((v: string) => v)) : []
editForm.usage_notes = row.usage_notes || ''
editForm.doctor_name = row.doctor_name || ''
editForm.is_shared = row.is_shared ?? 0
editForm.diagnosis_id = row.diagnosis_id ?? 0
showEdit.value = true
}
// 编辑处方
const handleEdit = (row: any) => {
editMode.value = 'edit'
// 深拷贝数据到表单
editForm.id = row.id
editForm.prescription_name = row.prescription_name || ''
editForm.prescription_type = row.prescription_type || '浓缩水丸'
editForm.patient_name = row.patient_name || ''
editForm.gender = row.gender ?? 1
editForm.age = row.age ?? 0
editForm.visit_no = row.visit_no || ''
editForm.prescription_date = row.prescription_date || ''
editForm.tongue = row.tongue || ''
editForm.tongue_image = row.tongue_image || ''
editForm.pulse = row.pulse || ''
editForm.pulse_condition = row.pulse_condition || ''
editForm.clinical_diagnosis = row.clinical_diagnosis || ''
editForm.herbs = row.herbs ? JSON.parse(JSON.stringify(row.herbs)) : []
editForm.dose_count = row.dose_count ?? 7
editForm.dose_unit = row.dose_unit || '剂'
editForm.usage_days = row.usage_days ?? 7
editForm.usage_instruction = row.usage_instruction || '水煎服,一日二次'
editForm.usage_time = row.usage_time || '饭前'
editForm.usage_way = row.usage_way || '温水送服'
editForm.dietary_taboo = row.dietary_taboo ? (Array.isArray(row.dietary_taboo) ? row.dietary_taboo : row.dietary_taboo.split(',').filter((v: string) => v)) : []
editForm.usage_notes = row.usage_notes || ''
editForm.doctor_name = row.doctor_name || ''
editForm.is_shared = row.is_shared ?? 0
editForm.diagnosis_id = row.diagnosis_id ?? 0
showEdit.value = true
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate()
// 验证药材
if (!editForm.herbs || editForm.herbs.length === 0) {
feedback.msgError('请至少添加一味药材')
return
}
for (let i = 0; i < editForm.herbs.length; i++) {
const herb = editForm.herbs[i]
if (!herb.name || !herb.name.trim()) {
feedback.msgError(`${i + 1}味药材名称不能为空`)
return
}
if (!herb.dosage || herb.dosage <= 0) {
feedback.msgError(`${i + 1}味药材剂量必须大于0`)
return
}
}
submitLoading.value = true
try {
const params = { ...editForm }
if (editMode.value === 'add') {
await prescriptionAdd(params)
feedback.msgSuccess('添加成功')
} else {
await prescriptionEdit(params)
feedback.msgSuccess('编辑成功')
}
showEdit.value = false
getLists()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
// 删除处方
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除该处方吗?')
await prescriptionDelete({ id })
feedback.msgSuccess('删除成功')
getLists()
}
onMounted(() => {
getLists()
})
</script>
<style scoped lang="scss">
.prescription-list {
padding: 20px;
}
</style>
@@ -0,0 +1,332 @@
<!-- 处方库 -->
<template>
<div class="prescription-library">
<el-card class="!border-none" shadow="never">
<el-form class="mb-[-16px]" :model="formData" inline>
<el-form-item class="w-[280px]" label="处方名称">
<el-input
v-model="formData.prescription_name"
placeholder="请输入处方名称"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item class="w-[200px]" label="是否公开">
<el-select v-model="formData.is_public" placeholder="全部" clearable>
<el-option label="全部" :value="''" />
<el-option label="仅自己可见" :value="0" />
<el-option label="所有人可见" :value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card v-loading="pager.loading" class="mt-4 !border-none" shadow="never">
<el-button type="primary" @click="handleAdd">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增处方
</el-button>
<div class="mt-4">
<el-table :data="pager.lists" size="large">
<el-table-column label="ID" prop="id" min-width="60" />
<el-table-column label="处方名称" prop="prescription_name" min-width="200" show-overflow-tooltip />
<el-table-column label="药材数量" min-width="100">
<template #default="{ row }">
{{ row.herbs?.length || 0 }}
</template>
</el-table-column>
<el-table-column label="药材明细" min-width="300" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.herbs && row.herbs.length > 0">
{{ row.herbs.map((h: any) => `${h.name} ${h.dosage}g`).join('、') }}
</span>
<span v-else class="text-gray-400">暂无药材</span>
</template>
</el-table-column>
<el-table-column label="是否公开" min-width="120">
<template #default="{ row }">
<el-tag :type="row.is_public ? 'success' : 'info'">
{{ row.is_public ? '所有人可见' : '仅自己可见' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建人" prop="creator_name" min-width="100" />
<el-table-column label="创建时间" prop="create_time" min-width="160" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">
查看
</el-button>
<el-button type="primary" link @click="handleEdit(row)">
编辑
</el-button>
<el-button type="danger" link @click="handleDelete(row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex mt-4 justify-end">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="showEdit"
:title="editMode === 'add' ? '新增处方' : editMode === 'edit' ? '编辑处方' : '查看处方'"
width="800px"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="editForm"
:rules="rules"
label-width="120px"
:disabled="editMode === 'view'"
>
<el-form-item label="处方名称" prop="prescription_name">
<el-input
v-model="editForm.prescription_name"
placeholder="请输入处方名称,例如:六味地黄丸、补中益气汤"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="药材配方" prop="herbs" required>
<div class="w-full">
<el-button
type="primary"
size="small"
@click="addHerb"
:disabled="editMode === 'view'"
>
添加药材
</el-button>
<el-table :data="editForm.herbs" class="mt-2" border>
<el-table-column label="序号" type="index" width="60" />
<el-table-column label="药材名称" min-width="200">
<template #default="{ row }">
<el-input
v-model="row.name"
placeholder="请输入药材名称"
:disabled="editMode === 'view'"
/>
</template>
</el-table-column>
<el-table-column label="剂量(克)" min-width="150">
<template #default="{ row }">
<el-input-number
v-model="row.dosage"
:min="0"
:precision="1"
:step="0.5"
placeholder="剂量"
class="w-full"
:disabled="editMode === 'view'"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" v-if="editMode !== 'view'">
<template #default="{ $index }">
<el-button
type="danger"
link
@click="removeHerb($index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form-item>
<el-form-item label="是否公开" prop="is_public">
<el-switch
v-model="editForm.is_public"
:active-value="1"
:inactive-value="0"
active-text="所有人可见"
inactive-text="仅自己可见"
/>
<div class="text-xs text-gray-400 mt-1">
勾选后所有医生都可以查看和使用此处方模板
</div>
</el-form-item>
</el-form>
<template #footer v-if="editMode !== 'view'">
<el-button @click="showEdit = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup name="prescriptionLibrary">
import {
prescriptionLibraryLists,
prescriptionLibraryAdd,
prescriptionLibraryEdit,
prescriptionLibraryDelete
} from '@/api/tcm'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import type { FormInstance, FormRules } from 'element-plus'
// 表单数据
const formData = reactive({
prescription_name: '',
is_public: ''
})
const showEdit = ref(false)
const editMode = ref<'add' | 'edit' | 'view'>('add')
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
// 编辑表单
const editForm = reactive({
id: 0,
prescription_name: '',
herbs: [] as Array<{ name: string; dosage: number }>,
is_public: 0
})
// 表单验证规则
const rules: FormRules = {
prescription_name: [
{ required: true, message: '请输入处方名称', trigger: 'blur' }
]
}
const { pager, getLists, resetParams, resetPage } = usePaging({
fetchFun: prescriptionLibraryLists,
params: formData
})
// 添加药材
const addHerb = () => {
editForm.herbs.push({
name: '',
dosage: 0
})
}
// 删除药材
const removeHerb = (index: number) => {
editForm.herbs.splice(index, 1)
}
// 重置表单
const resetForm = () => {
editForm.id = 0
editForm.prescription_name = ''
editForm.herbs = []
editForm.is_public = 0
}
// 新增处方
const handleAdd = () => {
resetForm()
editMode.value = 'add'
showEdit.value = true
}
// 查看处方
const handleView = (row: any) => {
editMode.value = 'view'
editForm.id = row.id
editForm.prescription_name = row.prescription_name || ''
editForm.herbs = row.herbs ? JSON.parse(JSON.stringify(row.herbs)) : []
editForm.is_public = row.is_public ?? 0
showEdit.value = true
}
// 编辑处方
const handleEdit = (row: any) => {
editMode.value = 'edit'
editForm.id = row.id
editForm.prescription_name = row.prescription_name || ''
editForm.herbs = row.herbs ? JSON.parse(JSON.stringify(row.herbs)) : []
editForm.is_public = row.is_public ?? 0
showEdit.value = true
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate()
// 验证药材
if (!editForm.herbs || editForm.herbs.length === 0) {
feedback.msgError('请至少添加一味药材')
return
}
for (let i = 0; i < editForm.herbs.length; i++) {
const herb = editForm.herbs[i]
if (!herb.name || !herb.name.trim()) {
feedback.msgError(`${i + 1}味药材名称不能为空`)
return
}
if (!herb.dosage || herb.dosage <= 0) {
feedback.msgError(`${i + 1}味药材剂量必须大于0`)
return
}
}
submitLoading.value = true
try {
const params = { ...editForm }
if (editMode.value === 'add') {
await prescriptionLibraryAdd(params)
feedback.msgSuccess('添加成功')
} else {
await prescriptionLibraryEdit(params)
feedback.msgSuccess('编辑成功')
}
showEdit.value = false
getLists()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
// 删除处方
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除该处方吗?')
await prescriptionLibraryDelete({ id })
feedback.msgSuccess('删除成功')
getLists()
}
onMounted(() => {
getLists()
})
</script>
<style scoped lang="scss">
.prescription-library {
padding: 20px;
}
</style>