新增功能
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
# 腾讯云 Chat UIKit 集成说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
已成功将腾讯云 Chat UIKit Vue3 组件集成到预约管理系统的"聊天"按钮中,使用官方推荐的组件化方式。
|
||||||
|
|
||||||
|
## 实现内容
|
||||||
|
|
||||||
|
### 1. 创建聊天对话框组件
|
||||||
|
- 文件位置: `admin/src/components/chat-dialog/index.vue`
|
||||||
|
- 功能特性:
|
||||||
|
- 使用 Element Plus Dialog 作为容器
|
||||||
|
- 集成腾讯云 Chat UIKit 的官方组件:
|
||||||
|
- UIKitProvider: 提供 IM SDK 上下文
|
||||||
|
- ConversationList: 会话列表组件
|
||||||
|
- Chat: 聊天消息组件
|
||||||
|
- 自动获取医生签名并初始化聊天
|
||||||
|
- 加载状态和错误处理
|
||||||
|
- 响应式布局:左侧会话列表(300px)+ 右侧聊天区域
|
||||||
|
- 动态加载样式文件
|
||||||
|
|
||||||
|
### 2. 集成到预约列表页面
|
||||||
|
- 文件位置: `admin/src/views/tcm/appointment/list.vue`
|
||||||
|
- 修改内容:
|
||||||
|
- 导入 ChatDialog 组件
|
||||||
|
- 添加 chatDialogRef 引用
|
||||||
|
- 修改 handleCreatePrescription 方法,传递患者信息和诊单ID
|
||||||
|
- 在模板中添加 chat-dialog 组件
|
||||||
|
|
||||||
|
### 3. 使用的 API
|
||||||
|
- 复用现有的 `getCallSignature` API (`/tcm.diagnosis/getCallSignature`)
|
||||||
|
- 该接口返回腾讯云 IM 所需的:
|
||||||
|
- sdkAppId: 应用 ID
|
||||||
|
- userId: 当前用户 ID (医生)
|
||||||
|
- userSig: 用户签名
|
||||||
|
- patientUserId: 患者的 IM 用户 ID
|
||||||
|
|
||||||
|
### 4. 使用的组件
|
||||||
|
根据腾讯云官方文档 (https://cloud.tencent.com/document/product/269/123108):
|
||||||
|
- `UIKitProvider`: 提供 SDK 配置和上下文
|
||||||
|
- `ConversationList`: 显示会话列表
|
||||||
|
- `Chat`: 显示聊天消息和输入框
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
1. 在预约列表中找到状态为"已预约"的记录
|
||||||
|
2. 点击操作列中的"聊天"按钮
|
||||||
|
3. 系统会自动:
|
||||||
|
- 获取医生的腾讯云 IM 签名
|
||||||
|
- 初始化聊天组件
|
||||||
|
- 打开聊天对话框
|
||||||
|
4. 在对话框中可以:
|
||||||
|
- 查看左侧的会话列表
|
||||||
|
- 在右侧与患者进行实时聊天
|
||||||
|
- 发送文本、表情、图片等消息
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- @tencentcloud/chat-uikit-vue3: ^4.5.4
|
||||||
|
- @tencentcloud/chat: IM SDK
|
||||||
|
- Element Plus: 对话框和 UI 组件
|
||||||
|
- Vue 3 Composition API
|
||||||
|
|
||||||
|
## 样式处理
|
||||||
|
|
||||||
|
由于 Vite 对 node_modules 中 CSS 文件的导入限制,样式文件采用动态导入方式:
|
||||||
|
- 在组件 onMounted 时动态导入样式
|
||||||
|
- 如果样式加载失败,功能仍可正常使用,只是缺少部分美化
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 需要确保后端 `/tcm.diagnosis/getCallSignature` 接口正常工作
|
||||||
|
2. 需要配置腾讯云 IM 的 SDKAppID 和密钥
|
||||||
|
3. 聊天功能需要患者端也集成相应的 IM 功能
|
||||||
|
4. 确保网络环境支持 WebSocket 连接
|
||||||
|
5. 会话ID格式为 `C2C{patientUserId}`,表示单聊(C2C = Client to Client)
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
1. 可以自定义聊天界面的主题和样式
|
||||||
|
2. 可以添加消息提醒功能
|
||||||
|
3. 可以添加聊天记录的持久化存储
|
||||||
|
4. 可以添加更多富媒体消息类型(语音、视频等)
|
||||||
|
5. 可以优化样式加载方式,确保样式稳定加载
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# 腾讯云 Chat UIKit 完整集成说明
|
||||||
|
|
||||||
|
## ✅ 已完成功能
|
||||||
|
|
||||||
|
### 1. 聊天界面组件
|
||||||
|
- **文件位置**: `admin/src/components/chat-dialog/index.vue`
|
||||||
|
- **使用官方组件**:
|
||||||
|
- `UIKitProvider`: IM SDK 上下文提供者
|
||||||
|
- `ConversationList`: 会话列表(左侧)
|
||||||
|
- `Chat`: 聊天容器
|
||||||
|
- `ChatHeader`: 聊天头部
|
||||||
|
- `MessageList`: 消息列表
|
||||||
|
- `MessageInput`: 消息输入框
|
||||||
|
|
||||||
|
### 2. 已支持的功能
|
||||||
|
- ✅ **会话管理**: 显示所有聊天会话,支持切换
|
||||||
|
- ✅ **文本消息**: 发送和接收文本消息
|
||||||
|
- ✅ **表情选择**: 内置表情选择器,点击表情图标使用
|
||||||
|
- ✅ **图片上传**: 点击图片图标上传图片
|
||||||
|
- ✅ **文件上传**: 点击文件图标上传文件
|
||||||
|
- ✅ **视频上传**: 点击视频图标上传视频
|
||||||
|
- ✅ **消息状态**: 显示已读/未读状态
|
||||||
|
- ✅ **实时通信**: WebSocket 实时消息推送
|
||||||
|
|
||||||
|
### 3. 登录流程
|
||||||
|
```typescript
|
||||||
|
await login({
|
||||||
|
sdkAppId: Number(res.sdkAppId),
|
||||||
|
userId: res.userId,
|
||||||
|
userSig: res.userSig,
|
||||||
|
useUploadPlugin: true // 启用文件上传功能
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 使用说明
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
1. 在预约列表中找到"已预约"状态的记录
|
||||||
|
2. 点击"聊天"按钮
|
||||||
|
3. 等待初始化完成(约1-2秒)
|
||||||
|
4. 开始聊天
|
||||||
|
|
||||||
|
### 功能使用
|
||||||
|
- **发送文本**: 在输入框输入文字,点击"发送"按钮
|
||||||
|
- **发送表情**: 点击输入框左侧的表情图标 😊
|
||||||
|
- **发送图片**: 点击图片图标 🖼️,选择图片文件
|
||||||
|
- **发送文件**: 点击文件图标 📁,选择任意文件
|
||||||
|
- **发送视频**: 点击视频图标 🎬,选择视频文件
|
||||||
|
|
||||||
|
## 🔧 技术实现
|
||||||
|
|
||||||
|
### 依赖包
|
||||||
|
- `@tencentcloud/chat-uikit-vue3`: ^4.5.4
|
||||||
|
- `@tencentcloud/chat`: IM SDK(自动依赖)
|
||||||
|
|
||||||
|
### 样式文件
|
||||||
|
- 复制到: `admin/src/assets/chat-uikit.css`
|
||||||
|
- 导入方式: `import '@/assets/chat-uikit.css'`
|
||||||
|
|
||||||
|
### API 接口
|
||||||
|
- **接口**: `/tcm.diagnosis/getCallSignature`
|
||||||
|
- **参数**:
|
||||||
|
- `patient_id`: 患者ID
|
||||||
|
- `diagnosis_id`: 诊单ID
|
||||||
|
- **返回**:
|
||||||
|
- `sdkAppId`: 腾讯云应用ID
|
||||||
|
- `userId`: 医生IM账号
|
||||||
|
- `userSig`: 登录签名
|
||||||
|
- `patientUserId`: 患者IM账号
|
||||||
|
|
||||||
|
## ⚙️ 配置说明
|
||||||
|
|
||||||
|
### 后端配置
|
||||||
|
需要在 `server/.env` 中配置:
|
||||||
|
```env
|
||||||
|
[TRTC]
|
||||||
|
SDK_APP_ID = "1600127710"
|
||||||
|
SECRET_KEY = "your_secret_key"
|
||||||
|
ENABLE = "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件上传配置
|
||||||
|
- 已启用 `useUploadPlugin: true`
|
||||||
|
- 支持图片、文件、视频上传
|
||||||
|
- 需要确保服务器支持 CORS
|
||||||
|
|
||||||
|
## 📱 视频通话功能
|
||||||
|
|
||||||
|
当前聊天界面已包含基础的消息功能。如需集成音视频通话,需要:
|
||||||
|
|
||||||
|
1. 安装 TUICallKit 插件
|
||||||
|
2. 在 MessageInput 中配置通话按钮
|
||||||
|
3. 参考文档: https://cloud.tencent.com/document/product/647/115714
|
||||||
|
|
||||||
|
## 🎨 界面说明
|
||||||
|
|
||||||
|
### 布局结构
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 与 患者名 聊天 │
|
||||||
|
├──────────┬──────────────────────────┤
|
||||||
|
│ │ ChatHeader │
|
||||||
|
│ Conver- ├──────────────────────────┤
|
||||||
|
│ sation │ │
|
||||||
|
│ List │ MessageList │
|
||||||
|
│ │ │
|
||||||
|
│ (285px) ├──────────────────────────┤
|
||||||
|
│ │ MessageInput │
|
||||||
|
│ │ [😊][🖼️][📁][🎬] │
|
||||||
|
└──────────┴──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组件说明
|
||||||
|
- **ConversationList**: 左侧会话列表,宽度285px
|
||||||
|
- **ChatHeader**: 显示当前聊天对象信息
|
||||||
|
- **MessageList**: 消息展示区域,支持滚动
|
||||||
|
- **MessageInput**: 输入框和功能按钮
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **网络要求**: 需要支持 WebSocket 连接
|
||||||
|
2. **HTTPS**: 生产环境建议使用 HTTPS
|
||||||
|
3. **文件大小**: 注意文件上传大小限制
|
||||||
|
4. **患者端**: 患者端也需要集成相应的 IM 功能
|
||||||
|
5. **登出**: 关闭对话框时会自动登出 IM
|
||||||
|
|
||||||
|
## 🚀 后续优化
|
||||||
|
|
||||||
|
1. 集成 TUICallKit 实现音视频通话
|
||||||
|
2. 添加消息提醒和未读数提示
|
||||||
|
3. 自定义主题和样式
|
||||||
|
4. 添加消息撤回功能
|
||||||
|
5. 支持消息引用和转发
|
||||||
|
6. 添加聊天记录导出功能
|
||||||
|
|
||||||
|
## 📚 参考文档
|
||||||
|
|
||||||
|
- [Chat UIKit Vue3 官方文档](https://cloud.tencent.com/document/product/269/123108)
|
||||||
|
- [IM SDK API 文档](https://cloud.tencent.com/document/product/269/37411)
|
||||||
|
- [TUICallKit 集成文档](https://cloud.tencent.com/document/product/647/115714)
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# 表情功能故障排查指南
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
表情按钮显示但点击无反应
|
||||||
|
|
||||||
|
## 可能的原因
|
||||||
|
|
||||||
|
### 1. 表情包资源未加载
|
||||||
|
腾讯云 Chat UIKit 的表情包需要从 CDN 加载资源。
|
||||||
|
|
||||||
|
**检查方法**:
|
||||||
|
1. 打开浏览器开发者工具(F12)
|
||||||
|
2. 切换到 Console 标签
|
||||||
|
3. 查看是否有表情相关的错误信息
|
||||||
|
4. 切换到 Network 标签
|
||||||
|
5. 点击表情按钮,查看是否有请求失败
|
||||||
|
|
||||||
|
### 2. 表情包版权限制
|
||||||
|
根据腾讯云文档说明:
|
||||||
|
> 为尊重表情设计版权,chat-uikit-vue3 工程中不包含大表情元素切图,正式上线商用前请您替换为自己设计或拥有版权的其他表情包。默认的小黄脸表情包版权归腾讯云所有,仅供授权使用。
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- 使用腾讯云提供的默认表情包(需要授权)
|
||||||
|
- 或者配置自己的表情包
|
||||||
|
|
||||||
|
### 3. 样式文件加载问题
|
||||||
|
表情选择器的样式可能没有正确加载。
|
||||||
|
|
||||||
|
**检查方法**:
|
||||||
|
1. 打开开发者工具
|
||||||
|
2. 点击表情按钮
|
||||||
|
3. 检查 Elements 标签,看是否有表情选择器的 DOM 元素生成
|
||||||
|
4. 检查该元素的样式是否正确应用
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 方案 1:检查网络请求
|
||||||
|
```javascript
|
||||||
|
// 在浏览器控制台执行
|
||||||
|
console.log('检查表情资源加载情况')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案 2:使用自定义表情包
|
||||||
|
如果默认表情包无法使用,可以配置自定义表情包。
|
||||||
|
|
||||||
|
需要修改 `MessageInput` 组件的配置:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<MessageInput :emojiConfig="customEmojiConfig" />
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const customEmojiConfig = {
|
||||||
|
emojiList: [
|
||||||
|
{ name: '微笑', url: '/emoji/smile.png', code: '[微笑]' },
|
||||||
|
{ name: '大笑', url: '/emoji/laugh.png', code: '[大笑]' },
|
||||||
|
// ... 更多表情
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案 3:暂时禁用表情功能
|
||||||
|
如果表情功能不是必需的,可以暂时禁用:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<MessageInput :showEmoji="false" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案 4:联系腾讯云获取授权
|
||||||
|
如果需要使用腾讯云的默认表情包,需要:
|
||||||
|
1. 访问 [腾讯云工单系统](https://console.cloud.tencent.com/workorder/category)
|
||||||
|
2. 提交工单申请表情包使用授权
|
||||||
|
|
||||||
|
## 临时解决方案
|
||||||
|
|
||||||
|
目前其他功能(文本、图片、文件、视频)都可以正常使用。表情功能可以:
|
||||||
|
1. 先跳过,使用文本消息
|
||||||
|
2. 或者使用 Unicode 表情符号(😊😂❤️等)直接在文本中输入
|
||||||
|
|
||||||
|
## 调试步骤
|
||||||
|
|
||||||
|
### 步骤 1:检查控制台错误
|
||||||
|
打开浏览器控制台,点击表情按钮,查看是否有错误信息:
|
||||||
|
- 404 错误:表情资源文件未找到
|
||||||
|
- CORS 错误:跨域问题
|
||||||
|
- 其他 JavaScript 错误
|
||||||
|
|
||||||
|
### 步骤 2:检查 DOM 元素
|
||||||
|
点击表情按钮后,检查是否生成了表情选择器的 DOM:
|
||||||
|
```
|
||||||
|
<div class="emoji-picker">
|
||||||
|
<!-- 应该有表情列表 -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3:检查样式
|
||||||
|
如果 DOM 存在但不可见,可能是样式问题:
|
||||||
|
```css
|
||||||
|
/* 检查这些样式是否正确加载 */
|
||||||
|
.emoji-picker {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 建议
|
||||||
|
|
||||||
|
1. **短期方案**:使用文本消息和其他功能(图片、文件等)
|
||||||
|
2. **中期方案**:配置自定义表情包
|
||||||
|
3. **长期方案**:申请腾讯云表情包授权或购买商业表情包
|
||||||
|
|
||||||
|
## 参考文档
|
||||||
|
|
||||||
|
- [Chat UIKit Vue3 官方文档](https://cloud.tencent.com/document/product/269/123108)
|
||||||
|
- [表情包配置说明](https://cloud.tencent.com/document/product/269/37411)
|
||||||
|
- [工单提交](https://console.cloud.tencent.com/workorder/category)
|
||||||
@@ -51,6 +51,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script>
|
||||||
|
// 动态加载 Chat UIKit 样式
|
||||||
|
(function() {
|
||||||
|
var link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = 'https://web.sdk.qcloud.com/im/assets/latest/index.css';
|
||||||
|
document.head.appendChild(link);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|||||||
Generated
+1376
-12
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -12,7 +12,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"@highlightjs/vue-plugin": "^2.1.0",
|
"@highlightjs/vue-plugin": "^2.1.0",
|
||||||
"@trtc/calls-uikit-vue": "^4.2.2",
|
"@tencentcloud/chat-uikit-vue3": "^4.5.4",
|
||||||
|
"@trtc/calls-uikit-vue": "^4.4.6",
|
||||||
"@vue/shared": "^3.5.13",
|
"@vue/shared": "^3.5.13",
|
||||||
"@vueuse/core": "^12.7.0",
|
"@vueuse/core": "^12.7.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}:root{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,*:after,*:before{box-sizing:border-box}ul,li{list-style:none;padding:0;margin:0}picture,img,video,canvas,svg{display:block;max-width:100%}img{max-width:100%;height:auto;vertical-align:middle;image-rendering:-webkit-optimize-contrast;aspect-ratio:attr(width)/attr(height);display:inline-block;-webkit-user-drag:none;-webkit-user-select:none;user-select:none}img:not([src],[srcset]){visibility:hidden}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div class="side-tab" :class="{ dark: isDark }">
|
||||||
|
<!-- 用户头像 -->
|
||||||
|
<div class="avatar-wrapper">
|
||||||
|
<Avatar class="avatar" :src="loginUserInfo?.avatarUrl" />
|
||||||
|
<div class="tooltip">
|
||||||
|
<div class="tooltip-name">{{ loginUserInfo?.userName || '未命名' }}</div>
|
||||||
|
<div class="tooltip-id">ID: {{ loginUserInfo?.userId }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Tab 切换 -->
|
||||||
|
<div class="tabs">
|
||||||
|
<div
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: props.activeTab === 'conversations' }"
|
||||||
|
@click="handleTabChange('conversations')"
|
||||||
|
title="会话"
|
||||||
|
>
|
||||||
|
<IconChatNew size="24" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: props.activeTab === 'contacts' }"
|
||||||
|
@click="handleTabChange('contacts')"
|
||||||
|
title="联系人"
|
||||||
|
>
|
||||||
|
<IconContacts size="24" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useLoginState, useUIKit, Avatar } from '@tencentcloud/chat-uikit-vue3';
|
||||||
|
import { IconChatNew, IconContacts } from '@tencentcloud/uikit-base-component-vue3';
|
||||||
|
|
||||||
|
const { theme } = useUIKit();
|
||||||
|
const { loginUserInfo } = useLoginState();
|
||||||
|
|
||||||
|
const isDark = computed(() => theme.value === 'dark' || theme.value === 'serious');
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
activeTab?: 'conversations' | 'contacts';
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
activeTab: 'conversations'
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
tabChange: [tab: 'conversations' | 'contacts'];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const handleTabChange = (tab: 'conversations' | 'contacts') => {
|
||||||
|
emit('tabChange', tab);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.side-tab{width:72px;height:100vh;background:var(--bg-color-function);display:flex;flex-direction:column;align-items:center;padding:20px 0;transition:background 0.3s;}.avatar-wrapper{position:relative;margin-bottom:24px;cursor:pointer;}.avatar-wrapper:hover:deep(.avatar){transform:scale(1.05);box-shadow:0 4px 12px rgba(0,0,0,0.15);}.tooltip{position:absolute;left:60px;top:50%;transform:translateY(-50%);padding:8px 12px;background:rgba(0,0,0,0.85);color:#fff;border-radius:6px;white-space:nowrap;opacity:0;visibility:hidden;pointer-events:none;transition:all 0.3s;z-index:1000;}.tooltip::before{content:'';position:absolute;left:-6px;top:50%;transform:translateY(-50%);border:6px solid transparent;border-right-color:rgba(0,0,0,0.85);}.avatar-wrapper:hover .tooltip{opacity:1;visibility:visible;}.tooltip-name{font-size:14px;font-weight:500;margin-bottom:4px;}.tooltip-id{font-size:12px;opacity:0.8;}.tabs{display:flex;flex-direction:column;gap:16px;}.tab-item{width:48px;height:48px;display:flex;align-items:center;justify-content:center;border-radius:12px;cursor:pointer;transition:all 0.3s;color:var(--text-color-primary);}.tab-item:hover{background:rgba(0,0,0,0.05);}.tab-item.active{background:var(--button-color-primary-default);color:var(--text-color-button);}.side-tab.dark{background:#1a1a1a;}.side-tab.dark .avatar-wrapper:hover:deep(.avatar){box-shadow:0 4px 12px rgba(255,255,255,0.2);}.side-tab.dark .tooltip{background:rgba(255,255,255,0.95);color:#1a1a1a;}.side-tab.dark .tooltip::before{border-right-color:rgba(255,255,255,0.95);}.side-tab.dark .tab-item:hover{background:rgba(255,255,255,0.1);}.side-tab.dark .tab-item.active{background:#1890ff;color:#fff;}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="`与 ${patientName} 聊天`"
|
||||||
|
width="1200px"
|
||||||
|
top="5vh"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
class="chat-dialog"
|
||||||
|
>
|
||||||
|
<div class="chat-container">
|
||||||
|
<div v-if="!isReady" class="loading-container">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>{{ loadingText }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error" class="error-container">
|
||||||
|
<el-alert :title="error" type="error" :closable="false" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="chat-content">
|
||||||
|
<UIKitProvider>
|
||||||
|
<div class="chat-layout">
|
||||||
|
<ConversationList class="conversation-list" />
|
||||||
|
<Chat class="chat-area">
|
||||||
|
<MessageList />
|
||||||
|
<MessageInput />
|
||||||
|
</Chat>
|
||||||
|
</div>
|
||||||
|
</UIKitProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
|
import { getCallSignature } from '@/api/tcm'
|
||||||
|
import feedback from '@/utils/feedback'
|
||||||
|
import {
|
||||||
|
ConversationList,
|
||||||
|
Chat,
|
||||||
|
MessageList,
|
||||||
|
MessageInput,
|
||||||
|
UIKitProvider,
|
||||||
|
useLoginState
|
||||||
|
} from '@tencentcloud/chat-uikit-vue3'
|
||||||
|
import '@/assets/chat-uikit.css'
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const isReady = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const patientName = ref('')
|
||||||
|
const patientId = ref<number | null>(null)
|
||||||
|
const diagnosisId = ref<number | null>(null)
|
||||||
|
const loadingText = ref('正在初始化...')
|
||||||
|
const patientUserId = ref('')
|
||||||
|
|
||||||
|
const { login, logout } = useLoginState()
|
||||||
|
|
||||||
|
const open = async (data: { patientId: number; patientName: string; diagnosisId?: number }) => {
|
||||||
|
visible.value = true
|
||||||
|
patientName.value = data.patientName
|
||||||
|
patientId.value = data.patientId
|
||||||
|
diagnosisId.value = data.diagnosisId || null
|
||||||
|
error.value = ''
|
||||||
|
isReady.value = false
|
||||||
|
loadingText.value = '正在获取签名...'
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取医生的签名信息
|
||||||
|
const res = await getCallSignature({
|
||||||
|
patient_id: data.patientId,
|
||||||
|
diagnosis_id: data.diagnosisId || 0
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('后端返回的签名数据:', res)
|
||||||
|
|
||||||
|
if (!res || !res.userSig) {
|
||||||
|
throw new Error('获取签名失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
patientUserId.value = res.patientUserId || `patient_${data.patientId}`
|
||||||
|
|
||||||
|
loadingText.value = '正在登录聊天系统...'
|
||||||
|
|
||||||
|
// 登录 IM
|
||||||
|
await login({
|
||||||
|
sdkAppId: Number(res.sdkAppId),
|
||||||
|
userId: res.userId,
|
||||||
|
userSig: res.userSig,
|
||||||
|
useUploadPlugin: true
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('IM 登录成功')
|
||||||
|
|
||||||
|
loadingText.value = '加载聊天界面...'
|
||||||
|
|
||||||
|
// 等待一下让组件完全初始化
|
||||||
|
setTimeout(() => {
|
||||||
|
isReady.value = true
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('初始化聊天失败:', err)
|
||||||
|
error.value = err.message || '初始化聊天失败'
|
||||||
|
feedback.msgError(error.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
// 登出
|
||||||
|
logout()
|
||||||
|
visible.value = false
|
||||||
|
isReady.value = false
|
||||||
|
error.value = ''
|
||||||
|
patientId.value = null
|
||||||
|
diagnosisId.value = null
|
||||||
|
patientUserId.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.chat-dialog {
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding: 0;
|
||||||
|
height: 75vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__header) {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
height: 75vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #909399;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-layout {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.conversation-list {
|
||||||
|
width: 285px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -188,15 +188,15 @@
|
|||||||
>
|
>
|
||||||
完成视频面诊
|
完成视频面诊
|
||||||
</el-button>
|
</el-button>
|
||||||
<!-- <el-button
|
<el-button
|
||||||
v-if="row.status === 1"
|
v-if="row.status === 1"
|
||||||
v-perms="['doctor.appointment/prescription']"
|
v-perms="['doctor.appointment/prescription']"
|
||||||
type="success"
|
type="success"
|
||||||
link
|
link
|
||||||
@click="handleCreatePrescription(row)"
|
@click="handleCreatePrescription(row)"
|
||||||
>
|
>
|
||||||
创建处方单
|
聊天
|
||||||
</el-button> -->
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -262,6 +262,9 @@
|
|||||||
|
|
||||||
<!-- 处方单抽屉 -->
|
<!-- 处方单抽屉 -->
|
||||||
<prescription-drawer ref="prescriptionDrawerRef" @success="loadData" />
|
<prescription-drawer ref="prescriptionDrawerRef" @success="loadData" />
|
||||||
|
|
||||||
|
<!-- 聊天对话框 -->
|
||||||
|
<chat-dialog ref="chatDialogRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -272,6 +275,7 @@ import feedback from '@/utils/feedback'
|
|||||||
import EditPopup from '../diagnosis/edit.vue'
|
import EditPopup from '../diagnosis/edit.vue'
|
||||||
import VideoCall from '@/components/video-call/index.vue'
|
import VideoCall from '@/components/video-call/index.vue'
|
||||||
import PrescriptionDrawer from './components/prescription-drawer.vue'
|
import PrescriptionDrawer from './components/prescription-drawer.vue'
|
||||||
|
import ChatDialog from '@/components/chat-dialog/index.vue'
|
||||||
import useUserStore from '@/stores/modules/user'
|
import useUserStore from '@/stores/modules/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -364,6 +368,7 @@ const detailData = ref<any>(null)
|
|||||||
const editRef = ref()
|
const editRef = ref()
|
||||||
const videoCallRef = ref()
|
const videoCallRef = ref()
|
||||||
const prescriptionDrawerRef = ref()
|
const prescriptionDrawerRef = ref()
|
||||||
|
const chatDialogRef = ref()
|
||||||
|
|
||||||
// 查看详情
|
// 查看详情
|
||||||
const handleDetail = async (row: any) => {
|
const handleDetail = async (row: any) => {
|
||||||
@@ -456,13 +461,22 @@ const handleGroupVideoCall = (row: any) => {
|
|||||||
|
|
||||||
// 创建处方单
|
// 创建处方单
|
||||||
const handleCreatePrescription = async (row: any) => {
|
const handleCreatePrescription = async (row: any) => {
|
||||||
try {
|
if (!row.patient_id) {
|
||||||
// 获取详细信息
|
feedback.msgWarning('患者信息不完整')
|
||||||
const detail = await appointmentDetail({ id: row.id })
|
return
|
||||||
prescriptionDrawerRef.value?.open(detail)
|
|
||||||
} catch (error) {
|
|
||||||
feedback.msgError('获取患者信息失败')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否有诊单ID
|
||||||
|
if (!row.diagnosis_id && !row.id) {
|
||||||
|
feedback.msgWarning('预约信息不完整,无法发起聊天')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chatDialogRef.value?.open({
|
||||||
|
patientId: row.patient_id,
|
||||||
|
patientName: row.patient_name,
|
||||||
|
diagnosisId: row.diagnosis_id || row.id // 使用诊单ID,如果没有则使用预约ID
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData()
|
loadData()
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "uni-app"
|
"navigationBarTitleText": "uni-app"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "static/cs",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"subPackages": [
|
"subPackages": [
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user