更新
This commit is contained in:
+4
-2
@@ -2,13 +2,15 @@
|
||||
import { useDark, useThrottleFn, useWindowSize } from '@vueuse/core'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
|
||||
|
||||
import { ScreenEnum } from './enums/appEnums'
|
||||
import useAppStore from './stores/modules/app'
|
||||
import useSettingStore from './stores/modules/setting'
|
||||
import useUserStore from './stores/modules/user'
|
||||
import ChatNotifyToast from './components/chat-notify-toast/index.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const settingStore = useSettingStore()
|
||||
const userStore = useUserStore()
|
||||
const elConfig = {
|
||||
zIndex: 2000,
|
||||
locale: zhCn
|
||||
@@ -43,7 +45,7 @@ watch(
|
||||
<template>
|
||||
<el-config-provider :locale="elConfig.locale" :z-index="elConfig.zIndex">
|
||||
<router-view />
|
||||
|
||||
<ChatNotifyToast v-if="userStore.token" />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** 获取患者打开会话通知(轮询接口,获取后即消费) */
|
||||
export function getChatNotifications() {
|
||||
return request.get({ url: '/chat/notifications' })
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<TransitionGroup
|
||||
name="chat-notify-slide"
|
||||
tag="div"
|
||||
class="chat-notify-container"
|
||||
>
|
||||
<div
|
||||
v-for="item in notifications"
|
||||
:key="item.id"
|
||||
class="chat-notify-card"
|
||||
:class="{ 'chat-notify-card-complete': item.type === 'consultation_complete' }"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<div class="chat-notify-icon" :class="{ 'chat-notify-icon-complete': item.type === 'consultation_complete' }">
|
||||
<el-icon :size="22">
|
||||
<ChatDotRound v-if="item.type !== 'consultation_complete'" />
|
||||
<CircleCheck v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="chat-notify-body">
|
||||
<div class="chat-notify-title">{{ item.type === 'consultation_complete' ? '面诊结束' : '患者打开会话' }}</div>
|
||||
<div class="chat-notify-desc">
|
||||
<template v-if="item.type === 'consultation_complete'">
|
||||
<span class="patient-name">{{ item.patient_name || '患者' }}</span>
|
||||
的面诊已由
|
||||
<span class="doctor-name">{{ item.doctor_name || '医生' }}</span>
|
||||
完成,请及时跟进
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="patient-name">{{ item.patient_name || '患者' }}</span>
|
||||
已打开与您的会话,请及时查看
|
||||
</template>
|
||||
</div>
|
||||
<div class="chat-notify-time">{{ formatTime(item.created_at) }}</div>
|
||||
</div>
|
||||
<button class="chat-notify-close" @click.stop="remove(item.id)" aria-label="关闭">
|
||||
<el-icon :size="14"><Close /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ChatDotRound, CircleCheck, Close } from '@element-plus/icons-vue'
|
||||
import { getChatNotifications } from '@/api/chat'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
export interface ChatNotifyItem {
|
||||
id: string
|
||||
type?: 'patient_opened_chat' | 'consultation_complete'
|
||||
doctor_id?: number
|
||||
assistant_id?: number
|
||||
patient_id?: string
|
||||
patient_name: string
|
||||
doctor_name?: string
|
||||
diagnosis_id?: number
|
||||
created_at: number
|
||||
}
|
||||
|
||||
const notifications = ref<ChatNotifyItem[]>([])
|
||||
const pollTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const POLL_INTERVAL = 5000
|
||||
|
||||
function formatTime(ts: number) {
|
||||
if (!ts) return ''
|
||||
const d = new Date(ts * 1000)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - d.getTime()
|
||||
if (diff < 60000) return '刚刚'
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`
|
||||
return d.toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function remove(id: string) {
|
||||
notifications.value = notifications.value.filter((n) => n.id !== id)
|
||||
}
|
||||
|
||||
function handleClick(item: ChatNotifyItem) {
|
||||
remove(item.id)
|
||||
// 面诊结束:跳转到诊单列表并自动打开编辑
|
||||
if (item.type === 'consultation_complete' && item.diagnosis_id) {
|
||||
router.push({ path: '/tcm/diagnosis', query: { id: item.diagnosis_id } })
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNotifications() {
|
||||
try {
|
||||
const list = (await getChatNotifications()) as ChatNotifyItem[]
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
notifications.value = [...list, ...notifications.value].slice(0, 10)
|
||||
}
|
||||
} catch {
|
||||
// 静默失败,避免干扰用户
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchNotifications()
|
||||
pollTimer.value = setInterval(fetchNotifications, POLL_INTERVAL)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer.value) {
|
||||
clearInterval(pollTimer.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-notify-container {
|
||||
position: fixed;
|
||||
top: 72px;
|
||||
right: 24px;
|
||||
z-index: 3000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-width: 360px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chat-notify-container > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.chat-notify-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(34, 197, 94, 0.06);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
html.dark .chat-notify-card {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||
border-color: rgba(34, 197, 94, 0.3);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.chat-notify-card:hover {
|
||||
transform: translateX(-2px);
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(34, 197, 94, 0.12);
|
||||
}
|
||||
|
||||
html.dark .chat-notify-card:hover {
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.chat-notify-icon {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.chat-notify-icon-complete {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
}
|
||||
|
||||
.chat-notify-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-notify-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
html.dark .chat-notify-title {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.chat-notify-desc {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
html.dark .chat-notify-desc {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.patient-name {
|
||||
font-weight: 600;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.doctor-name {
|
||||
font-weight: 600;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.chat-notify-time {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
html.dark .chat-notify-time {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.chat-notify-close {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.chat-notify-close:hover {
|
||||
color: #64748b;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
html.dark .chat-notify-close:hover {
|
||||
color: #94a3b8;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* 入场动画 */
|
||||
.chat-notify-slide-enter-active,
|
||||
.chat-notify-slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-notify-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.chat-notify-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.chat-notify-slide-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -109,19 +109,25 @@
|
||||
|
||||
<div class="info-section">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="数量规格" prop="dose_count">
|
||||
<div class="flex gap-2 items-center">
|
||||
<el-input-number v-model="formData.dose_count" :min="1" :max="999" class="!w-28" />
|
||||
<el-select v-model="formData.dose_unit" placeholder="单位" class="!w-24">
|
||||
<el-option v-for="u in doseUnitOptions" :key="u" :label="u" :value="u" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="服用天数" prop="dose_count">
|
||||
<el-input-number v-model="formData.dose_count" :min="1" :max="999" class="!w-full" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用法" prop="usage_instruction">
|
||||
<el-input v-model="formData.usage_instruction" placeholder="如:水煎服一日二次" />
|
||||
<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-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="服用说明" prop="usage_instruction">
|
||||
<el-select v-model="formData.usage_instruction" placeholder="请选择服用说明" class="!w-full">
|
||||
<el-option label="饭前" value="饭前" />
|
||||
<el-option label="饭后" value="饭后" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -213,7 +219,7 @@
|
||||
<div class="prescription-footer">
|
||||
<div class="footer-left">
|
||||
<div class="footer-dosage">
|
||||
共{{ savedPrescription.dose_count }}{{ savedPrescription.dose_unit || '剂' }}, {{ savedPrescription.usage_instruction }}
|
||||
服用{{ savedPrescription.dose_count }}天, {{ savedPrescription.dose_unit || '浓缩水丸' }}, {{ savedPrescription.usage_instruction }}
|
||||
</div>
|
||||
<!-- <div class="footer-amount">金额 {{ savedPrescription.amount }}</div> -->
|
||||
</div>
|
||||
@@ -442,8 +448,8 @@ const formData = reactive<PrescriptionForm>({
|
||||
case_record: null as any,
|
||||
herbs: [],
|
||||
dose_count: 1,
|
||||
dose_unit: '剂',
|
||||
usage_instruction: '水煎服一日二次',
|
||||
dose_unit: '浓缩水丸',
|
||||
usage_instruction: '饭前',
|
||||
amount: 0,
|
||||
doctor_name: '',
|
||||
doctor_signature: ''
|
||||
@@ -606,8 +612,8 @@ const open = async (data: any, options?: { templateId?: number; stationName?: st
|
||||
formData.case_record = caseRecord
|
||||
formData.herbs = []
|
||||
formData.dose_count = 1
|
||||
formData.dose_unit = '剂'
|
||||
formData.usage_instruction = '水煎服一日二次'
|
||||
formData.dose_unit = '浓缩水丸'
|
||||
formData.usage_instruction = '饭前'
|
||||
formData.amount = 0
|
||||
formData.doctor_name = userStore.userInfo?.name || ''
|
||||
formData.doctor_signature = ''
|
||||
@@ -757,7 +763,7 @@ const handleSave = async () => {
|
||||
case_record: formData.case_record,
|
||||
herbs: formData.herbs,
|
||||
dose_count: formData.dose_count,
|
||||
dose_unit: formData.dose_unit || '剂',
|
||||
dose_unit: formData.dose_unit || '浓缩水丸',
|
||||
usage_instruction: formData.usage_instruction,
|
||||
amount: formData.amount,
|
||||
doctor_name: formData.doctor_name,
|
||||
@@ -830,8 +836,8 @@ const handleNewPrescription = async () => {
|
||||
formData.tongue = saved.tongue || ''
|
||||
formData.clinical_diagnosis = saved.clinical_diagnosis || ''
|
||||
formData.dose_count = 1
|
||||
formData.dose_unit = saved.dose_unit || '剂'
|
||||
formData.usage_instruction = '水煎服一日二次'
|
||||
formData.dose_unit = saved.dose_unit || '浓缩水丸'
|
||||
formData.usage_instruction = '饭前'
|
||||
formData.amount = 0
|
||||
formData.doctor_name = userStore.userInfo?.name || saved.doctor_name || ''
|
||||
formData.doctor_signature = ''
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="hasPermission(['tcm.diagnosis/assign'])" command="assign"><el-icon><User /></el-icon>指派</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.has_appointment && hasPermission(['tcm.diagnosis/videoQr'])" command="videoQr"><el-icon><Picture /></el-icon>视频二维码</el-dropdown-item>
|
||||
<el-dropdown-item v-if="hasPermission(['tcm.diagnosis/guahao'])" command="confirmQr"><el-icon><Picture /></el-icon>二维码</el-dropdown-item>
|
||||
<el-dropdown-item v-if="hasPermission(['tcm.diagnosis/guahao']) && row.has_appointment" command="confirmQr"><el-icon><Picture /></el-icon>二维码</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.has_appointment && hasPermission(['tcm.diagnosis/guahao'])" command="cancelApt"><el-icon><CircleClose /></el-icon>取消挂号</el-dropdown-item>
|
||||
<el-dropdown-item command="order" v-if="hasPermission(['tcm.diagnosis/order'])"><el-icon><Document /></el-icon>创建订单</el-dropdown-item>
|
||||
<el-dropdown-item v-if="hasPermission(['tcm.diagnosis/delete'])" command="delete" divided><el-icon><Delete /></el-icon><span class="text-danger">删除</span></el-dropdown-item>
|
||||
@@ -466,6 +466,8 @@ import EditPopup from './edit.vue'
|
||||
import DetailPopup from './detail.vue'
|
||||
import AppointmentPopup from './appointment.vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { nextTick } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 格式化时间戳为日期时间
|
||||
@@ -1137,12 +1139,18 @@ const handleDeleteRecord = async (id: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
onMounted(async () => {
|
||||
await getDictOptions()
|
||||
// 默认展示当天挂号,突出重点
|
||||
formData.appointment_date = todayStr.value
|
||||
getLists()
|
||||
fetchDateCounts()
|
||||
// 从 URL 带 id 进入时自动打开诊单编辑(如:面诊结束通知点击跳转)
|
||||
const id = route.query.id
|
||||
if (id && editRef.value) {
|
||||
nextTick(() => editRef.value.open('edit', Number(id)))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user