diff --git a/App.vue b/App.vue
index 0d882b8..6d885f3 100644
--- a/App.vue
+++ b/App.vue
@@ -8,8 +8,14 @@
leadInfo,
servInfo,
} from "@/api/all.js"
+ import { initWebSocket, disconnectWebSocket, rebindUser, addMessageHandler, isWebSocketConnected } from '@/utils/ws/initWebSocket.js';
+ import { chatRoomManager } from '@/utils/chat/chatRoomManager.js';
+
export default {
onLaunch() {
+ // 初始化WebSocket(如果已登录)
+ // 参考患者端的方式,直接初始化,不延迟
+ this.initWebSocketConnection();
// 获取小程序更新机制兼容
if (uni.canIUse('getUpdateManager')) {
const updateManager = uni.getUpdateManager()
@@ -68,12 +74,54 @@
if (uni.getStorageSync('token')) {
this.getInfo()
}
+
+ // 监听登录事件
+ uni.$on('user-login', () => {
+ this.initWebSocketConnection();
+ });
+
+ // 监听登出事件
+ uni.$on('user-logout', () => {
+ disconnectWebSocket();
+ });
+
+ // 监听token刷新事件
+ uni.$on('token-refreshed', () => {
+ rebindUser();
+ });
+
+ // 注册聊天室消息处理器
+ addMessageHandler((data) => {
+ chatRoomManager.handleWebSocketMessage(data);
+ });
+ },
+ onShow: function() {
+ // 应用显示时,如果已登录但WebSocket未连接,尝试连接
+ if (uni.getStorageSync('token') && !isWebSocketConnected()) {
+ this.initWebSocketConnection();
+ }
},
- onShow: function() {},
onHide: function() {
- // this.$store.dispatch('closeWebSocket')
+ // 应用隐藏时不断开WebSocket,保持连接以便接收消息
+ // 如果需要断开,可以调用 disconnectWebSocket()
},
methods: {
+ /**
+ * 初始化WebSocket连接
+ * 职责:检查登录状态,如果已登录则初始化WebSocket
+ * 参考患者端的方式,直接连接,不延迟
+ */
+ initWebSocketConnection() {
+ const token = uni.getStorageSync('token');
+ const doctorId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
+
+ if (token && doctorId) {
+ // 直接初始化,不延迟(参考患者端的方式)
+ initWebSocket();
+ }
+ },
+
+
async getInfo() {
let role = uni.getStorageSync('identity')
let req = {};
diff --git a/api/doctorOrder.js b/api/doctorOrder.js
index f59e6ed..2329ef5 100644
--- a/api/doctorOrder.js
+++ b/api/doctorOrder.js
@@ -11,7 +11,7 @@ import {
*/
export function doctorOrderCommonList(data) {
return req.request({
- url: '/newApi/doctor-order/common-list',
+ url: '/newApi/doctor-reception-wx/common-list',
method: 'get',
data
})
diff --git a/api/reception.js b/api/reception.js
new file mode 100644
index 0000000..f9f6a6c
--- /dev/null
+++ b/api/reception.js
@@ -0,0 +1,273 @@
+import {
+ req
+} from '@/common/js/index.js';
+
+// 转接方患者列表
+export function getPatientList(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/patient-list',
+ method: 'GET',
+ data
+ })
+}
+
+// 获取患者详情
+export function getPatientItem(id) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/patient-item',
+ method: 'GET',
+ data: { id }
+ })
+}
+
+/**
+ * 根据患者ID获取患者信息
+ * @param {number} patientId - 患者ID
+ * @returns {Promise}
+ */
+export function getPatientInfoByPatientId(patientId) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/get-patient-info-by-patient-id',
+ method: 'GET',
+ data: { patient_id: patientId }
+ })
+}
+
+// 接诊
+export function receptionApi(id) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/reception',
+ method: 'POST',
+ data: { id }
+ })
+}
+
+// 拒诊
+export function refuseReceptionApi(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/refuse-reception',
+ method: 'POST',
+ data
+ })
+}
+
+// 获取拒诊原因列表
+export function refuseReceptionListApi() {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/refuse-reception-list',
+ method: 'GET'
+ })
+}
+
+// 结束问诊
+export function endOfDiagnosisApi(id) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/end-of-diagnosis',
+ method: 'POST',
+ data: { id }
+ })
+}
+
+// 获取商品列表
+export function getProductListDoctorReception(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/product-list',
+ method: 'GET',
+ data
+ })
+}
+
+// 获取药品使用方式列表
+export function getDrugUseList() {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/drug-use-list',
+ method: 'GET'
+ })
+}
+
+// 获取药品使用方式及默认用法用量
+export function getDrugUseWithDefault(drugId) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/drug-use-with-default',
+ method: 'GET',
+ data: { drug_id: drugId }
+ })
+}
+
+// 获取疾病列表
+export function getDiseaseList(name, page = 1, pageSize = 20) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/disease-list',
+ // url: '/doctor-reception-wx/disease-list',
+ method: 'GET',
+ data: { name, page, page_size: pageSize }
+ })
+}
+
+// 获取医生的常用诊断列表
+export function getMyDiseaseList() {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/my-disease-list',
+ method: 'GET'
+ })
+}
+
+// 添加常用诊断
+export function addMyDisease(diseaseId) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/add-my-disease',
+ method: 'POST',
+ data: { disease_id: diseaseId }
+ })
+}
+
+// 删除常用诊断
+export function deleteMyDisease(id) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/delete-my-disease',
+ method: 'POST',
+ data: { id }
+ })
+}
+
+// 获取医嘱列表
+export function getDoctorOrderList() {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/doctor-order-list',
+ // url: '/doctor-reception-wx/doctor-order-list',
+ method: 'GET'
+ })
+}
+
+// 创建医嘱
+export function createDoctorOrder(content) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/create-doctor-order',
+ method: 'POST',
+ data: { content }
+ })
+}
+
+// 删除医嘱
+export function deleteDoctorOrder(id) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/delete-doctor-order',
+ method: 'POST',
+ data: { id }
+ })
+}
+
+// 添加西药处方
+export function addWestPrescription(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/add-west-prescription',
+ method: 'POST',
+ data
+ })
+}
+
+// 检查中药相冲
+export function checkChineseMedicineConflictApi(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/check-chinese-medicine-conflict',
+ method: 'POST',
+ data
+ })
+}
+
+// 获取我的诊所列表
+export function getMyStoreListApi() {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/get-my-store-list',
+ method: 'GET'
+ })
+}
+
+// 获取当前诊所类型
+export function getCurrentStoreTypeApi(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/get-current-store-type',
+ method: 'GET',
+ data
+ })
+}
+
+// 切换诊所
+export function switchStoreApi(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/switch-store',
+ method: 'POST',
+ data
+ })
+}
+
+// 获取处方详情
+export function getPrescriptionInfoApi(id) {
+ return req.request({
+ url: '/newApi/prescription/detail',
+ method: 'GET',
+ data: { id }
+ })
+}
+
+// 获取加工规则列表
+export function getProcessRuleList(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/process-rule-list',
+ method: 'GET',
+ data
+ })
+}
+
+// 获取煎煮方式列表
+export function getUseWayList() {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/drug-use-way-list',
+ method: 'GET'
+ })
+}
+
+// 获取常用方列表
+export function getCommonPrescriptionListApi(storeId) {
+ return req.request({
+ url: '/newApi/common-prescription/list',
+ method: 'GET',
+ data: { store_id: storeId }
+ })
+}
+
+// 获取常用方详情
+export function getCommonPrescriptionDetailApi(id, type) {
+ return req.request({
+ url: '/newApi/common-prescription/detail',
+ method: 'GET',
+ data: { id, type }
+ })
+}
+
+// 保存西药常用方
+export function saveWestCommonPrescriptionApi(data) {
+ return req.request({
+ url: '/newApi/common-prescription/save-west',
+ method: 'POST',
+ data
+ })
+}
+
+// 保存中药常用方
+export function saveChineseCommonPrescriptionApi(data) {
+ return req.request({
+ url: '/newApi/common-prescription/save-chinese',
+ method: 'POST',
+ data
+ })
+}
+
+// 获取公共医嘱列表
+export function getDoctorOrderCommonList(data) {
+ return req.request({
+ url: '/newApi/doctor-reception-wx/common-list',
+ method: 'GET',
+ data
+ })
+}
diff --git a/common/js/common.js b/common/js/common.js
index cea80f3..494bc00 100644
--- a/common/js/common.js
+++ b/common/js/common.js
@@ -3,9 +3,10 @@ import {config} from "@/common/js/index";
export async function requestConfig(ins, options, successHandler = null, failHandler = null, completeHandler = null){
//原来是:ins.baseUrl = options.baseUrl || ins.baseUrl
let baseUrl = options.baseUrl || ins.baseUrl;
+ // TODO 这里写dev还是生产环境
if (options.url.split('/').indexOf('newApi') !== -1) {
- // baseUrl = "http://127.0.0.1:18001/api/doctor/"
- baseUrl = "https://api.xiaokang88.com/api/doctor/"
+ baseUrl = "http://127.0.0.1:18001/api/doctor/"
+ // baseUrl = "https://api.xiaokang88.com/api/doctor/"
}
options.url = options.url.replace('/oldApi/', '');
options.url = options.url.replace('/newApi/', '');
@@ -41,6 +42,20 @@ export async function requestConfig(ins, options, successHandler = null, failHan
config["dataType"] = options.dataType || "json"
config["responseType"] = options.responseType || "text"
config['sslVerify'] = false
+
+ // 如果是需要提交复杂对象/数组的写操作(POST/PUT/PATCH),强制使用 JSON 提交
+ const method = (config["method"] || "GET").toUpperCase()
+ if (["POST", "PUT", "PATCH"].includes(method)) {
+ const data = config["data"]
+ if (data && typeof data === 'object') {
+ const hasComplexValue = Object.values(data).some((v) => {
+ return v && typeof v === 'object'
+ })
+ if (hasComplexValue) {
+ config.header["Content-Type"] = "application/json"
+ }
+ }
+ }
}else if(type === "upload"){
config["filePath"] = options.filePath
config["name"] = options.name
diff --git a/common/js/index.js b/common/js/index.js
index d7b01c8..93b21eb 100644
--- a/common/js/index.js
+++ b/common/js/index.js
@@ -1,8 +1,9 @@
import Request from './request.js';
import errorCode from './errorCode.js';
import {Base64} from "js-base64";
-let baseUrl = 'https://app.xiaokang88.com/service/v1/'; // 域名
-// let baseUrl = 'http://127.0.0.1:18000/service/v1/'; // 域名
+// TODO 这里写dev还是生产环境
+// let baseUrl = 'https://app.xiaokang88.com/service/v1/'; // 域名
+let baseUrl = 'http://127.0.0.1:18000/service/v1/'; // 域名
// let baseUrl = 'http://www.xk888.com/service/v1/'; // 域名
export const config = {
baseUrl: baseUrl
diff --git a/config/chat.js b/config/chat.js
new file mode 100644
index 0000000..913d18c
--- /dev/null
+++ b/config/chat.js
@@ -0,0 +1,74 @@
+/**
+ * 聊天室配置文件
+ * 统一管理聊天室相关配置
+ */
+
+/**
+ * 聊天室配置
+ */
+export const chatConfig = {
+ /**
+ * 消息类型映射
+ * 将消息类型名称映射到数字类型
+ */
+ messageTypes: {
+ text: 0, // 文本消息
+ image: 1, // 图片消息
+ audio: 2, // 语音消息
+ video: 3, // 视频消息
+ prescription: 4, // 处方消息
+ file: 5, // 文件消息
+ register: 10, // 挂号消息
+ 'patient-experience': 11, // 患者体验消息
+ 'product-card': 12, // 产品卡片消息
+ 'end-consultation': 13 // 结束问诊消息
+ },
+
+ /**
+ * 根据消息类型名称获取数字类型
+ * @param {string} typeName - 消息类型名称
+ * @returns {number} 消息类型数字
+ */
+ getMessageType(typeName) {
+ return this.messageTypes[typeName] || 0;
+ },
+
+ /**
+ * 根据数字类型获取消息类型名称
+ * @param {number} type - 消息类型数字
+ * @returns {string} 消息类型名称
+ */
+ getMessageTypeName(type) {
+ const entries = Object.entries(this.messageTypes);
+ const found = entries.find(([name, value]) => value === type);
+ return found ? found[0] : 'text';
+ },
+
+ /**
+ * 房间配置
+ */
+ room: {
+ messagePageSize: 20, // 每页消息数量
+ maxCacheSize: 100 // 最大缓存消息数
+ },
+
+ /**
+ * 未读数配置
+ */
+ unread: {
+ maxCount: 99 // 最大未读数显示(超过显示99+)
+ },
+
+ /**
+ * 格式化未读数显示
+ * @param {number} count - 未读数
+ * @returns {string} 格式化后的未读数
+ */
+ formatUnreadCount(count) {
+ if (count <= 0) return '';
+ if (count > this.unread.maxCount) {
+ return `${this.unread.maxCount}+`;
+ }
+ return count.toString();
+ }
+};
diff --git a/config/websocket.js b/config/websocket.js
new file mode 100644
index 0000000..98a3e76
--- /dev/null
+++ b/config/websocket.js
@@ -0,0 +1,55 @@
+/**
+ * WebSocket配置文件
+ * 统一管理WebSocket相关配置
+ */
+
+// 检查是否为开发环境
+// 在uni-app中,可以通过条件编译或manifest.json判断
+const isDev = true; // TODO: 根据实际环境配置
+
+/**
+ * WebSocket配置
+ */
+export const websocketConfig = {
+ /**
+ * WebSocket URL配置
+ * dev: 开发环境WebSocket地址
+ * prod: 生产环境WebSocket地址
+ */
+ url: {
+ dev: 'ws://127.0.0.1:12080/ws',
+ prod: 'wss://api.ws.g.xiaokang88.com/ws'
+ },
+
+ /**
+ * 获取当前环境的WebSocket URL
+ * @returns {string} WebSocket URL
+ */
+ getUrl() {
+ return isDev ? this.url.dev : this.url.prod;
+ },
+
+ /**
+ * 重连配置
+ */
+ reconnect: {
+ maxAttempts: 5, // 最大重连次数
+ initialDelay: 1000, // 初始重连延迟(毫秒)
+ maxDelay: 30000 // 最大重连延迟(毫秒)
+ },
+
+ /**
+ * 心跳配置
+ */
+ heartbeat: {
+ interval: 300000 // 心跳间隔(毫秒),5分钟
+ },
+
+ /**
+ * 用户标识配置
+ */
+ user: {
+ prefix: 'doctor-', // 医生小程序使用doctor-前缀
+ platform: 'doctor-miniprogram' // 平台标识(医生小程序)
+ }
+};
diff --git a/pages.json b/pages.json
index 7e45003..3ab4321 100644
--- a/pages.json
+++ b/pages.json
@@ -187,6 +187,13 @@
"navigationBarTitleText": "常用方详情"
}
},
+ {
+ "path": "prescription_v2/index",
+ "style": {
+ "navigationBarTitleText": "开方",
+ "navigationStyle": "custom"
+ }
+ },
{
"path": "workbench_basicInfo",
"style": {
@@ -394,6 +401,28 @@
"navigationBarTitleText": "视频"
}
}]
+ }, {
+ "root": "subPackages/sub_reception",
+ "pages": [{
+ "path": "reception_list",
+ "style": {
+ "navigationBarTitleText": "转接方",
+ "navigationStyle": "custom"
+ }
+ }, {
+ "path": "reception_chat",
+ "style": {
+ "navigationBarTitleText": "转接方聊天",
+ "navigationStyle": "custom",
+ "enablePullDownRefresh": false
+ }
+ }, {
+ "path": "reception_prescription",
+ "style": {
+ "navigationBarTitleText": "转接方开方",
+ "navigationStyle": "custom"
+ }
+ }]
}
],
"tabBar": {
diff --git a/pages/login/index.vue b/pages/login/index.vue
index 5939634..fa8f776 100644
--- a/pages/login/index.vue
+++ b/pages/login/index.vue
@@ -148,6 +148,15 @@
uni.setStorageSync('logInfo', res.data['ServiceUser'])
// 记录店铺id
uni.setStorageSync('store_id', res.data['StoreDoctor']['store_id'])
+
+ // 存储doctor_id或user_id,用于WebSocket绑定
+ if (res.data['ServiceUser'] && res.data['ServiceUser']['id']) {
+ uni.setStorageSync('doctor_id', res.data['ServiceUser']['id']);
+ uni.setStorageSync('user_id', res.data['ServiceUser']['id']);
+ }
+
+ // 触发WebSocket初始化(登录成功后)
+ uni.$emit('user-login');
const data = {
store_id: uni.getStorageSync('store_id') || res.data['StoreDoctor']['store_id'] || '11001',
diff --git a/pages/login/register.vue b/pages/login/register.vue
index f64fa93..628f148 100644
--- a/pages/login/register.vue
+++ b/pages/login/register.vue
@@ -120,6 +120,15 @@
if (res.errcode != -1) {
uni.setStorageSync('token', res.data['token'])
uni.setStorageSync('logInfo', res.data['user'])
+
+ // 存储doctor_id或user_id,用于WebSocket绑定
+ if (res.data['user'] && res.data['user']['id']) {
+ uni.setStorageSync('doctor_id', res.data['user']['id']);
+ uni.setStorageSync('user_id', res.data['user']['id']);
+ }
+
+ // 触发WebSocket初始化(注册成功后)
+ uni.$emit('user-login');
setTimeout(() => {
uni.showToast({
diff --git a/request/api/im.js b/request/api/im.js
new file mode 100644
index 0000000..7a531bd
--- /dev/null
+++ b/request/api/im.js
@@ -0,0 +1,60 @@
+import { req } from '@/common/js/index.js';
+
+// 发送消息
+export async function sendToUserApi(params) {
+ return req.request({
+ url: 'imApi/send-to-user',
+ method: 'POST',
+ data: params,
+ header: {
+ 'Content-Type': 'application/json'
+ }
+ });
+}
+
+// 获取房间聊天记录
+export async function getMessagesByRoomIdApi(params) {
+ return req.request({
+ url: 'chat-friends/messages-by-room-id',
+ method: 'GET',
+ data: params
+ });
+}
+
+// 获取聊天注册信息
+export async function getChatRegisterInfoApi(params) {
+ return req.request({
+ url: 'chat-friends/chat-register-info',
+ method: 'GET',
+ data: params
+ });
+}
+
+// 上传聊天文件
+export function upLoadChatFileApi(params) {
+ return req.request({
+ url: 'chat-friends/upload-chat-file',
+ method: 'UPLOAD',
+ filePath: params.filePath,
+ name: params.name || 'file',
+ formData: params.formData || {}
+ });
+}
+
+// 获取医生信息
+export async function getDoctorInfoApi(params) {
+ return req.request({
+ url: 'chat-friends/get-doctor-info',
+ method: 'GET',
+ data: params
+ });
+}
+
+// 获取房间状态
+export async function getRoomStatusApi(params) {
+ return req.request({
+ url: 'chat-friends/get-room-status',
+ method: 'GET',
+ data: params
+ });
+}
diff --git a/store/chat/chat.js b/store/chat/chat.js
new file mode 100644
index 0000000..45232ed
--- /dev/null
+++ b/store/chat/chat.js
@@ -0,0 +1,173 @@
+import { getChatRegisterInfoApi, getMessagesByRoomIdApi } from "@/request/api/im";
+
+/**
+ * 聊天消息管理器(简化版)
+ */
+const ChatManager = {
+ currentRoomId: null,
+ lastMessageId: 0,
+ unreadCounts: {},
+ messageCache: {},
+ roomMessages: {},
+
+ enterRoom(roomId) {
+ this.currentRoomId = roomId;
+ uni.setStorageSync('currentRoomId', roomId);
+ this.resetUnreadCount(roomId);
+ },
+
+ leaveRoom() {
+ this.currentRoomId = null;
+ uni.removeStorageSync('currentRoomId');
+ },
+
+ getCurrentRoomId() {
+ return this.currentRoomId || uni.getStorageSync('currentRoomId');
+ },
+
+ increaseUnreadCount(roomId) {
+ if (this.getCurrentRoomId() !== roomId) {
+ this.unreadCounts[roomId] = (this.unreadCounts[roomId] || 0) + 1;
+ this.notifyUnreadChange();
+ }
+ },
+
+ resetUnreadCount(roomId) {
+ if (this.unreadCounts[roomId]) {
+ this.unreadCounts[roomId] = 0;
+ this.notifyUnreadChange();
+ }
+ },
+
+ getUnreadCount(roomId) {
+ return this.unreadCounts[roomId] || 0;
+ },
+
+ getAllUnreadCount() {
+ return Object.values(this.unreadCounts).reduce((sum, count) => sum + count, 0);
+ },
+
+ notifyUnreadChange() {
+ uni.$emit('unread-message-update', {
+ total: this.getAllUnreadCount(),
+ byRoom: { ...this.unreadCounts }
+ });
+ },
+
+ addMessageToRoom(roomId, message) {
+ if (!this.roomMessages[roomId]) {
+ this.roomMessages[roomId] = [];
+ }
+ this.roomMessages[roomId].push(message);
+ },
+
+ getRoomMessages(roomId) {
+ let that = this;
+ this.roomMessages[roomId] = [];
+ getMessagesByRoomIdApi({
+ room_id: roomId,
+ last_message_id: 0,
+ }).then((res) => {
+ if (res && res.data && res.data.result) {
+ that.roomMessages[roomId] = that.checkMessage(res.data.result.list || []);
+ that.lastMessageId = res.data.result.last_message_id || 0;
+ uni.$emit('load-room-message', 1);
+ }
+ }).catch(err => {
+ console.error('获取消息失败:', err);
+ });
+ return this.roomMessages[roomId] || [];
+ },
+
+ checkMessage(message, type = 0) {
+ if (type === 1) {
+ return {
+ id: message.id,
+ sender_user_id: message.sender_user_id,
+ receiver_user_id: message.receiver_user_id,
+ message_type: message.message_type,
+ message_content: message.message_content,
+ duration: message.duration,
+ room_id: message.room_id,
+ created_at: message.created_at || message.send_time,
+ created_at_text: message.created_at_text || this.formatTime(message.send_time || message.created_at),
+ timestamp: message.send_time || message.created_at
+ };
+ }
+ return message.map((item) => {
+ const result = {
+ id: item.id,
+ sender_user_id: item.sender_user_id,
+ receiver_user_id: item.receiver_user_id,
+ message_type: item.message_type,
+ message_content: item.message_content,
+ duration: item.duration,
+ room_id: item.room_id,
+ created_at: item.created_at || item.send_time,
+ created_at_text: item.created_at_text || this.formatTime(item.send_time || item.created_at),
+ timestamp: item.send_time || item.created_at
+ };
+
+ if (item.message_type === 10) {
+ this.getChatRegisterInfo(result, message.indexOf(item));
+ }
+ return result;
+ });
+ },
+
+ formatTime(timestamp) {
+ if (!timestamp) return '';
+ const date = new Date(timestamp * 1000);
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ return `${hours}:${minutes}`;
+ },
+
+ getChatRegisterInfo(message, index) {
+ const parsedContent = this.parseMessageContent(message.message_content);
+ if (parsedContent._loaded || !parsedContent.id) return;
+
+ getChatRegisterInfoApi({ id: parsedContent.id, message_id: message.id || '' }).then((res) => {
+ if (res && res.data && res.data.result) {
+ const resultWithFlag = { ...res.data.result, _loaded: true };
+ const roomId = message.room_id;
+ if (this.roomMessages[roomId] && this.roomMessages[roomId][index]) {
+ this.roomMessages[roomId][index].message_content = JSON.stringify(resultWithFlag);
+ }
+ }
+ });
+ },
+
+ parseMessageContent(content) {
+ try {
+ return typeof content === 'object' ? content : JSON.parse(content);
+ } catch (e) {
+ return content;
+ }
+ },
+
+ loadHistory(roomId) {
+ getMessagesByRoomIdApi({
+ room_id: roomId,
+ last_message_id: this.lastMessageId,
+ }).then((res) => {
+ if (res && res.data && res.data.result) {
+ const newMessages = this.checkMessage(res.data.result.list || []);
+ if (newMessages.length > 0) {
+ this.roomMessages[roomId] = [...newMessages, ...(this.roomMessages[roomId] || [])];
+ this.lastMessageId = res.data.result.last_message_id || 0;
+ uni.$emit('load-room-message', 0);
+ } else {
+ uni.$emit('no-more-history');
+ }
+ } else {
+ uni.$emit('no-more-history');
+ }
+ }).catch(err => {
+ console.error('加载历史消息失败:', err);
+ uni.$emit('no-more-history');
+ });
+ }
+};
+
+export { ChatManager };
diff --git a/store/index.js b/store/index.js
index 6aba8d9..453551b 100644
--- a/store/index.js
+++ b/store/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue'
import Vuex from 'vuex'
-import WebSocket from '@/common/common.js';
import Utils from '@/common/utils.js';
+import { disconnectWebSocket } from '@/utils/ws/initWebSocket.js';
import {
registerList
@@ -9,8 +9,6 @@ import {
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
- // url: 'wss://wss.nbxlmy.com/ws',
- webSocket: null,
utils: null,
consult: [], //咨询会话列表
docSession: [], //医生首页消息会话列表
@@ -25,37 +23,37 @@ const store = new Vuex.Store({
},
},
actions: {
+ /**
+ * 登出
+ * 职责:断开WebSocket连接,清理状态
+ */
logout({
state,
dispatch
}) {
- dispatch('closeWebSocket')
- // state.webSocket.logout()
+ dispatch('closeWebSocket');
},
+
+ /**
+ * 初始化(保留用于其他初始化逻辑)
+ * 注意:WebSocket初始化已在App.vue中统一处理
+ */
init({
state,
dispatch
}) {
- console.log('这里是websocket init!!!!')
- // 拿到存储
- let userInfo = uni.getStorageSync('userInfo')
- if (userInfo) {
- console.log(1);
- // 连接socket
- state.webSocket = new WebSocket({
- url: state.url
- })
- // state.utils = new Utils()
- // 获取会话列表
- // dispatch('getSessionList')
- }
+ // WebSocket初始化已移至App.vue,这里保留其他初始化逻辑
+ // state.utils = new Utils()
+ // 获取会话列表
+ // dispatch('getSessionList')
},
- closeWebSocket({
- state
- }) {
- if (state.webSocket) {
- state.webSocket.close();
- }
+
+ /**
+ * 关闭WebSocket
+ * 职责:断开WebSocket连接
+ */
+ closeWebSocket() {
+ disconnectWebSocket();
},
// 获取挂号信息
getConsult({
diff --git a/subPackages/sub_reception/components/DiagnosisModal.vue b/subPackages/sub_reception/components/DiagnosisModal.vue
new file mode 100644
index 0000000..62021b8
--- /dev/null
+++ b/subPackages/sub_reception/components/DiagnosisModal.vue
@@ -0,0 +1,408 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.disease.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_reception/components/DoctorOrderModal.vue b/subPackages/sub_reception/components/DoctorOrderModal.vue
new file mode 100644
index 0000000..d9c2b24
--- /dev/null
+++ b/subPackages/sub_reception/components/DoctorOrderModal.vue
@@ -0,0 +1,392 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确认
+
+
+
+
+
+
+ {{ item.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_reception/components/MessageBubble.vue b/subPackages/sub_reception/components/MessageBubble.vue
new file mode 100644
index 0000000..8e2b192
--- /dev/null
+++ b/subPackages/sub_reception/components/MessageBubble.vue
@@ -0,0 +1,615 @@
+
+
+
+
+
+
+
+
+
+
+ {{ parsedContent }}
+
+
+
+
+
+
+
+
+ {{ msg.duration }}''
+
+
+
+
+
+
+
+
+ {{ msg.duration }}''
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 变动说明
+ {{ parsedContent.message }}
+
+
+ 最新应付
+ ¥{{ parsedContent.total_pay_price }}
+
+
+ 订单编号
+ {{ parsedContent.order_no }}
+
+
+
+
+ 调整项目
+ {{ parsedContent.drug_name || '医疗服务' }}
+
+
+ 价格变动
+
+ ¥{{ parsedContent.old_price }}
+
+ ¥{{ parsedContent.new_price }}
+
+
+
+ 变动金额
+
+ {{ parsedContent.adjust_amount >= 0 ? '增加' : '减少' }} ¥{{ Math.abs(parsedContent.adjust_amount) }}
+
+
+
+ 当前总额
+ ¥{{ parsedContent.total_pay_price }}
+
+
+
+
+
+
+ {{ msg.message_content }}
+
+
+
+
+
+
+
+
+
+ 订单号{{ parsedContent.order_no }}
+ 就诊人{{ (parsedContent.user_patient && parsedContent.user_patient.name) || '未知' }}
+ 费用¥{{ parsedContent.price }}
+
+
+
+
+
+
+
+
+
+ 药品名称
+ {{ parsedContent.drug_name }}
+
+
+ 症状描述
+ {{ parsedContent.illness_info }}
+
+ 是否就诊过{{ parsedContent.has_visited === '1' ? '是' : '否' }}
+ 是否使用过药品{{ parsedContent.has_used_drug === '1' ? '是' : '否' }}
+
+
+
+
+
+
+
+ 诊断单号{{ parsedContent.order_no }}
+ 金额¥{{ parsedContent.total_pay_price }}
+
+
+
+
+
+
+
+
+ 名称{{ parsedContent.product_name }}
+ 价格¥{{ parsedContent.price }}
+
+
+
+
+
+
+
+ {{ parsedContent.reason || '本次服务已完成' }}
+
+
+
+
+ 不支持的消息类型
+
+
+
+
+ {{ parsedContent }}
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_reception/components/WesternMedicineUsageModal.vue b/subPackages/sub_reception/components/WesternMedicineUsageModal.vue
new file mode 100644
index 0000000..13feed1
--- /dev/null
+++ b/subPackages/sub_reception/components/WesternMedicineUsageModal.vue
@@ -0,0 +1,382 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (usageData.use_type && usageData.use_type.name) || '请选择' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (usageData.use_frequency && usageData.use_frequency.name) || '请选择' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (usageData.use_num && usageData.use_num.name) || '请选择' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (usageData.unit && usageData.unit.name) || '请选择' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_reception/reception_chat.vue b/subPackages/sub_reception/reception_chat.vue
new file mode 100644
index 0000000..13bb964
--- /dev/null
+++ b/subPackages/sub_reception/reception_chat.vue
@@ -0,0 +1,1135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+ {{ formatTime(msg.created_at || msg.timestamp) }}
+
+
+
+
+
+
+
+
+
+ {{ getParsedContent(msg) }}
+
+
+
+
+
+
+
+
+ {{ msg.duration }}''
+
+
+
+
+
+
+
+
+ {{ msg.duration }}''
+
+
+
+
+
+
+
+ 点击查看处方详情
+
+
+
+
+
+
+
+
+
+ 暂无消息
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatRecordingTime(recordingTime) }}
+ 松开结束录音
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_reception/reception_list.vue b/subPackages/sub_reception/reception_list.vue
new file mode 100644
index 0000000..4293351
--- /dev/null
+++ b/subPackages/sub_reception/reception_list.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+ {{ patient.name || '未知患者' }}
+ {{ patient.mobile || '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_reception/reception_prescription.vue b/subPackages/sub_reception/reception_prescription.vue
new file mode 100644
index 0000000..ce6035a
--- /dev/null
+++ b/subPackages/sub_reception/reception_prescription.vue
@@ -0,0 +1,980 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 中药
+
+
+ 西药
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加商品
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 调整用量
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加商品
+
+
+
+
+
+ 药材:
+
+
+
+
+
+
+
+
+
+
+
+ 调整用量
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 诊疗费:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 另存常用方
+
+ 发送处方
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/ChineseMedicineConfig.vue b/subPackages/sub_workbench/prescription_v2/components/ChineseMedicineConfig.vue
new file mode 100644
index 0000000..5c77eb1
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/components/ChineseMedicineConfig.vue
@@ -0,0 +1,364 @@
+
+
+
+
+
+
+
+
+
+ 自制剂
+
+
+ 委托调剂
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+ {{ item.note || item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue
new file mode 100644
index 0000000..58a0495
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue
@@ -0,0 +1,729 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 已选
+
+
+
+ {{ qty }}g
+
+
+
+ 用量:
+
+ -
+
+ +
+
+ {{ getUnitName(item) }}
+
+
+
+
+
+
+ 加载中...
+
+
+ 没有更多了
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/CommonPrescriptionModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/CommonPrescriptionModal.vue
new file mode 100644
index 0000000..0370ada
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/components/modals/CommonPrescriptionModal.vue
@@ -0,0 +1,487 @@
+
+
+
+
+
+
+
+
+
+
+
+ 药品:
+
+
+ {{ formatDrugName(drug) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue
new file mode 100644
index 0000000..32c0d3c
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue
@@ -0,0 +1,517 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.disease.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+ 没有更多了
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/DoctorOrderModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/DoctorOrderModal.vue
new file mode 100644
index 0000000..d4f4410
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/components/modals/DoctorOrderModal.vue
@@ -0,0 +1,423 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确认
+
+
+
+
+
+
+ {{ item.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/SimpleProductModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/SimpleProductModal.vue
new file mode 100644
index 0000000..0555ad5
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/components/modals/SimpleProductModal.vue
@@ -0,0 +1,286 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/WesternMedicineModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/WesternMedicineModal.vue
new file mode 100644
index 0000000..bf27f55
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/components/modals/WesternMedicineModal.vue
@@ -0,0 +1,345 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ isSelected(item) ? '已选' : '添加' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/WesternMedicineUsageModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/WesternMedicineUsageModal.vue
new file mode 100644
index 0000000..1a1235c
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/components/modals/WesternMedicineUsageModal.vue
@@ -0,0 +1,455 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (usageData.use_type && usageData.use_type.name) || '请选择' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (usageData.use_frequency && usageData.use_frequency.name) || '请选择' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (usageData.use_num && usageData.use_num.name) || '请选择' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (usageData.unit && usageData.unit.name) || '请选择' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/index.vue b/subPackages/sub_workbench/prescription_v2/index.vue
new file mode 100644
index 0000000..2873b3a
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/index.vue
@@ -0,0 +1,2043 @@
+
+
+
+
+
+
+
+
+
+ {{ category.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 常用方
+
+
+
+ 添加商品
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 调整用量
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 常用方
+
+
+
+ 添加商品
+
+
+
+
+
+ 药材:
+
+
+
+
+
+
+
+
+
+
+
+ 调整用量
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加商品
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 诊疗费:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 另存常用方
+
+ {{ isCommonPrescription ? '保存常用方' : '发送处方' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/utils/prescriptionCalculator.js b/subPackages/sub_workbench/prescription_v2/utils/prescriptionCalculator.js
new file mode 100644
index 0000000..ab4fc52
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/utils/prescriptionCalculator.js
@@ -0,0 +1,116 @@
+/**
+ * 处方价格计算工具类
+ * 职责:统一管理处方价格计算逻辑
+ */
+export class PrescriptionCalculator {
+ /**
+ * 计算中药商品总价
+ * @param {Array} drugs - 药品列表
+ * @param {number} dosage - 剂数(天数)
+ * @returns {number} 商品总价
+ */
+ static calculateChineseMedicineProductPrice(drugs, dosage = 7) {
+ if (!drugs || drugs.length === 0) {
+ return 0;
+ }
+ return drugs.reduce((sum, drug) => {
+ const price = parseFloat(drug.price || 0);
+ const quantity = parseFloat(drug.number || 1);
+ return sum + (price * quantity * dosage);
+ }, 0);
+ }
+
+ /**
+ * 计算西药商品总价
+ * @param {Array} drugs - 药品列表
+ * @returns {number} 商品总价
+ */
+ static calculateWesternMedicineProductPrice(drugs) {
+ if (!drugs || drugs.length === 0) {
+ return 0;
+ }
+ return drugs.reduce((sum, drug) => {
+ const price = parseFloat(drug.price || 0);
+ const quantity = parseFloat(drug.select_number || drug.number || 1);
+ return sum + (price * quantity);
+ }, 0);
+ }
+
+ /**
+ * 计算简单产品总价(保健食品、服务包、非药品、医疗器械)
+ * @param {Array} products - 产品列表
+ * @returns {number} 商品总价
+ */
+ static calculateSimpleProductPrice(products) {
+ if (!products || products.length === 0) {
+ return 0;
+ }
+ return products.reduce((sum, product) => {
+ const price = parseFloat(product.price || 0);
+ const quantity = parseFloat(product.select_number || product.number || 1);
+ return sum + (price * quantity);
+ }, 0);
+ }
+
+ /**
+ * 计算加工费(委托调剂)
+ * @param {Object} processRule - 加工规则对象
+ * @param {number} dosage - 剂数
+ * @param {number} totalDrugQuantity - 总药品数量(用于按数量计算)
+ * @returns {number} 加工费
+ */
+ static calculateProcessingFee(processRule, dosage = 7, totalDrugQuantity = 0) {
+ if (!processRule || !processRule.calc_method) {
+ return 0;
+ }
+
+ const calcMethod = processRule.calc_method; // 1=固定价格,2=按剂数,3=按数量
+ const price = parseFloat(processRule.price || 0);
+
+ switch (calcMethod) {
+ case 1:
+ // 固定价格
+ return price;
+ case 2:
+ // 按剂数计算
+ return price * dosage;
+ case 3:
+ // 按数量计算
+ return price * dosage * totalDrugQuantity;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * 计算处方总价
+ * @param {Object} params - 计算参数
+ * @param {number} params.category - 处方类型
+ * @param {Array} params.drugs - 药品列表
+ * @param {number} params.dosage - 剂数(中药需要)
+ * @param {number} params.processingFee - 加工费(委托调剂需要)
+ * @param {number} params.treatmentPrice - 诊疗费
+ * @returns {number} 总价
+ */
+ static calculateTotalPrice(params) {
+ const { category, drugs, dosage = 7, processingFee = 0, treatmentPrice = 0 } = params;
+
+ let productPrice = 0;
+
+ // 根据类型计算商品价格
+ if (category === 1) {
+ // 中药
+ productPrice = this.calculateChineseMedicineProductPrice(drugs, dosage);
+ } else if (category === 2) {
+ // 西药
+ productPrice = this.calculateWesternMedicineProductPrice(drugs);
+ } else if ([3, 5, 6, 7].includes(category)) {
+ // 简单产品
+ productPrice = this.calculateSimpleProductPrice(drugs);
+ }
+
+ // 总价 = 商品价格 + 加工费 + 诊疗费
+ const total = productPrice + processingFee + treatmentPrice;
+ return parseFloat(total.toFixed(2));
+ }
+}
diff --git a/subPackages/sub_workbench/prescription_v2/utils/prescriptionStorage.js b/subPackages/sub_workbench/prescription_v2/utils/prescriptionStorage.js
new file mode 100644
index 0000000..f958525
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/utils/prescriptionStorage.js
@@ -0,0 +1,129 @@
+/**
+ * 处方数据存储工具类
+ * 职责:统一管理处方数据的本地存储操作
+ */
+export class PrescriptionStorage {
+ /**
+ * 存储前缀
+ */
+ static STORAGE_PREFIX = 'prescriptionV2-';
+
+ /**
+ * 获取存储键名
+ * @param {number} category - 处方类型:1=中药,2=西药,3=保健食品,5=产品服务包,6=非药品,7=医疗器械
+ * @param {number} registerId - 挂号ID
+ * @returns {string} 存储键名
+ */
+ static getStorageKey(category, registerId) {
+ return `${this.STORAGE_PREFIX}prescriptionData_${category}_${registerId}`;
+ }
+
+ /**
+ * 获取激活分类的存储键名
+ * @param {number} registerId - 挂号ID
+ * @returns {string} 存储键名
+ */
+ static getActiveCategoryKey(registerId) {
+ return `${this.STORAGE_PREFIX}activeCategory_${registerId}`;
+ }
+
+ /**
+ * 保存处方数据到本地存储
+ * @param {number} category - 处方类型
+ * @param {number} registerId - 挂号ID
+ * @param {Object} data - 处方数据(药品列表、诊断、医嘱等)
+ */
+ static savePrescriptionData(category, registerId, data) {
+ try {
+ const key = this.getStorageKey(category, registerId);
+ uni.setStorageSync(key, JSON.stringify(data));
+ console.log('保存处方数据成功:', key, data);
+ } catch (error) {
+ console.error('保存处方数据失败:', error);
+ }
+ }
+
+ /**
+ * 从本地存储加载处方数据
+ * @param {number} category - 处方类型
+ * @param {number} registerId - 挂号ID
+ * @returns {Object|null} 处方数据
+ */
+ static loadPrescriptionData(category, registerId) {
+ try {
+ const key = this.getStorageKey(category, registerId);
+ const data = uni.getStorageSync(key);
+ if (data) {
+ return JSON.parse(data);
+ }
+ return null;
+ } catch (error) {
+ console.error('加载处方数据失败:', error);
+ return null;
+ }
+ }
+
+ /**
+ * 清除指定处方的本地存储数据
+ * @param {number} category - 处方类型
+ * @param {number} registerId - 挂号ID
+ */
+ static clearPrescriptionData(category, registerId) {
+ try {
+ const key = this.getStorageKey(category, registerId);
+ uni.removeStorageSync(key);
+ } catch (error) {
+ console.error('清除处方数据失败:', error);
+ }
+ }
+
+ /**
+ * 清除所有处方的本地存储数据
+ * @param {number} registerId - 挂号ID
+ */
+ static clearAllPrescriptionData(registerId) {
+ try {
+ const categories = [1, 2, 3, 5, 6, 7];
+ categories.forEach(category => {
+ this.clearPrescriptionData(category, registerId);
+ });
+ // 清除激活分类
+ uni.removeStorageSync(this.getActiveCategoryKey(registerId));
+ } catch (error) {
+ console.error('清除所有处方数据失败:', error);
+ }
+ }
+
+ /**
+ * 保存当前激活的分类
+ * @param {number} category - 处方类型
+ * @param {number} registerId - 挂号ID
+ */
+ static saveActiveCategory(category, registerId) {
+ try {
+ const key = this.getActiveCategoryKey(registerId);
+ uni.setStorageSync(key, category.toString());
+ } catch (error) {
+ console.error('保存激活分类失败:', error);
+ }
+ }
+
+ /**
+ * 获取当前激活的分类
+ * @param {number} registerId - 挂号ID
+ * @returns {number|null} 激活的分类,如果没有则返回null
+ */
+ static getActiveCategory(registerId) {
+ try {
+ const key = this.getActiveCategoryKey(registerId);
+ const category = uni.getStorageSync(key);
+ if (category) {
+ return parseInt(category);
+ }
+ return null;
+ } catch (error) {
+ console.error('获取激活分类失败:', error);
+ return null;
+ }
+ }
+}
diff --git a/subPackages/sub_workbench/prescription_v2/utils/prescriptionValidator.js b/subPackages/sub_workbench/prescription_v2/utils/prescriptionValidator.js
new file mode 100644
index 0000000..03a1210
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/utils/prescriptionValidator.js
@@ -0,0 +1,139 @@
+/**
+ * 处方数据验证工具类
+ * 职责:统一管理处方数据验证逻辑
+ */
+export class PrescriptionValidator {
+ /**
+ * 验证处方数据
+ * @param {Object} params - 验证参数
+ * @param {number} params.category - 处方类型
+ * @param {Array} params.drugs - 药品列表
+ * @param {Array} params.diagnoses - 诊断列表
+ * @param {string} params.medicalAdvice - 医嘱
+ * @param {Object} params.chineseConfig - 中药配置(仅中药需要)
+ * @returns {Object} { valid: boolean, message: string }
+ */
+ static validatePrescriptionData(params) {
+ const { category, drugs, diagnoses, medicalAdvice, chineseConfig } = params;
+
+ // 验证药品
+ if (!drugs || drugs.length === 0) {
+ return {
+ valid: false,
+ message: '请添加药品'
+ };
+ }
+
+ // 验证诊断
+ if (!diagnoses || diagnoses.length === 0) {
+ return {
+ valid: false,
+ message: '请添加诊断'
+ };
+ }
+
+ // 验证医嘱
+ if (!medicalAdvice || medicalAdvice.trim() === '') {
+ return {
+ valid: false,
+ message: '请输入医嘱'
+ };
+ }
+
+ // 验证中药配置
+ if (category === 1 && chineseConfig) {
+ const chineseValidation = this.validateChineseMedicineConfig(chineseConfig);
+ if (!chineseValidation.valid) {
+ return chineseValidation;
+ }
+ }
+
+ return {
+ valid: true,
+ message: '验证通过'
+ };
+ }
+
+ /**
+ * 验证中药配置
+ * @param {Object} config - 中药配置
+ * @param {number} config.ruleType - 规则类型:1=自制剂,2=委托调剂
+ * @param {number} config.packageMethodId - 包法ID(自制剂需要)
+ * @param {number} config.processRuleId - 制剂ID(委托调剂需要)
+ * @param {number} config.childProcessRuleId - 煎法ID(委托调剂需要)
+ * @param {number} config.processRuleNoteId - 备注ID(委托调剂需要)
+ * @returns {Object} { valid: boolean, message: string }
+ */
+ static validateChineseMedicineConfig(config) {
+ const { ruleType, packageMethodId, processRuleId, childProcessRuleId, processRuleNoteId } = config;
+
+ if (ruleType === 1) {
+ // 自制剂:需要包法
+ if (!packageMethodId) {
+ return {
+ valid: false,
+ message: '请选择包法'
+ };
+ }
+ } else if (ruleType === 2) {
+ // 委托调剂:需要制剂、煎法、备注
+ if (!processRuleId) {
+ return {
+ valid: false,
+ message: '请选择制剂'
+ };
+ }
+ if (!childProcessRuleId) {
+ return {
+ valid: false,
+ message: '请选择煎法'
+ };
+ }
+ if (!processRuleNoteId) {
+ return {
+ valid: false,
+ message: '请选择备注'
+ };
+ }
+ }
+
+ return {
+ valid: true,
+ message: '验证通过'
+ };
+ }
+
+ /**
+ * 验证药品是否已存在
+ * @param {Array} drugs - 当前药品列表
+ * @param {Object} newDrug - 新药品
+ * @returns {boolean} 是否已存在
+ */
+ static isDrugExists(drugs, newDrug) {
+ if (!drugs || !newDrug) {
+ return false;
+ }
+ // 通过药品ID判断
+ const drugId = newDrug.id || newDrug.drug_id;
+ return drugs.some(drug => (drug.id || drug.drug_id) === drugId);
+ }
+
+ /**
+ * 验证诊断是否已存在
+ * @param {Array} diagnoses - 当前诊断列表
+ * @param {Object} newDiagnosis - 新诊断
+ * @returns {boolean} 是否已存在
+ */
+ static isDiagnosisExists(diagnoses, newDiagnosis) {
+ if (!diagnoses || !newDiagnosis) {
+ return false;
+ }
+ // 通过诊断ID或名称判断
+ const diagnosisId = newDiagnosis.id || newDiagnosis.disease_id;
+ const diagnosisName = newDiagnosis.name;
+ return diagnoses.some(diagnosis =>
+ (diagnosis.id || diagnosis.disease_id) === diagnosisId ||
+ diagnosis.name === diagnosisName
+ );
+ }
+}
diff --git a/subPackages/sub_workbench/workbench_infoPatient.vue b/subPackages/sub_workbench/workbench_infoPatient.vue
index 6db84ab..2b6b47d 100644
--- a/subPackages/sub_workbench/workbench_infoPatient.vue
+++ b/subPackages/sub_workbench/workbench_infoPatient.vue
@@ -168,6 +168,15 @@
+
+
+
@@ -187,7 +196,7 @@
plain :ripple="true">结束问诊
开处方
@@ -212,7 +221,21 @@
id: "",
re_id: "",
show: false,
- content: '确认结束本次问诊吗?'
+ content: '确认结束本次问诊吗?',
+ showVersionModal: false,
+ versionModalTitle: '选择开方版本',
+ versionList: [
+ {
+ text: '开方2.0(新模块,推荐)',
+ value: 'v2',
+ color: '#6ACDBB'
+ },
+ {
+ text: '旧版开方(原有模块)',
+ value: 'old',
+ color: '#333'
+ }
+ ]
};
},
computed: {
@@ -281,6 +304,31 @@
}
});
},
+ // 打开版本选择弹窗
+ openPrescriptionVersionModal() {
+ this.showVersionModal = true;
+ },
+ // 处理版本选择
+ handleVersionSelect(index) {
+ const version = this.versionList[index];
+ if (version.value === 'v2') {
+ this.goToPrescriptionV2();
+ } else if (version.value === 'old') {
+ this.goToOldPrescription();
+ }
+ },
+ // 跳转到开方2.0页面
+ goToPrescriptionV2() {
+ const registerId = this.infoList.id;
+ const patientId = this.infoList.user_patient_id;
+ this.$go(`/subPackages/sub_workbench/prescription_v2/index?register_id=${registerId}&patient_id=${patientId}`);
+ },
+ // 跳转到旧版开方页面
+ goToOldPrescription() {
+ const patientId = this.infoList.user_patient_id;
+ const registerId = this.infoList.id;
+ this.$go(`../../subPackages/sub_workbench/workbench_recipe/index?id=${patientId}&r_id=${registerId}`);
+ },
},
onShow() {
this.getList();
diff --git a/utils/chat/chatRoomManager.js b/utils/chat/chatRoomManager.js
new file mode 100644
index 0000000..7bc9483
--- /dev/null
+++ b/utils/chat/chatRoomManager.js
@@ -0,0 +1,424 @@
+/**
+ * 聊天室管理器
+ * 封装聊天室相关操作,包括进入房间、离开房间、发送消息、接收消息、管理未读数等
+ */
+import { chatConfig } from '@/config/chat.js';
+import { sendWebSocketMessage, isWebSocketConnected } from '@/utils/ws/initWebSocket.js';
+import { getChatRegisterInfoApi, getMessagesByRoomIdApi } from '@/request/api/im.js';
+
+/**
+ * 聊天室管理器类
+ */
+class ChatRoomManager {
+ constructor() {
+ // 当前房间ID
+ this.currentRoomId = null;
+
+ // 最后一条消息ID(用于分页加载)
+ this.lastMessageId = 0;
+
+ // 未读数(按房间ID存储)
+ this.unreadCounts = {};
+
+ // 消息缓存(按房间ID存储)
+ this.roomMessages = {};
+
+ // 消息处理器列表
+ this.messageHandlers = [];
+ }
+
+ /**
+ * 进入聊天室
+ * @param {string} roomId - 房间ID
+ * 职责:设置当前房间,重置未读数,保存到本地存储
+ */
+ enterRoom(roomId) {
+ if (!roomId) {
+ console.warn('房间ID不能为空');
+ return;
+ }
+
+ this.currentRoomId = roomId;
+ uni.setStorageSync('currentRoomId', roomId);
+ this.resetUnreadCount(roomId);
+
+ console.log('进入聊天室:', roomId);
+ }
+
+ /**
+ * 离开聊天室
+ * 职责:清空当前房间,移除本地存储
+ */
+ leaveRoom() {
+ this.currentRoomId = null;
+ uni.removeStorageSync('currentRoomId');
+ console.log('离开聊天室');
+ }
+
+ /**
+ * 获取当前房间ID
+ * @returns {string|null} 当前房间ID
+ */
+ getCurrentRoomId() {
+ return this.currentRoomId || uni.getStorageSync('currentRoomId');
+ }
+
+ /**
+ * 增加未读数
+ * @param {string} roomId - 房间ID
+ * 职责:如果房间不是当前房间,则增加未读数并通知
+ */
+ increaseUnreadCount(roomId) {
+ if (!roomId) return;
+
+ // 如果是当前房间,不增加未读数
+ if (this.getCurrentRoomId() === roomId) {
+ return;
+ }
+
+ this.unreadCounts[roomId] = (this.unreadCounts[roomId] || 0) + 1;
+ this.notifyUnreadChange();
+ }
+
+ /**
+ * 重置未读数
+ * @param {string} roomId - 房间ID
+ * 职责:将指定房间的未读数重置为0并通知
+ */
+ resetUnreadCount(roomId) {
+ if (this.unreadCounts[roomId]) {
+ this.unreadCounts[roomId] = 0;
+ this.notifyUnreadChange();
+ }
+ }
+
+ /**
+ * 获取未读数
+ * @param {string} roomId - 房间ID
+ * @returns {number} 未读数
+ */
+ getUnreadCount(roomId) {
+ return this.unreadCounts[roomId] || 0;
+ }
+
+ /**
+ * 获取所有未读数总和
+ * @returns {number} 总未读数
+ */
+ getAllUnreadCount() {
+ return Object.values(this.unreadCounts).reduce((sum, count) => sum + count, 0);
+ }
+
+ /**
+ * 格式化未读数显示
+ * @param {string} roomId - 房间ID
+ * @returns {string} 格式化后的未读数
+ */
+ getFormattedUnreadCount(roomId) {
+ const count = this.getUnreadCount(roomId);
+ return chatConfig.formatUnreadCount(count);
+ }
+
+ /**
+ * 通知未读数变化
+ * 职责:触发未读数更新事件
+ */
+ notifyUnreadChange() {
+ uni.$emit('unread-message-update', {
+ total: this.getAllUnreadCount(),
+ byRoom: { ...this.unreadCounts }
+ });
+ }
+
+ /**
+ * 添加消息到房间
+ * @param {string} roomId - 房间ID
+ * @param {Object} message - 消息对象
+ * 职责:将消息添加到指定房间的消息列表
+ */
+ addMessageToRoom(roomId, message) {
+ if (!roomId || !message) return;
+
+ if (!this.roomMessages[roomId]) {
+ this.roomMessages[roomId] = [];
+ }
+
+ // 检查消息是否已存在(避免重复)
+ const existingIndex = this.roomMessages[roomId].findIndex(m => m.id === message.id);
+ if (existingIndex !== -1) {
+ this.roomMessages[roomId][existingIndex] = message;
+ } else {
+ this.roomMessages[roomId].push(message);
+ }
+ }
+
+ /**
+ * 获取房间消息列表
+ * @param {string} roomId - 房间ID
+ * @returns {Array} 消息列表
+ */
+ getRoomMessages(roomId) {
+ return this.roomMessages[roomId] || [];
+ }
+
+ /**
+ * 加载房间消息
+ * @param {string} roomId - 房间ID
+ * @param {number} lastMessageId - 最后一条消息ID(可选,用于分页)
+ * @returns {Promise} 消息列表
+ */
+ async loadRoomMessages(roomId, lastMessageId = 0) {
+ if (!roomId) {
+ console.warn('房间ID不能为空');
+ return [];
+ }
+
+ try {
+ const res = await getMessagesByRoomIdApi({
+ room_id: roomId,
+ last_message_id: lastMessageId || 0
+ });
+
+ if (res && res.data && res.data.result) {
+ const messages = this.processMessages(res.data.result.list || []);
+ this.lastMessageId = res.data.result.last_message_id || 0;
+
+ // 如果是首次加载,替换消息列表;否则追加到前面
+ if (lastMessageId === 0) {
+ this.roomMessages[roomId] = messages;
+ } else {
+ this.roomMessages[roomId] = [...messages, ...(this.roomMessages[roomId] || [])];
+ }
+
+ uni.$emit('load-room-message', lastMessageId === 0 ? 1 : 0);
+ return messages;
+ }
+
+ return [];
+ } catch (error) {
+ console.error('加载房间消息失败:', error);
+ return [];
+ }
+ }
+
+ /**
+ * 处理消息数据
+ * @param {Array|Object} messages - 消息数据(数组或单个消息)
+ * @returns {Array|Object} 处理后的消息
+ */
+ processMessages(messages) {
+ if (!Array.isArray(messages)) {
+ return this.processSingleMessage(messages);
+ }
+
+ return messages.map((item) => {
+ const result = {
+ id: item.id,
+ sender_user_id: item.sender_user_id,
+ receiver_user_id: item.receiver_user_id,
+ message_type: item.message_type,
+ message_content: item.message_content,
+ duration: item.duration,
+ room_id: item.room_id,
+ created_at: item.created_at || item.send_time,
+ created_at_text: item.created_at_text || this.formatTime(item.send_time || item.created_at),
+ timestamp: item.send_time || item.created_at
+ };
+
+ // 如果是挂号消息(type=10),异步加载详细信息
+ if (item.message_type === 10) {
+ this.loadRegisterInfo(result);
+ }
+
+ return result;
+ });
+ }
+
+ /**
+ * 处理单个消息
+ * @param {Object} message - 消息对象
+ * @returns {Object} 处理后的消息
+ */
+ processSingleMessage(message) {
+ return {
+ id: message.id,
+ sender_user_id: message.sender_user_id,
+ receiver_user_id: message.receiver_user_id,
+ message_type: message.message_type,
+ message_content: message.message_content,
+ duration: message.duration,
+ room_id: message.room_id,
+ created_at: message.created_at || message.send_time,
+ created_at_text: message.created_at_text || this.formatTime(message.send_time || message.created_at),
+ timestamp: message.send_time || message.created_at
+ };
+ }
+
+ /**
+ * 格式化时间
+ * @param {number} timestamp - 时间戳(秒)
+ * @returns {string} 格式化后的时间(HH:mm)
+ */
+ formatTime(timestamp) {
+ if (!timestamp) return '';
+ const date = new Date(timestamp * 1000);
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ return `${hours}:${minutes}`;
+ }
+
+ /**
+ * 加载挂号信息(异步)
+ * @param {Object} message - 消息对象
+ * 职责:为挂号消息加载详细信息
+ */
+ loadRegisterInfo(message) {
+ const parsedContent = this.parseMessageContent(message.message_content);
+ if (parsedContent._loaded || !parsedContent.id) return;
+
+ getChatRegisterInfoApi({
+ id: parsedContent.id,
+ message_id: message.id || ''
+ }).then((res) => {
+ if (res && res.data && res.data.result) {
+ const resultWithFlag = { ...res.data.result, _loaded: true };
+ const roomId = message.room_id;
+ const roomMessages = this.roomMessages[roomId];
+
+ if (roomMessages) {
+ const index = roomMessages.findIndex(m => m.id === message.id);
+ if (index !== -1) {
+ roomMessages[index].message_content = JSON.stringify(resultWithFlag);
+ uni.$emit('message-updated', { roomId, messageId: message.id });
+ }
+ }
+ }
+ }).catch(error => {
+ console.error('加载挂号信息失败:', error);
+ });
+ }
+
+ /**
+ * 解析消息内容
+ * @param {string|Object} content - 消息内容
+ * @returns {Object|string} 解析后的内容
+ */
+ parseMessageContent(content) {
+ try {
+ return typeof content === 'object' ? content : JSON.parse(content);
+ } catch (e) {
+ return content;
+ }
+ }
+
+ /**
+ * 发送消息
+ * @param {Object} messageData - 消息数据
+ * @param {string} messageData.room_id - 房间ID
+ * @param {string} messageData.sender_user_id - 发送者ID
+ * @param {string} messageData.receiver_user_id - 接收者ID
+ * @param {number|string} messageData.message_type - 消息类型
+ * @param {string} messageData.message_content - 消息内容
+ * @param {number} messageData.duration - 语音时长(可选)
+ * @returns {boolean} 是否发送成功
+ */
+ sendMessage(messageData) {
+ if (!isWebSocketConnected()) {
+ console.error('WebSocket未连接,无法发送消息');
+ return false;
+ }
+
+ // 确保消息类型是数字
+ let messageType = messageData.message_type;
+ if (typeof messageType === 'string') {
+ messageType = chatConfig.getMessageType(messageType);
+ }
+
+ const message = {
+ request_type: 'send_message',
+ room_id: messageData.room_id,
+ sender_user_id: messageData.sender_user_id,
+ receiver_user_id: messageData.receiver_user_id,
+ message_type: messageType,
+ message_content: messageData.message_content || '',
+ duration: messageData.duration || 0
+ };
+
+ return sendWebSocketMessage(message);
+ }
+
+ /**
+ * 注册消息处理器
+ * @param {Function} handler - 消息处理函数
+ * 职责:注册消息处理器,当收到WebSocket消息时调用
+ */
+ onMessage(handler) {
+ if (typeof handler !== 'function') {
+ console.warn('消息处理器必须是函数');
+ return;
+ }
+
+ this.messageHandlers.push(handler);
+ }
+
+ /**
+ * 移除消息处理器
+ * @param {Function} handler - 消息处理函数
+ */
+ offMessage(handler) {
+ const index = this.messageHandlers.indexOf(handler);
+ if (index !== -1) {
+ this.messageHandlers.splice(index, 1);
+ }
+ }
+
+ /**
+ * 处理收到的WebSocket消息
+ * @param {Object} data - WebSocket消息数据
+ * 职责:处理收到的消息,更新未读数,调用注册的处理器
+ */
+ handleWebSocketMessage(data) {
+ // 只处理聊天消息
+ if (data.request_type !== 'receive_message' && !data.room_id) {
+ return;
+ }
+
+ const roomId = data.room_id;
+ if (!roomId) return;
+
+ // 处理消息
+ const message = this.processSingleMessage(data);
+
+ // 添加到房间消息列表
+ this.addMessageToRoom(roomId, message);
+
+ // 如果不是当前房间,增加未读数
+ if (this.getCurrentRoomId() !== roomId) {
+ this.increaseUnreadCount(roomId);
+ }
+
+ // 调用所有注册的处理器
+ this.messageHandlers.forEach(handler => {
+ try {
+ handler(message, roomId);
+ } catch (error) {
+ console.error('消息处理器执行失败:', error);
+ }
+ });
+ }
+
+ /**
+ * 清空房间消息缓存
+ * @param {string} roomId - 房间ID(可选,不传则清空所有)
+ */
+ clearRoomCache(roomId = null) {
+ if (roomId) {
+ delete this.roomMessages[roomId];
+ } else {
+ this.roomMessages = {};
+ }
+ }
+}
+
+// 导出单例
+export const chatRoomManager = new ChatRoomManager();
diff --git a/utils/ws/initWebSocket.js b/utils/ws/initWebSocket.js
new file mode 100644
index 0000000..309b512
--- /dev/null
+++ b/utils/ws/initWebSocket.js
@@ -0,0 +1,118 @@
+/**
+ * WebSocket初始化工具
+ * 统一管理WebSocket的初始化、连接、断开等操作
+ */
+import { webSocketManager } from './websocket.js';
+
+/**
+ * 检查是否已登录
+ * @returns {boolean} 是否已登录
+ */
+function checkLoginStatus() {
+ const token = uni.getStorageSync('token');
+ const doctorId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
+ return !!(token && doctorId);
+}
+
+/**
+ * 初始化WebSocket
+ * 职责:检查登录状态,如果已登录则连接WebSocket并绑定用户
+ * @returns {boolean} 是否初始化成功
+ */
+export function initWebSocket() {
+ // 检查登录状态
+ if (!checkLoginStatus()) {
+ console.log('未登录,跳过WebSocket初始化');
+ return false;
+ }
+
+ // 如果已经连接,不需要重复连接
+ if (webSocketManager.isConnected) {
+ console.log('WebSocket已连接,跳过初始化');
+ return true;
+ }
+
+ try {
+ console.log('开始初始化WebSocket...');
+ webSocketManager.connect();
+ return true;
+ } catch (error) {
+ console.error('WebSocket初始化失败:', error);
+ return false;
+ }
+}
+
+/**
+ * 断开WebSocket连接
+ * 职责:断开WebSocket连接,清理资源
+ */
+export function disconnectWebSocket() {
+ try {
+ console.log('断开WebSocket连接...');
+ webSocketManager.disconnect();
+ } catch (error) {
+ console.error('断开WebSocket失败:', error);
+ }
+}
+
+/**
+ * 重新绑定用户
+ * 职责:在token刷新或用户信息变更后,重新绑定用户
+ * @returns {boolean} 是否绑定成功
+ */
+export function rebindUser() {
+ if (!checkLoginStatus()) {
+ console.warn('未登录,无法重新绑定用户');
+ return false;
+ }
+
+ try {
+ console.log('重新绑定WebSocket用户...');
+ return webSocketManager.rebindUser();
+ } catch (error) {
+ console.error('重新绑定用户失败:', error);
+ return false;
+ }
+}
+
+/**
+ * 获取WebSocket连接状态
+ * @returns {boolean} 是否已连接
+ */
+export function isWebSocketConnected() {
+ return webSocketManager.isConnected;
+}
+
+/**
+ * 获取WebSocket认证状态
+ * @returns {boolean} 是否已认证
+ */
+export function isWebSocketAuthenticated() {
+ return webSocketManager.isAuthenticated;
+}
+
+/**
+ * 添加消息处理器
+ * @param {Function} handler - 消息处理函数
+ */
+export function addMessageHandler(handler) {
+ webSocketManager.addMessageHandler(handler);
+}
+
+/**
+ * 移除消息处理器
+ * @param {Function} handler - 消息处理函数
+ */
+export function removeMessageHandler(handler) {
+ webSocketManager.removeMessageHandler(handler);
+}
+
+/**
+ * 发送WebSocket消息
+ * @param {Object} message - 消息对象
+ * @param {boolean} requireAuth - 是否需要认证
+ * @returns {boolean} 是否发送成功
+ */
+export function sendWebSocketMessage(message, requireAuth = true) {
+ return webSocketManager.send(message, requireAuth);
+}
diff --git a/utils/ws/websocket.js b/utils/ws/websocket.js
new file mode 100644
index 0000000..1d1d294
--- /dev/null
+++ b/utils/ws/websocket.js
@@ -0,0 +1,314 @@
+/**
+ * WebSocket管理器
+ * 负责WebSocket连接、消息收发、重连、心跳等功能
+ */
+import { websocketConfig } from '@/config/websocket.js';
+
+export class WebSocketManager {
+ constructor(url = null) {
+ // 使用配置文件中的URL,如果传入了url则使用传入的
+ this.url = url || websocketConfig.getUrl();
+ this.socket = null;
+ this.isConnected = false;
+ this.isAuthenticated = false;
+ this.reconnectAttempts = 0;
+ // 从配置文件读取重连配置
+ this.maxReconnectAttempts = websocketConfig.reconnect.maxAttempts;
+ this.reconnectDelay = websocketConfig.reconnect.initialDelay;
+ this.maxReconnectDelay = websocketConfig.reconnect.maxDelay;
+ this.messageHandlers = [];
+ this.pingInterval = null;
+ this.cachedToken = null;
+ // 从配置文件读取用户配置
+ this.userPrefix = websocketConfig.user.prefix;
+ this.platform = websocketConfig.user.platform;
+ }
+
+ getToken() {
+ let token = uni.getStorageSync('token');
+ if (token) {
+ token = token.replace('Bearer ', '');
+ this.cachedToken = token;
+ }
+ return token || null;
+ }
+
+ clearCachedToken() {
+ this.cachedToken = null;
+ }
+
+ rebindUser() {
+ const userId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
+ if (!userId) {
+ console.warn('重新绑定用户失败:doctor_id不存在');
+ return false;
+ }
+
+ this.clearCachedToken();
+
+ if (this.isConnected) {
+ return this.bindUser(userId, true);
+ } else {
+ this.connect();
+ return true;
+ }
+ }
+
+ connect() {
+ if (this.isConnected) return;
+
+ console.log('正在连接WebSocket...');
+ this.socket = uni.connectSocket({
+ url: this.url,
+ success: () => {
+ console.log('WebSocket连接成功');
+ },
+ fail: (err) => {
+ console.error('WebSocket连接失败:', err);
+ this.handleReconnect();
+ }
+ });
+
+ this.socket.onOpen(() => {
+ console.log('WebSocket连接已打开');
+ this.isConnected = true;
+ this.reconnectAttempts = 0;
+
+ this.startHeartbeat();
+
+ // 绑定当前用户(医生小程序使用doctor-前缀)
+ const userId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
+ if (userId) {
+ this.bindUser(userId, true);
+ }
+ });
+
+ this.socket.onMessage((res) => {
+ try {
+ const data = JSON.parse(res.data);
+ console.log('收到消息:', data);
+ this.handleMessage(data);
+ } catch (e) {
+ console.error('解析消息失败:', e);
+ }
+ });
+
+ this.socket.onClose((res) => {
+ console.log('WebSocket连接关闭:', res);
+ this.isConnected = false;
+ this.stopHeartbeat();
+ this.handleReconnect();
+ });
+
+ this.socket.onError((err) => {
+ console.error('WebSocket错误:', err);
+ });
+ }
+
+ unbindUser(userId) {
+ const token = this.getToken();
+ if (!token) {
+ console.warn('解绑用户失败:token不存在');
+ return false;
+ }
+ // 使用配置文件中的用户前缀
+ const normalizedUserId = userId.toString().startsWith(this.userPrefix) ? userId : `${this.userPrefix}${userId}`;
+ console.log('发送unbind请求,清理旧连接:', normalizedUserId);
+ return this.send({
+ request_type: 'unbind',
+ user_type: 'doctor',
+ sender_user_id: normalizedUserId,
+ token: token
+ }, false);
+ }
+
+ bindUser(userId, cleanOldConnections = true) {
+ const token = this.getToken();
+ if (!token) {
+ console.error('绑定用户失败:token不存在');
+ return false;
+ }
+
+ // 使用配置文件中的用户前缀
+ const normalizedUserId = userId.toString().startsWith(this.userPrefix) ? userId : `${this.userPrefix}${userId}`;
+
+ // 构建bind请求对象
+ const bindMessage = {
+ request_type: 'bind',
+ user_type: 'doctor',
+ sender_user_id: normalizedUserId,
+ token: token,
+ platform: this.platform, // 使用配置文件中的平台标识
+ clean_old_connections: cleanOldConnections
+ };
+
+ // 输出详细的调试日志
+ console.log('========== WebSocket Bind 请求 ==========');
+ console.log('用户ID:', normalizedUserId);
+ console.log('用户类型:', 'doctor');
+ console.log('平台标识:', this.platform);
+ console.log('清理旧连接:', cleanOldConnections);
+ console.log('Token存在:', !!token);
+ console.log('完整请求内容:', JSON.stringify(bindMessage, null, 2));
+ console.log('==========================================');
+
+ return this.send(bindMessage);
+ }
+
+ send(message, requireAuth = true) {
+ if (!this.isConnected) {
+ console.error('WebSocket未连接');
+ return false;
+ }
+
+ try {
+ if (requireAuth && !message.token && message.request_type !== 'ping') {
+ const token = this.getToken();
+ if (token) {
+ message.token = token;
+ }
+ }
+
+ // 如果是发送消息,确保添加platform标识
+ if (message.request_type === 'send_message' && !message.platform) {
+ message.platform = this.platform;
+ }
+
+ const messageStr = JSON.stringify(message);
+ this.socket.send({
+ data: messageStr,
+ success: () => {
+ console.log('消息发送成功:', message);
+ },
+ fail: (err) => {
+ console.error('消息发送失败:', err);
+ }
+ });
+ return true;
+ } catch (err) {
+ console.error('发送消息出错:', err);
+ return false;
+ }
+ }
+
+ addMessageHandler(handler) {
+ this.messageHandlers.push(handler);
+ }
+
+ removeMessageHandler(handler) {
+ const index = this.messageHandlers.indexOf(handler);
+ if (index !== -1) {
+ this.messageHandlers.splice(index, 1);
+ }
+ }
+
+ handleMessage(data) {
+ if (data.auth_status !== undefined) {
+ if (data.auth_status === 'success') {
+ console.log('========== WebSocket 认证成功 ==========');
+ console.log('认证状态:', data.auth_status);
+ console.log('完整响应:', JSON.stringify(data, null, 2));
+ console.log('========================================');
+ this.isAuthenticated = true;
+ } else if (data.auth_status === 'failed') {
+ console.error('========== WebSocket 认证失败 ==========');
+ console.error('认证状态:', data.auth_status);
+ console.error('错误消息:', data.message || '未知错误');
+ console.error('错误代码:', data.code || '无');
+ console.error('完整响应:', JSON.stringify(data, null, 2));
+ console.error('当前用户ID:', uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id'));
+ console.error('Token存在:', !!this.getToken());
+ console.error('平台标识:', this.platform);
+ console.error('========================================');
+ this.isAuthenticated = false;
+ this.cachedToken = null;
+ } else if (data.auth_status === 'token_expired') {
+ console.warn('========== Token 已过期 ==========');
+ console.warn('认证状态:', data.auth_status);
+ console.warn('错误消息:', data.message || 'Token已过期');
+ console.warn('完整响应:', JSON.stringify(data, null, 2));
+ console.warn('==================================');
+ this.isAuthenticated = false;
+ this.cachedToken = null;
+ uni.$emit('ws-token-expired');
+ const userId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
+ if (userId) {
+ console.log('尝试重新绑定用户...');
+ this.bindUser(userId, true);
+ }
+ } else {
+ console.warn('========== 未知的认证状态 ==========');
+ console.warn('认证状态:', data.auth_status);
+ console.warn('完整响应:', JSON.stringify(data, null, 2));
+ console.warn('==================================');
+ }
+ return;
+ }
+
+ if (data.clientId) {
+ const userId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
+ if (userId) {
+ this.bindUser(userId, true);
+ }
+ }
+
+ this.messageHandlers.forEach(handler => {
+ try {
+ handler(data);
+ } catch (e) {
+ console.error('消息处理出错:', e);
+ }
+ });
+ }
+
+ /**
+ * 启动心跳
+ * 职责:按照配置的心跳间隔发送ping消息
+ */
+ startHeartbeat() {
+ this.pingInterval = setInterval(() => {
+ if (this.isConnected) {
+ this.send({ request_type: 'ping' }, false);
+ }
+ }, websocketConfig.heartbeat.interval);
+ }
+
+ stopHeartbeat() {
+ if (this.pingInterval) {
+ clearInterval(this.pingInterval);
+ this.pingInterval = null;
+ }
+ }
+
+ /**
+ * 处理重连
+ * 职责:按照配置的重连策略进行重连
+ */
+ handleReconnect() {
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ console.log('已达到最大重连次数');
+ return;
+ }
+
+ setTimeout(() => {
+ this.reconnectAttempts++;
+ // 使用指数退避策略,但不超过最大延迟
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
+ console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
+ this.connect();
+ }, this.reconnectDelay);
+ }
+
+ disconnect() {
+ if (this.socket) {
+ this.socket.close();
+ this.socket = null;
+ }
+ this.isConnected = false;
+ this.stopHeartbeat();
+ }
+}
+
+// 全局WebSocket实例
+// 使用配置文件中的URL
+export const webSocketManager = new WebSocketManager();