This commit is contained in:
Your Name
2026-03-06 14:27:24 +08:00
parent 9a1b67ca18
commit 52bb80e2f1
134 changed files with 5337 additions and 401 deletions
+7
View File
@@ -98,3 +98,10 @@ export function endCall(params: any) {
export function getCallRecords(params: any) {
return request.get({ url: '/tcm.diagnosis/getCallRecords', params })
}
// ========== 小程序分享 ==========
// 生成小程序码
export function generateMiniProgramQrcode(params: any) {
return request.post({ url: '/tcm.diagnosis/generateMiniProgramQrcode', params })
}
+169 -58
View File
@@ -77,9 +77,9 @@
/>
<!-- 有排班时显示日期和时间段 -->
<template v-else>
<div v-else class="appointment-time-container">
<!-- 日期选择 -->
<div class="date-selector mb-5">
<div class="date-selector">
<el-button
v-for="dateOption in dateOptions"
:key="dateOption.date"
@@ -91,30 +91,46 @@
</el-button>
</div>
<!-- 时间段网格 -->
<div v-if="form.date" 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.appointmentTime === slot.time
}"
@click="selectTimeSlot(slot)"
>
<div class="slot-time">{{ slot.time }}</div>
<el-tag
:type="slot.available ? 'success' : 'info'"
<!-- 时间段区域 -->
<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"
class="slot-badge"
:loading="refreshing"
@click="handleRefreshSlots"
>
{{ slot.available ? '可约' : '已约' }}
</el-tag>
<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.appointmentTime === 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>
</template>
</div>
</el-form-item>
<!-- 备注 -->
@@ -147,6 +163,7 @@
<script setup lang="ts">
import { ref, reactive, computed, 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'
@@ -169,6 +186,7 @@ interface DateOption {
const visible = ref(false)
const loading = ref(false)
const submitting = ref(false)
const refreshing = ref(false)
const doctorList = ref<any[]>([])
const timeSlots = ref<TimeSlot[]>([])
const lastVisit = ref('')
@@ -373,7 +391,8 @@ const loadDoctorRoster = async () => {
// 提取有排班的日期
if (res?.lists && res.lists.length > 0) {
doctorRosterDates.value = [...new Set(res.lists.map((item: any) => item.date))].sort()
doctorRosterDates.value = [...new Set(res.lists.map((item: any) => item.date))] as string[]
doctorRosterDates.value.sort()
// 自动选择日期:优先选择今天,如果今天没有排班则选择第一个有排班的日期
await nextTick()
@@ -449,6 +468,35 @@ const selectTimeSlot = (slot: TimeSlot) => {
form.appointmentTime = slot.time
}
// 刷新时间段
const handleRefreshSlots = async () => {
if (!selectedDoctorId.value || !form.date) {
return
}
try {
refreshing.value = true
const previousSelection = form.appointmentTime
form.appointmentTime = ''
await loadTimeSlots()
if (previousSelection) {
const slot = timeSlots.value.find(s => s.time === previousSelection)
if (slot && slot.available) {
form.appointmentTime = previousSelection
}
}
feedback.msgSuccess('刷新成功')
} catch (error) {
console.error('刷新失败:', error)
feedback.msgError('刷新失败,请重试')
} finally {
refreshing.value = false
}
}
// 确认挂号
const handleConfirm = async () => {
if (!selectedDoctorId.value) {
@@ -518,84 +566,147 @@ defineExpose({ open })
}
}
.appointment-time-container {
width: 100%;
}
.date-selector {
display: flex;
gap: 8px;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
.date-button {
min-width: 120px;
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(6, 1fr);
gap: 12px;
max-height: 400px;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 10px;
max-height: 450px;
overflow-y: auto;
padding: 4px;
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: space-between;
padding: 10px 12px;
border: 1px solid #e5e7eb;
border-radius: 6px;
justify-content: center;
padding: 0px 8px;
border: 2px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
transition: all 0.2s;
background-color: #fff;
min-height: 70px;
.slot-time {
font-size: 14px;
font-weight: 500;
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 6px;
}
.slot-badge {
margin-left: 8px;
.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: #d1d5db;
background-color: #fff;
border-color: #e4e7ed;
&:hover {
border-color: #3b82f6;
background-color: #eff6ff;
border-color: #409eff;
background-color: #ecf5ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
}
&.unavailable {
background-color: #f3f4f6;
border-color: #e5e7eb;
color: #9ca3af;
background-color: #f5f7fa;
border-color: #e4e7ed;
cursor: not-allowed;
opacity: 0.6;
.slot-time {
color: #9ca3af;
color: #c0c4cc;
}
:deep(.el-tag) {
background-color: #f3f4f6;
border-color: #e5e7eb;
color: #9ca3af;
.slot-status {
color: #c0c4cc;
background-color: #f5f7fa;
}
&:hover {
transform: none;
box-shadow: none;
}
}
&.selected {
border-color: #3b82f6;
background-color: #3b82f6;
color: white;
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: white;
color: #fff;
}
:deep(.el-tag) {
background-color: #2563eb;
border-color: #2563eb;
color: white;
.slot-status {
color: #fff;
background-color: rgba(255, 255, 255, 0.2);
}
}
}
+118 -2
View File
@@ -102,7 +102,11 @@
<el-tag v-else type="danger">禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="create_time" min-width="160" />
<el-table-column label="创建时间" prop="create_time" min-width="160">
<template #default="{ row }">
<span>{{ formatDateTime(row.create_time) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleDetail(row.id)">
@@ -139,6 +143,14 @@
@click="handleAppointment(row)"
>
挂号
</el-button>
<el-button
v-perms="['tcm.diagnosis/guahao']"
type="success"
link
@click="handleMiniProgramQRCode(row)"
>
小程序二维码
</el-button>
<el-button type="primary" link @click="handleEdit(row.id)" v-perms="['tcm.diagnosis/edit']">
编辑
@@ -159,6 +171,39 @@
<video-call ref="videoCallRef" />
<appointment-popup ref="appointmentRef" @success="getLists" />
<!-- 小程序二维码弹窗 -->
<el-dialog
v-model="qrcodeDialogVisible"
title="小程序二维码"
width="400px"
:close-on-click-modal="false"
>
<div class="flex flex-col items-center justify-center py-4">
<div v-if="qrcodeLoading" class="text-center">
<el-icon class="is-loading" :size="40">
<Loading />
</el-icon>
<div class="mt-4 text-gray-500">生成中...</div>
</div>
<div v-else-if="qrcodeUrl" class="text-center">
<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>
</div>
<div v-else class="text-center text-gray-500">
生成失败请重试
</div>
</div>
<template #footer>
<el-button @click="qrcodeDialogVisible = false">关闭</el-button>
<el-button v-if="!qrcodeLoading" type="primary" @click="handleRegenerateQRCode">
重新生成
</el-button>
</template>
</el-dialog>
<!-- 批量指派医助弹窗 -->
<el-dialog
v-model="assignDialogVisible"
@@ -208,14 +253,26 @@
</template>
<script setup lang="ts" name="tcmDiagnosis">
import { tcmDiagnosisLists, tcmDiagnosisDelete, tcmDiagnosisAssign, getAssistants } from '@/api/tcm'
import { tcmDiagnosisLists, tcmDiagnosisDelete, tcmDiagnosisAssign, getAssistants, generateMiniProgramQrcode } from '@/api/tcm'
import { usePaging } from '@/hooks/usePaging'
import { getDictData } from '@/api/app'
import { getWeappConfig } from '@/api/channel/weapp'
import feedback from '@/utils/feedback'
import { Loading } from '@element-plus/icons-vue'
import EditPopup from './edit.vue'
import DetailPopup from './detail.vue'
import VideoCall from '@/components/video-call/index.vue'
import AppointmentPopup from './appointment.vue'
import useUserStore from '@/stores/modules/user'
import dayjs from 'dayjs'
// 格式化时间戳为日期时间
const formatDateTime = (timestamp: number | string) => {
if (!timestamp) return '-'
// 如果是秒级时间戳(10位),转换为毫秒级
const ts = String(timestamp).length === 10 ? Number(timestamp) * 1000 : Number(timestamp)
return dayjs(ts).format('YYYY-MM-DD HH:mm:ss')
}
const formData = ref({
patient_name: '',
@@ -284,6 +341,13 @@ const editRef = ref()
const detailRef = ref()
const videoCallRef = ref()
const appointmentRef = ref()
const userStore = useUserStore()
// 小程序二维码相关
const qrcodeDialogVisible = ref(false)
const qrcodeLoading = ref(false)
const qrcodeUrl = ref('')
const currentQRCodePatient = ref<any>(null)
// 批量指派相关
const selectedIds = ref<number[]>([])
@@ -433,6 +497,58 @@ const handleAppointment = (row: any) => {
appointmentRef.value?.open(row)
}
// 生成小程序二维码
const handleMiniProgramQRCode = async (row: any) => {
if (!row.patient_id) {
feedback.msgWarning('患者信息不完整')
return
}
currentQRCodePatient.value = row
qrcodeDialogVisible.value = true
qrcodeLoading.value = true
qrcodeUrl.value = ''
try {
// 获取小程序配置
const config = await getWeappConfig()
if (!config?.app_id) {
feedback.msgError('小程序未配置,请先配置小程序信息')
qrcodeDialogVisible.value = false
return
}
// 获取当前登录用户信息
const currentUser = userStore.userInfo
// 调用生成二维码接口
const result = await generateMiniProgramQrcode({
diagnosis_id: row.id,
patient_id: row.patient_id,
share_user_id: currentUser?.id || ''
})
if (result?.qrcode_url) {
qrcodeUrl.value = result.qrcode_url
} else {
feedback.msgError('二维码生成失败')
}
} catch (error: any) {
console.error('生成小程序二维码失败:', error)
feedback.msgError(error?.msg || '生成二维码失败,请重试')
} finally {
qrcodeLoading.value = false
}
}
// 重新生成二维码
const handleRegenerateQRCode = () => {
if (currentQRCodePatient.value) {
handleMiniProgramQRCode(currentQRCodePatient.value)
}
}
onMounted(async () => {
await getDictOptions()
getLists()
Binary file not shown.