更新
This commit is contained in:
@@ -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>
|
||||
@@ -2,157 +2,419 @@
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="创建处方单"
|
||||
size="800px"
|
||||
size="90%"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
label-width="100px"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
class="prescription-form"
|
||||
>
|
||||
<!-- 患者基本信息 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">患者信息</div>
|
||||
<el-form-item label="姓名">
|
||||
<span>{{ formData.patient_name }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="性别">
|
||||
<span>{{ formData.gender }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="年龄">
|
||||
<span>{{ formData.age }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="诊号">
|
||||
<span>{{ formData.diagnosis_no }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="日期">
|
||||
<span>{{ formData.date }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="科室">
|
||||
<span>{{ formData.department }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="费别">
|
||||
<span>{{ formData.fee_type }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<span>{{ formData.phone }}</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 诊断信息 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">诊断信息</div>
|
||||
<el-form-item label="诊断" prop="diagnosis">
|
||||
<el-input
|
||||
v-model="formData.diagnosis"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入诊断信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 药品信息 -->
|
||||
<div class="info-section">
|
||||
<div class="section-title">
|
||||
药品信息
|
||||
<el-button type="primary" size="small" @click="handleAddMedicine">
|
||||
添加药品
|
||||
</el-button>
|
||||
</div>
|
||||
<div
|
||||
v-for="(medicine, index) in formData.medicines"
|
||||
:key="index"
|
||||
class="medicine-item"
|
||||
>
|
||||
<el-form-item :label="`药品${index + 1}`">
|
||||
<el-input
|
||||
v-model="medicine.name"
|
||||
placeholder="药品名称"
|
||||
class="mb-2"
|
||||
/>
|
||||
<el-input
|
||||
v-model="medicine.usage"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="用法用量"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@click="handleRemoveMedicine(index)"
|
||||
class="mt-2"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-form-item label="处方名称" prop="prescription_name">
|
||||
<el-input
|
||||
v-model="formData.prescription_name"
|
||||
placeholder="请输入处方名称"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="处方类型" prop="prescription_type">
|
||||
<el-select v-model="formData.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-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="患者姓名" prop="patient_name">
|
||||
<el-input v-model="formData.patient_name" placeholder="请输入患者姓名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="门诊号" prop="visit_no">
|
||||
<el-input v-model="formData.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="formData.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="formData.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="formData.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="formData.tongue" placeholder="请输入面象" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="舌象" prop="tongue_image">
|
||||
<el-input v-model="formData.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="formData.pulse" placeholder="请输入脉象" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="脉象详情" prop="pulse_condition">
|
||||
<el-input v-model="formData.pulse_condition" placeholder="请输入脉象详情" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="临床诊断" prop="clinical_diagnosis">
|
||||
<el-input
|
||||
v-model="formData.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"
|
||||
>
|
||||
添加药材
|
||||
</el-button>
|
||||
<el-table :data="formData.herbs" class="mt-2" border>
|
||||
<el-table-column label="序号" type="index" width="60" />
|
||||
<el-table-column label="药材名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.name"
|
||||
placeholder="请输入药材名称"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剂量(克)" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.dosage"
|
||||
:min="0"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
placeholder="剂量"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="removeHerb($index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="剂数" prop="dose_count">
|
||||
<el-input-number
|
||||
v-model="formData.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="formData.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="usage_days">
|
||||
<el-input-number
|
||||
v-model="formData.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="formData.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="formData.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="formData.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="formData.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="formData.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="formData.doctor_name" placeholder="请输入医师姓名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否共享" prop="is_shared">
|
||||
<el-switch
|
||||
v-model="formData.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>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
<el-button type="primary" @click="handleSave" :loading="submitLoading">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { prescriptionAdd } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
interface Medicine {
|
||||
name: string
|
||||
usage: string
|
||||
}
|
||||
|
||||
interface PrescriptionForm {
|
||||
appointment_id: number
|
||||
patient_name: string
|
||||
gender: string
|
||||
age: string
|
||||
diagnosis_no: string
|
||||
date: string
|
||||
department: string
|
||||
fee_type: string
|
||||
phone: string
|
||||
diagnosis: string
|
||||
medicines: Medicine[]
|
||||
}
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const formRef = ref()
|
||||
const formData = reactive<PrescriptionForm>({
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
appointment_id: 0,
|
||||
diagnosis_id: 0,
|
||||
prescription_name: '',
|
||||
prescription_type: '浓缩水丸',
|
||||
patient_name: '',
|
||||
gender: '',
|
||||
age: '',
|
||||
diagnosis_no: '',
|
||||
date: '',
|
||||
department: '',
|
||||
fee_type: '',
|
||||
phone: '',
|
||||
diagnosis: '',
|
||||
medicines: []
|
||||
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
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
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 addHerb = () => {
|
||||
formData.herbs.push({
|
||||
name: '',
|
||||
dosage: 0
|
||||
})
|
||||
}
|
||||
|
||||
// 删除药材
|
||||
const removeHerb = (index: number) => {
|
||||
formData.herbs.splice(index, 1)
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.appointment_id = 0
|
||||
formData.diagnosis_id = 0
|
||||
formData.prescription_name = ''
|
||||
formData.prescription_type = '浓缩水丸'
|
||||
formData.patient_name = ''
|
||||
formData.gender = 1
|
||||
formData.age = 0
|
||||
formData.visit_no = ''
|
||||
formData.prescription_date = new Date().toISOString().split('T')[0]
|
||||
formData.tongue = ''
|
||||
formData.tongue_image = ''
|
||||
formData.pulse = ''
|
||||
formData.pulse_condition = ''
|
||||
formData.clinical_diagnosis = ''
|
||||
formData.herbs = []
|
||||
formData.dose_count = 7
|
||||
formData.dose_unit = '剂'
|
||||
formData.usage_days = 7
|
||||
formData.usage_instruction = '水煎服,一日二次'
|
||||
formData.usage_time = '饭前'
|
||||
formData.usage_way = '温水送服'
|
||||
formData.dietary_taboo = []
|
||||
formData.usage_notes = ''
|
||||
formData.doctor_name = ''
|
||||
formData.is_shared = 0
|
||||
}
|
||||
|
||||
// 打开抽屉
|
||||
const open = (data: any) => {
|
||||
formData.appointment_id = data.id
|
||||
resetForm()
|
||||
|
||||
formData.appointment_id = data.id || 0
|
||||
formData.diagnosis_id = data.diagnosis_id || 0
|
||||
formData.patient_name = data.patient_name || ''
|
||||
formData.gender = data.patient_gender || ''
|
||||
formData.age = data.patient_age || ''
|
||||
formData.diagnosis_no = data.id.toString()
|
||||
formData.date = data.appointment_date || ''
|
||||
formData.department = '中医科'
|
||||
formData.fee_type = data.appointment_type_desc || ''
|
||||
formData.phone = data.patient_phone || ''
|
||||
formData.diagnosis = ''
|
||||
formData.medicines = []
|
||||
formData.gender = data.patient_gender === '男' ? 1 : 0
|
||||
formData.age = parseInt(data.patient_age) || 0
|
||||
formData.visit_no = data.id?.toString() || ''
|
||||
formData.prescription_date = data.appointment_date || new Date().toISOString().split('T')[0]
|
||||
formData.doctor_name = data.doctor_name || ''
|
||||
|
||||
visible.value = true
|
||||
}
|
||||
@@ -163,48 +425,41 @@ const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 添加药品
|
||||
const handleAddMedicine = () => {
|
||||
formData.medicines.push({
|
||||
name: '',
|
||||
usage: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 删除药品
|
||||
const handleRemoveMedicine = (index: number) => {
|
||||
formData.medicines.splice(index, 1)
|
||||
}
|
||||
|
||||
// 保存处方单
|
||||
const handleSave = async () => {
|
||||
if (!formData.diagnosis) {
|
||||
feedback.msgWarning('请输入诊断信息')
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate()
|
||||
|
||||
// 验证药材
|
||||
if (!formData.herbs || formData.herbs.length === 0) {
|
||||
feedback.msgError('请至少添加一味药材')
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.medicines.length === 0) {
|
||||
feedback.msgWarning('请至少添加一个药品')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证药品信息
|
||||
for (const medicine of formData.medicines) {
|
||||
if (!medicine.name || !medicine.usage) {
|
||||
feedback.msgWarning('请完善药品信息')
|
||||
for (let i = 0; i < formData.herbs.length; i++) {
|
||||
const herb = formData.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 {
|
||||
// TODO: 调用保存处方单的API
|
||||
// await savePrescription(formData)
|
||||
|
||||
await prescriptionAdd(formData)
|
||||
feedback.msgSuccess('处方单创建成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
feedback.msgError('保存失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,41 +470,26 @@ defineExpose({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.prescription-form {
|
||||
.info-section {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: #303133;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 12px;
|
||||
|
||||
span {
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
padding: 0 20px;
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.medicine-item {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="call-record-panel">
|
||||
<el-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
title="视频通话与云端录制"
|
||||
>
|
||||
<p class="call-record-tip">
|
||||
下列为与本诊单关联的通话记录。若在腾讯云 TRTC
|
||||
控制台开启了云端录制并填写回调地址,录制生成后会自动写入「录制回放」。
|
||||
</p>
|
||||
<p class="call-record-tip muted">
|
||||
回调 URL 示例:<code>{{ callbackHint }}</code>
|
||||
(若配置了 <code>trtc.recording_callback_token</code>,请在 URL 后附加
|
||||
<code>?token=你的密钥</code>)
|
||||
</p>
|
||||
</el-alert>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" border stripe empty-text="暂无通话记录">
|
||||
<el-table-column label="开始时间" width="170" prop="start_time_text" />
|
||||
<el-table-column label="结束时间" width="170" prop="end_time_text" />
|
||||
<el-table-column label="通话类型" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.call_type === 1 ? '语音' : '视频' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时长" width="110" prop="duration_text" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ statusText(row.status) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="录制" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.recording_status_text || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="录制回放" min-width="280">
|
||||
<template #default="{ row }">
|
||||
<div v-if="(row.recording_urls_list || []).length" class="recording-list">
|
||||
<div
|
||||
v-for="(url, idx) in row.recording_urls_list"
|
||||
:key="idx"
|
||||
class="recording-item"
|
||||
>
|
||||
<video
|
||||
v-if="isPlayableVideo(url)"
|
||||
:src="url"
|
||||
controls
|
||||
preload="metadata"
|
||||
class="recording-video"
|
||||
/>
|
||||
<el-link v-else :href="url" target="_blank" type="primary">
|
||||
打开链接 {{ idx + 1 }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">暂无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { getCallRecords } from '@/api/tcm'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<any[]>([])
|
||||
|
||||
const callbackHint = computed(() => {
|
||||
const origin = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
return `${origin}/api/trtc/recording-notify`
|
||||
})
|
||||
|
||||
const load = async () => {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
rows.value = (await getCallRecords({ diagnosis_id: props.diagnosisId })) || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: load })
|
||||
|
||||
function statusText(status: number) {
|
||||
const m: Record<number, string> = {
|
||||
1: '进行中',
|
||||
2: '已结束',
|
||||
3: '未接听',
|
||||
4: '已取消'
|
||||
}
|
||||
return m[status] ?? '—'
|
||||
}
|
||||
|
||||
function isPlayableVideo(url: string) {
|
||||
if (!url || typeof url !== 'string') return false
|
||||
return /\.(mp4|webm|ogg)(\?|$)/i.test(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.call-record-panel {
|
||||
.call-record-tip {
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.5;
|
||||
font-size: 13px;
|
||||
&.muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
code {
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.recording-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.recording-video {
|
||||
max-width: 100%;
|
||||
max-height: 180px;
|
||||
border-radius: 4px;
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
<div class="im-chat-record-panel">
|
||||
<el-alert type="info" show-icon :closable="false" class="mb-4" title="说明">
|
||||
<p class="panel-tip">
|
||||
展示腾讯云 IM 单聊记录:已合并患者 <code>{{ patientImHint }}</code> 与
|
||||
<strong>所有医生 / 医助账号</strong>(<code>doctor_*</code>)分别产生的会话,按时间排序。
|
||||
数据来自 IM 漫游;若未开通漫游或某账号与该患者无会话,对应侧无消息。
|
||||
</p>
|
||||
</el-alert>
|
||||
|
||||
<div class="toolbar mb-3">
|
||||
<el-button type="primary" link :loading="loading" @click="load">
|
||||
<el-icon class="mr-1"><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="chat-wrap">
|
||||
<template v-if="!loading && rows.length">
|
||||
<div class="chat-list">
|
||||
<div
|
||||
v-for="row in rows"
|
||||
:key="row.msg_id"
|
||||
class="chat-row"
|
||||
:class="row.is_from_doctor ? 'from-doctor' : 'from-patient'"
|
||||
>
|
||||
<div class="meta">
|
||||
<span class="name">{{ senderLabel(row) }}</span>
|
||||
<span class="time">{{ formatTime(row.time) }}</span>
|
||||
<el-tag v-if="typeLabel(row)" size="small" type="info" class="ml-2">
|
||||
{{ typeLabel(row) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="bubble">
|
||||
<template v-if="row.msg_type === 'image' && row.image_url">
|
||||
<el-image
|
||||
:src="row.image_url"
|
||||
:preview-src-list="[row.image_url]"
|
||||
fit="contain"
|
||||
class="chat-img"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="(row.msg_type === 'file' || row.msg_type === 'sound' || row.msg_type === 'video') && row.file_url">
|
||||
<el-link :href="row.file_url" target="_blank" type="primary">
|
||||
{{ row.file_name || '打开文件' }}
|
||||
</el-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="fr in [parseImFriendly(row)]" :key="row.msg_id + '-body'">
|
||||
<div v-if="fr" class="friendly-text">
|
||||
<div class="friendly-main">{{ fr.main }}</div>
|
||||
<div v-if="fr.sub" class="friendly-sub">{{ fr.sub }}</div>
|
||||
</div>
|
||||
<div v-else-if="row.msg_type === 'text' && row.text" class="text-content">
|
||||
{{ row.text }}
|
||||
</div>
|
||||
<div v-else-if="row.text" class="text-content">{{ row.text }}</div>
|
||||
<span v-else class="muted">(无法展示该消息类型)</span>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else-if="!loading" description="暂无 IM 聊天记录" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { getImChatMessages } from '@/api/tcm'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<any[]>([])
|
||||
const patientImId = ref('')
|
||||
const patientName = ref('')
|
||||
|
||||
const patientImHint = computed(() => patientImId.value || 'patient_*')
|
||||
|
||||
function formatTime(ts: number | undefined) {
|
||||
if (ts == null || !ts) return '—'
|
||||
const sec = ts > 1e12 ? Math.floor(ts / 1000) : ts
|
||||
return dayjs.unix(sec).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
type FriendlyParse = { main: string; sub?: string; tag?: string }
|
||||
|
||||
/** 业务时间:支持秒级时间戳或毫秒(字符串数字) */
|
||||
function formatBizTime(t: unknown): string | undefined {
|
||||
if (t == null || t === '') return undefined
|
||||
const n =
|
||||
typeof t === 'string' && /^\d+$/.test(t.trim())
|
||||
? parseInt(t.trim(), 10)
|
||||
: Number(t)
|
||||
if (!Number.isFinite(n) || n <= 0) return undefined
|
||||
return n > 1e12
|
||||
? dayjs(n).format('YYYY-MM-DD HH:mm:ss')
|
||||
: dayjs.unix(n).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
/** 将 IM 自定义 / 信令 JSON 解析为可读文案(医生进诊室、音视频通话等) */
|
||||
function parseImBusinessPayload(raw: string): FriendlyParse | null {
|
||||
const s = raw.trim()
|
||||
if (!s.startsWith('{')) return null
|
||||
let o: any
|
||||
try {
|
||||
o = JSON.parse(s)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!('businessID' in o) && !('cmd' in o)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (o.businessID === 'doctor_entered_consult_room') {
|
||||
const t = formatBizTime(o.time)
|
||||
return { main: '医生已进入诊室', sub: t, tag: '诊室' }
|
||||
}
|
||||
|
||||
/** 小程序端:患者打开与医生的会话时发送(与 TUIKit/chat.vue 一致) */
|
||||
if (o.businessID === 'patient_opened_chat') {
|
||||
const parts: string[] = []
|
||||
if (o.patientName) parts.push(`患者:${o.patientName}`)
|
||||
if (o.patientId != null && o.patientId !== '') parts.push(`患者 ID:${o.patientId}`)
|
||||
if (o.doctorId != null && o.doctorId !== '') parts.push(`关联医生:${o.doctorId}`)
|
||||
const t = formatBizTime(o.time)
|
||||
if (t) parts.push(t)
|
||||
return { main: '患者进入聊天', sub: parts.length ? parts.join(' · ') : undefined, tag: '诊室' }
|
||||
}
|
||||
|
||||
if (o.businessID === 'user_typing_status') {
|
||||
return { main: '对方正在输入…', tag: '状态' }
|
||||
}
|
||||
|
||||
if (o.businessID === 'consultation_complete') {
|
||||
return { main: '问诊已完成', sub: formatBizTime(o.time), tag: '诊室' }
|
||||
}
|
||||
|
||||
if (o.businessID === 1 || o.businessID === '1') {
|
||||
let inner: any = o.data
|
||||
if (typeof inner === 'string') {
|
||||
try {
|
||||
inner = JSON.parse(inner)
|
||||
} catch {
|
||||
return { main: '通话信令', sub: '内层数据解析失败', tag: '通话' }
|
||||
}
|
||||
}
|
||||
if (inner && typeof inner === 'object') {
|
||||
return parseRtcInner(inner)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (o.businessID === 'rtc_call' || o.cmd) {
|
||||
return parseRtcInner(o)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parseRtcInner(inner: any): FriendlyParse {
|
||||
const callType = inner.call_type
|
||||
const callTypeLabel =
|
||||
callType === 2 ? '视频通话' : callType === 1 ? '语音通话' : '通话'
|
||||
const cmd = String(inner.cmd || '')
|
||||
const cmdMap: Record<string, string> = {
|
||||
hangup: '已结束',
|
||||
invite: '发起通话',
|
||||
linebusy: '对方忙线',
|
||||
cancel: '已取消',
|
||||
reject: '已拒绝',
|
||||
accept: '已接听',
|
||||
timeout: '无人接听'
|
||||
}
|
||||
const action = cmdMap[cmd] || (cmd ? `状态:${cmd}` : '')
|
||||
const main = action ? `${callTypeLabel} · ${action}` : callTypeLabel
|
||||
const parts: string[] = []
|
||||
if (inner.inviter) parts.push(`发起方 ${inner.inviter}`)
|
||||
if (inner.invitee) parts.push(`对方 ${inner.invitee}`)
|
||||
if (inner.groupID) parts.push(`群组 ${inner.groupID}`)
|
||||
return {
|
||||
main,
|
||||
sub: parts.length ? parts.join(',') : undefined,
|
||||
tag: '通话'
|
||||
}
|
||||
}
|
||||
|
||||
function parseImFriendly(row: any): FriendlyParse | null {
|
||||
const raw = (row.text || '').trim()
|
||||
if (!raw) return null
|
||||
const looksLikeBizJson =
|
||||
raw.startsWith('{') && (/\bbusinessID\b/.test(raw) || /\bcmd\b/.test(raw))
|
||||
if (row.msg_type === 'custom' || looksLikeBizJson) {
|
||||
return parseImBusinessPayload(raw)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function typeLabel(row: any) {
|
||||
const fr = parseImFriendly(row)
|
||||
if (fr?.tag) return fr.tag
|
||||
const m: Record<string, string> = {
|
||||
text: '',
|
||||
image: '图片',
|
||||
file: '文件',
|
||||
sound: '语音',
|
||||
video: '视频',
|
||||
location: '位置',
|
||||
custom: '自定义',
|
||||
face: '表情',
|
||||
other: ''
|
||||
}
|
||||
return m[row.msg_type] || (row.msg_type !== 'other' ? row.msg_type : '')
|
||||
}
|
||||
|
||||
function senderLabel(row: any) {
|
||||
if (row.is_from_doctor) {
|
||||
return row.from_staff_name ? `医生(${row.from_staff_name})` : '医生/员工'
|
||||
}
|
||||
return patientName.value ? `患者(${patientName.value})` : '患者'
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = (await getImChatMessages({ diagnosis_id: props.diagnosisId })) as {
|
||||
lists?: any[]
|
||||
patient_im_id?: string
|
||||
patient_name?: string
|
||||
}
|
||||
rows.value = res?.lists || []
|
||||
patientImId.value = res?.patient_im_id || ''
|
||||
patientName.value = res?.patient_name || ''
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: load })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.im-chat-record-panel {
|
||||
min-height: 200px;
|
||||
}
|
||||
.panel-tip {
|
||||
margin: 0;
|
||||
line-height: 1.55;
|
||||
font-size: 13px;
|
||||
code {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.chat-wrap {
|
||||
min-height: 120px;
|
||||
}
|
||||
.chat-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: min(60vh, 520px);
|
||||
overflow-y: auto;
|
||||
padding: 4px 8px 12px;
|
||||
}
|
||||
.chat-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 88%;
|
||||
&.from-patient {
|
||||
align-self: flex-start;
|
||||
.bubble {
|
||||
background: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
&.from-doctor {
|
||||
align-self: flex-end;
|
||||
align-items: flex-end;
|
||||
.meta {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.bubble {
|
||||
background: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
}
|
||||
}
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 6px;
|
||||
.name {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
.bubble {
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.text-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.chat-img {
|
||||
max-width: 240px;
|
||||
max-height: 200px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.muted {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
.friendly-text {
|
||||
.friendly-main {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.friendly-sub {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -586,6 +586,28 @@
|
||||
</div>
|
||||
<el-empty v-else description="请先保存诊单,开方后病历将在此显示" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="回访记录" name="visit" :disabled="!formData.id" v-perms="['tcm.diagnosis/huifang']" v-if="hasPermission(['tcm.diagnosis/huifang'])">
|
||||
<call-record-panel
|
||||
v-if="formData.id"
|
||||
ref="callRecordPanelRef"
|
||||
:diagnosis-id="Number(formData.id)"
|
||||
/>
|
||||
<el-empty v-else description="请先保存诊单后再查看回访记录" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-if="hasPermission(['tcm.diagnosis/chat'])"
|
||||
label="聊天记录"
|
||||
name="chat"
|
||||
:disabled="!formData.id"
|
||||
lazy
|
||||
>
|
||||
<im-chat-record-panel
|
||||
v-if="formData.id"
|
||||
:diagnosis-id="Number(formData.id)"
|
||||
/>
|
||||
<el-empty v-else description="请先保存诊单后再查看聊天记录" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 处方详情(查看病历) -->
|
||||
@@ -613,18 +635,21 @@
|
||||
import { tcmDiagnosisAdd, tcmDiagnosisEdit, tcmDiagnosisDetail, checkPhone, checkIdCard } from '@/api/tcm'
|
||||
import { getDictData } from '@/api/app'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { hasPermission } from '@/utils/perm'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
import BloodRecordList from './components/BloodRecordList.vue'
|
||||
import CaseRecordList from './components/CaseRecordList.vue'
|
||||
import ImChatRecordPanel from './components/ImChatRecordPanel.vue'
|
||||
import TcmPrescription from '@/components/tcm-prescription/index.vue'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const appStore = useAppStore()
|
||||
const getImageUrl = (url: string) => (url?.indexOf?.('http') === 0 ? url : appStore.getImageUrl(url || ''))
|
||||
const caseRecordListRef = ref()
|
||||
const callRecordPanelRef = ref<{ refresh?: () => void } | null>(null)
|
||||
const prescriptionRef = ref()
|
||||
|
||||
const visible = ref(false)
|
||||
@@ -634,6 +659,12 @@ const submitting = ref(false)
|
||||
const activeTab = ref('basic')
|
||||
const drawerTitle = computed(() => (mode.value === 'add' ? '新增诊单' : '编辑诊单'))
|
||||
|
||||
watch([visible, activeTab], () => {
|
||||
if (visible.value && activeTab.value === 'chat' && !hasPermission(['tcm.diagnosis/chat'])) {
|
||||
activeTab.value = 'basic'
|
||||
}
|
||||
})
|
||||
|
||||
const formData = ref({
|
||||
id: '',
|
||||
patient_id: '',
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
<img :src="qrcodeUrl" alt="小程序二维码" class="w-64 h-64 border border-gray-200 rounded" />
|
||||
<div class="mt-4 text-sm text-gray-600">
|
||||
<div>患者:{{ currentQRCodePatient?.patient_name }}</div>
|
||||
<div class="mt-2">请使用微信扫描二维码</div>
|
||||
<div class="mt-2">请使用企业微信扫描二维码,然后转发给患者</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center text-gray-500">
|
||||
|
||||
@@ -84,8 +84,8 @@
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="hasIncomingCall || callConnected" id="call-container" class="call-container">
|
||||
<TUICallKit />
|
||||
<div v-if="hasIncomingCall || callConnected" id="call-container" class="call-container phone-call-shell">
|
||||
<TUICallKit class="TUICallKit-mobile" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="initialized" class="waiting-container">
|
||||
@@ -98,18 +98,30 @@
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的 TUICallKit,用于接收来电信号 -->
|
||||
<div v-show="false">
|
||||
<TUICallKit v-if="initialized && !hasIncomingCall && !callConnected" />
|
||||
<!-- 离屏挂载:手机视口尺寸,避免 display:none 宽高为 0;与小程序端比例接近 -->
|
||||
<div
|
||||
v-if="initialized && !hasIncomingCall && !callConnected"
|
||||
class="phone-call-kit-host phone-call-kit-host--offscreen"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<TUICallKit class="TUICallKit-mobile" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted, computed, onMounted } from 'vue'
|
||||
import { ref, onUnmounted, computed, onMounted, nextTick } from 'vue'
|
||||
import { Phone } from '@element-plus/icons-vue'
|
||||
import { TUICallKitServer, TUICallKit, STATUS } from '@tencentcloud/call-uikit-vue'
|
||||
import {
|
||||
TUICallKitServer,
|
||||
TUICallKit,
|
||||
STATUS,
|
||||
TUIStore,
|
||||
StoreName,
|
||||
NAME,
|
||||
CallRole
|
||||
} from '@tencentcloud/call-uikit-vue'
|
||||
import { ElMessage, ElNotification } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
@@ -128,22 +140,109 @@ const hasIncomingCall = ref(false)
|
||||
const callConnected = ref(false) // 添加通话连接状态
|
||||
const incomingCallInfo = ref<any>(null)
|
||||
const statusCheckTimer = ref<any>(null)
|
||||
/** 从 TUIStore 同步:解决「未打开本页时来电,进入后 statusChanged 不触发」的问题 */
|
||||
let storeUnwatch: (() => void) | null = null
|
||||
|
||||
// 页面加载时自动初始化
|
||||
onMounted(() => {
|
||||
initPatient()
|
||||
})
|
||||
|
||||
// 定时检查状态(作为备用方案)
|
||||
/**
|
||||
* 与 SDK 内 CallStatus 一致:idle / calling / connected
|
||||
* statusChanged 只在状态「变化」时回调;晚进页面时可能已在 calling,需读 Store。
|
||||
*/
|
||||
function syncIncomingFromTUIStore(from: string) {
|
||||
try {
|
||||
const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) as string
|
||||
const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE) as string
|
||||
const isGroup = !!TUIStore.getData(StoreName.CALL, NAME.IS_GROUP)
|
||||
|
||||
const wasIncoming = hasIncomingCall.value
|
||||
|
||||
// 被叫振铃:calling + callee
|
||||
if (callStatus === 'calling' && callRole === CallRole.CALLEE) {
|
||||
hasIncomingCall.value = true
|
||||
incomingCallInfo.value = { isGroup, from }
|
||||
if (!wasIncoming) {
|
||||
ElMessage.info('检测到待接听的来电,请点击接听')
|
||||
ElNotification({
|
||||
title: '来电提醒',
|
||||
message: '有待接听的视频通话',
|
||||
type: 'warning',
|
||||
duration: 0,
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
callConnected.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 已接通
|
||||
if (callStatus === 'connected') {
|
||||
hasIncomingCall.value = false
|
||||
incomingCallInfo.value = null
|
||||
callConnected.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 空闲
|
||||
if (callStatus === 'idle') {
|
||||
if (hasIncomingCall.value || callConnected.value) {
|
||||
hasIncomingCall.value = false
|
||||
callConnected.value = false
|
||||
incomingCallInfo.value = null
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('syncIncomingFromTUIStore:', from, e)
|
||||
}
|
||||
}
|
||||
|
||||
function onStoreCallField() {
|
||||
syncIncomingFromTUIStore('watch')
|
||||
}
|
||||
|
||||
function bindTUIStoreWatch() {
|
||||
if (storeUnwatch) {
|
||||
storeUnwatch()
|
||||
storeUnwatch = null
|
||||
}
|
||||
const opts = {
|
||||
[NAME.CALL_STATUS]: onStoreCallField,
|
||||
[NAME.CALL_ROLE]: onStoreCallField
|
||||
}
|
||||
try {
|
||||
TUIStore.watch(StoreName.CALL, opts)
|
||||
storeUnwatch = () => {
|
||||
try {
|
||||
TUIStore.unwatch(StoreName.CALL, opts)
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('TUIStore.watch 失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 定时从 Store 拉状态(TIM 同步可能有延迟,前 45 秒加密轮询)
|
||||
const startStatusCheck = () => {
|
||||
if (statusCheckTimer.value) {
|
||||
clearInterval(statusCheckTimer.value)
|
||||
}
|
||||
|
||||
let ticks = 0
|
||||
const maxTicks = 90
|
||||
statusCheckTimer.value = setInterval(() => {
|
||||
// 这里可以添加状态检查逻辑
|
||||
console.log('定时检查 - 当前时间:', new Date().toLocaleTimeString())
|
||||
}, 5000)
|
||||
ticks += 1
|
||||
syncIncomingFromTUIStore('poll')
|
||||
if (ticks >= maxTicks) {
|
||||
if (statusCheckTimer.value) {
|
||||
clearInterval(statusCheckTimer.value)
|
||||
statusCheckTimer.value = null
|
||||
}
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 监听状态变化
|
||||
@@ -267,10 +366,18 @@ const initPatient = async () => {
|
||||
|
||||
ElMessage.info('等待初始化完成...')
|
||||
|
||||
// 等待初始化完成
|
||||
// 等待 TIM/引擎就绪(未在本页时收到的邀请会在登录后由 SDK 同步)
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
initialized.value = true
|
||||
await nextTick()
|
||||
await new Promise((r) => setTimeout(r, 300))
|
||||
|
||||
// 需在 TUICallKit 挂载后订阅 Store(见模板里 v-if="initialized && ..." 的隐藏组件)
|
||||
bindTUIStoreWatch()
|
||||
syncIncomingFromTUIStore('post-init')
|
||||
startStatusCheck()
|
||||
|
||||
ElMessage.success('医疗助理端初始化成功,等待来电...')
|
||||
|
||||
console.log('=== 医疗助理端初始化完成 ===')
|
||||
@@ -278,9 +385,6 @@ const initPatient = async () => {
|
||||
console.log('等待来电中...')
|
||||
console.log('请在医生端发起群组通话,目标用户ID应该是:', userId)
|
||||
|
||||
// 启动状态检查
|
||||
startStatusCheck()
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('=== 初始化失败 ===', error)
|
||||
ElMessage.error(error.message || '初始化失败')
|
||||
@@ -289,6 +393,29 @@ const initPatient = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 接听后自动关闭本地麦克风(助理端只听不说) */
|
||||
async function muteLocalMicAfterAccept(): Promise<boolean> {
|
||||
await nextTick()
|
||||
await new Promise((r) => setTimeout(r, 400))
|
||||
try {
|
||||
const server = TUICallKitServer as typeof TUICallKitServer & {
|
||||
closeMicrophone?: () => Promise<void>
|
||||
}
|
||||
if (typeof server.closeMicrophone === 'function') {
|
||||
await server.closeMicrophone()
|
||||
return true
|
||||
}
|
||||
const engine = server.getTUICallEngineInstance?.()
|
||||
if (engine && typeof engine.closeMicrophone === 'function') {
|
||||
await engine.closeMicrophone()
|
||||
return true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('接听后关闭麦克风失败(可在通话条手动关麦):', e)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 接听来电
|
||||
const acceptCall = async () => {
|
||||
try {
|
||||
@@ -297,6 +424,10 @@ const acceptCall = async () => {
|
||||
ElMessage.success('已接听通话')
|
||||
hasIncomingCall.value = false
|
||||
callConnected.value = true // 设置通话连接状态
|
||||
const muted = await muteLocalMicAfterAccept()
|
||||
if (muted) {
|
||||
ElMessage.info('已关闭本地麦克风')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('接听失败:', error)
|
||||
ElMessage.error(error.message || '接听失败')
|
||||
@@ -329,6 +460,11 @@ const reset = () => {
|
||||
onUnmounted(() => {
|
||||
if (statusCheckTimer.value) {
|
||||
clearInterval(statusCheckTimer.value)
|
||||
statusCheckTimer.value = null
|
||||
}
|
||||
if (storeUnwatch) {
|
||||
storeUnwatch()
|
||||
storeUnwatch = null
|
||||
}
|
||||
|
||||
if (initialized.value) {
|
||||
@@ -370,12 +506,23 @@ onUnmounted(() => {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 手机视口:375×667(逻辑像素),圆角边框模拟机身 */
|
||||
.phone-call-shell {
|
||||
width: 375px;
|
||||
height: 667px;
|
||||
max-width: 100%;
|
||||
margin: 20px auto 0;
|
||||
padding: 0;
|
||||
border-radius: 36px;
|
||||
border: 12px solid #2c2c2e;
|
||||
box-shadow:
|
||||
0 0 0 2px #3a3a3c inset,
|
||||
0 16px 48px rgba(0, 0, 0, 0.22);
|
||||
background: #000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.call-container {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
margin-top: 20px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
@@ -391,7 +538,7 @@ onUnmounted(() => {
|
||||
|
||||
.waiting-container {
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
min-height: 360px;
|
||||
margin-top: 20px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
@@ -400,11 +547,29 @@ onUnmounted(() => {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 离屏监听:保持真实手机宽高,利于 SDK 内部布局 */
|
||||
.phone-call-kit-host {
|
||||
width: 375px;
|
||||
height: 667px;
|
||||
overflow: hidden;
|
||||
border-radius: 28px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.phone-call-kit-host--offscreen {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 确保 TUICallKit 组件正确显示 */
|
||||
:deep(.call-container) {
|
||||
.tui-call-kit {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
video {
|
||||
@@ -434,7 +599,7 @@ onUnmounted(() => {
|
||||
:deep(.tui-call-kit-window) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
min-height: 500px !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
/* 确保视频容器撑满 */
|
||||
|
||||
Reference in New Issue
Block a user