From eacca1bf7b4679c7acfec464a1683c73e72d97f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Thu, 14 May 2026 14:29:51 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E5=BC=80=E6=96=B9=E7=BA=BF=E4=B8=8A?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=9F=BA=E6=9C=AC=E5=AE=8C=E6=88=90=202.=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80=E4=BA=9B=E6=8A=BD=E5=B1=89?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E7=9A=84=E6=BB=91=E5=8A=A8=E5=A4=B1=E6=95=88?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/chat.js | 159 ++ api/reception.js | 101 +- common/js/common.js | 17 +- common/js/index.js | 12 +- components/d-empty/d-empty.vue | 1 + config/app-env.js | 34 + config/websocket.js | 44 +- pages.json | 21 +- pages/workbench/index.vue | 1765 +++++++++------- request/api/im.js | 60 - store/chat/chat.js | 21 +- .../components/MessageBubble.vue | 911 ++++++++ .../components/PatientDetailPanel.vue | 1 + .../components/ReceptionActionBar.vue | 64 + .../components/RefuseReceptionModal.vue | 104 + .../constants/messageTypes.js | 12 + .../sub_online_reception/pages/chat.vue | 1871 +++++++++++++++++ .../sub_online_reception/pages/list.vue | 493 +++++ .../utils/messageParse.js | 16 + .../utils/transferPrescriptionImport.js | 125 ++ .../components/DiagnosisModal.vue | 408 ---- .../components/DoctorOrderModal.vue | 392 ---- .../components/MessageBubble.vue | 665 ------ .../components/WesternMedicineUsageModal.vue | 382 ---- subPackages/sub_reception/reception_chat.vue | 1230 ----------- subPackages/sub_reception/reception_list.vue | 168 -- .../sub_reception/reception_prescription.vue | 967 --------- .../modals/ChineseMedicineModal.vue | 6 +- .../modals/CommonPrescriptionModal.vue | 92 +- .../components/modals/DiagnosisModal.vue | 87 +- .../components/modals/DoctorOrderModal.vue | 104 +- .../components/modals/SimpleProductModal.vue | 81 +- .../modals/TraditionalTermPickerModal.vue | 251 +++ .../modals/WesternMedicineModal.vue | 98 +- .../sub_workbench/prescription_v2/index.vue | 406 +++- .../utils/prescriptionValidator.js | 25 +- .../utils/traditionalTcmLocalCache.js | 63 + utils/chat/chatRoomManager.js | 175 +- utils/chat/messagePreview.js | 54 + utils/chat/sessionListFormat.js | 25 + 40 files changed, 6176 insertions(+), 5335 deletions(-) create mode 100644 api/chat.js create mode 100644 config/app-env.js delete mode 100644 request/api/im.js create mode 100644 subPackages/sub_online_reception/components/MessageBubble.vue create mode 100644 subPackages/sub_online_reception/components/PatientDetailPanel.vue create mode 100644 subPackages/sub_online_reception/components/ReceptionActionBar.vue create mode 100644 subPackages/sub_online_reception/components/RefuseReceptionModal.vue create mode 100644 subPackages/sub_online_reception/constants/messageTypes.js create mode 100644 subPackages/sub_online_reception/pages/chat.vue create mode 100644 subPackages/sub_online_reception/pages/list.vue create mode 100644 subPackages/sub_online_reception/utils/messageParse.js create mode 100644 subPackages/sub_online_reception/utils/transferPrescriptionImport.js delete mode 100644 subPackages/sub_reception/components/DiagnosisModal.vue delete mode 100644 subPackages/sub_reception/components/DoctorOrderModal.vue delete mode 100644 subPackages/sub_reception/components/MessageBubble.vue delete mode 100644 subPackages/sub_reception/components/WesternMedicineUsageModal.vue delete mode 100644 subPackages/sub_reception/reception_chat.vue delete mode 100644 subPackages/sub_reception/reception_list.vue delete mode 100644 subPackages/sub_reception/reception_prescription.vue create mode 100644 subPackages/sub_workbench/prescription_v2/components/modals/TraditionalTermPickerModal.vue create mode 100644 subPackages/sub_workbench/prescription_v2/utils/traditionalTcmLocalCache.js create mode 100644 utils/chat/messagePreview.js create mode 100644 utils/chat/sessionListFormat.js diff --git a/api/chat.js b/api/chat.js new file mode 100644 index 0000000..10d4eb2 --- /dev/null +++ b/api/chat.js @@ -0,0 +1,159 @@ +import { req } from '@/common/js/index.js'; +import { websocketConfig } from '@/config/websocket.js'; + +const PREFIX = '/newApi/doctor-chat-wx/'; + +/** 根据挂号解析/创建 IM 房间 */ +export function getChatRoomByRegisterApi(register_id) { + return req.request({ + url: `${PREFIX}chat-room-by-register`, + method: 'GET', + data: { register_id } + }); +} + +/** 分页拉取房间历史消息(POST,与患者端一致) */ +export function getMessagesByRoomIdApi(params) { + return req.request({ + url: `${PREFIX}messages-by-room-id`, + method: 'POST', + data: params + }); +} + +/** 挂号卡片信息(message_type=10 等) */ +export function getChatRegisterInfoApi(params) { + return req.request({ + url: `${PREFIX}chat-register-info`, + method: 'GET', + data: params + }); +} + +/** 房间是否结束问诊等 */ +export function getRoomStatusApi(params) { + return req.request({ + url: `${PREFIX}get-room-status`, + method: 'GET', + data: params + }); +} + +/** 聊天图片/语音上传 */ +export function uploadChatFileApi(params) { + return req.request({ + url: `${PREFIX}upload-chat-file`, + type: 'upload', + method: 'POST', + filePath: params.filePath, + name: params.name || 'file', + formData: params.formData || {} + }); +} + +function bearerAuthHeader() { + let t = uni.getStorageSync('token') || ''; + if (!t) return {}; + t = String(t).replace(/^Bearer\s+/i, '').trim(); + if (!t) return {}; + return { Authorization: `Bearer ${t}` }; +} + +function unwrapLaravelInner(res) { + if (res == null) return null; + if (res.result !== undefined && res.result !== null) return res.result; + if (res.data && res.data.result !== undefined) return res.data.result; + return res.data != null ? res.data : res; +} + +/** 判断 send-to-user 是否成功(兼容 Gin 与历史 Laravel jok) */ +export function isSendToUserResponseOk(res) { + if (res == null) return false; + if (res.wakaryReqToReject) return false; + if (res.status === 'success') return true; + const c = Number(res.code); + const ec = Number(res.errcode); + if (!Number.isNaN(c) && c !== 0) return false; + if (!Number.isNaN(ec) && ec !== 0) return false; + const inner = unwrapLaravelInner(res); + if (inner && typeof inner.status_code === 'number' && inner.status_code !== 200) return false; + const body = inner && inner.body; + if (body && body.error) return false; + if (body && body.status === 'failure') return false; + return true; +} + +/** 发送失败提示文案 */ +export function sendToUserFailMessage(res, err) { + if (err && (err.msg || err.message)) return String(err.msg || err.message); + if (res && res.msg) return String(res.msg); + if (res && res.error) return String(res.error); + const inner = unwrapLaravelInner(res); + const body = inner && inner.body; + if (body && body.error) return String(body.error); + return '发送失败'; +} + +/** 从成功响应中取消息 id(Go 可能暂无,依赖 WS 合并) */ +export function extractSendToUserMessageId(res) { + if (!res || typeof res !== 'object') return null; + if (res.id != null) return res.id; + if (res.message_id != null) return res.message_id; + const inner = unwrapLaravelInner(res); + const body = inner && inner.body; + const mid = + body && (body.id != null ? body.id : body.message_id != null ? body.message_id : null); + if (mid != null) return mid; + if (inner && inner.id != null) return inner.id; + if (res.data && (res.data.id != null || res.data.message_id != null)) { + return res.data.id != null ? res.data.id : res.data.message_id; + } + return null; +} + +/** + * 直连 Go IM:POST /api/send-to-user(与 config/websocket.js 中 WS 同基址) + * 不使用 PHP req 封装,避免加密与响应形态不一致。 + */ +export function sendToUserHttpApi(params) { + const url = websocketConfig.getSendToUserApiUrl(); + if (!url) { + return Promise.reject({ msg: '未配置 IM 服务地址' }); + } + const data = { ...params }; + if (data.platform == null && websocketConfig.user && websocketConfig.user.platform) { + data.platform = websocketConfig.user.platform; + } + return new Promise((resolve, reject) => { + uni.request({ + url, + method: 'POST', + data, + header: { + 'Content-Type': 'application/json', + ...bearerAuthHeader() + }, + success: (r) => { + const sc = Number(r.statusCode) || 0; + const payload = r.data; + const obj = payload && typeof payload === 'object' ? payload : null; + if (sc < 200 || sc >= 300) { + const msg = + (obj && (obj.error || obj.message)) != null + ? String(obj.error || obj.message) + : `HTTP ${sc}`; + reject({ msg, data: payload, statusCode: sc }); + return; + } + if (obj && obj.error != null && obj.error !== '') { + reject({ msg: String(obj.error), data: payload, statusCode: sc }); + return; + } + resolve(obj || {}); + }, + fail: (err) => { + reject({ msg: (err && (err.errMsg || err.message)) || '网络错误', err }); + } + }); + }); +} diff --git a/api/reception.js b/api/reception.js index 36bc112..cf7cadc 100644 --- a/api/reception.js +++ b/api/reception.js @@ -1,8 +1,13 @@ import { req } from '@/common/js/index.js'; +import { + loadValid as loadTraditionalTcmValid, + save as saveTraditionalTcm, + DEFAULT_TTL_MS, +} from '@/subPackages/sub_workbench/prescription_v2/utils/traditionalTcmLocalCache.js'; -// 转接方患者列表 +// 门店维度患者列表(全渠道挂号 + 当前门店),其它页面如需可继续调用 export function getPatientList(data) { return req.request({ url: '/newApi/doctor-reception-wx/patient-list', @@ -11,6 +16,15 @@ export function getPatientList(data) { }) } +/** 在线复诊患者列表(与 PC online-consultation/get-patient-list 一致;type: 1当前 2历史;可选 store_id) */ +export function getOnlineConsultationPatientListApi(data) { + return req.request({ + url: '/newApi/doctor-reception-wx/online-consultation-patient-list', + method: 'GET', + data + }) +} + // 获取正在接诊的患者列表 export function getAcceptingPatientList(data) { return req.request({ @@ -29,6 +43,14 @@ export function getPatientItem(id) { }) } +/** 常用回复模板(与 PC online-consultation/get-quick-reply-list 同源) */ +export function getQuickReplyListApi() { + return req.request({ + url: '/newApi/doctor-reception-wx/get-quick-reply-list', + method: 'GET' + }) +} + /** 本次挂号处方分页 */ export function getRegisterPrescriptionList(data) { return req.request({ @@ -211,6 +233,62 @@ export function checkChineseMedicineConflictApi(data) { }) } +function parseTraditionalTcmResponse(inner) { + if (!inner || typeof inner !== 'object') return null; + const { diseases, method, syndrome, expires_at, expires_at_ms, cache_ttl_seconds } = inner; + if (!Array.isArray(diseases) || !Array.isArray(method) || !Array.isArray(syndrome)) { + return null; + } + let expiresAtMs; + if (typeof expires_at_ms === 'number' && expires_at_ms > Date.now()) { + expiresAtMs = expires_at_ms; + } else if (typeof expires_at === 'number' && expires_at > 0) { + expiresAtMs = expires_at * 1000; + } else { + const ttlSec = + typeof cache_ttl_seconds === 'number' && cache_ttl_seconds > 0 + ? cache_ttl_seconds + : DEFAULT_TTL_MS / 1000; + expiresAtMs = Date.now() + ttlSec * 1000; + } + return { + payload: { diseases, method, syndrome }, + expiresAtMs, + }; +} + +/** + * 中医证候/治法/疾病术语表:未过期读本地,否则 GET doctor-reception-wx(与 PC 同源数据 + 过期元数据) + * @returns {Promise<{ diseases: array, method: array, syndrome: array }>} + */ +export function getTraditionalChineseMedicineJson() { + const cached = loadTraditionalTcmValid(); + if (cached) { + return Promise.resolve(cached); + } + return req + .request({ + url: '/newApi/doctor-reception-wx/traditional-chinese-medicine-all', + method: 'GET', + }) + .then((res) => { + if (res && res.wakaryReqToReject) { + return Promise.reject(res); + } + const code = res && res.code; + if (code != null && code !== 0 && code !== 200) { + return Promise.reject(new Error((res && res.message) || 'traditional tcm api error')); + } + const inner = res && (res.result != null ? res.result : res.data); + const parsed = parseTraditionalTcmResponse(inner); + if (!parsed) { + return Promise.reject(new Error('traditional tcm invalid')); + } + saveTraditionalTcm(parsed.payload, parsed.expiresAtMs); + return parsed.payload; + }); +} + // 获取我的诊所列表 export function getMyStoreListApi() { return req.request({ @@ -326,3 +404,24 @@ export function getDoctorOrderCommonList(data) { data }) } + +/** 根据挂号获取转诊处方信息(非转诊挂号时 result 为 null) */ +export function getTransferPrescriptionByRegisterApi(register_id) { + return req.request({ + url: '/newApi/doctor-reception-wx/transfer-prescription-by-register', + method: 'GET', + data: { register_id } + }) +} + +/** 根据挂号与门店获取就诊经历药品列表(与 PC online-consultation/get-drugs-by-register-id 对齐) */ +export function getDrugsByRegisterIdApi(data) { + return req.request({ + url: '/newApi/doctor-reception-wx/get-drugs-by-register-id', + method: 'GET', + data: { + register_id: data.register_id, + store_id: data.store_id + } + }) +} diff --git a/common/js/common.js b/common/js/common.js index 494bc00..9b501de 100644 --- a/common/js/common.js +++ b/common/js/common.js @@ -1,12 +1,23 @@ -import {config} from "@/common/js/index"; +import { getDoctorWxEnv } from '@/config/app-env.js'; + +/** 与 newApi 医生端接口一致;环境见 config/app-env.js */ +export const DOCTOR_NEWAPI_BASE = getDoctorWxEnv().newApiBase; + +/** 用于拉取同站点 public/traditional.json(与 DOCTOR_NEWAPI_BASE 同源) */ +export function getDoctorNewApiStaticOrigin() { + try { + return new URL(DOCTOR_NEWAPI_BASE).origin; + } catch (e) { + return ''; + } +} 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 = DOCTOR_NEWAPI_BASE; } options.url = options.url.replace('/oldApi/', ''); options.url = options.url.replace('/newApi/', ''); diff --git a/common/js/index.js b/common/js/index.js index b08f584..cf01543 100644 --- a/common/js/index.js +++ b/common/js/index.js @@ -1,12 +1,12 @@ import Request from './request.js'; import errorCode from './errorCode.js'; import {Base64} from "js-base64"; -// 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/'; // 域名 +import { getDoctorWxEnv } from '@/config/app-env.js'; + +const { legacyApiBase } = getDoctorWxEnv(); + export const config = { - baseUrl: baseUrl + baseUrl: legacyApiBase } const arr = [ @@ -34,7 +34,7 @@ const sensitiveData = [ */ const reqInterceptor = async (options) => { // 加密 - if (options.data != null) { + if (options.data != null && options.type !== 'upload') { options.data = getRes(options.data, false) } options.header = { diff --git a/components/d-empty/d-empty.vue b/components/d-empty/d-empty.vue index f203f86..34dd883 100644 --- a/components/d-empty/d-empty.vue +++ b/components/d-empty/d-empty.vue @@ -47,6 +47,7 @@ padding: 32rpx 0; display: flex; flex-direction: column; + align-items: center; width: 100%; } diff --git a/config/app-env.js b/config/app-env.js new file mode 100644 index 0000000..6d5c290 --- /dev/null +++ b/config/app-env.js @@ -0,0 +1,34 @@ +/** + * 医生小程序环境唯一开关:旧 HTTP、newApi、WebSocket 均由此派生。 + * 发布生产前将 USE_DOCTOR_WX_DEV 改为 false。 + */ +// const USE_DOCTOR_WX_DEV = true; +const USE_DOCTOR_WX_DEV = false; + +const DEV = { + legacyApiBase: 'http://127.0.0.1:18000/service/v1/', + newApiBase: 'http://127.0.0.1:18001/api/doctor/', + wsUrl: 'ws://127.0.0.1:12080/ws', +}; + +const PROD = { + legacyApiBase: 'https://app.xiaokang88.com/service/v1/', + newApiBase: 'https://api.xiaokang88.com/api/doctor/', + wsUrl: 'wss://api.ws.g.xiaokang88.com/ws', +}; + +export function isDoctorWxDev() { + return USE_DOCTOR_WX_DEV; +} + +export function getDoctorWxEnv() { + return USE_DOCTOR_WX_DEV ? DEV : PROD; +} + +/** 开发 / 生产 WebSocket 地址(与 getDoctorWxEnv 同源,供 websocket 配置展示) */ +export function getDoctorWxWsUrls() { + return { + dev: DEV.wsUrl, + prod: PROD.wsUrl, + }; +} diff --git a/config/websocket.js b/config/websocket.js index 98a3e76..292bf7b 100644 --- a/config/websocket.js +++ b/config/websocket.js @@ -1,11 +1,11 @@ /** * WebSocket配置文件 - * 统一管理WebSocket相关配置 + * 统一管理WebSocket相关配置(环境与 config/app-env.js 一致) */ -// 检查是否为开发环境 -// 在uni-app中,可以通过条件编译或manifest.json判断 -const isDev = true; // TODO: 根据实际环境配置 +import { getDoctorWxWsUrls, isDoctorWxDev } from './app-env.js'; + +const wsUrls = getDoctorWxWsUrls(); /** * WebSocket配置 @@ -16,19 +16,37 @@ export const websocketConfig = { * dev: 开发环境WebSocket地址 * prod: 生产环境WebSocket地址 */ - url: { - dev: 'ws://127.0.0.1:12080/ws', - prod: 'wss://api.ws.g.xiaokang88.com/ws' - }, - + url: wsUrls, + /** * 获取当前环境的WebSocket URL * @returns {string} WebSocket URL */ getUrl() { - return isDev ? this.url.dev : this.url.prod; + return isDoctorWxDev() ? wsUrls.dev : wsUrls.prod; }, - + + /** + * 由 WebSocket URL 推导 IM 服务 HTTP 基址(与 WS 同 host,用于 Go API) + * 例:ws://127.0.0.1:12080/ws -> http://127.0.0.1:12080 + */ + getHttpBaseUrl() { + const ws = this.getUrl(); + if (!ws || typeof ws !== 'string') return ''; + let base = ws + .replace(/^wss:\/\//i, 'https://') + .replace(/^ws:\/\//i, 'http://'); + base = base.replace(/\/?ws\/?$/i, ''); + return base.replace(/\/+$/, '') || base; + }, + + /** Go IM:POST /api/send-to-user */ + getSendToUserApiUrl() { + const base = this.getHttpBaseUrl(); + if (!base) return ''; + return `${base}/api/send-to-user`; + }, + /** * 重连配置 */ @@ -37,14 +55,14 @@ export const websocketConfig = { initialDelay: 1000, // 初始重连延迟(毫秒) maxDelay: 30000 // 最大重连延迟(毫秒) }, - + /** * 心跳配置 */ heartbeat: { interval: 300000 // 心跳间隔(毫秒),5分钟 }, - + /** * 用户标识配置 */ diff --git a/pages.json b/pages.json index 3ab4321..d39f755 100644 --- a/pages.json +++ b/pages.json @@ -22,7 +22,10 @@ "path": "pages/workbench/index", "style": { "navigationBarTitleText": "工作台", - "navigationStyle": "custom" + "navigationStyle": "custom", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark", + "backgroundColor": "#f7f8fa" } }, { @@ -402,26 +405,20 @@ } }] }, { - "root": "subPackages/sub_reception", + "root": "subPackages/sub_online_reception", "pages": [{ - "path": "reception_list", + "path": "pages/list", "style": { - "navigationBarTitleText": "转接方", + "navigationBarTitleText": "在线接诊", "navigationStyle": "custom" } }, { - "path": "reception_chat", + "path": "pages/chat", "style": { - "navigationBarTitleText": "转接方聊天", + "navigationBarTitleText": "问诊聊天", "navigationStyle": "custom", "enablePullDownRefresh": false } - }, { - "path": "reception_prescription", - "style": { - "navigationBarTitleText": "转接方开方", - "navigationStyle": "custom" - } }] } ], diff --git a/pages/workbench/index.vue b/pages/workbench/index.vue index 95fed0f..3513397 100644 --- a/pages/workbench/index.vue +++ b/pages/workbench/index.vue @@ -1,810 +1,1071 @@ + \ No newline at end of file diff --git a/request/api/im.js b/request/api/im.js deleted file mode 100644 index 7a531bd..0000000 --- a/request/api/im.js +++ /dev/null @@ -1,60 +0,0 @@ -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 index 45232ed..f3515d2 100644 --- a/store/chat/chat.js +++ b/store/chat/chat.js @@ -1,4 +1,4 @@ -import { getChatRegisterInfoApi, getMessagesByRoomIdApi } from "@/request/api/im"; +import { getChatRegisterInfoApi, getMessagesByRoomIdApi } from '@/api/chat.js'; /** * 聊天消息管理器(简化版) @@ -68,9 +68,10 @@ const ChatManager = { 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; + const payload = res && (res.result != null ? res.result : res.data && res.data.result); + if (res && payload) { + that.roomMessages[roomId] = that.checkMessage(payload.list || []); + that.lastMessageId = payload.last_message_id || 0; uni.$emit('load-room-message', 1); } }).catch(err => { @@ -128,8 +129,9 @@ const ChatManager = { 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 info = res && (res.result != null ? res.result : res.data && res.data.result); + if (res && info) { + const resultWithFlag = { ...info, _loaded: true }; const roomId = message.room_id; if (this.roomMessages[roomId] && this.roomMessages[roomId][index]) { this.roomMessages[roomId][index].message_content = JSON.stringify(resultWithFlag); @@ -151,11 +153,12 @@ const ChatManager = { 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 || []); + const payload = res && (res.result != null ? res.result : res.data && res.data.result); + if (res && payload) { + const newMessages = this.checkMessage(payload.list || []); if (newMessages.length > 0) { this.roomMessages[roomId] = [...newMessages, ...(this.roomMessages[roomId] || [])]; - this.lastMessageId = res.data.result.last_message_id || 0; + this.lastMessageId = payload.last_message_id || 0; uni.$emit('load-room-message', 0); } else { uni.$emit('no-more-history'); diff --git a/subPackages/sub_online_reception/components/MessageBubble.vue b/subPackages/sub_online_reception/components/MessageBubble.vue new file mode 100644 index 0000000..b2fdf9b --- /dev/null +++ b/subPackages/sub_online_reception/components/MessageBubble.vue @@ -0,0 +1,911 @@ + + + + + diff --git a/subPackages/sub_online_reception/components/PatientDetailPanel.vue b/subPackages/sub_online_reception/components/PatientDetailPanel.vue new file mode 100644 index 0000000..1cb5cf6 --- /dev/null +++ b/subPackages/sub_online_reception/components/PatientDetailPanel.vue @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/subPackages/sub_online_reception/components/ReceptionActionBar.vue b/subPackages/sub_online_reception/components/ReceptionActionBar.vue new file mode 100644 index 0000000..603cb87 --- /dev/null +++ b/subPackages/sub_online_reception/components/ReceptionActionBar.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/subPackages/sub_online_reception/components/RefuseReceptionModal.vue b/subPackages/sub_online_reception/components/RefuseReceptionModal.vue new file mode 100644 index 0000000..d0fc0bd --- /dev/null +++ b/subPackages/sub_online_reception/components/RefuseReceptionModal.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/subPackages/sub_online_reception/constants/messageTypes.js b/subPackages/sub_online_reception/constants/messageTypes.js new file mode 100644 index 0000000..32e6dde --- /dev/null +++ b/subPackages/sub_online_reception/constants/messageTypes.js @@ -0,0 +1,12 @@ +/** 与 config/chat.js、患者端 message_type 对齐 */ +export const MESSAGE_TYPE = { + TEXT: 0, + IMAGE: 1, + AUDIO: 2, + VIDEO: 3, + PRESCRIPTION: 4, + SYSTEM_NOTIFY: 9, + REGISTER: 10, + PATIENT_EXPERIENCE: 11, + END_CONSULTATION: 13 +}; diff --git a/subPackages/sub_online_reception/pages/chat.vue b/subPackages/sub_online_reception/pages/chat.vue new file mode 100644 index 0000000..216b581 --- /dev/null +++ b/subPackages/sub_online_reception/pages/chat.vue @@ -0,0 +1,1871 @@ + + + + + diff --git a/subPackages/sub_online_reception/pages/list.vue b/subPackages/sub_online_reception/pages/list.vue new file mode 100644 index 0000000..5120136 --- /dev/null +++ b/subPackages/sub_online_reception/pages/list.vue @@ -0,0 +1,493 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_online_reception/utils/messageParse.js b/subPackages/sub_online_reception/utils/messageParse.js new file mode 100644 index 0000000..7cf336b --- /dev/null +++ b/subPackages/sub_online_reception/utils/messageParse.js @@ -0,0 +1,16 @@ +/** + * @param {number} messageType + * @param {string|object} messageContent + * @returns {any} + */ +export function parseMessageContent(messageType, messageContent) { + // 与患者端一致:卡片类 JSON;3 视频可能是 URL 字符串或 { url } + if ([3, 4, 9, 10, 11, 12, 13, 14].includes(messageType)) { + try { + return typeof messageContent === 'object' ? messageContent : JSON.parse(messageContent); + } catch (e) { + return messageContent; + } + } + return messageContent; +} diff --git a/subPackages/sub_online_reception/utils/transferPrescriptionImport.js b/subPackages/sub_online_reception/utils/transferPrescriptionImport.js new file mode 100644 index 0000000..b0d1b0f --- /dev/null +++ b/subPackages/sub_online_reception/utils/transferPrescriptionImport.js @@ -0,0 +1,125 @@ +/** + * 解析转诊处方数据,输出与 PC TransferPrescriptionCard 一致的药品列表与诊断/医嘱。 + * @param {object|string} transferData 接口返回的 transfer 对象或 JSON 字符串 + * @returns {{ clinical_diagnose: string, doctor_order: string, drugList: array, prescription_type: number } | null} + */ +export function parseTransferPrescriptionContent(transferData) { + let transferInfo = transferData; + if (typeof transferInfo === 'string') { + try { + transferInfo = JSON.parse(transferInfo); + } catch { + return null; + } + } + if (!transferInfo || typeof transferInfo !== 'object') return null; + + let prescriptionType = 1; + if (transferInfo.prescription_type != null) { + prescriptionType = Number(transferInfo.prescription_type) || 1; + } + + if (transferInfo.prescription && transferInfo.prescription.content && Array.isArray(transferInfo.prescription.content)) { + return { + clinical_diagnose: transferInfo.prescription.clinical_diagnose || '', + doctor_order: transferInfo.prescription.doctor_order || '', + drugList: transferInfo.prescription.content, + prescription_type: transferInfo.prescription.prescription_type != null ? Number(transferInfo.prescription.prescription_type) : prescriptionType + }; + } + + const content = transferInfo.content; + if (!content) return null; + + let drugList = []; + if (typeof content === 'object' && !Array.isArray(content)) { + if (content.repice && Array.isArray(content.repice)) { + for (const recipe of content.repice) { + if (recipe.content) { + let recipeContent = recipe.content; + if (typeof recipeContent === 'string') { + try { + recipeContent = JSON.parse(recipeContent); + } catch { + recipeContent = []; + } + } + if (Array.isArray(recipeContent)) { + drugList = drugList.concat(recipeContent); + } + } + } + } + const orig = transferInfo.original_prescription || {}; + return { + clinical_diagnose: content.clinical_diagnose || orig.clinical_diagnose || '', + doctor_order: content.doctor_order || orig.doctor_order || '', + drugList, + prescription_type: prescriptionType + }; + } + if (Array.isArray(content)) { + const orig = transferInfo.original_prescription || {}; + return { + clinical_diagnose: orig.clinical_diagnose || '', + doctor_order: orig.doctor_order || '', + drugList: content, + prescription_type: prescriptionType + }; + } + + return null; +} + +/** + * 转诊处方接口 payload 归一化:空对象、无实质字段时视为无转诊卡,避免 v-if 误判为 true。 + * @param {object|null|undefined} data + * @returns {object|null} + */ +export function normalizeTransferCardPayload(data) { + if (data == null || typeof data !== 'object' || Array.isArray(data)) return null; + const keys = Object.keys(data); + if (keys.length === 0) return null; + const has = + (data.prescription_no != null && String(data.prescription_no).trim() !== '') || + (data.prescription && typeof data.prescription === 'object') || + (data.content != null && data.content !== '') || + (data.transfer_reason != null && String(data.transfer_reason).trim() !== '') || + (data.delegate_store && (data.delegate_store.name || data.delegate_store.id)) || + (data.transfer_store && (data.transfer_store.name || data.transfer_store.id)) || + (data.id != null && + data.id !== '' && + (data.status !== undefined && data.status !== null)); + if (!has) return null; + return data; +} + +/** + * 转接方药品行 → 小程序中药处方行(对齐 PC convertDrugDataForImport + handleSelectSimpleProduct 最小字段) + */ +export function transferDrugToChineseRow(drug, prescriptionType) { + const pt = prescriptionType != null ? Number(prescriptionType) : 1; + const drugId = drug.drug_id || drug.id || 0; + const name = drug.name || drug.drug_name || ''; + return { + index_id: drug.id || drugId, + id: drugId, + drug_id: drugId, + drug_name: name, + number: drug.number != null ? Number(drug.number) : 1, + price: drug.price != null ? parseFloat(drug.price) : 0, + image: drug.image || '', + instruction: drug.instruction || '', + type: drug.type != null ? drug.type : pt, + select_number: drug.select_number != null ? Number(drug.select_number) : drug.number != null ? Number(drug.number) : 1, + specification: drug.specification || '' + }; +} + +export function canImportTransferPrescription(transferCard) { + const card = normalizeTransferCardPayload(transferCard); + if (!card) return false; + if (card.status === 3) return false; + if (card.status === 2) return false; + return true; +} diff --git a/subPackages/sub_reception/components/DiagnosisModal.vue b/subPackages/sub_reception/components/DiagnosisModal.vue deleted file mode 100644 index 62021b8..0000000 --- a/subPackages/sub_reception/components/DiagnosisModal.vue +++ /dev/null @@ -1,408 +0,0 @@ - - - - - diff --git a/subPackages/sub_reception/components/DoctorOrderModal.vue b/subPackages/sub_reception/components/DoctorOrderModal.vue deleted file mode 100644 index d9c2b24..0000000 --- a/subPackages/sub_reception/components/DoctorOrderModal.vue +++ /dev/null @@ -1,392 +0,0 @@ - - - - - diff --git a/subPackages/sub_reception/components/MessageBubble.vue b/subPackages/sub_reception/components/MessageBubble.vue deleted file mode 100644 index e583bab..0000000 --- a/subPackages/sub_reception/components/MessageBubble.vue +++ /dev/null @@ -1,665 +0,0 @@ - - - - - diff --git a/subPackages/sub_reception/components/WesternMedicineUsageModal.vue b/subPackages/sub_reception/components/WesternMedicineUsageModal.vue deleted file mode 100644 index 13feed1..0000000 --- a/subPackages/sub_reception/components/WesternMedicineUsageModal.vue +++ /dev/null @@ -1,382 +0,0 @@ - - - - - diff --git a/subPackages/sub_reception/reception_chat.vue b/subPackages/sub_reception/reception_chat.vue deleted file mode 100644 index c476f94..0000000 --- a/subPackages/sub_reception/reception_chat.vue +++ /dev/null @@ -1,1230 +0,0 @@ - - - - - diff --git a/subPackages/sub_reception/reception_list.vue b/subPackages/sub_reception/reception_list.vue deleted file mode 100644 index 4293351..0000000 --- a/subPackages/sub_reception/reception_list.vue +++ /dev/null @@ -1,168 +0,0 @@ - - - - - diff --git a/subPackages/sub_reception/reception_prescription.vue b/subPackages/sub_reception/reception_prescription.vue deleted file mode 100644 index e23f893..0000000 --- a/subPackages/sub_reception/reception_prescription.vue +++ /dev/null @@ -1,967 +0,0 @@ - - - - - diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue index 100bf66..5e09a0d 100644 --- a/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue +++ b/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue @@ -38,7 +38,7 @@ @@ -58,8 +58,8 @@ - - - - - - - - - - {{ tagText }} + + + + + + + + + + + + {{ tagText }} + + + + 药品: + + + {{ formatDrugName(drug) }} + + + + + 移除 + 使用 + - - 药品: - - - {{ formatDrugName(drug) }} - - - - - 移除 - 使用 + + + + - - - - - - + @@ -354,6 +358,8 @@ export default { display: flex; flex-direction: column; height: 100%; + min-height: 0; + overflow: hidden; background-color: #fff; } @@ -365,7 +371,21 @@ export default { .modal-content { flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.list-scroll { + flex: 1; + min-height: 0; overflow-y: auto; + display: flex; + flex-direction: column; +} + +.list-scroll-inner { padding: 32rpx; } diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue index 73906d6..6a8d231 100644 --- a/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue +++ b/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue @@ -30,41 +30,41 @@ - - - - - - - - - - {{ item.disease.name }} - - + + + + + + + + + + + {{ item.disease.name }} + + + - - - - - - - - + + + + + + 没有更多了 - - + - - - + + + + - + @@ -462,6 +462,7 @@ export default { display: flex; flex-direction: column; height: 100%; + min-height: 0; overflow: hidden; } @@ -473,6 +474,7 @@ export default { .modal-content { flex: 1; + min-height: 0; display: flex; flex-direction: column; overflow: hidden; @@ -481,19 +483,18 @@ export default { .scroll-container { flex: 1; + min-height: 0; overflow-y: auto; - /* 底部防遮挡边距 */ - padding-bottom: 24rpx; display: flex; flex-direction: column; } -.search-box { - margin-bottom: 32rpx; +.scroll-body-pad { + padding-bottom: 140rpx; } -.tag-container-scroll { - max-height: 50vh; +.search-box { + margin-bottom: 32rpx; } .modern-tag-container { diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/DoctorOrderModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/DoctorOrderModal.vue index f9593bc..511d9b0 100644 --- a/subPackages/sub_workbench/prescription_v2/components/modals/DoctorOrderModal.vue +++ b/subPackages/sub_workbench/prescription_v2/components/modals/DoctorOrderModal.vue @@ -15,50 +15,54 @@ - - - - - - - 添加医嘱 - - + + + + + + + + + 添加医嘱 + + - - - - {{ item.content }} - - + + + + {{ item.content }} + + + + + + + + + + + + + + + {{ item.content }} + - - - - - - - - - - {{ item.content }} - - - + @@ -363,6 +367,7 @@ export default { display: flex; flex-direction: column; height: 100%; + min-height: 0; overflow: hidden; } @@ -374,10 +379,23 @@ export default { .modal-content { flex: 1; - overflow-y: auto; + min-height: 0; + overflow: hidden; padding: 32rpx; - /* 底部防遮挡边距 */ - padding-bottom: 24rpx; + display: flex; + flex-direction: column; +} + +.order-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.order-scroll-inner { + padding-bottom: 140rpx; } .add-btn-modern { diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/SimpleProductModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/SimpleProductModal.vue index 8a4c5cc..3f925f6 100644 --- a/subPackages/sub_workbench/prescription_v2/components/modals/SimpleProductModal.vue +++ b/subPackages/sub_workbench/prescription_v2/components/modals/SimpleProductModal.vue @@ -27,43 +27,45 @@ /> - - - - - - - - {{item.drug.drug_name || item.drug.name}} - ¥{{parseFloat(item.price || 0).toFixed(2)}} - - - 规格:{{item.drug.specification || '--'}} - + + + + + + + + + {{item.drug.drug_name || item.drug.name}} + ¥{{parseFloat(item.price || 0).toFixed(2)}} + + + 规格:{{item.drug.specification || '--'}} + - - - 选用此产品 + + + 选用此产品 + - - - - - + + + + + @@ -260,6 +262,7 @@ export default { display: flex; flex-direction: column; height: 100%; + min-height: 0; overflow: hidden; } @@ -271,18 +274,22 @@ export default { .modal-content { flex: 1; - overflow-y: auto; + min-height: 0; padding: 32rpx; display: flex; flex-direction: column; + overflow: hidden; +} + +.list-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; } -/* 底部防遮挡边距 */ -.product-list { - padding-bottom: 24rpx; -} - -/* 底部防遮挡边距 */ +/* 底部防遮挡边距(避免最后一行被底部按钮遮挡) */ .product-list { padding-bottom: 120rpx; } diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/TraditionalTermPickerModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/TraditionalTermPickerModal.vue new file mode 100644 index 0000000..a774ae5 --- /dev/null +++ b/subPackages/sub_workbench/prescription_v2/components/modals/TraditionalTermPickerModal.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/WesternMedicineModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/WesternMedicineModal.vue index 0a0dfb7..7f34377 100644 --- a/subPackages/sub_workbench/prescription_v2/components/modals/WesternMedicineModal.vue +++ b/subPackages/sub_workbench/prescription_v2/components/modals/WesternMedicineModal.vue @@ -27,47 +27,49 @@ /> - - - - - - - - {{item.drug.drug_name || item.drug.name}} - ¥{{parseFloat(item.price || 0).toFixed(2)}} - - - 规格:{{item.drug.specification || '--'}} - - - - 已添加处方 + + + + + + + + + {{item.drug.drug_name || item.drug.name}} + ¥{{parseFloat(item.price || 0).toFixed(2)}} - - 选用此药 + + 规格:{{item.drug.specification || '--'}} + + + + 已添加处方 + + + 选用此药 + - - - - - + + + + + @@ -126,6 +128,11 @@ export default { currentDrugs: { type: Array, default: () => [] + }, + /** 打开时预填搜索关键词(如接诊聊天「加入处方」) */ + initialSearchKey: { + type: String, + default: '' } }, data() { @@ -141,7 +148,9 @@ export default { value(newVal) { this.show = newVal; if (newVal) { - this.loadDrugList(); + const kw = (this.initialSearchKey || '').trim(); + this.searchKey = kw; + this.loadDrugList(kw); } }, show(newVal) { @@ -272,6 +281,7 @@ export default { display: flex; flex-direction: column; height: 100%; + min-height: 0; overflow: hidden; } @@ -283,18 +293,22 @@ export default { .modal-content { flex: 1; - overflow-y: auto; + min-height: 0; padding: 32rpx; display: flex; flex-direction: column; + overflow: hidden; +} + +.list-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; } -/* 底部防遮挡边距 */ -.drug-list { - padding-bottom: 24rpx; -} - -/* 底部防遮挡边距 */ +/* 底部防遮挡边距(避免最后一行被底部按钮遮挡) */ .drug-list { padding-bottom: 120rpx; } diff --git a/subPackages/sub_workbench/prescription_v2/index.vue b/subPackages/sub_workbench/prescription_v2/index.vue index 4dcc1cd..53abe5e 100644 --- a/subPackages/sub_workbench/prescription_v2/index.vue +++ b/subPackages/sub_workbench/prescription_v2/index.vue @@ -1,9 +1,13 @@