更新
This commit is contained in:
@@ -20,3 +20,23 @@ export function getUserInfo() {
|
||||
export function setUserInfo(params: any) {
|
||||
return request.post({ url: '/auth.admin/editSelf', params })
|
||||
}
|
||||
|
||||
// 获取企业微信登录配置
|
||||
export function getWorkWechatConfig() {
|
||||
return request.get({ url: '/login/workWechatConfig' })
|
||||
}
|
||||
|
||||
// 企业微信授权登录
|
||||
export function workWechatLogin(params: { code: string }) {
|
||||
return request.post({ url: '/login/workWechatLogin', params: { ...params, terminal: config.terminal } })
|
||||
}
|
||||
|
||||
// 绑定企业微信
|
||||
export function bindWorkWechat(params: { code: string }) {
|
||||
return request.post({ url: '/auth.admin/bindWorkWechat', params })
|
||||
}
|
||||
|
||||
// 解绑企业微信
|
||||
export function unbindWorkWechat() {
|
||||
return request.post({ url: '/auth.admin/unbindWorkWechat' })
|
||||
}
|
||||
|
||||
@@ -22,31 +22,40 @@
|
||||
<div class="chat-layout">
|
||||
<ConversationList class="conversation-list" />
|
||||
<Chat class="chat-area">
|
||||
<MessageList :conversationID="conversationId"/>
|
||||
<MessageList :conversationID="conversationId" />
|
||||
<MessageInput>
|
||||
<template #headerToolbar>
|
||||
<div class="message-toolbar">
|
||||
<EmojiPicker />
|
||||
<ImagePicker />
|
||||
<FilePicker />
|
||||
<AudioCallPicker />
|
||||
<VideoCallPicker />
|
||||
<!-- 仅在 TUICallKit 初始化成功后展示音视频入口,避免“初始化登录未完成”错误 -->
|
||||
<AudioCallPicker v-if="isCallReady" />
|
||||
<VideoCallPicker v-if="isCallReady" />
|
||||
<!-- 群组视频通话(基于 TUICallKitServer.calls,多人通话入口) -->
|
||||
<el-button
|
||||
v-if="isCallReady"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="startGroupVideoCall"
|
||||
>
|
||||
群视频
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</MessageInput>
|
||||
</Chat>
|
||||
</div>
|
||||
<!-- TUICallKit 放在 dialog 外部,独立渲染 -->
|
||||
|
||||
</UIKitProvider>
|
||||
</div>
|
||||
</div>
|
||||
<TUICallKit
|
||||
|
||||
:allowedMinimized="true"
|
||||
:allowedFullScreen="true"
|
||||
class="chat-dialog-call-kit"
|
||||
/>
|
||||
<!-- 挂载 TUICallKit 组件(固定居中显示) -->
|
||||
<TUICallKit
|
||||
v-if="isCallReady"
|
||||
:allowedMinimized="true"
|
||||
:allowedFullScreen="true"
|
||||
class="chat-dialog-call-kit"
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
</template>
|
||||
@@ -73,8 +82,8 @@ import {
|
||||
} from '@tencentcloud/chat-uikit-vue3'
|
||||
import TencentCloudChat from '@tencentcloud/chat'
|
||||
import '@/assets/chat-uikit.css'
|
||||
import { TUICallKit } from '@tencentcloud/call-uikit-vue'
|
||||
|
||||
import { TUICallKit, TUICallKitServer, TUICallType } from '@tencentcloud/call-uikit-vue'
|
||||
const assistant_ids=ref('');
|
||||
const visible = ref(false)
|
||||
const isReady = ref(false)
|
||||
const error = ref('')
|
||||
@@ -83,6 +92,8 @@ const patientId = ref<number | null>(null)
|
||||
const diagnosisId = ref<number | null>(null)
|
||||
const loadingText = ref('正在初始化...')
|
||||
const patientUserId = ref('')
|
||||
const conversationId = ref('')
|
||||
const isCallReady = ref(false)
|
||||
const { login, logout } = useLoginState()
|
||||
const { setActiveConversation, createC2CConversation, activeConversation } = useConversationListState()
|
||||
const userStore = useUserStore()
|
||||
@@ -127,84 +138,29 @@ const updateUserAvatarToIM = async (avatarUrl: string,appid: number) => {
|
||||
}
|
||||
|
||||
// 监听当前激活的会话变化,更新标题
|
||||
watch(() => activeConversation.value, (newConversation) => {
|
||||
watch(() => activeConversation.value, async (newConversation) => {
|
||||
if (newConversation) {
|
||||
// 更新标题为当前会话的显示名称
|
||||
patientName.value = newConversation.getShowName() || patientName.value
|
||||
console.log('会话切换:', newConversation.conversationID, '名称:', patientName.value)
|
||||
}
|
||||
})
|
||||
|
||||
// 限制 TUICallKit 拖动范围 - 使用事件监听而不是 MutationObserver
|
||||
const setupCallKitDragLimit = () => {
|
||||
setTimeout(() => {
|
||||
const callKitElements = document.querySelectorAll('[class*="TUICallKit"], [id*="tui-call"]')
|
||||
console.log('会话切换:', newConversation.conversationID, '名称:', patientName.value, newConversation)
|
||||
|
||||
callKitElements.forEach((element: any) => {
|
||||
if (!element) return
|
||||
|
||||
// 监听鼠标释放事件(拖动结束)
|
||||
element.addEventListener('mouseup', () => {
|
||||
setTimeout(() => {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const dialogElement = document.querySelector('.chat-dialog .el-dialog')
|
||||
|
||||
if (!dialogElement) return
|
||||
|
||||
const dialogRect = dialogElement.getBoundingClientRect()
|
||||
|
||||
// 获取当前位置
|
||||
const currentLeft = parseInt(element.style.left || '0')
|
||||
const currentTop = parseInt(element.style.top || '0')
|
||||
|
||||
let newLeft = currentLeft
|
||||
let newTop = currentTop
|
||||
let needAdjust = false
|
||||
|
||||
// 限制左边界
|
||||
if (rect.left < dialogRect.left) {
|
||||
newLeft = currentLeft + (dialogRect.left - rect.left)
|
||||
needAdjust = true
|
||||
}
|
||||
|
||||
// 限制右边界
|
||||
if (rect.right > dialogRect.right) {
|
||||
newLeft = currentLeft - (rect.right - dialogRect.right)
|
||||
needAdjust = true
|
||||
}
|
||||
|
||||
// 限制上边界
|
||||
if (rect.top < dialogRect.top) {
|
||||
newTop = currentTop + (dialogRect.top - rect.top)
|
||||
needAdjust = true
|
||||
}
|
||||
|
||||
// 限制下边界
|
||||
if (rect.bottom > dialogRect.bottom) {
|
||||
newTop = currentTop - (rect.bottom - dialogRect.bottom)
|
||||
needAdjust = true
|
||||
}
|
||||
|
||||
// 应用新位置(使用过渡动画)
|
||||
if (needAdjust) {
|
||||
element.style.transition = 'left 0.3s ease, top 0.3s ease'
|
||||
element.style.left = `${newLeft}px`
|
||||
element.style.top = `${newTop}px`
|
||||
|
||||
setTimeout(() => {
|
||||
element.style.transition = ''
|
||||
}, 300)
|
||||
}
|
||||
}, 10)
|
||||
})
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 监听 isReady 变化,当通话组件加载后设置拖动限制
|
||||
watch(() => isReady.value, (ready) => {
|
||||
if (ready) {
|
||||
setupCallKitDragLimit()
|
||||
if (newConversation.userProfile.userID.includes("doctor")) {
|
||||
console.log('当前会话是医生,跳过获取 assistant_id')
|
||||
} else {
|
||||
try {
|
||||
const res = await getCallSignature({
|
||||
patient_id: newConversation.userProfile.userID.replace("patient_", ""),
|
||||
diagnosis_id: diagnosisId.value
|
||||
})
|
||||
patientUserId.value = res.patientUserId
|
||||
assistant_ids.value = res.assistant_id
|
||||
console.log('会话切换后获取到 assistant_id:', assistant_ids.value, '完整响应:', res)
|
||||
} catch (err) {
|
||||
console.error('获取 assistant_id 失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('会话切换完成,userID:', newConversation.userProfile.userID)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -215,21 +171,27 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
diagnosisId.value = data.diagnosisId || null
|
||||
error.value = ''
|
||||
isReady.value = false
|
||||
isCallReady.value = false
|
||||
loadingText.value = '正在获取签名...'
|
||||
|
||||
|
||||
try {
|
||||
// 获取医生的签名信息
|
||||
const res = await getCallSignature({
|
||||
patient_id: data.patientId,
|
||||
diagnosis_id: data.diagnosisId || 0
|
||||
})
|
||||
|
||||
assistant_ids.value = res.assistant_id
|
||||
console.log('后端返回的签名数据:', res)
|
||||
console.log('assistant_id:', assistant_ids.value)
|
||||
|
||||
if (!res || !res.userSig) {
|
||||
throw new Error('获取签名失败')
|
||||
}
|
||||
|
||||
if (!res.assistant_id) {
|
||||
console.warn('警告:后端未返回 assistant_id,群视频通话可能无法正常工作')
|
||||
}
|
||||
|
||||
patientUserId.value = res.patientUserId || `patient_${data.patientId}`
|
||||
|
||||
loadingText.value = '正在登录聊天...'
|
||||
@@ -241,8 +203,23 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
userSig: res.userSig,
|
||||
useUploadPlugin: true
|
||||
})
|
||||
const sdkAPPid=Number(res.sdkAppId);
|
||||
|
||||
const sdkAPPid = Number(res.sdkAppId)
|
||||
|
||||
// 初始化 TUICallKit(聊天内音视频通话依赖此初始化);只需全局成功一次
|
||||
if (!(window as any).__TUICallKitInited__) {
|
||||
loadingText.value = '正在初始化通话能力...'
|
||||
try {
|
||||
await TUICallKitServer.init({
|
||||
userID: res.userId,
|
||||
userSig: res.userSig,
|
||||
SDKAppID: sdkAPPid
|
||||
})
|
||||
;(window as any).__TUICallKitInited__ = true
|
||||
} catch (err: any) {
|
||||
console.warn('TUICallKit 初始化失败,聊天仍可用,音视频通话不可用:', err?.message || err)
|
||||
}
|
||||
}
|
||||
isCallReady.value = !!(window as any).__TUICallKitInited__
|
||||
console.log('Chat UIKit 登录成功,患者用户ID:', patientUserId.value)
|
||||
|
||||
loadingText.value = '加载聊天界面...'
|
||||
@@ -286,6 +263,51 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
|
||||
}
|
||||
}
|
||||
|
||||
// 群组视频通话(多人通话):当前医生作为发起方,默认先邀请当前会话患者
|
||||
const startGroupVideoCall = async () => {
|
||||
if (!isCallReady.value) {
|
||||
feedback.msgWarning('通话服务初始化中,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
if (!patientUserId.value) {
|
||||
feedback.msgWarning('未找到患者通话ID,无法发起群视频')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保 assistant_id 存在
|
||||
if (!assistant_ids.value) {
|
||||
console.warn('assistant_id 为空,尝试重新获取')
|
||||
const res = await getCallSignature({
|
||||
patient_id: patientId.value!,
|
||||
diagnosis_id: diagnosisId.value
|
||||
})
|
||||
assistant_ids.value = res.assistant_id
|
||||
console.log('重新获取 assistant_id:', assistant_ids.value)
|
||||
}
|
||||
|
||||
// 构建用户列表,过滤掉空值
|
||||
const userIDList = [patientUserId.value, assistant_ids.value].filter(id => id)
|
||||
|
||||
if (userIDList.length < 2) {
|
||||
feedback.msgWarning('助理ID获取失败,无法发起群视频')
|
||||
console.error('userIDList 不完整:', userIDList, 'assistant_ids:', assistant_ids.value)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('发起群视频通话,参与者:', userIDList)
|
||||
|
||||
await TUICallKitServer.calls({
|
||||
userIDList,
|
||||
type: TUICallType.VIDEO_CALL
|
||||
})
|
||||
} catch (err: any) {
|
||||
console.error('发起群视频失败:', err)
|
||||
feedback.msgError(err?.message || '发起群视频失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = async () => {
|
||||
try {
|
||||
// 登出 Chat
|
||||
@@ -392,6 +414,20 @@ defineExpose({ open })
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* TUICallKit 容器样式 - 限制在对话框内拖动 */
|
||||
.chat-dialog-call-kit {
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
width: 600px !important;
|
||||
height: 400px !important;
|
||||
max-width: 90% !important;
|
||||
max-height: 80% !important;
|
||||
z-index: 300000 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -413,47 +449,4 @@ body > div[data-reka-focus-guard] {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* TUICallKit 容器样式 - 限制在对话框内拖动 */
|
||||
.chat-dialog-call-kit {
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
width: 600px !important;
|
||||
height: 400px !important;
|
||||
max-width: 90% !important;
|
||||
max-height: 80% !important;
|
||||
z-index: 300000 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
.float-window-container{
|
||||
z-index: 3000;
|
||||
}
|
||||
|
||||
/* 限制 TUICallKit 的拖动范围在对话框内 */
|
||||
.chat-content {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* 确保 TUICallKit 相关的所有容器都在对话框之上 */
|
||||
body > div[class*="TUICallKit"],
|
||||
body > div[id*="tui-call"],
|
||||
body > div[class*="t-call"],
|
||||
body > .TUICallKit-desktop,
|
||||
body > .TUICallKit-mobile {
|
||||
position: fixed !important;
|
||||
z-index: 300000 !important;
|
||||
max-width: 90vw !important;
|
||||
max-height: 90vh !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* 确保最小化后的通话窗口也在最上层 */
|
||||
[class*="TUICallKit"][class*="minimized"],
|
||||
[class*="TUICallKit"][class*="minimize"],
|
||||
[class*="call-kit-mini"],
|
||||
[class*="t-call-mini"] {
|
||||
z-index: 300000 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -564,6 +564,15 @@ const startCall = async () => {
|
||||
statusText.value = '等待初始化完成...'
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
// 生产包中 init 返回后引擎可能尚未挂载,短暂轮询等待 .calls 可用
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (typeof TUICallKitAPI.calls === 'function') break
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
}
|
||||
if (typeof TUICallKitAPI.calls !== 'function') {
|
||||
throw new Error('通话引擎未就绪,请关闭窗口后重试或刷新页面')
|
||||
}
|
||||
|
||||
console.log('初始化完成,显示通话界面')
|
||||
statusText.value = '准备发起通话...'
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { App } from 'vue'
|
||||
|
||||
export default function (app: App) {
|
||||
// TUIKit 初始化会在组件使用时自动进行
|
||||
// 这里只是确保插件被正确加载
|
||||
if (typeof window !== 'undefined') {
|
||||
// 防止 TUIKit 在初始化时出现错误
|
||||
; (window as any).__TUIKIT_INITIALIZED__ = true
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -3,15 +3,14 @@ import './styles/index.scss'
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { TUICallKit } from '@trtc/calls-uikit-vue'
|
||||
|
||||
import { getConfig } from './api/app'
|
||||
import App from './App.vue'
|
||||
import install from './install'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(install)
|
||||
app.use(TUICallKit)
|
||||
app.mount('#app')
|
||||
|
||||
getConfig().then((res) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import { getUserInfo, login, logout } from '@/api/user'
|
||||
import { getUserInfo, login, logout, workWechatLogin } from '@/api/user'
|
||||
import { TOKEN_KEY } from '@/enums/cacheEnums'
|
||||
import { PageEnum } from '@/enums/pageEnum'
|
||||
import router, { filterAsyncRoutes } from '@/router'
|
||||
@@ -64,6 +64,19 @@ const useUserStore = defineStore({
|
||||
})
|
||||
})
|
||||
},
|
||||
workWechatLogin(code: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
workWechatLogin({ code })
|
||||
.then((data) => {
|
||||
this.token = data.token
|
||||
cache.set(TOKEN_KEY, data.token)
|
||||
resolve(data)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
getUserInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getUserInfo()
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 检查是否使用 HTTPS 协议
|
||||
* WebRTC 功能(视频通话、音频通话)需要 HTTPS 才能正常工作
|
||||
*/
|
||||
export function checkHttpsProtocol(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return true
|
||||
}
|
||||
|
||||
const isHttps = window.location.protocol === 'https:'
|
||||
const isLocalhost = window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1'
|
||||
|
||||
return isHttps || isLocalhost
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 HTTPS 警告提示
|
||||
*/
|
||||
export function showHttpsWarning(): void {
|
||||
if (!checkHttpsProtocol()) {
|
||||
console.warn(
|
||||
'%c[WebRTC 警告] 当前使用 HTTP 协议访问',
|
||||
'color: #ff6b6b; font-size: 14px; font-weight: bold;'
|
||||
)
|
||||
console.warn(
|
||||
'%c视频通话、音频通话等功能需要 HTTPS 协议才能正常工作。\n' +
|
||||
'请使用 HTTPS 访问或在 localhost 环境下测试。',
|
||||
'color: #ff6b6b; font-size: 12px;'
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,41 +6,79 @@
|
||||
<image-contain :src="config.login_image" :width="400" height="100%" />
|
||||
</div>
|
||||
<div
|
||||
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[400px] w-[375px] flex-none mx-auto"
|
||||
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[420px] w-[380px] flex-none mx-auto"
|
||||
>
|
||||
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
|
||||
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
|
||||
<el-form-item prop="account">
|
||||
<el-input
|
||||
v-model="formData.account"
|
||||
placeholder="请输入账号"
|
||||
@keyup.enter="handleEnter"
|
||||
>
|
||||
<template #prepend>
|
||||
<icon name="el-icon-User" size="16" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
ref="passwordRef"
|
||||
v-model="formData.password"
|
||||
show-password
|
||||
placeholder="请输入密码"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prepend>
|
||||
<icon name="el-icon-Lock" size="16" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="mb-5">
|
||||
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
|
||||
|
||||
<!-- 企业微信自动授权中 -->
|
||||
<div v-if="wxWorkAutoLogin" class="text-center py-10">
|
||||
<el-icon class="is-loading mb-4" :size="40" color="var(--el-color-primary)">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<div class="text-gray-500">企业微信授权登录中...</div>
|
||||
</div>
|
||||
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
|
||||
登录
|
||||
</el-button>
|
||||
|
||||
<template v-else>
|
||||
<!-- 登录方式切换标签 -->
|
||||
<div v-if="wxWorkEnabled" class="flex justify-center mb-6">
|
||||
<el-radio-group v-model="loginMode" size="large">
|
||||
<el-radio-button value="account">账号登录</el-radio-button>
|
||||
<el-radio-button value="wxwork">企业微信</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 账号密码登录 -->
|
||||
<template v-if="loginMode === 'account'">
|
||||
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
|
||||
<el-form-item prop="account">
|
||||
<el-input
|
||||
v-model="formData.account"
|
||||
placeholder="请输入账号"
|
||||
@keyup.enter="handleEnter"
|
||||
>
|
||||
<template #prepend>
|
||||
<icon name="el-icon-User" size="16" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
ref="passwordRef"
|
||||
v-model="formData.password"
|
||||
show-password
|
||||
placeholder="请输入密码"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prepend>
|
||||
<icon name="el-icon-Lock" size="16" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="mb-5">
|
||||
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
|
||||
</div>
|
||||
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
|
||||
登录
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<!-- 企业微信扫码登录(非企业微信内浏览器) -->
|
||||
<template v-if="loginMode === 'wxwork'">
|
||||
<div class="wxwork-qrcode-wrap">
|
||||
<div v-if="wxWorkLoading" class="text-center py-10">
|
||||
<el-icon class="is-loading" :size="32" color="var(--el-color-primary)">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<div class="mt-2 text-gray-400 text-sm">加载企业微信扫码...</div>
|
||||
</div>
|
||||
<div v-else id="wxwork_qrcode_container" class="wxwork-qrcode"></div>
|
||||
</div>
|
||||
<div class="text-center text-sm text-gray-400 mt-4">
|
||||
请使用企业微信扫描二维码登录
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,8 +88,10 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance, InputInstance } from 'element-plus'
|
||||
import { computed, onMounted, reactive, ref, shallowRef } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ACCOUNT_KEY } from '@/enums/cacheEnums'
|
||||
import { PageEnum } from '@/enums/pageEnum'
|
||||
import { useLockFn } from '@/hooks/useLockFn'
|
||||
@@ -59,6 +99,7 @@ import LayoutFooter from '@/layout/components/footer.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import cache from '@/utils/cache'
|
||||
import { getWorkWechatConfig } from '@/api/user'
|
||||
|
||||
const passwordRef = shallowRef<InputInstance>()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
@@ -73,51 +114,154 @@ const formData = reactive({
|
||||
password: ''
|
||||
})
|
||||
const rules = {
|
||||
account: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入账号',
|
||||
trigger: ['blur']
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: ['blur']
|
||||
}
|
||||
]
|
||||
account: [{ required: true, message: '请输入账号', trigger: ['blur'] }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: ['blur'] }]
|
||||
}
|
||||
// 回车按键监听
|
||||
|
||||
// 企业微信相关
|
||||
const loginMode = ref<'account' | 'wxwork'>('account')
|
||||
const wxWorkEnabled = ref(false)
|
||||
const wxWorkConfig = ref<{ corp_id: string; agent_id: string }>({ corp_id: '', agent_id: '' })
|
||||
const wxWorkAutoLogin = ref(false)
|
||||
const wxWorkLoading = ref(false)
|
||||
|
||||
const isInWxWork = () => /wxwork/i.test(navigator.userAgent)
|
||||
|
||||
const getRedirectUri = () => {
|
||||
return window.location.origin + window.location.pathname
|
||||
}
|
||||
|
||||
const handleEnter = () => {
|
||||
if (!formData.password) {
|
||||
return passwordRef.value?.focus()
|
||||
}
|
||||
handleLogin()
|
||||
}
|
||||
// 登录处理
|
||||
|
||||
const handleLogin = async () => {
|
||||
await formRef.value?.validate()
|
||||
// 记住账号,缓存
|
||||
cache.set(ACCOUNT_KEY, {
|
||||
remember: remAccount.value,
|
||||
account: remAccount.value ? formData.account : ''
|
||||
})
|
||||
await userStore.login(formData)
|
||||
const {
|
||||
query: { redirect }
|
||||
} = route
|
||||
redirectAfterLogin()
|
||||
}
|
||||
|
||||
const { isLock, lockFn: lockLogin } = useLockFn(handleLogin)
|
||||
|
||||
const redirectAfterLogin = () => {
|
||||
const { query: { redirect } } = route
|
||||
const path = typeof redirect === 'string' ? redirect : PageEnum.INDEX
|
||||
router.push(path)
|
||||
}
|
||||
const { isLock, lockFn: lockLogin } = useLockFn(handleLogin)
|
||||
|
||||
onMounted(() => {
|
||||
// 企业微信 code 登录
|
||||
const handleWxWorkCodeLogin = async (code: string) => {
|
||||
wxWorkAutoLogin.value = true
|
||||
try {
|
||||
await userStore.workWechatLogin(code)
|
||||
redirectAfterLogin()
|
||||
} catch (error: any) {
|
||||
console.error('企业微信登录失败:', error)
|
||||
ElMessage.error(error?.msg || error?.message || '企业微信登录失败,请使用账号密码登录')
|
||||
wxWorkAutoLogin.value = false
|
||||
loginMode.value = 'account'
|
||||
}
|
||||
}
|
||||
|
||||
// 加载企业微信扫码 JS SDK 并渲染二维码
|
||||
const renderWxWorkQRCode = async () => {
|
||||
if (!wxWorkConfig.value.corp_id) return
|
||||
|
||||
wxWorkLoading.value = true
|
||||
|
||||
// 动态加载企业微信扫码 JS
|
||||
if (!(window as any).WwLogin) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://wwcdn.weixin.qq.com/node/wework/wwopen/js/wwLogin-1.2.7.js'
|
||||
script.onload = () => resolve()
|
||||
script.onerror = () => reject(new Error('加载企业微信JS失败'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
wxWorkLoading.value = false
|
||||
|
||||
await nextTick()
|
||||
|
||||
const redirectUri = encodeURIComponent(getRedirectUri())
|
||||
|
||||
new (window as any).WwLogin({
|
||||
id: 'wxwork_qrcode_container',
|
||||
appid: wxWorkConfig.value.corp_id,
|
||||
agentid: wxWorkConfig.value.agent_id,
|
||||
redirect_uri: redirectUri,
|
||||
lang: 'zh',
|
||||
state: 'admin_login',
|
||||
href: '',
|
||||
self_redirect: false,
|
||||
})
|
||||
}
|
||||
|
||||
// 切换到企业微信扫码时自动渲染
|
||||
watch(loginMode, (val) => {
|
||||
if (val === 'wxwork') {
|
||||
nextTick(() => renderWxWorkQRCode())
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// 恢复记住账号
|
||||
const value = cache.get(ACCOUNT_KEY)
|
||||
if (value?.remember) {
|
||||
remAccount.value = value.remember
|
||||
formData.account = value.account
|
||||
}
|
||||
|
||||
// 检查 URL 中是否有企业微信回调 code
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const wxCode = urlParams.get('code')
|
||||
const wxState = urlParams.get('state')
|
||||
|
||||
if (wxCode && wxState === 'admin_login') {
|
||||
// 清除 URL 中的 code 参数(避免刷新重复使用)
|
||||
const cleanUrl = window.location.pathname + (route.query.redirect ? `?redirect=${route.query.redirect}` : '')
|
||||
window.history.replaceState({}, '', cleanUrl)
|
||||
|
||||
handleWxWorkCodeLogin(wxCode)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取企业微信配置
|
||||
try {
|
||||
const res = await getWorkWechatConfig()
|
||||
if (res?.enabled) {
|
||||
wxWorkEnabled.value = true
|
||||
wxWorkConfig.value = { corp_id: res.corp_id, agent_id: res.agent_id }
|
||||
|
||||
// 在企业微信内:自动跳转 OAuth 授权
|
||||
if (isInWxWork()) {
|
||||
const redirectUri = encodeURIComponent(getRedirectUri())
|
||||
const authUrl =
|
||||
`https://open.weixin.qq.com/connect/oauth2/authorize` +
|
||||
`?appid=${res.corp_id}` +
|
||||
`&redirect_uri=${redirectUri}` +
|
||||
`&response_type=code` +
|
||||
`&scope=snsapi_privateinfo` +
|
||||
`&agentid=${res.agent_id}` +
|
||||
`&state=admin_login` +
|
||||
`#wechat_redirect`
|
||||
window.location.href = authUrl
|
||||
wxWorkAutoLogin.value = true
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('获取企业微信配置失败:', e)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -126,7 +270,27 @@ onMounted(() => {
|
||||
background-image: url('./images/login_bg.png');
|
||||
@apply min-h-screen bg-no-repeat bg-center bg-cover;
|
||||
.login-card {
|
||||
height: 400px;
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.wxwork-qrcode-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.wxwork-qrcode {
|
||||
width: 340px;
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(iframe) {
|
||||
width: 340px !important;
|
||||
height: 400px !important;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="360" fixed="right">
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['doctor.appointment/detail']"
|
||||
@@ -154,22 +154,6 @@
|
||||
>
|
||||
编辑患者
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/video-call']"
|
||||
type="success"
|
||||
link
|
||||
@click="handleVideoCall(row)"
|
||||
>
|
||||
1V1通话
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/video-group']"
|
||||
type="success"
|
||||
link
|
||||
@click="handleGroupVideoCall(row)"
|
||||
>
|
||||
群通话
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
v-perms="['doctor.appointment/cancel']"
|
||||
@@ -186,9 +170,9 @@
|
||||
link
|
||||
@click="handleComplete(row)"
|
||||
>
|
||||
完成视频面诊
|
||||
完成面诊
|
||||
</el-button>
|
||||
<el-button
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
v-perms="['doctor.appointment/prescription']"
|
||||
type="success"
|
||||
@@ -197,6 +181,15 @@
|
||||
>
|
||||
聊天
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
v-perms="['tcm.diagnosis/guahao']"
|
||||
type="primary"
|
||||
link
|
||||
@click="handleMiniProgramQRCode(row)"
|
||||
>
|
||||
确认诊单二维码
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -257,27 +250,59 @@
|
||||
<!-- 编辑患者弹窗 -->
|
||||
<edit-popup ref="editRef" @success="getLists" />
|
||||
|
||||
<!-- 视频通话组件 -->
|
||||
<video-call ref="videoCallRef" />
|
||||
|
||||
<!-- 处方单抽屉 -->
|
||||
<prescription-drawer ref="prescriptionDrawerRef" @success="loadData" />
|
||||
|
||||
<!-- 聊天对话框 -->
|
||||
<chat-dialog ref="chatDialogRef" />
|
||||
|
||||
<!-- 小程序二维码弹窗 -->
|
||||
<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>患者:{{ currentQRCodeRow?.patient_name }}</div>
|
||||
<div class="mt-2">请使用微信扫描二维码</div>
|
||||
<div class="mt-1 text-xs text-gray-400">跳转小程序路径:pages/login/login</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { appointmentLists, cancelAppointment, completeAppointment, appointmentDetail } from '@/api/doctor'
|
||||
import { getCallSignature } from '@/api/tcm'
|
||||
import { getCallSignature, generateMiniProgramQrcode } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
import EditPopup from '../diagnosis/edit.vue'
|
||||
import VideoCall from '@/components/video-call/index.vue'
|
||||
import PrescriptionDrawer from './components/prescription-drawer.vue'
|
||||
import ChatDialog from '@/components/chat-dialog/index.vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { getWeappConfig } from '@/api/channel/weapp'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
@@ -367,10 +392,15 @@ const handleReset = () => {
|
||||
const detailVisible = ref(false)
|
||||
const detailData = ref<any>(null)
|
||||
const editRef = ref()
|
||||
const videoCallRef = ref()
|
||||
const prescriptionDrawerRef = ref()
|
||||
const chatDialogRef = ref()
|
||||
|
||||
// 小程序二维码相关
|
||||
const qrcodeDialogVisible = ref(false)
|
||||
const qrcodeLoading = ref(false)
|
||||
const qrcodeUrl = ref('')
|
||||
const currentQRCodeRow = ref<any>(null)
|
||||
|
||||
// 查看详情
|
||||
const handleDetail = async (row: any) => {
|
||||
try {
|
||||
@@ -416,50 +446,6 @@ const handleEditPatient = (row: any) => {
|
||||
editRef.value?.open('edit', row.patient_id)
|
||||
}
|
||||
|
||||
// 1v1视频通话
|
||||
const handleVideoCall = (row: any) => {
|
||||
if (!row.patient_id) {
|
||||
feedback.msgWarning('患者信息不完整')
|
||||
return
|
||||
}
|
||||
|
||||
videoCallRef.value?.open({
|
||||
diagnosisId: row.diagnosis_id || row.id,
|
||||
patientId: row.patient_id,
|
||||
patientName: row.patient_name,
|
||||
userId: `patient_${row.patient_id}`,
|
||||
isGroup: false
|
||||
})
|
||||
}
|
||||
|
||||
// 群组视频通话
|
||||
const handleGroupVideoCall = (row: any) => {
|
||||
if (!row.patient_id) {
|
||||
feedback.msgWarning('患者信息不完整')
|
||||
return
|
||||
}
|
||||
|
||||
if (!row.doctor_id) {
|
||||
feedback.msgWarning('该预约没有医生信息,无法发起群组通话')
|
||||
return
|
||||
}
|
||||
|
||||
// 群组通话参与者:患者和医生
|
||||
const userIds = [
|
||||
`patient_${row.patient_id}`,
|
||||
`doctor_${row.doctor_id}`
|
||||
]
|
||||
|
||||
videoCallRef.value?.open({
|
||||
diagnosisId: row.diagnosis_id || row.id,
|
||||
patientId: row.patient_id,
|
||||
patientName: row.patient_name,
|
||||
doctorId: row.doctor_id,
|
||||
userIds: userIds,
|
||||
isGroup: true
|
||||
})
|
||||
}
|
||||
|
||||
// 聊天
|
||||
const handleChat = async (row: any) => {
|
||||
if (!row.patient_id) {
|
||||
@@ -495,6 +481,57 @@ const handleChat = async (row: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 生成小程序二维码(跳转小程序路径:pages/login/login)
|
||||
const handleMiniProgramQRCode = async (row: any) => {
|
||||
if (!row.patient_id) {
|
||||
feedback.msgWarning('患者信息不完整')
|
||||
return
|
||||
}
|
||||
|
||||
currentQRCodeRow.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.diagnosis_id || row.id,
|
||||
patient_id: row.patient_id,
|
||||
share_user_id: currentUser?.id || '',
|
||||
mini_program_path: 'pages/login/login'
|
||||
})
|
||||
|
||||
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 (currentQRCodeRow.value) {
|
||||
handleMiniProgramQRCode(currentQRCodeRow.value)
|
||||
}
|
||||
}
|
||||
|
||||
loadData()
|
||||
</script>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
label-width="160px"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<el-divider content-position="left">基本信息</el-divider>
|
||||
@@ -94,6 +94,98 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 生命体征 -->
|
||||
<el-divider content-position="left">生命体征</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="婚姻状态" prop="marital_status">
|
||||
<el-select
|
||||
v-model="formData.marital_status"
|
||||
placeholder="请选择婚姻状态"
|
||||
class="w-full"
|
||||
:popper-options="{ strategy: 'fixed' }"
|
||||
popper-class="high-z-index"
|
||||
>
|
||||
<el-option label="未婚" :value="0" />
|
||||
<el-option label="已婚" :value="1" />
|
||||
<el-option label="离异" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="身高(cm)" prop="height">
|
||||
<el-input-number
|
||||
v-model="formData.height"
|
||||
:min="0"
|
||||
:max="300"
|
||||
placeholder="请输入身高"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="体重(kg)" prop="weight">
|
||||
<el-input-number
|
||||
v-model="formData.weight"
|
||||
:min="0"
|
||||
:max="500"
|
||||
:step="0.1"
|
||||
placeholder="请输入体重"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="地区" prop="region">
|
||||
<el-input
|
||||
v-model="formData.region"
|
||||
placeholder="请输入地区"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="收缩压(mmHg)" prop="systolic_pressure">
|
||||
<el-input-number
|
||||
v-model="formData.systolic_pressure"
|
||||
:min="0"
|
||||
:max="250"
|
||||
placeholder="请输入收缩压"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="舒张压(mmHg)" prop="diastolic_pressure">
|
||||
<el-input-number
|
||||
v-model="formData.diastolic_pressure"
|
||||
:min="0"
|
||||
:max="150"
|
||||
placeholder="请输入舒张压"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="空腹血糖(mmol/L)" prop="fasting_blood_sugar">
|
||||
<el-input-number
|
||||
v-model="formData.fasting_blood_sugar"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:step="0.1"
|
||||
placeholder="请输入空腹血糖"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="诊断类型" prop="diagnosis_type">
|
||||
@@ -161,144 +253,186 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider content-position="left">主诉</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发现糖尿病就患病史" prop="diabetes_discovery_year">
|
||||
<el-input-number
|
||||
v-model="formData.diabetes_discovery_year"
|
||||
:min="0"
|
||||
:max="100"
|
||||
placeholder="请输入年数"
|
||||
class="w-full"
|
||||
/>
|
||||
<template #append>年</template>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 现病史 -->
|
||||
<el-form-item label="当地医院诊断结果" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.local_hospital_diagnosis">
|
||||
<el-checkbox-button label="糖尿病">糖尿病</el-checkbox-button>
|
||||
<el-checkbox-button label="高血压">高血压</el-checkbox-button>
|
||||
<el-checkbox-button label="糖尿病高血压">糖尿病高血压</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="当地就诊医院名称" prop="local_hospital_name">
|
||||
<el-input
|
||||
v-model="formData.local_hospital_name"
|
||||
placeholder="请输入当地就诊医院名称(不能填写当地、本地、互联网等关键词)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">现病史</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="口腔感觉" class="compact-form-item">
|
||||
<el-radio-group v-model="formData.appetite">
|
||||
<el-radio v-for="item in appetiteOptions" :key="item.value" :label="item.value">
|
||||
<el-radio-button v-for="item in appetiteOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-radio>
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="20">
|
||||
<el-form-item label="每日饮水量" class="compact-form-item">
|
||||
<el-radio-group v-model="formData.water_intake">
|
||||
<el-radio v-for="item in waterIntakeOptions" :key="item.value" :label="item.value">
|
||||
<el-radio-button v-for="item in waterIntakeOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-radio>
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="体重变化" class="compact-form-item">
|
||||
<el-form-item label="近一个月体重变化" class="compact-form-item">
|
||||
<el-radio-group v-model="formData.weight_change">
|
||||
<el-radio v-for="item in weightChangeOptions" :key="item.value" :label="item.value">
|
||||
<el-radio-button v-for="item in weightChangeOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-radio>
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="脂肪肝程度" class="compact-form-item">
|
||||
<el-radio-group v-model="formData.fatty_liver_degree">
|
||||
<el-radio v-for="item in fattyLiverDegreeOptions" :key="item.value" :label="item.value">
|
||||
<el-radio-button v-for="item in fattyLiverDegreeOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-radio>
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</el-row>
|
||||
<el-form-item label="饮食情况" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.diet_condition">
|
||||
<el-checkbox v-for="item in dietConditionOptions" :key="item.value" :label="item.value">
|
||||
<el-checkbox-button v-for="item in dietConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="肢体感觉" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.body_feeling">
|
||||
<el-checkbox v-for="item in bodyFeelingOptions" :key="item.value" :label="item.value">
|
||||
<el-checkbox-button v-for="item in bodyFeelingOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="睡眠情况" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.sleep_condition">
|
||||
<el-checkbox v-for="item in sleepConditionOptions" :key="item.value" :label="item.value">
|
||||
<el-checkbox-button v-for="item in sleepConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="眼睛情况" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.eye_condition">
|
||||
<el-checkbox v-for="item in eyeConditionOptions" :key="item.value" :label="item.value">
|
||||
<el-checkbox-button v-for="item in eyeConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="头部感觉" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.head_feeling">
|
||||
<el-checkbox v-for="item in headFeelingOptions" :key="item.value" :label="item.value">
|
||||
<el-checkbox-button v-for="item in headFeelingOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="出汗情况" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.sweat_condition">
|
||||
<el-checkbox v-for="item in sweatConditionOptions" :key="item.value" :label="item.value">
|
||||
<el-checkbox-button v-for="item in sweatConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="皮肤情况" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.skin_condition">
|
||||
<el-checkbox v-for="item in skinConditionOptions" :key="item.value" :label="item.value">
|
||||
<el-checkbox-button v-for="item in skinConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="小便情况" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.urine_condition">
|
||||
<el-checkbox v-for="item in urineConditionOptions" :key="item.value" :label="item.value">
|
||||
<el-checkbox-button v-for="item in urineConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="大便情况" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.stool_condition">
|
||||
<el-checkbox v-for="item in stoolConditionOptions" :key="item.value" :label="item.value">
|
||||
<el-checkbox-button v-for="item in stoolConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="腰肾情况" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.kidney_condition">
|
||||
<el-checkbox v-for="item in kidneyConditionOptions" :key="item.value" :label="item.value">
|
||||
<el-checkbox-button v-for="item in kidneyConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="病逝补充" prop="symptoms">
|
||||
<el-input
|
||||
v-model="formData.symptoms"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入症状"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 既往史 -->
|
||||
<el-divider content-position="left">既往史</el-divider>
|
||||
|
||||
<el-form-item label="既往史" prop="past_history">
|
||||
<el-form-item label="" prop="past_history" class="checkbox-form-item">
|
||||
<el-checkbox-group v-model="formData.past_history">
|
||||
<el-checkbox
|
||||
<el-checkbox-button
|
||||
v-for="item in pastHistoryOptions"
|
||||
:key="item.value"
|
||||
:label="item.value"
|
||||
>
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
@@ -309,24 +443,24 @@
|
||||
<el-col :span="8">
|
||||
<el-form-item label="外伤史">
|
||||
<el-radio-group v-model="formData.trauma_history">
|
||||
<el-radio :label="1">有</el-radio>
|
||||
<el-radio :label="0">无</el-radio>
|
||||
<el-radio-button :label="1">有</el-radio-button>
|
||||
<el-radio-button :label="0">无</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="手术史">
|
||||
<el-radio-group v-model="formData.surgery_history">
|
||||
<el-radio :label="1">有</el-radio>
|
||||
<el-radio :label="0">无</el-radio>
|
||||
<el-radio-button :label="1">有</el-radio-button>
|
||||
<el-radio-button :label="0">无</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="过敏史">
|
||||
<el-radio-group v-model="formData.allergy_history">
|
||||
<el-radio :label="1">有</el-radio>
|
||||
<el-radio :label="0">无</el-radio>
|
||||
<el-radio-button :label="1">有</el-radio-button>
|
||||
<el-radio-button :label="0">无</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -336,16 +470,16 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="家族病史">
|
||||
<el-radio-group v-model="formData.family_history">
|
||||
<el-radio :label="1">有</el-radio>
|
||||
<el-radio :label="0">无</el-radio>
|
||||
<el-radio-button :label="1">有</el-radio-button>
|
||||
<el-radio-button :label="0">无</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="妊娠哺乳史">
|
||||
<el-radio-group v-model="formData.pregnancy_history">
|
||||
<el-radio :label="1">有</el-radio>
|
||||
<el-radio :label="0">无</el-radio>
|
||||
<el-radio-button :label="1">有</el-radio-button>
|
||||
<el-radio-button :label="0">无</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -372,14 +506,6 @@
|
||||
<div class="form-tips">支持上传检查报告、病历、彩超等影像科检查图片,最多10个文件</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="症状" prop="symptoms">
|
||||
<el-input
|
||||
v-model="formData.symptoms"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入症状"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
@@ -494,7 +620,16 @@ const formData = ref({
|
||||
diagnosis_type: '',
|
||||
syndrome_type: '',
|
||||
diabetes_type: '',
|
||||
// 现病史字段
|
||||
diabetes_discovery_year: undefined as number | undefined,
|
||||
local_hospital_diagnosis: [],
|
||||
local_hospital_name: '',
|
||||
marital_status: undefined as number | undefined,
|
||||
height: undefined as number | undefined,
|
||||
weight: undefined as number | undefined,
|
||||
region: '',
|
||||
systolic_pressure: undefined as number | undefined,
|
||||
diastolic_pressure: undefined as number | undefined,
|
||||
fasting_blood_sugar: undefined as number | undefined,
|
||||
appetite: '',
|
||||
water_intake: '',
|
||||
diet_condition: [],
|
||||
@@ -509,18 +644,14 @@ const formData = ref({
|
||||
stool_condition: [],
|
||||
kidney_condition: [],
|
||||
fatty_liver_degree: '',
|
||||
// 既往史
|
||||
past_history: [],
|
||||
// 其他病史
|
||||
trauma_history: 0,
|
||||
surgery_history: 0,
|
||||
allergy_history: 0,
|
||||
family_history: 0,
|
||||
pregnancy_history: 0,
|
||||
// 文件上传
|
||||
tongue_images: [],
|
||||
report_files: [],
|
||||
// 诊断信息
|
||||
symptoms: '',
|
||||
tongue_coating: '',
|
||||
pulse: '',
|
||||
@@ -746,11 +877,17 @@ const handleClose = () => {
|
||||
id_card: '',
|
||||
phone: '',
|
||||
gender: 1,
|
||||
age: undefined as number | undefined,
|
||||
diagnosis_date: '',
|
||||
diagnosis_type: '',
|
||||
syndrome_type: '',
|
||||
diabetes_type: '',
|
||||
marital_status: undefined as number | undefined,
|
||||
height: undefined as number | undefined,
|
||||
weight: undefined as number | undefined,
|
||||
region: '',
|
||||
systolic_pressure: undefined as number | undefined,
|
||||
diastolic_pressure: undefined as number | undefined,
|
||||
fasting_blood_sugar: undefined as number | undefined,
|
||||
diabetes_discovery_year: undefined as number | undefined,
|
||||
local_hospital_diagnosis: [],
|
||||
local_hospital_name: '',
|
||||
appetite: '',
|
||||
water_intake: '',
|
||||
diet_condition: [],
|
||||
@@ -792,15 +929,18 @@ defineExpose({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-drawer {
|
||||
font-size: 12px;
|
||||
|
||||
:deep(.el-form) {
|
||||
padding-bottom: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-divider) {
|
||||
margin: 24px 0 20px;
|
||||
|
||||
.el-divider__text {
|
||||
font-size: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
background: #f5f7fa;
|
||||
@@ -813,9 +953,30 @@ defineExpose({
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-radio-button__inner) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox-button__inner) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 紧凑型表单项(单选框)
|
||||
.compact-form-item {
|
||||
|
||||
@@ -1,778 +0,0 @@
|
||||
<template>
|
||||
<div class="edit-drawer">
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
:title="drawerTitle"
|
||||
size="70%"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<el-tabs v-model="activeTab" class="px-4">
|
||||
<!-- 基本信息标签页 -->
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<el-divider content-position="left">基本信息</el-divider>
|
||||
|
||||
<el-form-item label="患者ID">
|
||||
<el-input v-model="formData.patient_id" disabled placeholder="系统自动生成" />
|
||||
</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="id_card">
|
||||
<el-input
|
||||
v-model="formData.id_card"
|
||||
placeholder="请输入身份证号"
|
||||
maxlength="18"
|
||||
@blur="handleIdCardBlur"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input
|
||||
v-model="formData.phone"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
@blur="handlePhoneBlur"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<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="12">
|
||||
<el-form-item label="诊断日期" prop="diagnosis_date">
|
||||
<el-date-picker
|
||||
v-model="formData.diagnosis_date"
|
||||
type="date"
|
||||
placeholder="请选择诊断日期"
|
||||
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="diagnosis_type">
|
||||
<el-select
|
||||
v-model="formData.diagnosis_type"
|
||||
placeholder="请选择诊断类型"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in diagnosisTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="证型" prop="syndrome_type">
|
||||
<el-select
|
||||
v-model="formData.syndrome_type"
|
||||
placeholder="请选择证型"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in syndromeTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 现病史 -->
|
||||
<el-divider content-position="left">现病史</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="口腔感觉">
|
||||
<el-radio-group v-model="formData.appetite">
|
||||
<el-radio v-for="item in appetiteOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="每日饮水量">
|
||||
<el-radio-group v-model="formData.water_intake">
|
||||
<el-radio v-for="item in waterIntakeOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="饮食情况">
|
||||
<el-checkbox-group v-model="formData.diet_condition">
|
||||
<el-checkbox v-for="item in dietConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="体重变化">
|
||||
<el-radio-group v-model="formData.weight_change">
|
||||
<el-radio v-for="item in weightChangeOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="肢体感觉">
|
||||
<el-checkbox-group v-model="formData.body_feeling">
|
||||
<el-checkbox v-for="item in bodyFeelingOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="睡眠情况">
|
||||
<el-checkbox-group v-model="formData.sleep_condition">
|
||||
<el-checkbox v-for="item in sleepConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="眼睛情况">
|
||||
<el-checkbox-group v-model="formData.eye_condition">
|
||||
<el-checkbox v-for="item in eyeConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="头部感觉">
|
||||
<el-checkbox-group v-model="formData.head_feeling">
|
||||
<el-checkbox v-for="item in headFeelingOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="出汗情况">
|
||||
<el-checkbox-group v-model="formData.sweat_condition">
|
||||
<el-checkbox v-for="item in sweatConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="皮肤情况">
|
||||
<el-checkbox-group v-model="formData.skin_condition">
|
||||
<el-checkbox v-for="item in skinConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="小便情况">
|
||||
<el-checkbox-group v-model="formData.urine_condition">
|
||||
<el-checkbox v-for="item in urineConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="大便情况">
|
||||
<el-checkbox-group v-model="formData.stool_condition">
|
||||
<el-checkbox v-for="item in stoolConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="腰肾情况">
|
||||
<el-checkbox-group v-model="formData.kidney_condition">
|
||||
<el-checkbox v-for="item in kidneyConditionOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="脂肪肝程度">
|
||||
<el-radio-group v-model="formData.fatty_liver_degree">
|
||||
<el-radio v-for="item in fattyLiverDegreeOptions" :key="item.value" :label="item.value">
|
||||
{{ item.name }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 既往史 -->
|
||||
<el-divider content-position="left">既往史</el-divider>
|
||||
|
||||
<el-form-item label="既往史" prop="past_history">
|
||||
<el-checkbox-group v-model="formData.past_history">
|
||||
<el-checkbox
|
||||
v-for="item in pastHistoryOptions"
|
||||
:key="item.value"
|
||||
:label="item.value"
|
||||
>
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 其他病史 -->
|
||||
<el-divider content-position="left">其他病史</el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="外伤史">
|
||||
<el-radio-group v-model="formData.trauma_history">
|
||||
<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="手术史">
|
||||
<el-radio-group v-model="formData.surgery_history">
|
||||
<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="过敏史">
|
||||
<el-radio-group v-model="formData.allergy_history">
|
||||
<el-radio :label="1">有</el-radio>
|
||||
<el-radio :label="0">无</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="家族病史">
|
||||
<el-radio-group v-model="formData.family_history">
|
||||
<el-radio :label="1">有</el-radio>
|
||||
<el-radio :label="0">无</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="妊娠哺乳史">
|
||||
<el-radio-group v-model="formData.pregnancy_history">
|
||||
<el-radio :label="1">有</el-radio>
|
||||
<el-radio :label="0">无</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 诊断信息 -->
|
||||
<el-divider content-position="left">诊断信息</el-divider>
|
||||
|
||||
<el-form-item label="舌苔照片">
|
||||
<material-picker
|
||||
v-model="formData.tongue_images"
|
||||
:limit="9"
|
||||
type="image"
|
||||
/>
|
||||
<div class="form-tips">支持上传多张舌苔照片,最多9张</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="检查报告">
|
||||
<material-picker
|
||||
v-model="formData.report_files"
|
||||
:limit="10"
|
||||
type="image"
|
||||
/>
|
||||
<div class="form-tips">支持上传检查报告、病历、彩超等影像科检查图片,最多10个文件</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="症状" prop="symptoms">
|
||||
<el-input
|
||||
v-model="formData.symptoms"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入症状"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="舌苔" prop="tongue_coating">
|
||||
<el-input
|
||||
v-model="formData.tongue_coating"
|
||||
placeholder="请输入舌苔情况"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="脉象" prop="pulse">
|
||||
<el-input
|
||||
v-model="formData.pulse"
|
||||
placeholder="请输入脉象"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="治则" prop="treatment_principle">
|
||||
<el-input
|
||||
v-model="formData.treatment_principle"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入治则"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="处方" prop="prescription">
|
||||
<el-input
|
||||
v-model="formData.prescription"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入处方"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="医嘱" prop="doctor_advice">
|
||||
<el-input
|
||||
v-model="formData.doctor_advice"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入医嘱"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 血糖血压记录标签页 -->
|
||||
<el-tab-pane label="血糖血压记录" name="blood" :disabled="!formData.id">
|
||||
<blood-record-list
|
||||
v-if="formData.id"
|
||||
:diagnosis-id="Number(formData.id)"
|
||||
:patient-id="Number(formData.patient_id)"
|
||||
/>
|
||||
<el-empty v-else description="请先保存诊单后再添加血糖血压记录" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3 px-4 pb-4">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'basic'"
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitting"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { tcmDiagnosisAdd, tcmDiagnosisEdit, tcmDiagnosisDetail, checkPhone, checkIdCard } from '@/api/tcm'
|
||||
import { getDictData } from '@/api/app'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import BloodRecordList from './components/BloodRecordList.vue'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const formRef = ref()
|
||||
const mode = ref('add')
|
||||
const submitting = ref(false)
|
||||
const activeTab = ref('basic')
|
||||
const drawerTitle = computed(() => (mode.value === 'add' ? '新增诊单' : '编辑诊单'))
|
||||
|
||||
const formData = ref({
|
||||
id: '',
|
||||
patient_id: '',
|
||||
patient_name: '',
|
||||
id_card: '',
|
||||
phone: '',
|
||||
gender: 1,
|
||||
age: undefined as number | undefined,
|
||||
diagnosis_date: '',
|
||||
diagnosis_type: '',
|
||||
syndrome_type: '',
|
||||
// 现病史字段
|
||||
appetite: '',
|
||||
water_intake: '',
|
||||
diet_condition: [],
|
||||
weight_change: '',
|
||||
body_feeling: [],
|
||||
sleep_condition: [],
|
||||
eye_condition: [],
|
||||
head_feeling: [],
|
||||
sweat_condition: [],
|
||||
skin_condition: [],
|
||||
urine_condition: [],
|
||||
stool_condition: [],
|
||||
kidney_condition: [],
|
||||
fatty_liver_degree: '',
|
||||
// 既往史
|
||||
past_history: [],
|
||||
// 其他病史
|
||||
trauma_history: 0,
|
||||
surgery_history: 0,
|
||||
allergy_history: 0,
|
||||
family_history: 0,
|
||||
pregnancy_history: 0,
|
||||
// 文件上传
|
||||
tongue_images: [],
|
||||
report_files: [],
|
||||
// 诊断信息
|
||||
symptoms: '',
|
||||
tongue_coating: '',
|
||||
pulse: '',
|
||||
treatment_principle: '',
|
||||
prescription: '',
|
||||
doctor_advice: '',
|
||||
remark: '',
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 自定义验证规则
|
||||
const validatePhone = (rule: any, value: any, callback: any) => {
|
||||
if (!value) {
|
||||
callback(new Error('请输入手机号'))
|
||||
return
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(value)) {
|
||||
callback(new Error('手机号格式不正确'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
const validateIdCard = (rule: any, value: any, callback: any) => {
|
||||
if (value && !/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(value)) {
|
||||
callback(new Error('身份证号格式不正确'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
const formRules = {
|
||||
patient_name: [{ required: true, message: '请输入患者姓名', trigger: 'blur' }],
|
||||
id_card: [{ validator: validateIdCard, trigger: 'blur' }],
|
||||
phone: [{ required: true, validator: validatePhone, trigger: 'blur' }],
|
||||
gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
|
||||
age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
|
||||
diagnosis_date: [{ required: true, message: '请选择诊断日期', trigger: 'change' }],
|
||||
diagnosis_type: [{ required: true, message: '请选择诊断类型', trigger: 'change' }],
|
||||
syndrome_type: [{ required: true, message: '请选择证型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 获取字典选项
|
||||
const diagnosisTypeOptions = ref<any[]>([])
|
||||
const syndromeTypeOptions = ref<any[]>([])
|
||||
const pastHistoryOptions = ref<any[]>([])
|
||||
// 现病史字典选项
|
||||
const appetiteOptions = ref<any[]>([])
|
||||
const waterIntakeOptions = ref<any[]>([])
|
||||
const dietConditionOptions = ref<any[]>([])
|
||||
const weightChangeOptions = ref<any[]>([])
|
||||
const bodyFeelingOptions = ref<any[]>([])
|
||||
const sleepConditionOptions = ref<any[]>([])
|
||||
const eyeConditionOptions = ref<any[]>([])
|
||||
const headFeelingOptions = ref<any[]>([])
|
||||
const sweatConditionOptions = ref<any[]>([])
|
||||
const skinConditionOptions = ref<any[]>([])
|
||||
const urineConditionOptions = ref<any[]>([])
|
||||
const stoolConditionOptions = ref<any[]>([])
|
||||
const kidneyConditionOptions = ref<any[]>([])
|
||||
const fattyLiverDegreeOptions = ref<any[]>([])
|
||||
|
||||
const getDictOptions = async () => {
|
||||
try {
|
||||
const [
|
||||
diagnosisType,
|
||||
syndromeType,
|
||||
pastHistory,
|
||||
appetite,
|
||||
waterIntake,
|
||||
dietCondition,
|
||||
weightChange,
|
||||
bodyFeeling,
|
||||
sleepCondition,
|
||||
eyeCondition,
|
||||
headFeeling,
|
||||
sweatCondition,
|
||||
skinCondition,
|
||||
urineCondition,
|
||||
stoolCondition,
|
||||
kidneyCondition,
|
||||
fattyLiverDegree
|
||||
] = await Promise.all([
|
||||
getDictData({ type: 'diagnosis_type' }),
|
||||
getDictData({ type: 'syndrome_type' }),
|
||||
getDictData({ type: 'past_history' }),
|
||||
getDictData({ type: 'appetite' }),
|
||||
getDictData({ type: 'water_intake' }),
|
||||
getDictData({ type: 'diet_condition' }),
|
||||
getDictData({ type: 'weight_change' }),
|
||||
getDictData({ type: 'body_feeling' }),
|
||||
getDictData({ type: 'sleep_condition' }),
|
||||
getDictData({ type: 'eye_condition' }),
|
||||
getDictData({ type: 'head_feeling' }),
|
||||
getDictData({ type: 'sweat_condition' }),
|
||||
getDictData({ type: 'skin_condition' }),
|
||||
getDictData({ type: 'urine_condition' }),
|
||||
getDictData({ type: 'stool_condition' }),
|
||||
getDictData({ type: 'kidney_condition' }),
|
||||
getDictData({ type: 'fatty_liver_degree' })
|
||||
])
|
||||
|
||||
diagnosisTypeOptions.value = diagnosisType?.diagnosis_type || []
|
||||
syndromeTypeOptions.value = syndromeType?.syndrome_type || []
|
||||
pastHistoryOptions.value = pastHistory?.past_history || []
|
||||
appetiteOptions.value = appetite?.appetite || []
|
||||
waterIntakeOptions.value = waterIntake?.water_intake || []
|
||||
dietConditionOptions.value = dietCondition?.diet_condition || []
|
||||
weightChangeOptions.value = weightChange?.weight_change || []
|
||||
bodyFeelingOptions.value = bodyFeeling?.body_feeling || []
|
||||
sleepConditionOptions.value = sleepCondition?.sleep_condition || []
|
||||
eyeConditionOptions.value = eyeCondition?.eye_condition || []
|
||||
headFeelingOptions.value = headFeeling?.head_feeling || []
|
||||
sweatConditionOptions.value = sweatCondition?.sweat_condition || []
|
||||
skinConditionOptions.value = skinCondition?.skin_condition || []
|
||||
urineConditionOptions.value = urineCondition?.urine_condition || []
|
||||
stoolConditionOptions.value = stoolCondition?.stool_condition || []
|
||||
kidneyConditionOptions.value = kidneyCondition?.kidney_condition || []
|
||||
fattyLiverDegreeOptions.value = fattyLiverDegree?.fatty_liver_degree || []
|
||||
} catch (error) {
|
||||
console.error('获取字典数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 手机号失焦检查
|
||||
const handlePhoneBlur = async () => {
|
||||
if (!formData.value.phone || !/^1[3-9]\d{9}$/.test(formData.value.phone)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await checkPhone({
|
||||
phone: formData.value.phone,
|
||||
id: formData.value.id || ''
|
||||
})
|
||||
|
||||
if (result.exists) {
|
||||
ElMessage.warning(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查手机号失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 身份证号失焦检查
|
||||
const handleIdCardBlur = async () => {
|
||||
if (!formData.value.id_card || !/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(formData.value.id_card)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await checkIdCard({
|
||||
id_card: formData.value.id_card,
|
||||
id: formData.value.id || ''
|
||||
})
|
||||
|
||||
if (result.exists) {
|
||||
ElMessage.warning(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查身份证号失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const open = async (type: string, id?: number) => {
|
||||
mode.value = type
|
||||
visible.value = true
|
||||
activeTab.value = 'basic' // 重置到基本信息标签页
|
||||
|
||||
// 加载字典数据
|
||||
await getDictOptions()
|
||||
|
||||
if (type === 'edit' && id) {
|
||||
const data = await tcmDiagnosisDetail({ id })
|
||||
formData.value = data
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate()
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (mode.value === 'add') {
|
||||
const result = await tcmDiagnosisAdd(formData.value)
|
||||
feedback.msgSuccess('添加成功')
|
||||
// 如果是新增,保存成功后切换到编辑模式,这样可以添加血糖血压记录
|
||||
if (result && result.id) {
|
||||
mode.value = 'edit'
|
||||
formData.value.id = result.id
|
||||
// 重新获取详情,确保patient_id等字段正确
|
||||
try {
|
||||
const detail = await tcmDiagnosisDetail({ id: result.id })
|
||||
formData.value.patient_id = detail.patient_id
|
||||
feedback.msgSuccess('诊单已保存,现在可以添加血糖血压记录了')
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
// 即使获取详情失败,也不影响主流程
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await tcmDiagnosisEdit(formData.value)
|
||||
feedback.msgSuccess('编辑成功')
|
||||
}
|
||||
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
formData.value = {
|
||||
id: '',
|
||||
patient_id: '',
|
||||
patient_name: '',
|
||||
id_card: '',
|
||||
phone: '',
|
||||
gender: 1,
|
||||
age: undefined as number | undefined,
|
||||
diagnosis_date: '',
|
||||
diagnosis_type: '',
|
||||
syndrome_type: '',
|
||||
appetite: '',
|
||||
water_intake: '',
|
||||
diet_condition: [],
|
||||
weight_change: '',
|
||||
body_feeling: [],
|
||||
sleep_condition: [],
|
||||
eye_condition: [],
|
||||
head_feeling: [],
|
||||
sweat_condition: [],
|
||||
skin_condition: [],
|
||||
urine_condition: [],
|
||||
stool_condition: [],
|
||||
kidney_condition: [],
|
||||
fatty_liver_degree: '',
|
||||
past_history: [],
|
||||
trauma_history: 0,
|
||||
surgery_history: 0,
|
||||
allergy_history: 0,
|
||||
family_history: 0,
|
||||
pregnancy_history: 0,
|
||||
tongue_images: [],
|
||||
report_files: [],
|
||||
symptoms: '',
|
||||
tongue_coating: '',
|
||||
pulse: '',
|
||||
treatment_principle: '',
|
||||
prescription: '',
|
||||
doctor_advice: '',
|
||||
remark: '',
|
||||
status: 1
|
||||
}
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-drawer {
|
||||
:deep(.el-checkbox-group) {
|
||||
.el-checkbox {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-radio-group) {
|
||||
.el-radio {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tips {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -55,7 +55,7 @@
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="mb-4 flex justify-between items-center">
|
||||
<div>
|
||||
<el-button type="primary" @click="handleAdd" v-perms="['tcm.diagnosis/add']">新增患者</el-button>
|
||||
<el-button type="primary" @click="handleAdd" v-perms="['tcm.diagnosis/add']">新增诊单</el-button>
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/assign']"
|
||||
type="success"
|
||||
@@ -76,8 +76,8 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="ID" prop="id" width="80" />
|
||||
<el-table-column label="患者姓名" prop="patient_name" min-width="100" />
|
||||
<el-table-column label="账户编号" prop="id" width="80" />
|
||||
<el-table-column label="账户姓名" prop="patient_name" min-width="100" />
|
||||
<el-table-column label="性别" prop="gender_desc" width="80" />
|
||||
<el-table-column label="年龄" prop="age" width="80" />
|
||||
<el-table-column label="诊断日期" prop="diagnosis_date_text" min-width="120" />
|
||||
@@ -91,7 +91,7 @@
|
||||
<span>{{ getDictLabel(syndromeTypeOptions, row.syndrome_type) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="指派医助" prop="assistant_name" min-width="100">
|
||||
<el-table-column label="归属助理" prop="assistant_name" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span>{{ getAssistantName(row.assistant_id || row.assistant) }}</span>
|
||||
</template>
|
||||
@@ -120,7 +120,7 @@
|
||||
>
|
||||
指派
|
||||
</el-button>
|
||||
<el-button
|
||||
<!-- <el-button
|
||||
v-perms="['tcm.diagnosis/video-call']"
|
||||
type="success"
|
||||
link
|
||||
@@ -135,14 +135,14 @@
|
||||
@click="handleGroupVideoCall(row)"
|
||||
>
|
||||
群通话
|
||||
</el-button>
|
||||
</el-button>-->
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/guahao']"
|
||||
type="success"
|
||||
link
|
||||
@click="handleAppointment(row)"
|
||||
>
|
||||
挂号
|
||||
>
|
||||
预约医生
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['tcm.diagnosis/guahao']"
|
||||
@@ -150,10 +150,10 @@
|
||||
link
|
||||
@click="handleMiniProgramQRCode(row)"
|
||||
>
|
||||
小程序二维码
|
||||
确认诊单二维码
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="handleEdit(row.id)" v-perms="['tcm.diagnosis/edit']">
|
||||
编辑
|
||||
诊单
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row.id)" v-perms="['tcm.diagnosis/delete']">
|
||||
删除
|
||||
@@ -168,7 +168,6 @@
|
||||
|
||||
<edit-popup ref="editRef" @success="getLists" />
|
||||
<detail-popup ref="detailRef" />
|
||||
<video-call ref="videoCallRef" />
|
||||
<appointment-popup ref="appointmentRef" @success="getLists" />
|
||||
|
||||
<!-- 小程序二维码弹窗 -->
|
||||
@@ -261,7 +260,6 @@ 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'
|
||||
@@ -339,7 +337,6 @@ const { pager, getLists, resetParams, resetPage } = usePaging({
|
||||
|
||||
const editRef = ref()
|
||||
const detailRef = ref()
|
||||
const videoCallRef = ref()
|
||||
const appointmentRef = ref()
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -443,50 +440,6 @@ const handleDelete = async (id: number) => {
|
||||
getLists()
|
||||
}
|
||||
|
||||
// 视频通话(一对一)
|
||||
const handleVideoCall = (row: any) => {
|
||||
if (!row.patient_id) {
|
||||
feedback.msgWarning('患者信息不完整')
|
||||
return
|
||||
}
|
||||
|
||||
videoCallRef.value?.open({
|
||||
diagnosisId: row.id,
|
||||
patientId: row.patient_id,
|
||||
patientName: row.patient_name,
|
||||
userId: `patient_${row.patient_id}`, // 患者的用户ID
|
||||
isGroup: false
|
||||
})
|
||||
}
|
||||
|
||||
// 群组视频通话(三方通话)
|
||||
const handleGroupVideoCall = (row: any) => {
|
||||
if (!row.patient_id) {
|
||||
feedback.msgWarning('患者信息不完整')
|
||||
return
|
||||
}
|
||||
|
||||
if (!row.assistant_id) {
|
||||
feedback.msgWarning('该诊单未指派医助,无法发起群组通话')
|
||||
return
|
||||
}
|
||||
|
||||
// 群组通话参与者顺序:当前登录用户(发起者)、患者、医助
|
||||
const userIds = [
|
||||
`patient_${row.patient_id}`, // 先邀请患者
|
||||
`doctor_${row.assistant_id}` // 再邀请医助
|
||||
]
|
||||
|
||||
videoCallRef.value?.open({
|
||||
diagnosisId: row.id,
|
||||
patientId: row.patient_id,
|
||||
patientName: row.patient_name,
|
||||
assistantId: row.assistant_id,
|
||||
userIds: userIds,
|
||||
isGroup: true
|
||||
})
|
||||
}
|
||||
|
||||
// 患者挂号
|
||||
const handleAppointment = (row: any) => {
|
||||
if (!row.patient_id) {
|
||||
|
||||
@@ -3,18 +3,10 @@
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>患者端测试 - 接听来电</span>
|
||||
<span>医疗助理端 - 接听来电</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
>
|
||||
<p>此页面用于测试音视频通话功能</p>
|
||||
<p>页面打开后会自动初始化患者端,等待医生端发起通话</p>
|
||||
</el-alert>
|
||||
|
||||
<div v-if="!initialized" class="init-section">
|
||||
<el-alert
|
||||
@@ -48,18 +40,21 @@
|
||||
<el-icon class="mr-2" :class="{ 'animate-pulse': hasIncomingCall }">
|
||||
<Phone />
|
||||
</el-icon>
|
||||
<span>{{ hasIncomingCall ? '收到来电!' : '患者端已初始化,等待来电...' }}</span>
|
||||
<span>{{ hasIncomingCall ? '收到来电!' : '医疗助理端已初始化,等待来电...' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="mt-2">
|
||||
<p><strong>患者ID:</strong> {{ currentUserId }}</p>
|
||||
<p><strong>医疗助理ID:</strong> {{ currentUserId }}</p>
|
||||
<p><strong>状态:</strong> {{ hasIncomingCall ? '来电中' : '在线' }}</p>
|
||||
<p v-if="hasIncomingCall && incomingCallInfo">
|
||||
<strong>通话类型:</strong> 视频通话
|
||||
<strong>通话类型:</strong> {{ incomingCallInfo.isGroup ? '群组视频通话' : '视频通话' }}
|
||||
</p>
|
||||
</div>
|
||||
</el-alert>
|
||||
|
||||
<!-- 调试信息 -->
|
||||
|
||||
|
||||
<!-- 来电操作按钮 -->
|
||||
<div v-if="hasIncomingCall" class="call-actions mt-4">
|
||||
<el-button
|
||||
@@ -89,8 +84,23 @@
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div id="call-container" class="call-container">
|
||||
<TUICallKit v-if="initialized" />
|
||||
<div v-if="hasIncomingCall || callConnected" id="call-container" class="call-container">
|
||||
<TUICallKit />
|
||||
</div>
|
||||
|
||||
<div v-else-if="initialized" class="waiting-container">
|
||||
<el-empty description="等待来电中...">
|
||||
<template #image>
|
||||
<el-icon :size="80" color="#909399">
|
||||
<Phone />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的 TUICallKit,用于接收来电信号 -->
|
||||
<div v-show="false">
|
||||
<TUICallKit v-if="initialized && !hasIncomingCall && !callConnected" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -99,7 +109,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted, computed, onMounted } from 'vue'
|
||||
import { Phone } from '@element-plus/icons-vue'
|
||||
import { TUICallKitAPI, TUICallKit, STATUS } from '@trtc/calls-uikit-vue'
|
||||
import { TUICallKitServer, TUICallKit, STATUS } from '@tencentcloud/call-uikit-vue'
|
||||
import { ElMessage, ElNotification } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
@@ -115,39 +125,69 @@ const loading = ref(false)
|
||||
const initialized = ref(false)
|
||||
const currentUserId = ref('')
|
||||
const hasIncomingCall = ref(false)
|
||||
const callConnected = ref(false) // 添加通话连接状态
|
||||
const incomingCallInfo = ref<any>(null)
|
||||
const statusCheckTimer = ref<any>(null)
|
||||
|
||||
// 页面加载时自动初始化
|
||||
onMounted(() => {
|
||||
initPatient()
|
||||
})
|
||||
|
||||
// 定时检查状态(作为备用方案)
|
||||
const startStatusCheck = () => {
|
||||
if (statusCheckTimer.value) {
|
||||
clearInterval(statusCheckTimer.value)
|
||||
}
|
||||
|
||||
statusCheckTimer.value = setInterval(() => {
|
||||
// 这里可以添加状态检查逻辑
|
||||
console.log('定时检查 - 当前时间:', new Date().toLocaleTimeString())
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
// 监听状态变化
|
||||
const handleStatusChange = ({ oldStatus, newStatus }: any) => {
|
||||
console.log('通话状态变化:', { oldStatus, newStatus })
|
||||
console.log('========================================')
|
||||
console.log('=== 通话状态变化回调触发 ===')
|
||||
console.log('oldStatus:', oldStatus)
|
||||
console.log('newStatus:', newStatus)
|
||||
console.log('STATUS 常量:', STATUS)
|
||||
console.log('========================================')
|
||||
|
||||
// 收到来电邀请
|
||||
// 收到来电邀请(与聊天组件同一套 SDK:@tencentcloud/call-uikit-vue)
|
||||
if (newStatus === STATUS.BE_INVITED) {
|
||||
console.log('>>> 收到来电邀请')
|
||||
hasIncomingCall.value = true
|
||||
incomingCallInfo.value = { oldStatus, newStatus }
|
||||
|
||||
// 显示来电通知
|
||||
ElNotification({
|
||||
title: '来电提醒',
|
||||
message: '收到视频通话请求',
|
||||
type: 'info',
|
||||
duration: 0, // 不自动关闭
|
||||
duration: 0,
|
||||
position: 'top-right'
|
||||
})
|
||||
|
||||
ElMessage.success('收到来电,请点击接听按钮')
|
||||
}
|
||||
|
||||
// 通话结束,回到空闲状态
|
||||
if (newStatus === STATUS.IDLE && hasIncomingCall.value) {
|
||||
// 通话接通
|
||||
if (newStatus === STATUS.CALLING_C2C_VIDEO || newStatus === STATUS.CALLING_GROUP_VIDEO) {
|
||||
console.log('>>> 通话已接通')
|
||||
ElMessage.success('通话已接通')
|
||||
hasIncomingCall.value = false
|
||||
incomingCallInfo.value = null
|
||||
ElMessage.info('通话已结束')
|
||||
callConnected.value = true // 设置通话连接状态
|
||||
}
|
||||
|
||||
// 通话结束,回到空闲状态(STATUS.IDLE 与聊天组件一致)
|
||||
if (newStatus === STATUS.IDLE) {
|
||||
if (hasIncomingCall.value || callConnected.value) {
|
||||
console.log('>>> 通话已结束')
|
||||
hasIncomingCall.value = false
|
||||
callConnected.value = false
|
||||
incomingCallInfo.value = null
|
||||
ElMessage.info('通话已结束')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,9 +200,11 @@ const initPatient = async () => {
|
||||
throw new Error('未获取到当前用户ID,请重新登录')
|
||||
}
|
||||
|
||||
console.log('=== 开始初始化医疗助理端 ===')
|
||||
console.log('当前用户ID:', patientId)
|
||||
ElMessage.info('正在获取签名...')
|
||||
|
||||
// 获取患者签名
|
||||
// 获取医疗助理签名
|
||||
const result = await request.get({
|
||||
url: '/tcm.diagnosis/getDoctorSignature',
|
||||
params: {
|
||||
@@ -177,35 +219,70 @@ const initPatient = async () => {
|
||||
const { sdkAppId, userId, userSig } = result
|
||||
currentUserId.value = userId
|
||||
|
||||
console.log('患者签名获取成功:', { userId, sdkAppId, userSigLength: userSig.length })
|
||||
console.log('医疗助理签名获取成功:', { userId, sdkAppId, userSigLength: userSig.length })
|
||||
console.log('完整签名信息:', result)
|
||||
ElMessage.info('正在初始化通话组件...')
|
||||
|
||||
// 设置状态变化回调
|
||||
TUICallKitAPI.setCallback({
|
||||
statusChanged: handleStatusChange
|
||||
})
|
||||
// 与聊天组件一致:使用 @tencentcloud/call-uikit-vue 的 TUICallKitServer(正式打包可收视频)
|
||||
console.log('=== 开始调用 TUICallKitServer.init ===')
|
||||
|
||||
// 初始化(注意:SDKAppID必须是数字类型)
|
||||
await TUICallKitAPI.init({
|
||||
userID: userId,
|
||||
userSig: userSig,
|
||||
SDKAppID: Number(sdkAppId) // 转换为数字类型
|
||||
})
|
||||
if (!TUICallKitServer || typeof TUICallKitServer.init !== 'function') {
|
||||
throw new Error('TUICallKitServer 未正确加载,请刷新页面重试')
|
||||
}
|
||||
|
||||
try {
|
||||
await TUICallKitServer.init({
|
||||
userID: userId,
|
||||
userSig: userSig,
|
||||
SDKAppID: Number(sdkAppId)
|
||||
})
|
||||
} catch (initError: any) {
|
||||
console.warn('TUICallKitServer.init 出现错误,但继续执行:', initError)
|
||||
}
|
||||
|
||||
console.log('=== TUICallKitServer.init 完成 ===')
|
||||
|
||||
const callbacks = {
|
||||
statusChanged: handleStatusChange,
|
||||
onError: (error: any) => {
|
||||
console.error('=== TUICallKit 错误回调 ===', error)
|
||||
ElMessage.error(`通话错误: ${error?.message || '未知错误'}`)
|
||||
},
|
||||
onInvited: (inviteData: any) => {
|
||||
console.log('=== 收到邀请回调 (onInvited) ===', inviteData)
|
||||
ElMessage.info('收到通话邀请 (onInvited)')
|
||||
},
|
||||
afterCalling: () => {
|
||||
console.log('=== afterCalling 回调 ===')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
TUICallKitServer.setCallback(callbacks)
|
||||
} catch (callbackError: any) {
|
||||
console.warn('设置回调时出现错误,但继续执行:', callbackError)
|
||||
}
|
||||
|
||||
console.log('回调设置完成')
|
||||
|
||||
console.log('TUICallKitAPI.init 完成')
|
||||
console.log('已注册状态监听回调')
|
||||
ElMessage.info('等待初始化完成...')
|
||||
|
||||
// 等待初始化完成
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
initialized.value = true
|
||||
ElMessage.success('患者端初始化成功,等待来电...')
|
||||
ElMessage.success('医疗助理端初始化成功,等待来电...')
|
||||
|
||||
console.log('患者端初始化完成,userId:', userId)
|
||||
console.log('=== 医疗助理端初始化完成 ===')
|
||||
console.log('userId:', userId)
|
||||
console.log('等待来电中...')
|
||||
console.log('请在医生端发起群组通话,目标用户ID应该是:', userId)
|
||||
|
||||
// 启动状态检查
|
||||
startStatusCheck()
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('初始化失败:', error)
|
||||
console.error('=== 初始化失败 ===', error)
|
||||
ElMessage.error(error.message || '初始化失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -216,9 +293,10 @@ const initPatient = async () => {
|
||||
const acceptCall = async () => {
|
||||
try {
|
||||
console.log('接听来电')
|
||||
await TUICallKitAPI.accept()
|
||||
await TUICallKitServer.accept()
|
||||
ElMessage.success('已接听通话')
|
||||
hasIncomingCall.value = false
|
||||
callConnected.value = true // 设置通话连接状态
|
||||
} catch (error: any) {
|
||||
console.error('接听失败:', error)
|
||||
ElMessage.error(error.message || '接听失败')
|
||||
@@ -229,7 +307,7 @@ const acceptCall = async () => {
|
||||
const rejectCall = async () => {
|
||||
try {
|
||||
console.log('拒绝来电')
|
||||
await TUICallKitAPI.reject()
|
||||
await TUICallKitServer.reject()
|
||||
ElMessage.info('已拒绝通话')
|
||||
hasIncomingCall.value = false
|
||||
incomingCallInfo.value = null
|
||||
@@ -243,14 +321,20 @@ const reset = () => {
|
||||
initialized.value = false
|
||||
currentUserId.value = ''
|
||||
hasIncomingCall.value = false
|
||||
callConnected.value = false
|
||||
incomingCallInfo.value = null
|
||||
}
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (statusCheckTimer.value) {
|
||||
clearInterval(statusCheckTimer.value)
|
||||
}
|
||||
|
||||
if (initialized.value) {
|
||||
// 清理资源
|
||||
hasIncomingCall.value = false
|
||||
callConnected.value = false
|
||||
incomingCallInfo.value = null
|
||||
}
|
||||
})
|
||||
@@ -287,6 +371,25 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.call-container {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
margin-top: 20px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.call-container > * {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.waiting-container {
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
margin-top: 20px;
|
||||
@@ -295,7 +398,6 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 确保 TUICallKit 组件正确显示 */
|
||||
@@ -312,6 +414,50 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 强制 TUICallKit 组件撑满容器 */
|
||||
:deep(.TUICallKit) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
:deep(.tui-call-kit) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.tui-call-kit-mini) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 隐藏 TUICallKit 的默认最小化窗口 */
|
||||
:deep(.tui-call-kit-window) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
min-height: 500px !important;
|
||||
}
|
||||
|
||||
/* 确保视频容器撑满 */
|
||||
:deep(.tui-video-container) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.tui-call-kit-video) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 隐藏所有可能的最小化或浮窗样式 */
|
||||
:deep([class*="mini"]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:deep([class*="float"]) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -361,4 +507,26 @@ onUnmounted(() => {
|
||||
:deep(.el-alert__description li) {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.debug-info code {
|
||||
background: #f5f5f5;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
color: #e6a23c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.debug-info ul {
|
||||
margin: 8px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.debug-info li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,6 +25,26 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 企业微信绑定 -->
|
||||
<el-form-item label="企业微信:" v-if="wxWorkEnabled">
|
||||
<div class="flex items-center gap-3">
|
||||
<template v-if="formData.work_wechat_userid">
|
||||
<el-tag type="success" size="large">
|
||||
<el-icon class="mr-1"><CircleCheck /></el-icon>
|
||||
已绑定({{ formData.work_wechat_userid }})
|
||||
</el-tag>
|
||||
<el-button type="danger" link @click="handleUnbind">解绑</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag type="info" size="large">未绑定</el-tag>
|
||||
<el-button type="primary" @click="showBindDialog">
|
||||
<el-icon class="mr-1"><Connection /></el-icon>
|
||||
扫码绑定
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="当前密码:" prop="password_old">
|
||||
<div class="w-80">
|
||||
<el-input
|
||||
@@ -62,97 +82,213 @@
|
||||
<footer-btns>
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</footer-btns>
|
||||
|
||||
<!-- 企业微信扫码绑定弹窗 -->
|
||||
<el-dialog
|
||||
v-model="bindDialogVisible"
|
||||
title="扫码绑定企业微信"
|
||||
width="440px"
|
||||
:close-on-click-modal="false"
|
||||
@close="onBindDialogClose"
|
||||
>
|
||||
<div class="bind-qrcode-wrap">
|
||||
<div v-if="bindLoading" class="text-center py-8">
|
||||
<el-icon class="is-loading" :size="36" color="var(--el-color-primary)">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<div class="mt-3 text-gray-400">加载企业微信扫码...</div>
|
||||
</div>
|
||||
<div v-else id="bind_wxwork_qrcode" class="bind-qrcode"></div>
|
||||
</div>
|
||||
<div class="text-center text-sm text-gray-400 mt-2 mb-2">
|
||||
请使用企业微信扫描二维码完成绑定
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="userSetting">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { nextTick, onMounted, ref, reactive } from 'vue'
|
||||
import { Loading, CircleCheck, Connection } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { setUserInfo } from '@/api/user'
|
||||
import { setUserInfo, getWorkWechatConfig, bindWorkWechat, unbindWorkWechat } from '@/api/user'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const userStore = useUserStore()
|
||||
// 表单数据
|
||||
|
||||
const formData = reactive({
|
||||
avatar: '', // 头像
|
||||
account: '', // 账号
|
||||
name: '', // 名称
|
||||
password_old: '', // 当前密码
|
||||
password: '', // 新的密码
|
||||
password_confirm: '' // 确定密码
|
||||
avatar: '',
|
||||
account: '',
|
||||
name: '',
|
||||
work_wechat_userid: '',
|
||||
password_old: '',
|
||||
password: '',
|
||||
password_confirm: ''
|
||||
})
|
||||
|
||||
// 表单校验规则
|
||||
const rules = reactive<object>({
|
||||
avatar: [
|
||||
{
|
||||
required: true,
|
||||
message: '头像不能为空',
|
||||
trigger: ['change']
|
||||
}
|
||||
],
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入名称',
|
||||
trigger: ['blur']
|
||||
}
|
||||
]
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: ['change'] }],
|
||||
name: [{ required: true, message: '请输入名称', trigger: ['blur'] }]
|
||||
})
|
||||
|
||||
// 企业微信相关
|
||||
const wxWorkEnabled = ref(false)
|
||||
const wxWorkConfig = ref<{ corp_id: string; agent_id: string }>({ corp_id: '', agent_id: '' })
|
||||
const bindDialogVisible = ref(false)
|
||||
const bindLoading = ref(false)
|
||||
|
||||
// 获取个人设置
|
||||
const getUser = async () => {
|
||||
const userInfo = userStore.userInfo
|
||||
for (const key in formData) {
|
||||
//@ts-ignore
|
||||
formData[key] = userInfo[key]
|
||||
if (userInfo[key] !== undefined) formData[key] = userInfo[key]
|
||||
}
|
||||
}
|
||||
|
||||
// 设置个人设置
|
||||
const setUser = async () => {
|
||||
if (formData.password_old || formData.password || formData.password_confirm) {
|
||||
if (!formData.password_old) {
|
||||
return feedback.msgError('请输入当前密码')
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
return feedback.msgError('请输入新的密码')
|
||||
}
|
||||
|
||||
if (!formData.password_confirm) {
|
||||
return feedback.msgError('请输入确定密码')
|
||||
}
|
||||
|
||||
if (formData.password_confirm != formData.password) {
|
||||
return feedback.msgError('两次输入的密码不一样')
|
||||
}
|
||||
if (!formData.password_old) return feedback.msgError('请输入当前密码')
|
||||
if (!formData.password) return feedback.msgError('请输入新的密码')
|
||||
if (!formData.password_confirm) return feedback.msgError('请输入确定密码')
|
||||
if (formData.password_confirm != formData.password) return feedback.msgError('两次输入的密码不一样')
|
||||
}
|
||||
|
||||
if (formData.password_old && formData.password && formData.password_confirm) {
|
||||
if (formData.password_old.length < 6 || formData.password_old.length > 32) {
|
||||
return feedback.msgError('密码长度在6到32之间')
|
||||
}
|
||||
if (formData.password.length < 6 || formData.password.length > 32) {
|
||||
return feedback.msgError('密码长度在6到32之间')
|
||||
}
|
||||
if (formData.password_confirm.length < 6 || formData.password_confirm.length > 32) {
|
||||
return feedback.msgError('密码长度在6到32之间')
|
||||
}
|
||||
if (formData.password_old.length < 6 || formData.password_old.length > 32) return feedback.msgError('密码长度在6到32之间')
|
||||
if (formData.password.length < 6 || formData.password.length > 32) return feedback.msgError('密码长度在6到32之间')
|
||||
if (formData.password_confirm.length < 6 || formData.password_confirm.length > 32) return feedback.msgError('密码长度在6到32之间')
|
||||
}
|
||||
await setUserInfo(formData)
|
||||
|
||||
const { work_wechat_userid, ...saveData } = formData
|
||||
await setUserInfo(saveData)
|
||||
userStore.getUserInfo()
|
||||
}
|
||||
|
||||
// 提交数据
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate()
|
||||
setUser()
|
||||
}
|
||||
|
||||
// 生成绑定用的 redirect_uri(带 bind_wxwork=1 标记)
|
||||
const getBindRedirectUri = () => {
|
||||
const base = window.location.origin + window.location.pathname
|
||||
return base + '?bind_wxwork=1'
|
||||
}
|
||||
|
||||
// 打开绑定弹窗,渲染扫码
|
||||
const showBindDialog = async () => {
|
||||
bindDialogVisible.value = true
|
||||
bindLoading.value = true
|
||||
|
||||
if (!(window as any).WwLogin) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://wwcdn.weixin.qq.com/node/wework/wwopen/js/wwLogin-1.2.7.js'
|
||||
script.onload = () => resolve()
|
||||
script.onerror = () => reject(new Error('加载企业微信JS失败'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
bindLoading.value = false
|
||||
await nextTick()
|
||||
|
||||
const redirectUri = encodeURIComponent(getBindRedirectUri())
|
||||
new (window as any).WwLogin({
|
||||
id: 'bind_wxwork_qrcode',
|
||||
appid: wxWorkConfig.value.corp_id,
|
||||
agentid: wxWorkConfig.value.agent_id,
|
||||
redirect_uri: redirectUri,
|
||||
lang: 'zh',
|
||||
state: 'bind_wxwork',
|
||||
href: '',
|
||||
self_redirect: false,
|
||||
})
|
||||
}
|
||||
|
||||
const onBindDialogClose = () => {
|
||||
bindLoading.value = false
|
||||
}
|
||||
|
||||
// 解绑
|
||||
const handleUnbind = async () => {
|
||||
await feedback.confirm('确定要解绑企业微信吗?解绑后将无法使用企业微信扫码登录。')
|
||||
try {
|
||||
await unbindWorkWechat()
|
||||
formData.work_wechat_userid = ''
|
||||
ElMessage.success('解绑成功')
|
||||
userStore.getUserInfo()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || '解绑失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理绑定回调 code
|
||||
const handleBindCallback = async (code: string) => {
|
||||
try {
|
||||
const res = await bindWorkWechat({ code })
|
||||
formData.work_wechat_userid = res?.work_wechat_userid || ''
|
||||
ElMessage.success('企业微信绑定成功')
|
||||
userStore.getUserInfo()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '绑定失败')
|
||||
} finally {
|
||||
// 清除 URL 参数
|
||||
const cleanUrl = window.location.pathname
|
||||
window.history.replaceState({}, '', cleanUrl)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 加载企业微信配置
|
||||
try {
|
||||
const res = await getWorkWechatConfig()
|
||||
if (res?.enabled) {
|
||||
wxWorkEnabled.value = true
|
||||
wxWorkConfig.value = { corp_id: res.corp_id, agent_id: res.agent_id }
|
||||
}
|
||||
} catch (e) {
|
||||
// 未配置则不显示
|
||||
}
|
||||
|
||||
// 检查是否是绑定回调
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const bindCode = urlParams.get('code')
|
||||
const state = urlParams.get('state')
|
||||
const isBind = urlParams.get('bind_wxwork')
|
||||
|
||||
if (bindCode && (state === 'bind_wxwork' || isBind === '1')) {
|
||||
handleBindCallback(bindCode)
|
||||
}
|
||||
})
|
||||
|
||||
getUser()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.bind-qrcode-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.bind-qrcode {
|
||||
width: 340px;
|
||||
height: 360px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(iframe) {
|
||||
width: 340px !important;
|
||||
height: 360px !important;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user