1. 开方线上功能基本完成
2. 修复了一些抽屉组件的滑动失效问题
This commit is contained in:
159
api/chat.js
Normal file
159
api/chat.js
Normal file
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
101
api/reception.js
101
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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/', '');
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
padding: 32rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
34
config/app-env.js
Normal file
34
config/app-env.js
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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分钟
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 用户标识配置
|
||||
*/
|
||||
|
||||
21
pages.json
21
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"
|
||||
}
|
||||
}]
|
||||
}
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
911
subPackages/sub_online_reception/components/MessageBubble.vue
Normal file
911
subPackages/sub_online_reception/components/MessageBubble.vue
Normal file
@@ -0,0 +1,911 @@
|
||||
<template>
|
||||
<view class="bubble-container" :class="{ mine: isMine, other: !isMine }">
|
||||
<image class="avatar" :src="avatar" mode="aspectFill"></image>
|
||||
|
||||
<view class="content-wrapper">
|
||||
<view
|
||||
class="message-bubble"
|
||||
:class="['type-' + msg.message_type, { 'no-padding': isMediaOrCard || isSystemPriceUpdate }]"
|
||||
>
|
||||
<!-- 0. 文本 -->
|
||||
<text v-if="msg.message_type === 0" class="text-content" user-select>{{ parsedText }}</text>
|
||||
|
||||
<!-- 1. 图片 -->
|
||||
<image
|
||||
v-else-if="msg.message_type === 1"
|
||||
:src="parsedText"
|
||||
mode="widthFix"
|
||||
class="media-content image"
|
||||
@click="$emit('previewImage', parsedText)"
|
||||
></image>
|
||||
|
||||
<!-- 2. 语音 -->
|
||||
<view
|
||||
v-else-if="msg.message_type === 2"
|
||||
class="audio-content"
|
||||
:style="{ width: getAudioWidth(msg.duration) }"
|
||||
@click="$emit('playAudio', msg)"
|
||||
>
|
||||
<block v-if="isMine">
|
||||
<text class="duration">{{ msg.duration }}''</text>
|
||||
<view class="voice-icon-wrap mine" :class="{ playing: isPlaying }">
|
||||
<u-icon name="volume-fill" size="20" color="#fff"></u-icon>
|
||||
</view>
|
||||
</block>
|
||||
<block v-else>
|
||||
<view class="voice-icon-wrap other" :class="{ playing: isPlaying }">
|
||||
<u-icon name="volume-fill" size="20" color="#333"></u-icon>
|
||||
</view>
|
||||
<text class="duration">{{ msg.duration }}''</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 3. 视频 -->
|
||||
<video
|
||||
v-else-if="msg.message_type === 3"
|
||||
:src="videoSrc"
|
||||
controls
|
||||
class="media-content video"
|
||||
></video>
|
||||
|
||||
<!-- 9. 系统消息 -->
|
||||
<block v-else-if="msg.message_type === 9">
|
||||
<view
|
||||
v-if="parsedObj && (parsedObj.type === 'price_update' || parsedObj.type === 'price_adjust')"
|
||||
class="service-notify-card"
|
||||
>
|
||||
<view class="sn-header">
|
||||
<text class="sn-title">{{ parsedObj.type === 'price_update' ? '订单改价通知' : '费用调整提醒' }}</text>
|
||||
</view>
|
||||
<view class="sn-body">
|
||||
<block v-if="parsedObj.type === 'price_update'">
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">变动说明</text>
|
||||
<text class="sn-value">{{ parsedObj.message }}</text>
|
||||
</view>
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">最新应付</text>
|
||||
<text class="sn-value price">¥{{ parsedObj.total_pay_price }}</text>
|
||||
</view>
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">订单编号</text>
|
||||
<text class="sn-value sub">{{ parsedObj.order_no }}</text>
|
||||
</view>
|
||||
</block>
|
||||
<block v-else-if="parsedObj.type === 'price_adjust'">
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">调整项目</text>
|
||||
<text class="sn-value truncate">{{ parsedObj.drug_name || '医疗服务' }}</text>
|
||||
</view>
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">价格变动</text>
|
||||
<view class="sn-value-group">
|
||||
<text class="old">¥{{ parsedObj.old_price }}</text>
|
||||
<u-icon name="arrow-right" size="10" color="#999" style="margin: 0 4rpx"></u-icon>
|
||||
<text class="new">¥{{ parsedObj.new_price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">变动金额</text>
|
||||
<text class="sn-value" :class="parsedObj.adjust_amount >= 0 ? 'color-orange' : 'color-green'">
|
||||
{{ parsedObj.adjust_amount >= 0 ? '增加' : '减少' }} ¥{{ Math.abs(parsedObj.adjust_amount) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">当前总额</text>
|
||||
<text class="sn-value strong">¥{{ parsedObj.total_pay_price }}</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="system-inner">
|
||||
<text>{{ systemNinePlainText }}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 卡片类 4,10,11,12,13,14 -->
|
||||
<view v-else-if="isCardType" class="card-content">
|
||||
<!-- 10 挂号 -->
|
||||
<block v-if="msg.message_type === 10">
|
||||
<view class="card-header bg-green">
|
||||
<view class="title-group">
|
||||
<u-icon name="calendar-fill" size="16" color="#059669"></u-icon>
|
||||
<text class="card-title">预约挂号</text>
|
||||
</view>
|
||||
<text class="status-tag" :class="'status-' + parsedObj.status">{{ getRegisterStatusText(parsedObj.status) }}</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="info-row">
|
||||
<text class="label">订单号</text>
|
||||
<text class="value order-font">{{ parsedObj.order_no }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">就诊人</text>
|
||||
<text class="value">{{ (parsedObj.user_patient && parsedObj.user_patient.name) || '未知' }}</text>
|
||||
</view>
|
||||
<view class="info-row highlight">
|
||||
<text class="label">费用</text>
|
||||
<text class="value price">¥{{ parsedObj.price }}</text>
|
||||
</view>
|
||||
<view v-if="parsedObj.chief_complaint" class="text-block">
|
||||
<text class="block-label">主诉</text>
|
||||
<text class="block-content">{{ parsedObj.chief_complaint }}</text>
|
||||
</view>
|
||||
<view v-if="registerCardDrugNamesLine" class="info-row">
|
||||
<text class="label">所选药品</text>
|
||||
<text class="value">{{ registerCardDrugNamesLine }}</text>
|
||||
</view>
|
||||
<view v-if="parsedObj.number != null && parsedObj.number !== ''" class="info-row">
|
||||
<text class="label">数量</text>
|
||||
<text class="value">各 {{ parsedObj.number }} 盒</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 11 复诊用药 -->
|
||||
<block v-else-if="msg.message_type === 11 && parsedObj && parsedObj.flow === 'follow_up_drug'">
|
||||
<view class="card-header bg-gray">
|
||||
<view class="title-group">
|
||||
<u-icon name="question-circle-fill" size="16" color="#64748B"></u-icon>
|
||||
<text class="card-title">医生助理</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view v-if="parsedObj.question" class="text-block">
|
||||
<text class="block-content">{{ parsedObj.question }}</text>
|
||||
</view>
|
||||
<view v-for="(row, idx) in (parsedObj.drugs || [])" :key="'fd-' + idx" class="info-row">
|
||||
<text class="label">{{ row.name || '药品' }}</text>
|
||||
<text class="value">×{{ row.quantity != null ? row.quantity : 1 }}</text>
|
||||
</view>
|
||||
<view v-if="followUpAnsweredLabel" class="info-row">
|
||||
<text class="label">患者选择</text>
|
||||
<text class="value">{{ followUpAnsweredLabel }}</text>
|
||||
</view>
|
||||
<view v-if="parsedObj.answer_status === 'pending'" class="follow-up-pending-readonly">
|
||||
<text class="follow-up-pending-tip">请在挂号后的用药确认页完成选择,此处不可更改</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 11 就诊经历 -->
|
||||
<block v-else-if="msg.message_type === 11">
|
||||
<view class="card-header bg-gray">
|
||||
<view class="title-group">
|
||||
<u-icon name="file-text-fill" size="16" color="#64748B"></u-icon>
|
||||
<text class="card-title">患者就诊经历</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view v-if="parsedObj.drug_name" class="info-row">
|
||||
<text class="label">药品名称</text>
|
||||
<text class="value">{{ parsedObj.drug_name }}</text>
|
||||
</view>
|
||||
<view v-if="parsedObj.illness_info" class="text-block">
|
||||
<text class="block-label">症状描述</text>
|
||||
<text class="block-content">{{ parsedObj.illness_info }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">是否就诊过</text>
|
||||
<text class="value">{{ parsedObj.has_visited === '1' ? '是' : '否' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">是否使用过药品</text>
|
||||
<text class="value">{{ parsedObj.has_used_drug === '1' ? '是' : '否' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="parsedObj.drug_name && canAddExperienceToRx && !isMine"
|
||||
class="card-footer card-footer--compact"
|
||||
>
|
||||
<button
|
||||
class="action-btn action-btn--ghost"
|
||||
@click.stop="$emit('addExperienceToRx', experienceRxPayload)"
|
||||
>
|
||||
加入处方
|
||||
</button>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 4 电子处方(医生端仅详情) -->
|
||||
<block v-else-if="msg.message_type === 4">
|
||||
<view class="card-header bg-blue">
|
||||
<view class="title-group">
|
||||
<u-icon name="order" size="16" color="#0A84FF"></u-icon>
|
||||
<text class="card-title">电子处方</text>
|
||||
</view>
|
||||
<text class="status-tag blue">{{ getStatusText(parsedObj.status) }}</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="info-row">
|
||||
<text class="label">诊断单号</text>
|
||||
<text class="value small">{{ parsedObj.order_no }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">金额</text>
|
||||
<text class="value price">¥{{ parsedObj.total_pay_price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<button class="action-btn" @click="$emit('viewPrescription', parsedObj.id, parsedObj.order_no)">查看详情</button>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 12 商品 -->
|
||||
<block v-else-if="msg.message_type === 12">
|
||||
<view class="card-header bg-blue">
|
||||
<view class="title-group">
|
||||
<u-icon name="shopping-cart-fill" size="16" color="#0A84FF"></u-icon>
|
||||
<text class="card-title">推荐商品</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="info-row">
|
||||
<text class="label">名称</text>
|
||||
<text class="value">{{ parsedObj.product_name }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">价格</text>
|
||||
<text class="value price">¥{{ parsedObj.price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 13 结束问诊 -->
|
||||
<block v-else-if="msg.message_type === 13">
|
||||
<view class="card-header bg-green">
|
||||
<view class="title-group">
|
||||
<u-icon name="checkmark-circle-fill" size="16" color="#059669"></u-icon>
|
||||
<text class="card-title">问诊结束</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="desc-text">{{ parsedObj.reason || '本次服务已完成' }}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 14 转诊(只读,与患者端视觉一致) -->
|
||||
<block v-else-if="msg.message_type === 14">
|
||||
<view class="transfer-readonly">
|
||||
<view class="transfer-r-head">
|
||||
<u-icon name="order" size="20" color="#ea580c"></u-icon>
|
||||
<text class="transfer-r-title">转诊消息</text>
|
||||
</view>
|
||||
<view class="transfer-r-body">
|
||||
<view v-if="parsedObj.transfer_store" class="transfer-r-row">
|
||||
<text class="transfer-r-k">转诊诊所</text>
|
||||
<text class="transfer-r-v">{{ parsedObj.transfer_store.name || '未知' }}</text>
|
||||
</view>
|
||||
<view v-if="parsedObj.delegate_store" class="transfer-r-row">
|
||||
<text class="transfer-r-k">委托诊所</text>
|
||||
<text class="transfer-r-v">{{ parsedObj.delegate_store.name || '未知' }}</text>
|
||||
</view>
|
||||
<view v-if="parsedObj.prescription_no" class="transfer-r-row">
|
||||
<text class="transfer-r-k">处方编号</text>
|
||||
<text class="transfer-r-v">{{ parsedObj.prescription_no }}</text>
|
||||
</view>
|
||||
<view v-if="parsedObj.transfer_reason" class="transfer-r-row">
|
||||
<text class="transfer-r-k">转诊原因</text>
|
||||
<text class="transfer-r-v">{{ parsedObj.transfer_reason }}</text>
|
||||
</view>
|
||||
<view v-if="parsedObj.questions && parsedObj.questions.length" class="transfer-qa">
|
||||
<text class="transfer-qa-title">咨询问题</text>
|
||||
<view v-for="(qa, qix) in parsedObj.questions" :key="qix" class="transfer-qa-item">
|
||||
<text class="transfer-qa-q">{{ qix + 1 }}. {{ qa.question }}</text>
|
||||
<text class="transfer-qa-a">{{ qa.answer }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="transfer-r-foot">
|
||||
<text v-if="parsedObj.status === 0" class="transfer-r-status">待患者确认转诊</text>
|
||||
<text v-else-if="parsedObj.status === 1" class="transfer-r-status ok">患者已同意转诊</text>
|
||||
<text v-else-if="parsedObj.status === 3" class="transfer-r-status no">患者已拒绝转诊</text>
|
||||
<text v-else class="transfer-r-status">转诊状态:{{ parsedObj.status }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block v-else>
|
||||
<view class="card-body"><text>不支持的消息类型</text></view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<text v-else class="text-content">{{ unknownText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { parseMessageContent } from '../utils/messageParse.js';
|
||||
|
||||
export default {
|
||||
name: 'MessageBubble',
|
||||
props: {
|
||||
msg: { type: Object, required: true },
|
||||
isMine: { type: Boolean, default: false },
|
||||
avatar: { type: String, default: '' },
|
||||
isPlaying: { type: Boolean, default: false },
|
||||
/** 接诊中且可开方时,就诊经历卡片显示「加入处方」 */
|
||||
canAddExperienceToRx: { type: Boolean, default: false }
|
||||
},
|
||||
computed: {
|
||||
isMediaOrCard() {
|
||||
return [1, 3, 4, 10, 11, 12, 13, 14].includes(this.msg.message_type);
|
||||
},
|
||||
isSystemPriceUpdate() {
|
||||
const p = this.parsedObj;
|
||||
return this.msg.message_type === 9 && p && (p.type === 'price_update' || p.type === 'price_adjust');
|
||||
},
|
||||
isCardType() {
|
||||
return [4, 10, 11, 12, 13, 14].includes(this.msg.message_type);
|
||||
},
|
||||
parsedObj() {
|
||||
const { message_type, message_content } = this.msg;
|
||||
if (![4, 9, 10, 11, 12, 13, 14].includes(message_type)) return null;
|
||||
try {
|
||||
return typeof message_content === 'object' ? message_content : JSON.parse(message_content);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
parsedText() {
|
||||
const raw = parseMessageContent(this.msg.message_type, this.msg.message_content);
|
||||
if (this.msg.message_type === 0 || this.msg.message_type === 1) {
|
||||
return typeof raw === 'string' ? raw : '';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
videoSrc() {
|
||||
if (this.msg.message_type !== 3) return '';
|
||||
const p = parseMessageContent(3, this.msg.message_content);
|
||||
if (typeof p === 'string') return p;
|
||||
if (p && typeof p === 'object' && p.url) return p.url;
|
||||
return '';
|
||||
},
|
||||
systemNinePlainText() {
|
||||
const c = this.msg.message_content;
|
||||
if (c == null) return '';
|
||||
if (typeof c === 'object') return JSON.stringify(c);
|
||||
return String(c);
|
||||
},
|
||||
registerCardDrugNamesLine() {
|
||||
if (this.msg.message_type !== 10) return '';
|
||||
const pc = this.parsedObj;
|
||||
if (!pc || typeof pc !== 'object') return '';
|
||||
const arr = pc.selected_western_drugs;
|
||||
if (Array.isArray(arr) && arr.length) {
|
||||
return arr.map((d) => d && d.name).filter(Boolean).join('、');
|
||||
}
|
||||
if (pc.drug && pc.drug.name) return pc.drug.name;
|
||||
return '';
|
||||
},
|
||||
followUpAnsweredLabel() {
|
||||
const pc = this.parsedObj;
|
||||
if (!pc || pc.flow !== 'follow_up_drug') return '';
|
||||
const st = pc.answer_status;
|
||||
if (st === 'yes' || st === '1') return '是,曾使用过';
|
||||
if (st === 'no' || st === '0') return '否,未使用过';
|
||||
return '';
|
||||
},
|
||||
experienceRxPayload() {
|
||||
const pc = this.parsedObj;
|
||||
if (!pc || this.msg.message_type !== 11) return {};
|
||||
return {
|
||||
drug_name: pc.drug_name || pc.name,
|
||||
drug_id: pc.drug_id != null ? pc.drug_id : pc.id
|
||||
};
|
||||
},
|
||||
unknownText() {
|
||||
const c = this.msg.message_content;
|
||||
if (c == null) return '[消息]';
|
||||
if (typeof c === 'object') return '[卡片消息]';
|
||||
const s = String(c);
|
||||
return s.length > 120 ? s.slice(0, 120) + '…' : s;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getAudioWidth(duration) {
|
||||
const min = 120;
|
||||
const max = 350;
|
||||
const unit = 12;
|
||||
let w = min + (parseInt(duration, 10) || 0) * unit;
|
||||
return Math.min(w, max) + 'rpx';
|
||||
},
|
||||
getStatusText(status) {
|
||||
const map = { 0: '待审核', 1: '已审核', 2: '未通过' };
|
||||
return map[status] || '状态未知';
|
||||
},
|
||||
getRegisterStatusText(status) {
|
||||
const map = { 0: '待就诊', 1: '已缴费', 2: '已就诊', 3: '已取消', 4: '已退费' };
|
||||
return map[status] || '状态未知';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$primary: #0a84ff;
|
||||
$bg-mine: #0a84ff;
|
||||
$bg-other: #ffffff;
|
||||
$text-main: #333333;
|
||||
$text-sub: #666666;
|
||||
$radius-bubble: 12rpx;
|
||||
|
||||
.bubble-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 30rpx;
|
||||
width: 100%;
|
||||
|
||||
.avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 10rpx;
|
||||
flex-shrink: 0;
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 72%;
|
||||
}
|
||||
|
||||
&.other {
|
||||
flex-direction: row;
|
||||
.avatar {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.message-bubble {
|
||||
background-color: $bg-other;
|
||||
color: $text-main;
|
||||
border-radius: 4rpx $radius-bubble $radius-bubble $radius-bubble;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: -10rpx;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10rpx solid transparent;
|
||||
border-bottom: 10rpx solid transparent;
|
||||
border-right: 12rpx solid $bg-other;
|
||||
}
|
||||
&.no-padding::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mine {
|
||||
flex-direction: row-reverse;
|
||||
.avatar {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.message-bubble {
|
||||
background-color: $bg-mine;
|
||||
color: #fff;
|
||||
border-radius: $radius-bubble 4rpx $radius-bubble $radius-bubble;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: -10rpx;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10rpx solid transparent;
|
||||
border-bottom: 10rpx solid transparent;
|
||||
border-left: 12rpx solid $bg-mine;
|
||||
}
|
||||
&.no-padding::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 18rpx 24rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
word-break: break-all;
|
||||
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||
min-height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.text-content {
|
||||
color: inherit;
|
||||
}
|
||||
&.mine.type-0 .text-content {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.no-padding {
|
||||
padding: 0;
|
||||
background: transparent !important;
|
||||
box-shadow: none;
|
||||
&::before,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-content {
|
||||
border-radius: 8rpx;
|
||||
&.image {
|
||||
max-width: 300rpx;
|
||||
display: block;
|
||||
}
|
||||
&.video {
|
||||
width: 300rpx;
|
||||
height: 170rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.duration {
|
||||
font-size: 26rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.voice-icon-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
&.other {
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
&.mine {
|
||||
margin-left: 6rpx;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
&.playing {
|
||||
animation: voice-pulse 1.2s infinite ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes voice-pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(var(--rotate-angle, 0deg));
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
transform: scale(0.92) rotate(var(--rotate-angle, 0deg));
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(var(--rotate-angle, 0deg));
|
||||
}
|
||||
}
|
||||
.voice-icon-wrap.mine.playing {
|
||||
--rotate-angle: 180deg;
|
||||
animation: voice-pulse 1.2s infinite ease-in-out;
|
||||
}
|
||||
.voice-icon-wrap.other.playing {
|
||||
--rotate-angle: 0deg;
|
||||
animation: voice-pulse 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
border: 1rpx solid #eee;
|
||||
width: 480rpx;
|
||||
|
||||
.card-header {
|
||||
padding: 16rpx 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid #f9f9f9;
|
||||
&.bg-green {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
&.bg-blue {
|
||||
background: #eff6ff;
|
||||
}
|
||||
&.bg-gray {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
.card-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
.status-tag {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #fff;
|
||||
color: $text-sub;
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||
&.blue {
|
||||
color: $primary;
|
||||
border-color: rgba(10, 132, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 24rpx;
|
||||
.label {
|
||||
color: #888;
|
||||
}
|
||||
.value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
max-width: 70%;
|
||||
}
|
||||
.order-font {
|
||||
font-family: Courier, monospace;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
.price {
|
||||
color: #ef4444;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.small {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
.desc-text {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
.text-block {
|
||||
background: #f5f5f5;
|
||||
padding: 16rpx;
|
||||
border-radius: 8rpx;
|
||||
.block-label {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.block-content {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 16rpx 20rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
&.card-footer--compact {
|
||||
padding: 12rpx 20rpx;
|
||||
}
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
font-size: 26rpx;
|
||||
border-radius: 30rpx;
|
||||
background: $primary;
|
||||
color: #fff;
|
||||
border: none;
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
&.action-btn--ghost {
|
||||
background: #f0fdf4;
|
||||
color: #059669;
|
||||
border: 1rpx solid #bbf7d0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.service-notify-card {
|
||||
background: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
width: 480rpx;
|
||||
border: 1rpx solid #eaeaea;
|
||||
overflow: hidden;
|
||||
.sn-header {
|
||||
padding: 24rpx 24rpx 16rpx 24rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
.sn-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
.sn-body {
|
||||
padding: 20rpx 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
.sn-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
line-height: 1.4;
|
||||
.sn-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
min-width: 110rpx;
|
||||
}
|
||||
.sn-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
&.truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 320rpx;
|
||||
}
|
||||
&.price {
|
||||
font-weight: 600;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
&.sub {
|
||||
color: #999;
|
||||
font-family: monospace;
|
||||
}
|
||||
&.strong {
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
&.color-orange {
|
||||
color: #fa8c16;
|
||||
}
|
||||
&.color-green {
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
.sn-value-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.old {
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.new {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.system-inner {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
align-self: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.follow-up-pending-readonly {
|
||||
margin-top: 12rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
.follow-up-pending-tip {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.transfer-readonly {
|
||||
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid #fed7aa;
|
||||
padding: 20rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.transfer-r-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding-bottom: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
border-bottom: 2rpx solid #fed7aa;
|
||||
}
|
||||
.transfer-r-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
.transfer-r-body {
|
||||
font-size: 26rpx;
|
||||
color: #334155;
|
||||
}
|
||||
.transfer-r-row {
|
||||
display: flex;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.transfer-r-k {
|
||||
color: #64748b;
|
||||
width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.transfer-r-v {
|
||||
flex: 1;
|
||||
color: #1e293b;
|
||||
}
|
||||
.transfer-qa {
|
||||
margin-top: 16rpx;
|
||||
padding: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #fed7aa;
|
||||
}
|
||||
.transfer-qa-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.transfer-qa-item {
|
||||
margin-bottom: 12rpx;
|
||||
padding: 12rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.transfer-qa-q {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #334155;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.transfer-qa-a {
|
||||
font-size: 24rpx;
|
||||
color: #0a84ff;
|
||||
}
|
||||
.transfer-r-foot {
|
||||
margin-top: 16rpx;
|
||||
padding-top: 12rpx;
|
||||
border-top: 2rpx solid #fed7aa;
|
||||
}
|
||||
.transfer-r-status {
|
||||
font-size: 26rpx;
|
||||
color: #64748b;
|
||||
&.ok {
|
||||
color: #16a34a;
|
||||
}
|
||||
&.no {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
<template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<view class="bar" v-if="visible">
|
||||
<button v-if="showAccept" class="btn primary" size="mini" @click="$emit('accept')">接诊</button>
|
||||
<button v-if="showRefuse" class="btn warn" size="mini" @click="$emit('refuse')">拒诊</button>
|
||||
<button v-if="showPrescription" class="btn primary" size="mini" @click="$emit('prescription')">开方</button>
|
||||
<button v-if="showEnd" class="btn plain" size="mini" @click="$emit('end')">结束诊断</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/** 与 RegisterStatusEnum 对齐:1=已支付待接诊 2=接诊中 */
|
||||
export default {
|
||||
name: 'ReceptionActionBar',
|
||||
props: {
|
||||
registerStatus: { type: Number, default: 0 },
|
||||
roomEnded: { type: Boolean, default: false }
|
||||
},
|
||||
computed: {
|
||||
visible() {
|
||||
if (this.roomEnded) return false;
|
||||
return this.showAccept || this.showRefuse || this.showPrescription || this.showEnd;
|
||||
},
|
||||
showAccept() {
|
||||
return this.registerStatus === 1;
|
||||
},
|
||||
showRefuse() {
|
||||
return this.registerStatus === 1;
|
||||
},
|
||||
showPrescription() {
|
||||
return this.registerStatus === 2;
|
||||
},
|
||||
showEnd() {
|
||||
return this.registerStatus === 2;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 16rpx 24rpx;
|
||||
gap: 16rpx;
|
||||
background: #f7f8fa;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
.primary {
|
||||
background: #6acdbb !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.warn {
|
||||
background: #ff9800 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.plain {
|
||||
background: #fff !important;
|
||||
color: #333 !important;
|
||||
border: 1rpx solid #ddd !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<u-popup v-model="show" mode="bottom" border-radius="16">
|
||||
<view class="modal">
|
||||
<view class="title">拒诊原因</view>
|
||||
<view class="list">
|
||||
<view
|
||||
v-for="(item, idx) in reasons"
|
||||
:key="idx"
|
||||
class="item"
|
||||
:class="{ active: selected === refuseLabel(item) }"
|
||||
@click="selected = refuseLabel(item)"
|
||||
>
|
||||
{{ refuseLabel(item) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="actions">
|
||||
<button class="btn cancel" @click="close">取消</button>
|
||||
<button class="btn ok" :disabled="!selected" @click="confirm">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { refuseReceptionListApi } from '@/api/reception.js';
|
||||
|
||||
export default {
|
||||
name: 'RefuseReceptionModal',
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
reasons: [],
|
||||
selected: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
refuseLabel(item) {
|
||||
if (!item) return '';
|
||||
if (typeof item === 'string') return item;
|
||||
return item.name || item.value || item.label || '';
|
||||
},
|
||||
async open() {
|
||||
this.selected = '';
|
||||
this.show = true;
|
||||
try {
|
||||
const res = await refuseReceptionListApi();
|
||||
const list = (res && res.result) || (res && res.data && res.data.result) || res || [];
|
||||
this.reasons = Array.isArray(list) ? list : [];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.reasons = [];
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.show = false;
|
||||
},
|
||||
async confirm() {
|
||||
if (!this.selected) return;
|
||||
this.$emit('confirm', this.selected);
|
||||
this.show = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal {
|
||||
padding: 24rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.list {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.item {
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
background: #f5f5f5;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.item.active {
|
||||
background: #e8f7f4;
|
||||
border: 1rpx solid #6acdbb;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.btn {
|
||||
min-width: 160rpx;
|
||||
}
|
||||
.ok {
|
||||
background: #6acdbb;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
12
subPackages/sub_online_reception/constants/messageTypes.js
Normal file
12
subPackages/sub_online_reception/constants/messageTypes.js
Normal file
@@ -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
|
||||
};
|
||||
1871
subPackages/sub_online_reception/pages/chat.vue
Normal file
1871
subPackages/sub_online_reception/pages/chat.vue
Normal file
File diff suppressed because it is too large
Load Diff
493
subPackages/sub_online_reception/pages/list.vue
Normal file
493
subPackages/sub_online_reception/pages/list.vue
Normal file
@@ -0,0 +1,493 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<u-navbar class="navbar" :is-back="true" title="在线接诊" title-color="#1a1a1a" :border-bottom="true"></u-navbar>
|
||||
|
||||
<view class="toolbar">
|
||||
<view class="store" @click="storePickerOpen = true">
|
||||
<text class="store-name">{{ currentStoreName || '选择门店' }}</text>
|
||||
<u-icon name="arrow-down-fill" size="20" color="#666"></u-icon>
|
||||
</view>
|
||||
<text class="store-hint">在线复诊列表与 PC 一致,不按门店筛选</text>
|
||||
</view>
|
||||
|
||||
<view class="tabs-wrap white">
|
||||
<u-tabs
|
||||
bar-width="40"
|
||||
:height="70"
|
||||
:list="listTabs"
|
||||
:current="listTabIndex"
|
||||
:is-scroll="false"
|
||||
active-color="#6ACDBB"
|
||||
inactive-color="#8A92A3"
|
||||
bg-color="#fff"
|
||||
@change="onListTabChange"
|
||||
></u-tabs>
|
||||
</view>
|
||||
|
||||
<view class="list-wrap">
|
||||
<view v-if="patientList.length" class="list">
|
||||
<view
|
||||
v-for="(row, idx) in patientList"
|
||||
:key="rowKey(row, idx)"
|
||||
class="ol-card flex-row flex-ali-center"
|
||||
:data-idx="idx"
|
||||
@tap="onPatientCardTap"
|
||||
>
|
||||
<view class="ol-avatar-wrap">
|
||||
<u-image width="80rpx" height="80rpx" shape="circle" :src="rowAvatar(row)">
|
||||
<u-loading slot="loading"></u-loading>
|
||||
</u-image>
|
||||
<u-badge
|
||||
:key="'ol-ub-' + rowKey(row, idx) + '-' + Number(row.unread_count || 0)"
|
||||
v-if="Number(row.unread_count) > 0"
|
||||
:count="Number(row.unread_count)"
|
||||
:offset="[-4, -4]"
|
||||
bgColor="#F44336"
|
||||
:overflow-count="99"
|
||||
></u-badge>
|
||||
</view>
|
||||
|
||||
<view class="ol-card-body flex-col flex-1">
|
||||
<!-- 第一行:姓名、基本信息、手机号、转诊标签 | 右侧时间 -->
|
||||
<view class="flex-row flex-jus-sp flex-ali-center">
|
||||
<view class="flex-row flex-ali-center flex-1 text-hide p-r-20">
|
||||
<text class="ol-name">{{ displayName(row) }}</text>
|
||||
<text class="ol-meta">{{ patientSexAgeText(row) }}</text>
|
||||
<text v-if="displayMobile(row)" class="ol-meta m-l-12">{{ displayMobile(row) }}</text>
|
||||
<u-tag
|
||||
v-if="Number(row.is_from_transfer) === 1"
|
||||
text="转诊"
|
||||
type="warning"
|
||||
size="mini"
|
||||
class="ol-tag"
|
||||
></u-tag>
|
||||
</view>
|
||||
<text class="ol-time">{{ formatRowMsgTime(row) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:最新消息预览 | 右侧状态 -->
|
||||
<view class="flex-row flex-jus-sp flex-ali-center m-t-12">
|
||||
<view class="ol-preview text-hide flex-1 p-r-20">
|
||||
<text>{{ lastMessageDisplayLine(row) }}</text>
|
||||
</view>
|
||||
<view class="ol-status">
|
||||
<d-text
|
||||
:text="statusText(row.status)"
|
||||
:color="rowStatusColor(row.status)"
|
||||
size="24"
|
||||
></d-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 单号暂时隐藏 -->
|
||||
<!--
|
||||
<view v-if="row.order_no" class="flex-row m-t-8">
|
||||
<text class="ol-sub">单号:{{ row.order_no }}</text>
|
||||
</view>
|
||||
-->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty">
|
||||
<d-empty text="暂无在线接诊患者"></d-empty>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<u-popup v-model="storePickerOpen" mode="bottom" border-radius="24" length="55%" :safe-area-inset-bottom="true">
|
||||
<view class="popup-inner">
|
||||
<view class="popup-title">选择诊所</view>
|
||||
<scroll-view scroll-y class="popup-scroll">
|
||||
<view
|
||||
v-for="item in storeList"
|
||||
:key="item.id"
|
||||
class="store-row"
|
||||
@click="changeStore(item.id)"
|
||||
>
|
||||
<text :class="{'active-store-text': String(storeId) === String(item.id)}">{{ item.name || item.store_name }}</text>
|
||||
<u-icon v-if="String(storeId) === String(item.id)" name="checkmark-circle-fill" color="#6ACDBB" size="36"></u-icon>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="popup-footer">
|
||||
<view class="u-btn qx" @click="storePickerOpen = false">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getOnlineConsultationPatientListApi, getMyStoreListApi, switchStoreApi } from '@/api/reception.js';
|
||||
import { normalizeRoomId } from '@/utils/chat/chatRoomManager.js';
|
||||
import { formatSessionRelativeTime } from '@/utils/chat/sessionListFormat.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
patientList: [],
|
||||
listType: 1,
|
||||
listTabs: [{ name: '当前接诊' }, { name: '历史接诊' }],
|
||||
storeList: [],
|
||||
currentStoreName: '',
|
||||
storePickerOpen: false,
|
||||
storeId: uni.getStorageSync('store_id') || '',
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
listTabIndex() {
|
||||
return this.listType === 1 ? 0 : 1;
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.bootstrap();
|
||||
},
|
||||
onLoad() {
|
||||
this.__doctorImRoomUpdateHandler = this.applyDoctorImRoomUpdate.bind(this);
|
||||
uni.$on('doctor-im-room-update', this.__doctorImRoomUpdateHandler);
|
||||
},
|
||||
onUnload() {
|
||||
if (this.__doctorImRoomUpdateHandler) {
|
||||
uni.$off('doctor-im-room-update', this.__doctorImRoomUpdateHandler);
|
||||
this.__doctorImRoomUpdateHandler = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
applyDoctorImRoomUpdate(payload) {
|
||||
const rid = normalizeRoomId(payload && payload.roomId);
|
||||
if (!rid) return;
|
||||
const idx = this.patientList.findIndex((r) => normalizeRoomId(r.room_id) === rid);
|
||||
if (idx === -1) return;
|
||||
const row = this.patientList[idx];
|
||||
const copy = { ...row };
|
||||
if (payload.last_message_preview != null && String(payload.last_message_preview).trim() !== '') {
|
||||
copy.last_message_preview = payload.last_message_preview;
|
||||
}
|
||||
if (payload.last_message_time != null && payload.last_message_time !== '') {
|
||||
copy.last_message_time = payload.last_message_time;
|
||||
}
|
||||
if (payload.incrementUnread) {
|
||||
if (payload.unreadForRoom != null) {
|
||||
copy.unread_count = Number(payload.unreadForRoom);
|
||||
} else {
|
||||
copy.unread_count = Number(copy.unread_count || 0) + 1;
|
||||
}
|
||||
}
|
||||
const next = [...this.patientList];
|
||||
next.splice(idx, 1, copy);
|
||||
this.patientList = this.sortPatientList(next);
|
||||
},
|
||||
unwrap(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;
|
||||
},
|
||||
rowKey(row, idx) {
|
||||
const room = row.room_id != null && row.room_id !== '' ? String(row.room_id) : 'r0';
|
||||
const reg = row.register_id != null ? row.register_id : row.id;
|
||||
const ord = row.order_no != null && row.order_no !== '' ? String(row.order_no) : '';
|
||||
return `${room}-${reg != null ? reg : 'x'}-${ord}-${idx}`;
|
||||
},
|
||||
lastMessageSortTs(row) {
|
||||
const t = row.last_message_time || row.updated_at || row.register_time;
|
||||
if (!t) return 0;
|
||||
const n = new Date(typeof t === 'number' && t < 1e12 ? t * 1000 : t).getTime();
|
||||
return Number.isNaN(n) ? 0 : n;
|
||||
},
|
||||
sortPatientList(rows) {
|
||||
if (!Array.isArray(rows)) return [];
|
||||
return [...rows].sort((a, b) => this.lastMessageSortTs(b) - this.lastMessageSortTs(a));
|
||||
},
|
||||
lastMessagePreview(row) {
|
||||
const p = row.last_message_preview;
|
||||
if (p != null && String(p).trim() !== '') return String(p);
|
||||
return '暂无消息';
|
||||
},
|
||||
patientSexAgeText(row) {
|
||||
const up = row && row.user_patient;
|
||||
if (!up) return '';
|
||||
const sex = Number(up.sex) % 2 !== 0 ? '男' : '女';
|
||||
const age = up.age != null && up.age !== '' ? `${up.age}岁` : '';
|
||||
const parts = [sex, age].filter(Boolean);
|
||||
return parts.length ? parts.join(' ') : '';
|
||||
},
|
||||
lastMessageDisplayLine(row) {
|
||||
return this.lastMessagePreview(row);
|
||||
},
|
||||
formatRowMsgTime(row) {
|
||||
return formatSessionRelativeTime(row);
|
||||
},
|
||||
rowAvatar(row) {
|
||||
const up = row && row.user_patient;
|
||||
if (up && up.avatar) return up.avatar;
|
||||
const sex = up ? Number(up.sex) : 0;
|
||||
return require(`@/static/image/${sex % 2 === 0 ? 'nv' : 'nan'}.png`);
|
||||
},
|
||||
rowStatusColor(st) {
|
||||
const s = Number(st);
|
||||
if (s === 1) return '#F44336';
|
||||
if (s === 2) return '#00C853';
|
||||
return '#C4C7CC';
|
||||
},
|
||||
onPatientCardTap(e) {
|
||||
const idx = Number(e?.currentTarget?.dataset?.idx);
|
||||
if (Number.isNaN(idx) || idx < 0) return;
|
||||
const row = this.patientList[idx];
|
||||
if (!row) return;
|
||||
this.goChat(row);
|
||||
},
|
||||
async bootstrap() {
|
||||
await this.loadStores();
|
||||
await this.loadPatientList();
|
||||
},
|
||||
async loadStores() {
|
||||
try {
|
||||
const res = await getMyStoreListApi();
|
||||
const raw = this.unwrap(res);
|
||||
this.storeList = Array.isArray(raw) ? raw : raw?.list || raw?.stores || [];
|
||||
this.storeId = uni.getStorageSync('store_id') || this.storeId;
|
||||
const cur = this.storeList.find((s) => String(s.id) === String(this.storeId));
|
||||
this.currentStoreName = cur ? (cur.name || cur.store_name) : '';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
async changeStore(id) {
|
||||
try {
|
||||
await switchStoreApi({ store_id: id });
|
||||
uni.setStorageSync('store_id', id);
|
||||
this.storeId = id;
|
||||
const cur = this.storeList.find((s) => String(s.id) === String(id));
|
||||
this.currentStoreName = cur ? (cur.name || cur.store_name) : '';
|
||||
this.storePickerOpen = false;
|
||||
} catch (err) {
|
||||
uni.showToast({ title: '切换门店失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
onListTabChange(index) {
|
||||
const next = index === 0 ? 1 : 2;
|
||||
if (this.listType === next) return;
|
||||
this.listType = next;
|
||||
this.loadPatientList();
|
||||
},
|
||||
async loadPatientList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await getOnlineConsultationPatientListApi({ type: this.listType });
|
||||
const raw = this.unwrap(res);
|
||||
const arr = Array.isArray(raw) ? raw : [];
|
||||
this.patientList = this.sortPatientList(arr);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.patientList = [];
|
||||
uni.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
displayName(row) {
|
||||
return row.user_patient?.name || row.user?.nick_name || row.name || '患者';
|
||||
},
|
||||
displayMobile(row) {
|
||||
return row.user_patient?.mobile || row.user?.mobile || row.mobile || '';
|
||||
},
|
||||
statusText(st) {
|
||||
const m = { 1: '待接诊', 2: '接诊中', 3: '已结束' };
|
||||
return m[st] || '其他';
|
||||
},
|
||||
goChat(row) {
|
||||
if (!row || typeof row !== 'object') {
|
||||
return;
|
||||
}
|
||||
const regId = row.register_id != null ? row.register_id : row.id;
|
||||
const name = encodeURIComponent(this.displayName(row));
|
||||
const room = row.room_id || '';
|
||||
const up = row.user_patient_id || row.user_patient?.id || '';
|
||||
uni.navigateTo({
|
||||
url: `/subPackages/sub_online_reception/pages/chat?register_id=${regId}&room_id=${encodeURIComponent(
|
||||
String(room || '')
|
||||
)}&patient_name=${name}&user_patient_id=${up}`
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.white {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 20rpx 24rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tabs-wrap {
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.store {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.store-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.store-hint {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 扁平、紧凑的卡片样式 */
|
||||
.ol-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ol-avatar-wrap {
|
||||
width: 80rpx;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.ol-card-body {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ol-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.ol-meta {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.ol-tag {
|
||||
margin-left: 12rpx;
|
||||
transform: scale(0.9);
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
.ol-time {
|
||||
flex-shrink: 0;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.ol-sub {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ol-status {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
/* 消息预览只保留纯文本,去除臃肿背景 */
|
||||
.ol-preview {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.text-hide {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m-t-8 { margin-top: 8rpx; }
|
||||
.m-t-12 { margin-top: 12rpx; }
|
||||
.m-l-12 { margin-left: 12rpx; }
|
||||
.p-r-20 { padding-right: 20rpx; }
|
||||
|
||||
.empty {
|
||||
padding: 120rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 弹窗精简 */
|
||||
.popup-inner {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.popup-scroll {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.store-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 26rpx 16rpx;
|
||||
border-bottom: 1rpx solid #f5f6f8;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.active-store-text {
|
||||
color: #6ACDBB;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
padding-top: 24rpx;
|
||||
}
|
||||
|
||||
.qx {
|
||||
text-align: center;
|
||||
padding: 20rpx;
|
||||
color: #666;
|
||||
background: #f7f8fa;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
16
subPackages/sub_online_reception/utils/messageParse.js
Normal file
16
subPackages/sub_online_reception/utils/messageParse.js
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
<template>
|
||||
<u-popup
|
||||
v-model="show"
|
||||
mode="bottom"
|
||||
:closeable="true"
|
||||
@close="handleClose"
|
||||
:safe-area-inset-bottom="true"
|
||||
:mask-close-able="false"
|
||||
z-index="10078"
|
||||
height="80%"
|
||||
>
|
||||
<view class="diagnosis-modal">
|
||||
<view class="modal-header">
|
||||
<d-text text="常用诊断" className="fs-32 main-c"></d-text>
|
||||
</view>
|
||||
|
||||
<view class="modal-content">
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-box">
|
||||
<u-input
|
||||
v-model="searchKey"
|
||||
placeholder="请输入想要搜索的诊断名称..."
|
||||
:custom-style="searchStyle"
|
||||
@input="handleSearch"
|
||||
@clear="handleSearch"
|
||||
>
|
||||
<template slot="suffix">
|
||||
<u-icon name="search" size="20" color="#999"></u-icon>
|
||||
</template>
|
||||
</u-input>
|
||||
</view>
|
||||
|
||||
<!-- 常用诊断列表 -->
|
||||
<view class="section" v-if="doctorMyDiseaseList.length > 0">
|
||||
<view class="section-header">
|
||||
<d-text text="常用诊断" className="fs-28 content-c"></d-text>
|
||||
<d-text :text="`${doctorMyDiseaseList.length}`" className="fs-24 tips-c"></d-text>
|
||||
</view>
|
||||
<view class="tag-container">
|
||||
<view
|
||||
v-for="item in doctorMyDiseaseList"
|
||||
:key="item.id"
|
||||
class="tag-item"
|
||||
:class="{ 'selected': item.disease.isSelect === 1 }"
|
||||
@click="selectDiagnosis(item.disease)"
|
||||
>
|
||||
<text class="tag-text">{{ item.disease.name }}</text>
|
||||
<view class="tag-action" @click.stop="removeFromMyDiagnosis(item)">
|
||||
<u-icon name="close" size="14" color="#999"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果列表 -->
|
||||
<view class="section" v-if="allDiagnosisList.length > 0">
|
||||
<view class="section-header">
|
||||
<d-text text="搜索结果" className="fs-28 content-c"></d-text>
|
||||
<d-text :text="`共 ${allDiagnosisList.length} 条`" className="fs-24 tips-c"></d-text>
|
||||
</view>
|
||||
<view class="tag-container">
|
||||
<view
|
||||
v-for="item in paginatedDiagnosisList"
|
||||
:key="item.id"
|
||||
class="tag-item"
|
||||
:class="{ 'selected': item.isSelect === 1 }"
|
||||
@click="selectDiagnosis(item)"
|
||||
>
|
||||
<text class="tag-text">{{ item.name }}</text>
|
||||
<view class="tag-action" @click.stop="addToMyDiagnosis(item)" v-if="!item.isInMyList">
|
||||
<u-icon name="plus" size="14" color="#6ACDBB"></u-icon>
|
||||
</view>
|
||||
<view class="tag-action" v-else>
|
||||
<u-icon name="checkmark" size="14" color="#6ACDBB"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view class="pagination" v-if="allDiagnosisList.length > pageSize">
|
||||
<u-button
|
||||
:disabled="currentPage === 1"
|
||||
@click="currentPage--"
|
||||
size="mini"
|
||||
plain
|
||||
>上一页</u-button>
|
||||
<text class="page-info">{{ currentPage }} / {{ totalPages }}</text>
|
||||
<u-button
|
||||
:disabled="currentPage >= totalPages"
|
||||
@click="currentPage++"
|
||||
size="mini"
|
||||
plain
|
||||
>下一页</u-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="allDiagnosisList.length === 0 && searchKey">
|
||||
<d-empty text="暂无相关诊断"></d-empty>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="modal-footer">
|
||||
<u-button
|
||||
@click="handleClose"
|
||||
:custom-style="{ backgroundColor: '#f5f5f5', color: '#333' }"
|
||||
>取消</u-button>
|
||||
<u-button
|
||||
@click="handleConfirm"
|
||||
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff' }"
|
||||
>确定</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDiseaseList } from '@/api/reception.js';
|
||||
import { req } from '@/common/js/index.js';
|
||||
|
||||
export default {
|
||||
name: 'DiagnosisModal',
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedDiagnosis: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
searchKey: '',
|
||||
allDiagnosisList: [],
|
||||
doctorMyDiseaseList: [],
|
||||
selectDiagnosisList: '',
|
||||
currentPage: 1,
|
||||
pageSize: 30,
|
||||
searchStyle: {
|
||||
fontSize: '28rpx',
|
||||
backgroundColor: '#F3F4F5',
|
||||
borderRadius: '66rpx',
|
||||
height: '66rpx',
|
||||
padding: '8rpx 32rpx'
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
paginatedDiagnosisList() {
|
||||
const start = (this.currentPage - 1) * this.pageSize;
|
||||
const end = start + this.pageSize;
|
||||
return this.allDiagnosisList.slice(start, end);
|
||||
},
|
||||
totalPages() {
|
||||
return Math.ceil(this.allDiagnosisList.length / this.pageSize);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
if (newVal) {
|
||||
this.selectDiagnosisList = this.selectedDiagnosis || '';
|
||||
this.getDiagnosisList();
|
||||
this.getDoctorMyDiseaseList();
|
||||
}
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 获取诊断列表
|
||||
async getDiagnosisList(searchKey = '') {
|
||||
try {
|
||||
const res = await getDiseaseList(searchKey);
|
||||
const processedList = (res || []).map((item) => {
|
||||
item.isSelect = 0;
|
||||
item.isInMyList = this.doctorMyDiseaseList.some(
|
||||
(myItem) => myItem.disease.id === item.id
|
||||
);
|
||||
if (this.selectDiagnosisList) {
|
||||
const valuesArr = this.selectDiagnosisList.split(',');
|
||||
valuesArr.forEach((value) => {
|
||||
if (value === item.name) {
|
||||
item.isSelect = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.allDiagnosisList = processedList;
|
||||
this.currentPage = 1;
|
||||
} catch (error) {
|
||||
console.error('获取诊断列表失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取常用诊断列表
|
||||
async getDoctorMyDiseaseList() {
|
||||
try {
|
||||
const res = await req.request({
|
||||
url: 'doctor/my-disease-list',
|
||||
method: 'GET'
|
||||
});
|
||||
this.doctorMyDiseaseList = (res || []).map((item) => {
|
||||
item.disease.isSelect = 0;
|
||||
if (this.selectDiagnosisList) {
|
||||
const valuesArr = this.selectDiagnosisList.split(',');
|
||||
valuesArr.forEach((value) => {
|
||||
if (value === item.disease.name) {
|
||||
item.disease.isSelect = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取常用诊断列表失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索
|
||||
handleSearch() {
|
||||
clearTimeout(this.searchTimer);
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.getDiagnosisList(this.searchKey);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
// 选择诊断
|
||||
selectDiagnosis(item) {
|
||||
let arr = this.selectDiagnosisList ? this.selectDiagnosisList.split(',').filter(Boolean) : [];
|
||||
|
||||
if (item.isSelect === 1) {
|
||||
arr = arr.filter(name => name !== item.name);
|
||||
item.isSelect = 0;
|
||||
} else {
|
||||
if (!arr.includes(item.name)) {
|
||||
arr.push(item.name);
|
||||
}
|
||||
item.isSelect = 1;
|
||||
}
|
||||
|
||||
this.selectDiagnosisList = arr.join(',');
|
||||
},
|
||||
|
||||
// 添加到常用诊断
|
||||
async addToMyDiagnosis(item) {
|
||||
try {
|
||||
await req.request({
|
||||
url: 'doctor/add-my-disease',
|
||||
method: 'POST',
|
||||
data: { disease_id: item.id }
|
||||
});
|
||||
uni.showToast({ title: '已添加到常用诊断', icon: 'success' });
|
||||
await this.getDoctorMyDiseaseList();
|
||||
item.isInMyList = true;
|
||||
} catch (error) {
|
||||
console.error('添加常用诊断失败:', error);
|
||||
uni.showToast({ title: '添加失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 从常用诊断中删除
|
||||
async removeFromMyDiagnosis(item) {
|
||||
try {
|
||||
await req.request({
|
||||
url: 'doctor/delete-my-disease',
|
||||
method: 'POST',
|
||||
data: { id: item.id }
|
||||
});
|
||||
uni.showToast({ title: '已从常用诊断中移除', icon: 'success' });
|
||||
await this.getDoctorMyDiseaseList();
|
||||
const searchItem = this.allDiagnosisList.find(d => d.id === item.disease.id);
|
||||
if (searchItem) {
|
||||
searchItem.isInMyList = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除常用诊断失败:', error);
|
||||
uni.showToast({ title: '删除失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 确认
|
||||
handleConfirm() {
|
||||
this.$emit('confirm', this.selectDiagnosisList);
|
||||
this.handleClose();
|
||||
},
|
||||
|
||||
// 关闭
|
||||
handleClose() {
|
||||
this.show = false;
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diagnosis-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.tag-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 24rpx;
|
||||
background-color: #f9fafb;
|
||||
border: 1rpx solid #e5e7eb;
|
||||
border-radius: 9999rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.selected {
|
||||
background-color: #eff6ff;
|
||||
border-color: #6ACDBB;
|
||||
|
||||
.tag-text {
|
||||
color: #6ACDBB;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
font-size: 28rpx;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.tag-action {
|
||||
margin-left: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24rpx;
|
||||
margin-top: 32rpx;
|
||||
padding-top: 32rpx;
|
||||
border-top: 1rpx dashed #eee;
|
||||
|
||||
.page-info {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
|
||||
.u-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,392 +0,0 @@
|
||||
<template>
|
||||
<u-popup
|
||||
v-model="show"
|
||||
mode="bottom"
|
||||
:closeable="true"
|
||||
@close="handleClose"
|
||||
:safe-area-inset-bottom="true"
|
||||
:mask-close-able="false"
|
||||
z-index="10078"
|
||||
height="80%"
|
||||
>
|
||||
<view class="doctor-order-modal">
|
||||
<view class="modal-header">
|
||||
<d-text text="常用医嘱" className="fs-32 main-c"></d-text>
|
||||
</view>
|
||||
|
||||
<view class="modal-content">
|
||||
<!-- 我的医嘱 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<d-text text="我的医嘱" className="fs-28 content-c"></d-text>
|
||||
<view class="add-btn" @click="showAddInput = true" v-if="!showAddInput">
|
||||
<u-icon name="plus" size="16" color="#6ACDBB"></u-icon>
|
||||
<text class="add-text">添加医嘱</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加输入框 -->
|
||||
<view class="add-input-box" v-if="showAddInput">
|
||||
<u-input
|
||||
v-model="newOrderContent"
|
||||
placeholder="输入内容后确认"
|
||||
:custom-style="inputStyle"
|
||||
@confirm="addMyDoctorOrder"
|
||||
/>
|
||||
<view class="input-actions">
|
||||
<u-button
|
||||
@click="cancelAdd"
|
||||
size="mini"
|
||||
plain
|
||||
:custom-style="{ marginRight: '16rpx' }"
|
||||
>取消</u-button>
|
||||
<u-button
|
||||
@click="addMyDoctorOrder"
|
||||
size="mini"
|
||||
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff' }"
|
||||
>确认</u-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的医嘱列表 -->
|
||||
<view class="tag-container">
|
||||
<view
|
||||
v-for="item in doctorOrderMyList"
|
||||
:key="item.id"
|
||||
class="tag-item"
|
||||
:class="{ 'selected': item.isSelect === 1 }"
|
||||
@click="selectDoctorOrder(item, 1)"
|
||||
>
|
||||
<text class="tag-text">{{ item.content }}</text>
|
||||
<view class="tag-action" @click.stop="deleteMyDoctorOrder(item)">
|
||||
<u-icon name="close" size="14" color="#999"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 公共医嘱 -->
|
||||
<view class="section" v-if="doctorOrderCommonList.length > 0">
|
||||
<view class="section-header">
|
||||
<d-text text="公共医嘱" className="fs-28 content-c"></d-text>
|
||||
</view>
|
||||
<view class="tag-container">
|
||||
<view
|
||||
v-for="item in doctorOrderCommonList"
|
||||
:key="item.id"
|
||||
class="tag-item"
|
||||
:class="{ 'selected': item.isSelect === 1 }"
|
||||
@click="selectDoctorOrder(item, 2)"
|
||||
>
|
||||
<text class="tag-text">{{ item.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="modal-footer">
|
||||
<u-button
|
||||
@click="handleClose"
|
||||
:custom-style="{ backgroundColor: '#f5f5f5', color: '#333' }"
|
||||
>取消</u-button>
|
||||
<u-button
|
||||
@click="handleConfirm"
|
||||
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff' }"
|
||||
>确定</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDoctorOrderList, getDoctorOrderCommonList, createDoctorOrder, deleteDoctorOrder } from '@/api/reception.js';
|
||||
import { req } from '@/common/js/index.js';
|
||||
|
||||
export default {
|
||||
name: 'DoctorOrderModal',
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedDoctorOrder: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
doctorOrderCommonList: [],
|
||||
doctorOrderMyList: [],
|
||||
selectDoctorOrderList: '',
|
||||
showAddInput: false,
|
||||
newOrderContent: '',
|
||||
inputStyle: {
|
||||
fontSize: '28rpx',
|
||||
backgroundColor: '#F3F4F5',
|
||||
borderRadius: '8rpx',
|
||||
height: '66rpx',
|
||||
padding: '8rpx 16rpx'
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
if (newVal) {
|
||||
this.selectDoctorOrderList = this.selectedDoctorOrder || '';
|
||||
this.getDoctorOrderListData();
|
||||
}
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 获取医嘱列表
|
||||
async getDoctorOrderListData() {
|
||||
try {
|
||||
// 获取我的医嘱
|
||||
const myRes = await getDoctorOrderList();
|
||||
if (myRes && (myRes.code === 0 || myRes.errcode === 0)) {
|
||||
const list = myRes.data || myRes.result || [];
|
||||
this.doctorOrderMyList = list.map((item) => {
|
||||
item.isSelect = 0;
|
||||
if (this.selectDoctorOrderList) {
|
||||
const valuesArr = this.selectDoctorOrderList.split(',');
|
||||
valuesArr.forEach((value) => {
|
||||
if (value === item.content) {
|
||||
item.isSelect = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
this.doctorOrderMyList = [];
|
||||
}
|
||||
|
||||
// 获取公共医嘱
|
||||
const commonRes = await getDoctorOrderCommonList({
|
||||
store_id: uni.getStorageSync('store_id') || 11001
|
||||
});
|
||||
if (commonRes && (commonRes.code === 0 || commonRes.errcode === 0)) {
|
||||
const list = commonRes.data || commonRes.result || [];
|
||||
this.doctorOrderCommonList = list.map((item) => {
|
||||
item.isSelect = 0;
|
||||
if (this.selectDoctorOrderList) {
|
||||
const valuesArr = this.selectDoctorOrderList.split(',');
|
||||
valuesArr.forEach((value) => {
|
||||
if (value === item.content) {
|
||||
item.isSelect = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
this.doctorOrderCommonList = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取医嘱列表失败:', error);
|
||||
uni.showToast({ title: '获取失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 选择医嘱
|
||||
selectDoctorOrder(item, type = 1) {
|
||||
let arr = this.selectDoctorOrderList ? this.selectDoctorOrderList.split(',').filter(Boolean) : [];
|
||||
|
||||
if (type === 1) {
|
||||
if (item.isSelect === 1) {
|
||||
arr = arr.filter(name => name !== item.content);
|
||||
item.isSelect = 0;
|
||||
} else {
|
||||
if (!arr.includes(item.content)) {
|
||||
arr.push(item.content);
|
||||
}
|
||||
item.isSelect = 1;
|
||||
}
|
||||
} else {
|
||||
// 公共医嘱
|
||||
if (item.isSelect === 1) {
|
||||
arr = arr.filter(name => name !== item.content);
|
||||
item.isSelect = 0;
|
||||
} else {
|
||||
if (!arr.includes(item.content)) {
|
||||
arr.push(item.content);
|
||||
}
|
||||
item.isSelect = 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.selectDoctorOrderList = arr.join(',');
|
||||
},
|
||||
|
||||
// 添加自定义医嘱
|
||||
async addMyDoctorOrder() {
|
||||
if (!this.newOrderContent.trim()) {
|
||||
uni.showToast({ title: '请输入医嘱内容', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await createDoctorOrder(this.newOrderContent.trim());
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
uni.showToast({ title: '添加成功', icon: 'success' });
|
||||
this.newOrderContent = '';
|
||||
this.showAddInput = false;
|
||||
}
|
||||
await this.getDoctorOrderListData();
|
||||
} catch (error) {
|
||||
console.error('添加医嘱失败:', error);
|
||||
uni.showToast({ title: '添加失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 删除我的医嘱
|
||||
async deleteMyDoctorOrder(item) {
|
||||
try {
|
||||
const res = await deleteDoctorOrder(item.id);
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
uni.showToast({ title: '删除成功', icon: 'success' });
|
||||
}
|
||||
await this.getDoctorOrderListData();
|
||||
} catch (error) {
|
||||
console.error('删除医嘱失败:', error);
|
||||
uni.showToast({ title: '删除失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 取消添加
|
||||
cancelAdd() {
|
||||
this.newOrderContent = '';
|
||||
this.showAddInput = false;
|
||||
},
|
||||
|
||||
// 确认
|
||||
handleConfirm() {
|
||||
this.$emit('confirm', this.selectDoctorOrderList);
|
||||
this.handleClose();
|
||||
},
|
||||
|
||||
// 关闭
|
||||
handleClose() {
|
||||
this.show = false;
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.doctor-order-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border: 1rpx dashed #6ACDBB;
|
||||
border-radius: 32rpx;
|
||||
|
||||
.add-text {
|
||||
font-size: 24rpx;
|
||||
color: #6ACDBB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-input-box {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 16rpx;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8rpx;
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 24rpx;
|
||||
background-color: #f9fafb;
|
||||
border: 1rpx solid #e5e7eb;
|
||||
border-radius: 9999rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.selected {
|
||||
background-color: #eff6ff;
|
||||
border-color: #6ACDBB;
|
||||
|
||||
.tag-text {
|
||||
color: #6ACDBB;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
font-size: 28rpx;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.tag-action {
|
||||
margin-left: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
|
||||
.u-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,665 +0,0 @@
|
||||
<template>
|
||||
<view class="bubble-container" :class="{ 'mine': isMine, 'other': !isMine }">
|
||||
<!-- 头像 -->
|
||||
<image class="avatar" :src="avatar" mode="aspectFill"></image>
|
||||
|
||||
<!-- 消息内容包裹 -->
|
||||
<view class="content-wrapper">
|
||||
<!-- 文本/语音/视频/图片 气泡 -->
|
||||
<view class="message-bubble"
|
||||
:class="['type-' + msg.message_type, { 'no-padding': isMediaOrCard || isSystemPriceUpdate }]">
|
||||
<!-- 0. 文本 -->
|
||||
<text v-if="msg.message_type === 0" class="text-content" user-select>{{ parsedContent }}</text>
|
||||
|
||||
<!-- 1. 图片 -->
|
||||
<image v-else-if="msg.message_type === 1"
|
||||
:src="parsedContent"
|
||||
mode="widthFix"
|
||||
class="media-content image"
|
||||
@click="$emit('previewImage', parsedContent)">
|
||||
</image>
|
||||
|
||||
<!-- 2. 语音 -->
|
||||
<view v-else-if="msg.message_type === 2"
|
||||
class="audio-content"
|
||||
:style="{ width: getAudioWidth(msg.duration) }"
|
||||
@click="$emit('playAudio', msg)">
|
||||
<block v-if="isMine">
|
||||
<text class="duration">{{ msg.duration }}''</text>
|
||||
<view class="voice-icon-wrap mine" :class="{ 'playing': isPlaying }">
|
||||
<u-icon name="volume-fill" size="20" color="#fff"></u-icon>
|
||||
</view>
|
||||
</block>
|
||||
<block v-else>
|
||||
<view class="voice-icon-wrap other" :class="{ 'playing': isPlaying }">
|
||||
<u-icon name="volume-fill" size="20" color="#333"></u-icon>
|
||||
</view>
|
||||
<text class="duration">{{ msg.duration }}''</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 3. 视频 -->
|
||||
<video v-else-if="msg.message_type === 3"
|
||||
:src="parsedContent"
|
||||
controls
|
||||
class="media-content video">
|
||||
</video>
|
||||
|
||||
<!-- 9. 系统消息 -->
|
||||
<block v-else-if="msg.message_type === 9">
|
||||
<view v-if="parsedContent.type === 'price_update' || parsedContent.type === 'price_adjust'"
|
||||
class="service-notify-card"
|
||||
@click="parsedContent.order_id && $emit('viewOrder', parsedContent.order_id, parsedContent.order_no)">
|
||||
<view class="sn-header">
|
||||
<text class="sn-title">{{ parsedContent.type === 'price_update' ? '订单改价通知' : '费用调整提醒' }}</text>
|
||||
</view>
|
||||
<view class="sn-body">
|
||||
<block v-if="parsedContent.type === 'price_update'">
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">变动说明</text>
|
||||
<text class="sn-value">{{ parsedContent.message }}</text>
|
||||
</view>
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">最新应付</text>
|
||||
<text class="sn-value price">¥{{ parsedContent.total_pay_price }}</text>
|
||||
</view>
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">订单编号</text>
|
||||
<text class="sn-value sub">{{ parsedContent.order_no }}</text>
|
||||
</view>
|
||||
</block>
|
||||
<block v-else-if="parsedContent.type === 'price_adjust'">
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">调整项目</text>
|
||||
<text class="sn-value truncate">{{ parsedContent.drug_name || '医疗服务' }}</text>
|
||||
</view>
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">价格变动</text>
|
||||
<view class="sn-value-group">
|
||||
<text class="old">¥{{ parsedContent.old_price }}</text>
|
||||
<u-icon name="arrow-right" size="10" color="#999" style="margin:0 4rpx;"></u-icon>
|
||||
<text class="new">¥{{ parsedContent.new_price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">变动金额</text>
|
||||
<text class="sn-value" :class="parsedContent.adjust_amount >= 0 ? 'color-orange' : 'color-green'">
|
||||
{{ parsedContent.adjust_amount >= 0 ? '增加' : '减少' }} ¥{{ Math.abs(parsedContent.adjust_amount) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="sn-row">
|
||||
<text class="sn-label">当前总额</text>
|
||||
<text class="sn-value strong">¥{{ parsedContent.total_pay_price }}</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="sn-footer">
|
||||
<text>查看详情</text>
|
||||
<u-icon name="arrow-right" size="12" color="#ccc"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="system-inner">
|
||||
<text>{{ msg.message_content }}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 卡片类消息 (4, 10, 11, 12, 13) -->
|
||||
<view v-else-if="isCardType" class="card-content">
|
||||
<!-- 类型10: 挂号 -->
|
||||
<block v-if="msg.message_type === 10">
|
||||
<view class="card-header bg-green">
|
||||
<view class="title-group"><u-icon name="calendar-fill" size="16" color="#059669"></u-icon><text class="card-title">预约挂号</text></view>
|
||||
<text class="status-tag" :class="'status-' + parsedContent.status">{{ getRegisterStatusText(parsedContent.status) }}</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="info-row"><text class="label">订单号</text><text class="value order-font">{{ parsedContent.order_no }}</text></view>
|
||||
<view class="info-row"><text class="label">就诊人</text><text class="value">{{ (parsedContent.user_patient && parsedContent.user_patient.name) || '未知' }}</text></view>
|
||||
<view class="info-row highlight"><text class="label">费用</text><text class="value price">¥{{ parsedContent.price }}</text></view>
|
||||
<view class="text-block" v-if="parsedContent.chief_complaint">
|
||||
<text class="block-label">主诉</text>
|
||||
<text class="block-content">{{ parsedContent.chief_complaint }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="registerCardDrugNamesLine">
|
||||
<text class="label">所选药品</text>
|
||||
<text class="value">{{ registerCardDrugNamesLine }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="parsedContent.number != null && parsedContent.number !== ''">
|
||||
<text class="label">数量</text>
|
||||
<text class="value">各 {{ parsedContent.number }} 盒</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-footer"><button class="action-btn" @click="$emit('viewRegister', parsedContent.id)">查看详情</button></view>
|
||||
</block>
|
||||
|
||||
<block v-else-if="msg.message_type === 11 && parsedContent && parsedContent.flow === 'follow_up_drug'">
|
||||
<view class="card-header bg-gray">
|
||||
<view class="title-group"><u-icon name="question-circle-fill" size="16" color="#64748B"></u-icon><text class="card-title">医生助理</text></view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="text-block" v-if="parsedContent.question">
|
||||
<text class="block-content">{{ parsedContent.question }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-for="(row, idx) in (parsedContent.drugs || [])" :key="'dfd-' + idx">
|
||||
<text class="label">{{ row.name || '药品' }}</text>
|
||||
<text class="value">×{{ row.quantity != null ? row.quantity : 1 }}</text>
|
||||
</view>
|
||||
<view v-if="followUpAnsweredLabel" class="info-row">
|
||||
<text class="label">患者选择</text>
|
||||
<text class="value">{{ followUpAnsweredLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 类型11: 患者就诊经历 -->
|
||||
<block v-else-if="msg.message_type === 11">
|
||||
<view class="card-header bg-gray">
|
||||
<view class="title-group"><u-icon name="file-text-fill" size="16" color="#64748B"></u-icon><text class="card-title">患者就诊经历</text></view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="info-row" v-if="parsedContent.drug_name">
|
||||
<text class="label">药品名称</text>
|
||||
<text class="value">{{ parsedContent.drug_name }}</text>
|
||||
</view>
|
||||
<view class="text-block" v-if="parsedContent.illness_info">
|
||||
<text class="block-label">症状描述</text>
|
||||
<text class="block-content">{{ parsedContent.illness_info }}</text>
|
||||
</view>
|
||||
<view class="info-row"><text class="label">是否就诊过</text><text class="value">{{ parsedContent.has_visited === '1' ? '是' : '否' }}</text></view>
|
||||
<view class="info-row"><text class="label">是否使用过药品</text><text class="value">{{ parsedContent.has_used_drug === '1' ? '是' : '否' }}</text></view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 类型4: 电子处方 -->
|
||||
<block v-else-if="msg.message_type === 4">
|
||||
<view class="card-header bg-blue">
|
||||
<view class="title-group"><u-icon name="order" size="16" color="#0A84FF"></u-icon><text class="card-title">电子处方</text></view>
|
||||
<text class="status-tag blue">{{ getStatusText(parsedContent.status) }}</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="info-row"><text class="label">诊断单号</text><text class="value small">{{ parsedContent.order_no }}</text></view>
|
||||
<view class="info-row"><text class="label">金额</text><text class="value price">¥{{ parsedContent.total_pay_price }}</text></view>
|
||||
</view>
|
||||
<view class="card-footer split">
|
||||
<text class="link-btn" @click="$emit('viewPrescription', parsedContent.id, parsedContent.order_no)">详情</text>
|
||||
<button class="action-btn primary" @click="$emit('goToOrderMedicine', parsedContent.order_id, parsedContent.order_no)">一键购药</button>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 类型12: 商品 -->
|
||||
<block v-else-if="msg.message_type === 12">
|
||||
<view class="card-header bg-blue">
|
||||
<view class="title-group"><u-icon name="shopping-cart-fill" size="16" color="#0A84FF"></u-icon><text class="card-title">推荐商品</text></view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="info-row"><text class="label">名称</text><text class="value">{{ parsedContent.product_name }}</text></view>
|
||||
<view class="info-row"><text class="label">价格</text><text class="value price">¥{{ parsedContent.price }}</text></view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 类型13: 结束问诊 -->
|
||||
<block v-else-if="msg.message_type === 13">
|
||||
<view class="card-header bg-green">
|
||||
<view class="title-group"><u-icon name="checkmark-circle-fill" size="16" color="#059669"></u-icon><text class="card-title">问诊结束</text></view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="desc-text">{{ parsedContent.reason || '本次服务已完成' }}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block v-else>
|
||||
<view class="card-body"><text>不支持的消息类型</text></view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 其他未知类型默认显示文本 -->
|
||||
<text v-else class="text-content">{{ parsedContent }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MessageBubble",
|
||||
props: {
|
||||
msg: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isMine: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isPlaying: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isMediaOrCard() {
|
||||
return [1, 3, 4, 10, 11, 12, 13].includes(this.msg.message_type);
|
||||
},
|
||||
isSystemPriceUpdate() {
|
||||
return this.msg.message_type === 9 && (this.parsedContent.type === 'price_update' || this.parsedContent.type === 'price_adjust');
|
||||
},
|
||||
isCardType() {
|
||||
return [4, 10, 11, 12, 13].includes(this.msg.message_type);
|
||||
},
|
||||
parsedContent() {
|
||||
const { message_type, message_content } = this.msg;
|
||||
if ([4, 9, 10, 11, 12, 13].includes(message_type)) {
|
||||
try {
|
||||
return typeof message_content === 'object' ? message_content : JSON.parse(message_content);
|
||||
} catch (e) {
|
||||
return message_content;
|
||||
}
|
||||
}
|
||||
return message_content;
|
||||
},
|
||||
registerCardDrugNamesLine() {
|
||||
if (this.msg.message_type !== 10) return '';
|
||||
const pc = this.parsedContent;
|
||||
if (!pc || typeof pc !== 'object') return '';
|
||||
const arr = pc.selected_western_drugs;
|
||||
if (Array.isArray(arr) && arr.length) {
|
||||
return arr.map((d) => d && d.name).filter(Boolean).join('、');
|
||||
}
|
||||
if (pc.drug && pc.drug.name) return pc.drug.name;
|
||||
return '';
|
||||
},
|
||||
followUpAnsweredLabel() {
|
||||
const pc = this.parsedContent;
|
||||
if (!pc || pc.flow !== 'follow_up_drug') return '';
|
||||
const st = pc.answer_status;
|
||||
if (st === 'yes' || st === '1') return '是,曾使用过';
|
||||
if (st === 'no' || st === '0') return '否,未使用过';
|
||||
return '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getAudioWidth(duration) {
|
||||
const min = 120;
|
||||
const max = 350;
|
||||
const unit = 12;
|
||||
let w = min + (parseInt(duration) || 0) * unit;
|
||||
return Math.min(w, max) + 'rpx';
|
||||
},
|
||||
getStatusText(status) {
|
||||
const map = { 0: '待审核', 1: '已审核', 2: '未通过' };
|
||||
return map[status] || '状态未知';
|
||||
},
|
||||
getRegisterStatusText(status) {
|
||||
const map = { 0: '待就诊', 1: '已缴费', 2: '已就诊', 3: '已取消', 4: '已退费' };
|
||||
return map[status] || '状态未知';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$primary: #0A84FF;
|
||||
$bg-mine: #0A84FF;
|
||||
$bg-other: #FFFFFF;
|
||||
$text-main: #333333;
|
||||
$text-sub: #666666;
|
||||
$radius-bubble: 12rpx;
|
||||
|
||||
.bubble-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 30rpx;
|
||||
width: 100%;
|
||||
|
||||
.avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 10rpx;
|
||||
flex-shrink: 0;
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 72%;
|
||||
}
|
||||
|
||||
&.other {
|
||||
flex-direction: row;
|
||||
.avatar { margin-right: 16rpx; }
|
||||
.message-bubble {
|
||||
background-color: $bg-other;
|
||||
color: $text-main;
|
||||
border-radius: 4rpx $radius-bubble $radius-bubble $radius-bubble;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: -10rpx;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10rpx solid transparent;
|
||||
border-bottom: 10rpx solid transparent;
|
||||
border-right: 12rpx solid $bg-other;
|
||||
}
|
||||
&.no-padding::before { display: none; }
|
||||
}
|
||||
}
|
||||
|
||||
&.mine {
|
||||
flex-direction: row-reverse;
|
||||
.avatar { margin-left: 16rpx; }
|
||||
.message-bubble {
|
||||
background-color: $bg-mine;
|
||||
color: #fff;
|
||||
border-radius: $radius-bubble 4rpx $radius-bubble $radius-bubble;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: -10rpx;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10rpx solid transparent;
|
||||
border-bottom: 10rpx solid transparent;
|
||||
border-left: 12rpx solid $bg-mine;
|
||||
}
|
||||
&.no-padding::after { display: none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 18rpx 24rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
word-break: break-all;
|
||||
box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
|
||||
min-height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.no-padding {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
&::before, &::after { display: none; }
|
||||
}
|
||||
}
|
||||
|
||||
.media-content {
|
||||
border-radius: 8rpx;
|
||||
&.image { max-width: 300rpx; display: block; }
|
||||
&.video { width: 300rpx; height: 170rpx; }
|
||||
}
|
||||
|
||||
.audio-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.duration {
|
||||
font-size: 26rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.voice-icon-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
|
||||
&.other {
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
|
||||
&.mine {
|
||||
margin-left: 6rpx;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
&.playing {
|
||||
animation: voice-pulse 1.2s infinite ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes voice-pulse {
|
||||
0% { opacity: 1; transform: scale(1) rotate(var(--rotate-angle, 0deg)); }
|
||||
50% { opacity: 0.4; transform: scale(0.92) rotate(var(--rotate-angle, 0deg)); }
|
||||
100% { opacity: 1; transform: scale(1) rotate(var(--rotate-angle, 0deg)); }
|
||||
}
|
||||
|
||||
.voice-icon-wrap.mine.playing { --rotate-angle: 180deg; animation: voice-pulse 1.2s infinite ease-in-out; }
|
||||
.voice-icon-wrap.other.playing { --rotate-angle: 0deg; animation: voice-pulse 1.2s infinite ease-in-out; }
|
||||
|
||||
.card-content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
border: 1rpx solid #eee;
|
||||
width: 480rpx;
|
||||
|
||||
.card-header {
|
||||
padding: 16rpx 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid #f9f9f9;
|
||||
|
||||
&.bg-green { background: #F0FDF4; }
|
||||
&.bg-blue { background: #EFF6FF; }
|
||||
&.bg-gray { background: #F8FAFC; }
|
||||
|
||||
.title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
.card-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
.status-tag {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #fff;
|
||||
color: $text-sub;
|
||||
border: 1rpx solid rgba(0,0,0,0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 24rpx;
|
||||
.label { color: #888; }
|
||||
.value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
max-width: 70%;
|
||||
&.order-font { font-family: Courier, monospace; letter-spacing: 0.5rpx; }
|
||||
&.price { color: #ef4444; font-size: 28rpx; font-weight: bold; }
|
||||
&.small { font-size: 22rpx; color: #999; }
|
||||
}
|
||||
}
|
||||
.desc-text {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
.text-block {
|
||||
background: #f5f5f5;
|
||||
padding: 16rpx;
|
||||
border-radius: 8rpx;
|
||||
.block-label {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.block-content {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 16rpx 20rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
font-size: 26rpx;
|
||||
border-radius: 30rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid #ddd;
|
||||
color: #555;
|
||||
&::after { border: none; }
|
||||
&.primary {
|
||||
background: $primary;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
&.split {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.link-btn {
|
||||
font-size: 24rpx;
|
||||
color: $primary;
|
||||
padding: 10rpx;
|
||||
}
|
||||
.action-btn {
|
||||
width: 160rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.service-notify-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 12rpx;
|
||||
width: 480rpx;
|
||||
border: 1rpx solid #eaeaea;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.sn-header {
|
||||
padding: 24rpx 24rpx 16rpx 24rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
.sn-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.sn-body {
|
||||
padding: 20rpx 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
|
||||
.sn-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
line-height: 1.4;
|
||||
|
||||
.sn-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
min-width: 110rpx;
|
||||
}
|
||||
|
||||
.sn-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
&.truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 320rpx;
|
||||
}
|
||||
&.price {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
&.sub {
|
||||
color: #999;
|
||||
font-family: monospace;
|
||||
}
|
||||
&.strong {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
&.color-orange { color: #fa8c16; }
|
||||
&.color-green { color: #52c41a; }
|
||||
}
|
||||
|
||||
.sn-value-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.old {
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.new {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sn-footer {
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
padding: 18rpx 24rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
background: #fff;
|
||||
|
||||
&:active {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.system-inner {
|
||||
background: rgba(0,0,0,0.05);
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
align-self: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,382 +0,0 @@
|
||||
<template>
|
||||
<u-popup
|
||||
v-model="show"
|
||||
mode="bottom"
|
||||
:closeable="true"
|
||||
@close="handleClose"
|
||||
:safe-area-inset-bottom="true"
|
||||
:mask-close-able="false"
|
||||
z-index="10078"
|
||||
height="80%"
|
||||
>
|
||||
<view class="western-usage-modal">
|
||||
<view class="modal-header">
|
||||
<d-text text="用法用量" className="fs-32 main-c"></d-text>
|
||||
</view>
|
||||
|
||||
<view class="modal-content">
|
||||
<view class="drug-info white b-r-8 p-32 m-t-2" v-if="drug">
|
||||
<view class="info-item">
|
||||
<d-text text="名称:" className="fs-3 color9"></d-text>
|
||||
<d-text :text="drug.drug_name" className="fs-3 color0"></d-text>
|
||||
</view>
|
||||
<view class="info-item m-t-16">
|
||||
<d-text text="规格:" className="fs-3 color9"></d-text>
|
||||
<d-text :text="drug.specification || '--'" className="fs-3 color0"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 药品数量 -->
|
||||
<view class="usage-item white b-r-8 p-32 m-t-2">
|
||||
<view class="item-label">
|
||||
<d-text text="药品数量" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<u-number-box
|
||||
v-model="usageData.select_number"
|
||||
:min="1"
|
||||
:max="(drug && drug.stock) || 999"
|
||||
/>
|
||||
<d-text text="盒" className="fs-3 color9 m-l-16"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用法 -->
|
||||
<view class="usage-item white b-r-8 p-32 m-t-2">
|
||||
<view class="item-label">
|
||||
<d-text text="用法" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="drugUseList.drug_use_type || []"
|
||||
range-key="name"
|
||||
:value="usageData.type_id && drugUseList.drug_use_type ? drugUseList.drug_use_type.findIndex(item => item.id === usageData.type_id) : 0"
|
||||
@change="handleChangeUsageType"
|
||||
>
|
||||
<view class="picker-view">
|
||||
{{ (usageData.use_type && usageData.use_type.name) || '请选择' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 频次 -->
|
||||
<view class="usage-item white b-r-8 p-32 m-t-2">
|
||||
<view class="item-label">
|
||||
<d-text text="频次" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="drugUseList.drug_use_frequency || []"
|
||||
range-key="name"
|
||||
:value="usageData.frequency_id && drugUseList.drug_use_frequency ? drugUseList.drug_use_frequency.findIndex(item => item.id === usageData.frequency_id) : 0"
|
||||
@change="handleChangeFrequency"
|
||||
>
|
||||
<view class="picker-view">
|
||||
{{ (usageData.use_frequency && usageData.use_frequency.name) || '请选择' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 时间 -->
|
||||
<view class="usage-item white b-r-8 p-32 m-t-2">
|
||||
<view class="item-label">
|
||||
<d-text text="时间" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="drugUseList.drug_time || []"
|
||||
range-key="name"
|
||||
:value="usageData.time_id && drugUseList.drug_time ? drugUseList.drug_time.findIndex(item => item.id === usageData.time_id) : 0"
|
||||
@change="handleChangeTime"
|
||||
>
|
||||
<view class="picker-view">
|
||||
{{ (usageData.use_num && usageData.use_num.name) || '请选择' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 单位 -->
|
||||
<view class="usage-item white b-r-8 p-32 m-t-2">
|
||||
<view class="item-label">
|
||||
<d-text text="每次用量" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<u-number-box v-model="usageData.number" :min="1" />
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="drugUseList.drug_unit || []"
|
||||
range-key="name"
|
||||
:value="usageData.unit_id && drugUseList.drug_unit ? drugUseList.drug_unit.findIndex(item => item.id === usageData.unit_id) : 0"
|
||||
@change="handleChangeUnit"
|
||||
>
|
||||
<view class="picker-view m-l-16">
|
||||
{{ (usageData.unit && usageData.unit.name) || '请选择' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="modal-footer">
|
||||
<u-button
|
||||
@click="handleClose"
|
||||
:custom-style="{ backgroundColor: '#f5f5f5', color: '#333' }"
|
||||
>取消</u-button>
|
||||
<u-button
|
||||
@click="handleConfirm"
|
||||
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff' }"
|
||||
>确定</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDrugUseList, getDrugUseWithDefault } from '@/api/reception.js';
|
||||
|
||||
export default {
|
||||
name: 'ReceptionWesternMedicineUsageModal',
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
drug: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
usageData: {
|
||||
select_number: 1,
|
||||
number: 1,
|
||||
type_id: 0,
|
||||
frequency_id: 0,
|
||||
time_id: 0,
|
||||
unit_id: 0,
|
||||
use_type: null,
|
||||
use_frequency: null,
|
||||
use_num: null,
|
||||
unit: null
|
||||
},
|
||||
drugUseList: {
|
||||
drug_use_type: [],
|
||||
drug_use_frequency: [],
|
||||
drug_time: [],
|
||||
drug_unit: []
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
if (newVal && this.drug) {
|
||||
// 打开时直接加载列表和默认值,避免默认值被初始化覆盖
|
||||
this.loadDrugUseList();
|
||||
}
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initUsageData() {
|
||||
if (this.drug) {
|
||||
this.usageData = {
|
||||
select_number: this.drug.select_number || this.drug.number || 1,
|
||||
number: this.drug.number || 1,
|
||||
type_id: this.drug.type_id || 0,
|
||||
frequency_id: this.drug.frequency_id || 0,
|
||||
time_id: this.drug.time_id || 0,
|
||||
unit_id: this.drug.unit_id || 0,
|
||||
use_type: this.drug.use_type || null,
|
||||
use_frequency: this.drug.use_frequency || null,
|
||||
use_num: this.drug.use_num || null,
|
||||
unit: this.drug.unit || null
|
||||
};
|
||||
}
|
||||
},
|
||||
async loadDrugUseList() {
|
||||
try {
|
||||
const drugId = this.drug && (this.drug.drug_id || this.drug.id);
|
||||
let payload = null;
|
||||
|
||||
if (drugId) {
|
||||
const res = await getDrugUseWithDefault(drugId);
|
||||
if (res) {
|
||||
payload = res.data || res.result || res;
|
||||
}
|
||||
} else {
|
||||
const res = await getDrugUseList();
|
||||
if (res) {
|
||||
payload = res.data || res.result || res;
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
this.drugUseList = { drug_use_type: [], drug_use_frequency: [], drug_time: [], drug_unit: [] };
|
||||
this.$toast('暂无用法用量配置,请联系管理员');
|
||||
return;
|
||||
}
|
||||
|
||||
const lists = payload.lists || payload;
|
||||
const def = payload.default || {};
|
||||
this.drugUseList = lists;
|
||||
|
||||
if (!this.usageData.type_id && def.type_id && lists.drug_use_type) {
|
||||
this.usageData.type_id = def.type_id;
|
||||
this.usageData.use_type = (lists.drug_use_type || []).find(i => i.id === def.type_id) || null;
|
||||
}
|
||||
if (!this.usageData.frequency_id && def.frequency_id && lists.drug_use_frequency) {
|
||||
this.usageData.frequency_id = def.frequency_id;
|
||||
this.usageData.use_frequency = (lists.drug_use_frequency || []).find(i => i.id === def.frequency_id) || null;
|
||||
}
|
||||
if (!this.usageData.time_id && def.time_id && lists.drug_time) {
|
||||
this.usageData.time_id = def.time_id;
|
||||
this.usageData.use_num = (lists.drug_time || []).find(i => i.id === def.time_id) || null;
|
||||
}
|
||||
if (!this.usageData.unit_id && def.unit_id && lists.drug_unit) {
|
||||
this.usageData.unit_id = def.unit_id;
|
||||
this.usageData.unit = (lists.drug_unit || []).find(i => i.id === def.unit_id) || null;
|
||||
}
|
||||
|
||||
const hasOptions =
|
||||
(lists.drug_use_type || []).length ||
|
||||
(lists.drug_use_frequency || []).length ||
|
||||
(lists.drug_time || []).length ||
|
||||
(lists.drug_unit || []).length;
|
||||
if (!hasOptions) {
|
||||
this.$toast('暂无用法用量配置,请联系管理员');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取药品使用方式列表失败:', error);
|
||||
this.$toast('获取用法用量失败');
|
||||
}
|
||||
},
|
||||
handleChangeUsageType(e) {
|
||||
const index = parseInt(e.detail.value);
|
||||
const item = this.drugUseList.drug_use_type[index];
|
||||
if (item) {
|
||||
this.usageData.type_id = item.id;
|
||||
this.usageData.use_type = item;
|
||||
}
|
||||
},
|
||||
handleChangeFrequency(e) {
|
||||
const index = parseInt(e.detail.value);
|
||||
const item = this.drugUseList.drug_use_frequency[index];
|
||||
if (item) {
|
||||
this.usageData.frequency_id = item.id;
|
||||
this.usageData.use_frequency = item;
|
||||
}
|
||||
},
|
||||
handleChangeTime(e) {
|
||||
const index = parseInt(e.detail.value);
|
||||
const item = this.drugUseList.drug_time[index];
|
||||
if (item) {
|
||||
this.usageData.time_id = item.id;
|
||||
this.usageData.use_num = item;
|
||||
}
|
||||
},
|
||||
handleChangeUnit(e) {
|
||||
const index = parseInt(e.detail.value);
|
||||
const item = this.drugUseList.drug_unit[index];
|
||||
if (item) {
|
||||
this.usageData.unit_id = item.id;
|
||||
this.usageData.unit = item;
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
this.show = false;
|
||||
},
|
||||
handleConfirm() {
|
||||
this.$emit('confirm', this.usageData);
|
||||
this.handleClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.western-usage-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.drug-info {
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
d-text + d-text {
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.usage-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 24rpx;
|
||||
|
||||
.item-label {
|
||||
width: 160rpx;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
min-width: 160rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 9999rpx;
|
||||
background-color: #f5f5f5;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
.u-button {
|
||||
flex: 1;
|
||||
margin: 0 8rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,168 +0,0 @@
|
||||
<template>
|
||||
<view class="reception-list-container">
|
||||
<u-navbar class="navbar" :is-back="true" title="转接方" title-color="#000"></u-navbar>
|
||||
|
||||
<view class="list-content">
|
||||
<!-- 患者列表 -->
|
||||
<view class="patient-list" v-if="patientList.length > 0">
|
||||
<view
|
||||
class="patient-item"
|
||||
v-for="(patient, index) in patientList"
|
||||
:key="patient.id"
|
||||
:class="{ 'active': activePatientId === patient.id }"
|
||||
@click="selectPatient(patient)"
|
||||
>
|
||||
<view class="patient-info">
|
||||
<text class="patient-name">{{ patient.name || '未知患者' }}</text>
|
||||
<text class="patient-mobile">{{ patient.mobile || '' }}</text>
|
||||
</view>
|
||||
<view class="patient-status">
|
||||
<u-tag
|
||||
:text="getStatusText(patient.status)"
|
||||
:type="getStatusType(patient.status)"
|
||||
size="mini"
|
||||
></u-tag>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无转接方患者"></d-empty>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getPatientList } from '@/api/reception.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
patientList: [],
|
||||
activePatientId: null,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadPatientList();
|
||||
},
|
||||
onShow() {
|
||||
// 每次显示时刷新列表
|
||||
this.loadPatientList();
|
||||
},
|
||||
methods: {
|
||||
// 加载患者列表
|
||||
async loadPatientList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await getPatientList({ type: 1 });
|
||||
this.patientList = res || [];
|
||||
|
||||
// 恢复之前选中的患者
|
||||
const savedId = uni.getStorageSync('doctorReceptionWx-id');
|
||||
if (savedId) {
|
||||
const patient = this.patientList.find(p => p.id === parseInt(savedId));
|
||||
if (patient) {
|
||||
this.selectPatient(patient);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载患者列表失败:', error);
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 选择患者
|
||||
selectPatient(patient) {
|
||||
this.activePatientId = patient.id;
|
||||
// 保存当前选中的患者ID
|
||||
uni.setStorageSync('doctorReceptionWx-id', patient.id.toString());
|
||||
|
||||
// 跳转到聊天室
|
||||
uni.navigateTo({
|
||||
url: `/subPackages/sub_reception/reception_chat?register_id=${patient.id}&room_id=${patient.room_id || ''}&patient_name=${patient.name || ''}`
|
||||
});
|
||||
},
|
||||
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
0: '待接诊',
|
||||
1: '问诊中',
|
||||
2: '已结束'
|
||||
};
|
||||
return statusMap[status] || '未知';
|
||||
},
|
||||
|
||||
// 获取状态类型
|
||||
getStatusType(status) {
|
||||
const typeMap = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
2: 'info'
|
||||
};
|
||||
return typeMap[status] || 'info';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.reception-list-container {
|
||||
background-color: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.patient-list {
|
||||
.patient-item {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&.active {
|
||||
border: 2rpx solid #6ACDBB;
|
||||
}
|
||||
|
||||
.patient-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.patient-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.patient-mobile {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.patient-status {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,967 +0,0 @@
|
||||
<template>
|
||||
<view class="safe-area-inset-bottom">
|
||||
<u-navbar class="navbar" :is-back="true" title="转接方开方" :custom-back="customBack" title-color="#000">
|
||||
</u-navbar>
|
||||
|
||||
<!-- 患者信息 -->
|
||||
<view class="patient-info white p-32 m-t-2" v-if="patientInfo">
|
||||
<view class="flex-row flex-jus-sp flex-ali-center">
|
||||
<d-text :text="patientInfo.name || '--'" className="fs-4 content-c" :bold="true"></d-text>
|
||||
<d-text :text="patientInfo.mobile || '--'" className="fs-3 tips-c"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类切换 -->
|
||||
<view class="tabs flex p-32 flex-ali-center flex-jus-sp">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
:plain="true"
|
||||
:type="activeCategory==1?'success':'default'"
|
||||
:custom-style="tabsBtnStyle"
|
||||
@click="switchCategory(1)"
|
||||
:hairline="true">
|
||||
中药
|
||||
</u-button>
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
:plain="true"
|
||||
:type="activeCategory==2?'success':'default'"
|
||||
:custom-style="tabsBtnStyle"
|
||||
@click="switchCategory(2)"
|
||||
:hairline="true">
|
||||
西药
|
||||
</u-button>
|
||||
</view>
|
||||
|
||||
<!-- 临床诊断 -->
|
||||
<view class="top white p-32 flex-col m-t-2">
|
||||
<view class="top_title flex-row flex-ali-center">
|
||||
<image :src="require('@/static/image/js.png')" class="size-32"></image>
|
||||
<d-text text="诊断" className="light-c fs-32 m-l-16"></d-text>
|
||||
</view>
|
||||
<view class="m-t-16 flex flex-wrap">
|
||||
<view class="m-r-2 m-t-16" v-for="(item,i) in diagnoses" :key="item.id ? item.id : `diag_${i}`">
|
||||
<u-tag
|
||||
shape="circle"
|
||||
:text="item.name"
|
||||
bg-color="#6ACDBB"
|
||||
color="#fff"
|
||||
close-color="#fff"
|
||||
:closeable="true"
|
||||
@close="removeDiagnosis(i)" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="m-t-24">
|
||||
<u-input
|
||||
placeholder="请输入疾病"
|
||||
:custom-style="illnessStyle"
|
||||
:value="diagnosisText"
|
||||
@click="openDiagnosisModal" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 药品列表 -->
|
||||
<scroll-view :style="scrollStyle" scroll-y="true">
|
||||
<!-- 西药 -->
|
||||
<view class="m-t-2 b-r-8 white p-32 flex-col" v-if="activeCategory==2">
|
||||
<view class="flex-row flex-jus-sp flex-ali-center">
|
||||
<d-text text="R:" className="main-c fs-4" :bold="true"></d-text>
|
||||
<view class="flex-row m-l-163" style="height: 50rpx;">
|
||||
<view class="m-l-16">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
shape="circle"
|
||||
size="mini"
|
||||
:custom-style="{ backgroundColor:'#6ACDBB',color:'#fff' }"
|
||||
@click="goAddDrug">
|
||||
添加商品
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bottom-border p-col-32" v-for="(item,index) in currentDrugs" :key="index">
|
||||
<view class="drug-item-content flex-row">
|
||||
<image
|
||||
v-if="item.image"
|
||||
:src="item.image"
|
||||
class="drug-image"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="drug-info-wrapper flex-1">
|
||||
<view class="flex-row flex-jus-sp flex-ali-center">
|
||||
<d-text :text="`${index+1}、${item.drug_name}`" className="fs-3 content-c"></d-text>
|
||||
<d-text :text="`¥${parseFloat(item.price || 0).toFixed(2)}`" className="error-c fs-3"></d-text>
|
||||
</view>
|
||||
<view class="flex-row flex-ali-center flex-jus-sp m-t-16">
|
||||
<view class="flex-row flex-ali-center m-t-16">
|
||||
<d-text text="规格:" color="#6C7380"></d-text>
|
||||
<d-text :text="item.specification || '--'" color="#6C7380"></d-text>
|
||||
</view>
|
||||
<view class="flex-row flex-ali-center m-t-16">
|
||||
<d-text :text="(item.select_number || item.number) + '盒'" color="#6C7380"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-row flex-ali-center">
|
||||
<d-text text="用法:" color="#6C7380"></d-text>
|
||||
<d-text :text="item.usage || '未设置用法用量'" color="#6C7380"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="m-t-24 flex-row flex-jus-sa btnBox">
|
||||
<view @click="changeInfo(item, index)" class="btn left">调整用量</view>
|
||||
<view class="btn right" @click="removeDrug(index)">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-row flex-jus-center" v-if="currentDrugs.length==0">
|
||||
<d-empty @click="goAddDrug"></d-empty>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中药 -->
|
||||
<view class="white b-r-8 p-32 p-b-0 m-t-2" v-if="activeCategory==1">
|
||||
<view class="flex-row flex-jus-sp flex-ali-center">
|
||||
<view class="flex-row flex-ali-end">
|
||||
<d-text text="R" className="main-c fs-4" :bold="true"></d-text>
|
||||
<d-text text=":" className="main-c fs-4" :bold="true"></d-text>
|
||||
</view>
|
||||
<view class="flex-row" style="height: 50rpx;">
|
||||
<view class="m-l-16">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
:custom-style="{ backgroundColor:'#6ACDBB',color:'#fff' }"
|
||||
shape="circle"
|
||||
size="mini"
|
||||
@click="goAddDrug">
|
||||
添加商品
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="p-col-32" v-if="currentDrugs.length>0">
|
||||
<view class="label">药材:</view>
|
||||
<view class="mBox">
|
||||
<view class="grid-tem-col-2 rp">
|
||||
<view class="" v-for="(it) in currentDrugs" :key="it.id">
|
||||
<view class="flex-row flex-jus-sp flex-ali-center m-t-12 delBox">
|
||||
<d-text :text="it.drug_name||it.name" className="fs-3 content-c"></d-text>
|
||||
<d-text :text="String(it.number)+(it.unit?it.unit.name:'g')" className="fs-3 tips-c"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="m-t-24 flex-row flex-jus-sa btnBox">
|
||||
<view @click="goAddDrug" class="btn left">调整用量</view>
|
||||
<view class="btn right" @click="removeAllDrugs">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-row flex-jus-center" v-else>
|
||||
<d-empty @click="goAddDrug"></d-empty>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 医嘱 -->
|
||||
<view class="flex-col b-r-8 white p-32 m-t-2">
|
||||
<view class="flex-jus-sp flex-row flex-ali-center">
|
||||
<d-text text="医嘱" color="#6C7380"></d-text>
|
||||
<d-text @click="openDoctorOrderModal" text="常用医嘱" color="#6ACDBB"></d-text>
|
||||
</view>
|
||||
<u-input
|
||||
v-model="entrust"
|
||||
placeholder="请输入"
|
||||
:custom-style="entrustStyle" />
|
||||
</view>
|
||||
|
||||
<!-- 诊疗费 -->
|
||||
<view class="flex-row p-2 flex-jus-end m-t-2">
|
||||
<view class="flex-row flex-ali-center" style="margin-right: 20rpx">
|
||||
<text>诊疗费:</text>
|
||||
<u-input
|
||||
v-model="treatement_price"
|
||||
placeholder="请输入"
|
||||
:custom-style="entrustStyle2"
|
||||
style="width: 200rpx" />
|
||||
</view>
|
||||
<view class="flex-row flex-ali-center">
|
||||
<d-text text="商品计费:" className="m-r-16 tips-c"></d-text>
|
||||
<d-text :text="`¥${totalProductCost.toFixed(2)}`" color="#F44336" :bold="true"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 总价显示 -->
|
||||
<view class="flex-row p-2 flex-jus-end m-t-2" v-if="currentDrugs.length > 0">
|
||||
<view class="flex-row flex-ali-center">
|
||||
<d-text text="总价:" className="m-r-16 tips-c fs-32"></d-text>
|
||||
<d-text :text="`¥${getSum}`" color="#F44336" :bold="true" className="fs-36"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<view class="ac m-t-2 p-32">
|
||||
<d-text text="请确认患者已在实体医院就诊,并有明确诊断" className="tips-c fs-24"></d-text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部按钮(另存常用方未对接保存接口,不展示) -->
|
||||
<view class="bottom flex-jus-sp flex-ali-center safe-area-inset-bottom">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
@click="sendPrescription"
|
||||
shape="circle"
|
||||
:custom-style="{backgroundColor:buttonLodging?'#A5DCD2':'#6ACDBB',color:'#fff',height:'86rpx', width: '686rpx'}"
|
||||
:disabled="buttonLodging">
|
||||
发送处方
|
||||
</u-button>
|
||||
</view>
|
||||
|
||||
<!-- 常用诊断弹窗 -->
|
||||
<DiagnosisModal
|
||||
v-model="showDiagnosisModal"
|
||||
:selectedDiagnosis="diagnosisText"
|
||||
@confirm="handleDiagnosisConfirm"
|
||||
/>
|
||||
|
||||
<!-- 常用医嘱弹窗 -->
|
||||
<DoctorOrderModal
|
||||
v-model="showDoctorOrderModal"
|
||||
:selectedDoctorOrder="entrust"
|
||||
@confirm="handleDoctorOrderConfirm"
|
||||
/>
|
||||
|
||||
<!-- 西药用法用量设置弹窗 -->
|
||||
<WesternMedicineUsageModal
|
||||
v-model="showWesternUsageModal"
|
||||
:drug="editingDrug"
|
||||
@confirm="handleConfirmWesternUsage"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { req } from '@/common/js/index.js';
|
||||
import {
|
||||
addWestPrescription,
|
||||
checkChineseMedicineConflictApi,
|
||||
getDiseaseList,
|
||||
getDrugUseList,
|
||||
getProductListDoctorReception,
|
||||
getPatientItem
|
||||
} from '@/api/reception.js';
|
||||
import DiagnosisModal from './components/DiagnosisModal.vue';
|
||||
import DoctorOrderModal from './components/DoctorOrderModal.vue';
|
||||
import WesternMedicineUsageModal from './components/WesternMedicineUsageModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DiagnosisModal,
|
||||
DoctorOrderModal,
|
||||
WesternMedicineUsageModal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
registerId: '',
|
||||
patientId: '',
|
||||
patientInfo: null,
|
||||
activeCategory: 2, // 1=中药,2=西药
|
||||
diagnoses: [],
|
||||
diagnosisText: '', // 诊断文本(用于显示)
|
||||
currentDrugs: [],
|
||||
entrust: '',
|
||||
treatement_price: '',
|
||||
buttonLodging: false,
|
||||
// 用法用量弹窗
|
||||
showWesternUsageModal: false,
|
||||
editingDrug: null,
|
||||
editingDrugIndex: -1,
|
||||
showDiagnosisModal: false,
|
||||
showDoctorOrderModal: false,
|
||||
// 中药相关字段
|
||||
ruleType: 1, // 1=包法,2=制剂
|
||||
dosage: 7, // 剂数
|
||||
dayDosage: 2, // 每日次数
|
||||
packageMethodId: null, // 包法ID
|
||||
processRuleId: null, // 加工规则ID
|
||||
processRuleNoteId: null, // 加工规则备注ID
|
||||
childProcessRuleId: null, // 子加工规则ID
|
||||
illnessStyle: {
|
||||
fontSize: '28rpx',
|
||||
backgroundColor: '#F3F4F5',
|
||||
borderRadius: '66rpx',
|
||||
height: '66rpx',
|
||||
padding: '8rpx 32rpx'
|
||||
},
|
||||
entrustStyle: {
|
||||
fontSize: '30rpx',
|
||||
backgroundColor: '#F3F4F5',
|
||||
height: '74rpx',
|
||||
padding: '16rpx',
|
||||
borderRadius: '8rpx',
|
||||
marginTop: '16rpx'
|
||||
},
|
||||
entrustStyle2: {
|
||||
fontSize: '30rpx',
|
||||
backgroundColor: '#F3F4F5',
|
||||
height: '52rpx',
|
||||
padding: '0 16rpx',
|
||||
borderRadius: '8rpx',
|
||||
},
|
||||
tabsBtnStyle: {
|
||||
padding: '8rpx 16px',
|
||||
height: '58rpx',
|
||||
fontSize: '30rpx',
|
||||
width: '212rpx'
|
||||
},
|
||||
scrollStyle: {
|
||||
height: 'calc(100vh - 600rpx)',
|
||||
marginBottom: '200rpx'
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 商品总价(参考PC端计算逻辑)
|
||||
totalProductCost() {
|
||||
if (this.currentDrugs.length === 0) return 0;
|
||||
// 中药:药品价格 * 用量 * 剂数
|
||||
if (this.activeCategory === 1) {
|
||||
return this.currentDrugs.reduce((sum, drug) => {
|
||||
return sum + (parseFloat(drug.price || 0) * parseFloat(drug.number || 1) * parseFloat(this.dosage || 1));
|
||||
}, 0);
|
||||
}
|
||||
// 西药:药品价格 * 数量
|
||||
return this.currentDrugs.reduce((sum, drug) => {
|
||||
return sum + (parseFloat(drug.price || 0) * parseInt(drug.select_number || drug.number || 1));
|
||||
}, 0);
|
||||
},
|
||||
// 加工费(仅中药,参考PC端计算逻辑)
|
||||
processingFee() {
|
||||
if (this.activeCategory !== 1 || this.ruleType === 1) return 0;
|
||||
// TODO: 根据加工规则计算加工费
|
||||
return 0;
|
||||
},
|
||||
// 总价
|
||||
getSum() {
|
||||
return (this.totalProductCost + this.processingFee + parseFloat(this.treatement_price || 0)).toFixed(2);
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.registerId = options.register_id || '';
|
||||
this.patientId = options.patient_id || '';
|
||||
|
||||
// 从本地存储恢复数据
|
||||
this.restoreFromLocalStorage();
|
||||
|
||||
// 获取患者信息
|
||||
this.getPatientInfo();
|
||||
},
|
||||
onShow() {
|
||||
// 从本地存储恢复当前分类的药品数据
|
||||
this.getCurrentDrugs();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 获取存储key(完全参考PC端结构,添加doctorReceptionWx-前缀)
|
||||
*/
|
||||
getStorageKey(category) {
|
||||
return `doctorReceptionWx-prescriptionData_${category}_${this.patientId}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 从本地存储恢复数据
|
||||
*/
|
||||
restoreFromLocalStorage() {
|
||||
// 恢复当前选中的患者ID
|
||||
const savedPatientId = uni.getStorageSync('doctorReceptionWx-id');
|
||||
if (savedPatientId) {
|
||||
this.patientId = savedPatientId;
|
||||
}
|
||||
|
||||
// 恢复当前激活的分类
|
||||
const savedCategory = uni.getStorageSync(`doctorReceptionWx-activeCategory${this.patientId}`);
|
||||
if (savedCategory) {
|
||||
this.activeCategory = parseInt(savedCategory);
|
||||
} else {
|
||||
// 检查哪个分类有数据
|
||||
const chineseData = uni.getStorageSync(this.getStorageKey(1));
|
||||
const westData = uni.getStorageSync(this.getStorageKey(2));
|
||||
if (chineseData && JSON.parse(chineseData).length > 0) {
|
||||
this.activeCategory = 1;
|
||||
} else if (westData && JSON.parse(westData).length > 0) {
|
||||
this.activeCategory = 2;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前药品数据
|
||||
*/
|
||||
getCurrentDrugs() {
|
||||
const key = this.getStorageKey(this.activeCategory);
|
||||
const data = uni.getStorageSync(key);
|
||||
this.currentDrugs = data ? JSON.parse(data) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存到本地存储
|
||||
*/
|
||||
saveToLocalStorage() {
|
||||
const key = this.getStorageKey(this.activeCategory);
|
||||
uni.setStorageSync(key, JSON.stringify(this.currentDrugs));
|
||||
// 保存当前激活的分类
|
||||
uni.setStorageSync(`doctorReceptionWx-activeCategory${this.patientId}`, this.activeCategory.toString());
|
||||
// 保存当前患者ID
|
||||
uni.setStorageSync('doctorReceptionWx-id', this.patientId);
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换分类
|
||||
*/
|
||||
switchCategory(category) {
|
||||
this.activeCategory = category;
|
||||
this.getCurrentDrugs();
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取患者信息
|
||||
*/
|
||||
async getPatientInfo() {
|
||||
if (!this.patientId) return;
|
||||
try {
|
||||
const res = await getPatientItem(this.patientId);
|
||||
this.patientInfo = res;
|
||||
if (res && res.user_patient) {
|
||||
this.patientInfo = {
|
||||
name: res.user_patient.name,
|
||||
mobile: res.user_patient.mobile
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取患者信息失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开诊断弹窗
|
||||
*/
|
||||
openDiagnosisModal() {
|
||||
this.showDiagnosisModal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理诊断确认
|
||||
*/
|
||||
handleDiagnosisConfirm(diagnosisText) {
|
||||
this.diagnosisText = diagnosisText;
|
||||
// 将诊断文本转换为诊断数组
|
||||
if (diagnosisText) {
|
||||
const diagnosisNames = diagnosisText.split(',').filter(Boolean);
|
||||
this.diagnoses = diagnosisNames.map((name, index) => ({
|
||||
id: `temp_${index}`,
|
||||
name: name
|
||||
}));
|
||||
} else {
|
||||
this.diagnoses = [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开医嘱弹窗
|
||||
*/
|
||||
openDoctorOrderModal() {
|
||||
this.showDoctorOrderModal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理医嘱确认
|
||||
*/
|
||||
handleDoctorOrderConfirm(doctorOrderText) {
|
||||
this.entrust = doctorOrderText;
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除诊断
|
||||
*/
|
||||
removeDiagnosis(index) {
|
||||
this.diagnoses.splice(index, 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* 去添加药品
|
||||
*/
|
||||
goAddDrug() {
|
||||
uni.navigateTo({
|
||||
url: `/subPackages/sub_reception/add_drug?category=${this.activeCategory}&patient_id=${this.patientId}®ister_id=${this.registerId}`
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 调整用量
|
||||
*/
|
||||
changeInfo(item, index) {
|
||||
// 仅西药支持调整用量
|
||||
if (this.activeCategory !== 2) {
|
||||
this.$toast('当前仅支持西药用法用量设置');
|
||||
return;
|
||||
}
|
||||
this.editingDrug = JSON.parse(JSON.stringify(item));
|
||||
this.editingDrugIndex = index;
|
||||
this.showWesternUsageModal = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认西药用法用量(接诊页)
|
||||
* @param {Object} usageData - 用法用量数据
|
||||
*/
|
||||
handleConfirmWesternUsage(usageData) {
|
||||
if (this.editingDrugIndex >= 0) {
|
||||
const drug = this.currentDrugs[this.editingDrugIndex];
|
||||
drug.time_id = usageData.time_id;
|
||||
drug.type_id = usageData.type_id;
|
||||
drug.frequency_id = usageData.frequency_id;
|
||||
drug.unit_id = usageData.unit_id;
|
||||
drug.number = usageData.number;
|
||||
drug.select_number = usageData.select_number || drug.select_number || 1;
|
||||
drug.use_num = usageData.use_num;
|
||||
drug.use_type = usageData.use_type;
|
||||
drug.use_frequency = usageData.use_frequency;
|
||||
drug.unit = usageData.unit;
|
||||
// 同步显示字段
|
||||
if (usageData.use_type && usageData.use_num && usageData.unit) {
|
||||
drug.usage = `${usageData.use_type.name},${usageData.use_num.name},每次${usageData.number}${usageData.unit.name}`;
|
||||
}
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
this.editingDrug = null;
|
||||
this.editingDrugIndex = -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除药品
|
||||
*/
|
||||
removeDrug(index) {
|
||||
uni.showModal({
|
||||
title: '删除药品',
|
||||
content: '是否要删除?',
|
||||
success: ({ confirm }) => {
|
||||
if (confirm) {
|
||||
this.currentDrugs.splice(index, 1);
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除所有药品(中药)
|
||||
*/
|
||||
removeAllDrugs() {
|
||||
uni.showModal({
|
||||
title: '删除药品',
|
||||
content: '是否要删除所有药品?',
|
||||
success: ({ confirm }) => {
|
||||
if (confirm) {
|
||||
this.currentDrugs = [];
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 发送处方
|
||||
*/
|
||||
async sendPrescription() {
|
||||
if (this.buttonLodging) {
|
||||
this.$toast('正在处理中,请勿重复提交');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证
|
||||
if (this.currentDrugs.length === 0) {
|
||||
this.$toast('请添加药品');
|
||||
return;
|
||||
}
|
||||
if (this.diagnoses.length === 0) {
|
||||
this.$toast('请添加诊断');
|
||||
return;
|
||||
}
|
||||
if (!this.entrust) {
|
||||
this.$toast('请输入医嘱');
|
||||
return;
|
||||
}
|
||||
|
||||
this.buttonLodging = true;
|
||||
|
||||
try {
|
||||
// 如果是中药,先检查相冲
|
||||
if (this.activeCategory === 1) {
|
||||
const names = this.currentDrugs.map((item) => item.drug_name || item.name);
|
||||
const checkRes = await checkChineseMedicineConflictApi({ names: names });
|
||||
if (checkRes && checkRes.is_exist === true) {
|
||||
uni.showModal({
|
||||
title: '药物相冲提示',
|
||||
content: checkRes.message || '检测到该处方内具有药物相冲,是否继续开方?',
|
||||
showCancel: true,
|
||||
success: ({ confirm }) => {
|
||||
if (confirm) {
|
||||
this.doAddWestPrescription(1);
|
||||
} else {
|
||||
this.buttonLodging = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送处方
|
||||
await this.doAddWestPrescription(0);
|
||||
} catch (e) {
|
||||
console.error('发送处方失败:', e);
|
||||
this.$toast('发送处方失败,请重试');
|
||||
this.buttonLodging = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行发送处方(参考PC端参数结构)
|
||||
*/
|
||||
async doAddWestPrescription(doctorSecondSign = 0) {
|
||||
const clinical_diagnose = this.diagnosisText || this.diagnoses.map((item) => item.name).join(',');
|
||||
|
||||
// 发送前参数校验(仅西药相关)
|
||||
if (this.activeCategory === 2) {
|
||||
if (!this.patientInfo || !this.patientId) {
|
||||
this.$toast('患者信息缺失,无法发送处方');
|
||||
return;
|
||||
}
|
||||
const invalidDrug = this.currentDrugs.find((drug) => {
|
||||
return !(
|
||||
drug.id &&
|
||||
(drug.select_number || drug.number) &&
|
||||
drug.time_id &&
|
||||
drug.type_id &&
|
||||
drug.frequency_id &&
|
||||
drug.unit_id &&
|
||||
drug.use_type && drug.use_type.name &&
|
||||
drug.use_num && drug.use_num.name &&
|
||||
drug.unit && drug.unit.name
|
||||
);
|
||||
});
|
||||
if (invalidDrug) {
|
||||
this.$toast('请先为西药设置完整的用法用量');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 参考PC端的参数结构
|
||||
const params = {
|
||||
patient: this.patientInfo ? { id: this.patientId, ...this.patientInfo } : { id: this.patientId },
|
||||
drugs: this.currentDrugs,
|
||||
diagnosis: clinical_diagnose,
|
||||
medicalAdvice: this.entrust,
|
||||
total: parseFloat(this.getSum),
|
||||
category: 1, // 1-自费,2-医保,TODO: 从配置获取
|
||||
drug_type: 2, // TODO: 根据实际需求设置
|
||||
register_id: parseInt(this.registerId),
|
||||
treatment_price: parseFloat(this.treatement_price || 0),
|
||||
prescription_type: this.activeCategory, // 1=中药,2=西药
|
||||
doctor_second_sign: doctorSecondSign,
|
||||
// 中药特有字段
|
||||
package_method_id: this.activeCategory === 1 ? (this.packageMethodId || null) : null,
|
||||
process_rule_id: this.activeCategory === 1 ? (this.processRuleId || null) : null,
|
||||
process_rule_note_id: this.activeCategory === 1 ? (this.processRuleNoteId || null) : null,
|
||||
child_process_rule_id: this.activeCategory === 1 ? (this.childProcessRuleId || null) : null,
|
||||
process_rule_type: this.activeCategory === 1 ? (this.ruleType || 1) : null,
|
||||
processing_fee: this.activeCategory === 1 ? this.processingFee : 0,
|
||||
dosage: this.activeCategory === 1 ? (this.dosage || 7) : null,
|
||||
day_dosage: this.activeCategory === 1 ? (this.dayDosage || 2) : null
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await addWestPrescription(params);
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
uni.showToast({ title: '发送成功', icon: 'success' });
|
||||
|
||||
// 清空当前数据
|
||||
this.currentDrugs = [];
|
||||
this.diagnoses = [];
|
||||
this.diagnosisText = '';
|
||||
this.entrust = '';
|
||||
this.treatement_price = '';
|
||||
// 清空本地存储
|
||||
uni.removeStorageSync(this.getStorageKey(1));
|
||||
uni.removeStorageSync(this.getStorageKey(2));
|
||||
uni.removeStorageSync(`doctorReceptionWx-activeCategory${this.patientId}`);
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} else {
|
||||
uni.showToast({ title: res.msg || res.message || '发送失败', icon: 'none' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('发送处方失败:', e);
|
||||
uni.showToast({ title: '发送处方失败,请重试', icon: 'none' });
|
||||
} finally {
|
||||
this.buttonLodging = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 返回
|
||||
*/
|
||||
customBack() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.patient-info {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.top {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.top_title {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.size-32 {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.m-t-2 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.p-32 {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.m-t-16 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-t-24 {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.m-r-2 {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-jus-sp {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-ali-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.white {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.b-r-8 {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.bottom-border {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 32rpx 0;
|
||||
}
|
||||
|
||||
.p-col-32 {
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.btnBox {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 8rpx;
|
||||
border: 1px solid #6ACDBB;
|
||||
color: #6ACDBB;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn.left {
|
||||
border-color: #6ACDBB;
|
||||
color: #6ACDBB;
|
||||
}
|
||||
|
||||
.btn.right {
|
||||
border-color: #FC3636;
|
||||
color: #FC3636;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.plr20 {
|
||||
padding: 0 40rpx;
|
||||
}
|
||||
|
||||
.listBox {
|
||||
padding: 32rpx;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.btnBox2 {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.commentModal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.mainTop {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.mainTop .l,
|
||||
.mainTop .r {
|
||||
color: #6ACDBB;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.mainTop .c {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 32rpx;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 24rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.item.act {
|
||||
background: #f0f9ff;
|
||||
color: #6ACDBB;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #6C7380;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.mBox {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.grid-tem-col-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.rp {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.delBox {
|
||||
padding: 16rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -38,7 +38,7 @@
|
||||
<view
|
||||
class="medicine-card"
|
||||
v-for="(item, index) in drugList"
|
||||
:key="item.id || index"
|
||||
:key="index"
|
||||
:class="{ 'is-selected': isInSelected(item) }"
|
||||
>
|
||||
<view class="drug-header flex-row flex-jus-sp flex-ali-center m-b-16">
|
||||
@@ -58,8 +58,8 @@
|
||||
<view class="common-quantity-tags m-b-24" v-if="commonQuantities.length > 0">
|
||||
<view
|
||||
class="modern-tag"
|
||||
v-for="qty in commonQuantities"
|
||||
:key="qty"
|
||||
v-for="(qty, qix) in commonQuantities"
|
||||
:key="qix"
|
||||
@click.stop="handleSelectCommonQuantity"
|
||||
:data-index="index"
|
||||
:data-quantity="qty"
|
||||
|
||||
@@ -15,46 +15,50 @@
|
||||
</view>
|
||||
|
||||
<view class="modal-content">
|
||||
<!-- 常用方列表 -->
|
||||
<view class="prescription-list" v-if="prescriptionList.length > 0">
|
||||
<view
|
||||
class="prescription-item white b-r-8 m-t-2"
|
||||
v-for="(item, index) in prescriptionList"
|
||||
:key="item.id"
|
||||
@click="handleSelectPrescription(item, index)"
|
||||
>
|
||||
<view class="item-header flex-row flex-jus-sp flex-ali-center">
|
||||
<view class="flex-row flex-ali-center">
|
||||
<d-text text="名称:" className="fs-3 color9"></d-text>
|
||||
<d-text :text="item.name" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="tag" :class="tagClass">
|
||||
{{ tagText }}
|
||||
<scroll-view class="list-scroll" scroll-y>
|
||||
<view class="list-scroll-inner">
|
||||
<!-- 常用方列表 -->
|
||||
<view class="prescription-list" v-if="prescriptionList.length > 0">
|
||||
<view
|
||||
class="prescription-item white b-r-8 m-t-2"
|
||||
v-for="(item, index) in prescriptionList"
|
||||
:key="item.id"
|
||||
@click="handleSelectPrescription(item, index)"
|
||||
>
|
||||
<view class="item-header flex-row flex-jus-sp flex-ali-center">
|
||||
<view class="flex-row flex-ali-center">
|
||||
<d-text text="名称:" className="fs-3 color9"></d-text>
|
||||
<d-text :text="item.name" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="tag" :class="tagClass">
|
||||
{{ tagText }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-content m-t-16">
|
||||
<view class="label">药品:</view>
|
||||
<view class="drug-list">
|
||||
<text
|
||||
v-for="(drug, idx) in getDrugList(item, index)"
|
||||
:key="idx"
|
||||
class="drug-item"
|
||||
>
|
||||
{{ formatDrugName(drug) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-footer m-t-24 btnBox" v-if="!isSelectMode">
|
||||
<view @click.stop="handleDeletePrescription(item, index)" class="btn left">移除</view>
|
||||
<view @click.stop="handleSelectPrescription(item, index)" class="btn right">使用</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-content m-t-16">
|
||||
<view class="label">药品:</view>
|
||||
<view class="drug-list">
|
||||
<text
|
||||
v-for="(drug, idx) in getDrugList(item, index)"
|
||||
:key="idx"
|
||||
class="drug-item"
|
||||
>
|
||||
{{ formatDrugName(drug) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-footer m-t-24 btnBox" v-if="!isSelectMode">
|
||||
<view @click.stop="handleDeletePrescription(item, index)" class="btn left">移除</view>
|
||||
<view @click.stop="handleSelectPrescription(item, index)" class="btn right">使用</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无常用方"></d-empty>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无常用方"></d-empty>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,41 +30,41 @@
|
||||
</u-input>
|
||||
</view>
|
||||
|
||||
<view class="scroll-container">
|
||||
<!-- 常用诊断列表 -->
|
||||
<view class="section white radius-16 p-32 m-b-24 shadow-sm" v-if="doctorMyDiseaseList.length > 0">
|
||||
<view class="section-header flex-row flex-jus-sp flex-ali-center m-b-24">
|
||||
<d-text text="常用诊断" className="fs-30 font-bold color-title"></d-text>
|
||||
<d-text :text="`${doctorMyDiseaseList.length}项`" className="fs-26 color-sub"></d-text>
|
||||
</view>
|
||||
<view class="modern-tag-container">
|
||||
<view
|
||||
v-for="item in doctorMyDiseaseList"
|
||||
:key="item.id"
|
||||
class="modern-tag-item"
|
||||
:class="{ 'is-selected': item.disease.isSelect === 1 }"
|
||||
@click="selectDiagnosis(item.disease)"
|
||||
>
|
||||
<text class="tag-text">{{ item.disease.name }}</text>
|
||||
<view class="tag-action" @click.stop="removeFromMyDiagnosis(item)">
|
||||
<u-icon name="close" size="20"></u-icon>
|
||||
<scroll-view
|
||||
class="scroll-container"
|
||||
scroll-y
|
||||
@scrolltolower="loadMore"
|
||||
:lower-threshold="100"
|
||||
>
|
||||
<view class="scroll-body-pad">
|
||||
<!-- 常用诊断列表 -->
|
||||
<view class="section white radius-16 p-32 m-b-24 shadow-sm" v-if="doctorMyDiseaseList.length > 0">
|
||||
<view class="section-header flex-row flex-jus-sp flex-ali-center m-b-24">
|
||||
<d-text text="常用诊断" className="fs-30 font-bold color-title"></d-text>
|
||||
<d-text :text="`${doctorMyDiseaseList.length}项`" className="fs-26 color-sub"></d-text>
|
||||
</view>
|
||||
<view class="modern-tag-container">
|
||||
<view
|
||||
v-for="item in doctorMyDiseaseList"
|
||||
:key="item.id"
|
||||
class="modern-tag-item"
|
||||
:class="{ 'is-selected': item.disease.isSelect === 1 }"
|
||||
@click="selectDiagnosis(item.disease)"
|
||||
>
|
||||
<text class="tag-text">{{ item.disease.name }}</text>
|
||||
<view class="tag-action" @click.stop="removeFromMyDiagnosis(item)">
|
||||
<u-icon name="close" size="20"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果列表 -->
|
||||
<view class="section white radius-16 p-32 shadow-sm" v-if="allDiagnosisList.length > 0">
|
||||
<view class="section-header flex-row flex-jus-sp flex-ali-center m-b-24">
|
||||
<d-text text="搜索结果" className="fs-30 font-bold color-title"></d-text>
|
||||
<d-text :text="`共 ${allDiagnosisList.length} 条`" className="fs-26 color-sub"></d-text>
|
||||
</view>
|
||||
<scroll-view
|
||||
class="tag-container-scroll"
|
||||
scroll-y
|
||||
@scrolltolower="loadMore"
|
||||
:lower-threshold="100"
|
||||
>
|
||||
<!-- 搜索结果列表 -->
|
||||
<view class="section white radius-16 p-32 shadow-sm" v-if="allDiagnosisList.length > 0">
|
||||
<view class="section-header flex-row flex-jus-sp flex-ali-center m-b-24">
|
||||
<d-text text="搜索结果" className="fs-30 font-bold color-title"></d-text>
|
||||
<d-text :text="`共 ${allDiagnosisList.length} 条`" className="fs-26 color-sub"></d-text>
|
||||
</view>
|
||||
<view class="modern-tag-container">
|
||||
<view
|
||||
v-for="item in allDiagnosisList"
|
||||
@@ -90,14 +90,14 @@
|
||||
<view class="load-more" v-else-if="!hasMore && allDiagnosisList.length > 0">
|
||||
<text class="load-more-text">没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="allDiagnosisList.length === 0 && searchKey">
|
||||
<d-empty text="暂无相关诊断"></d-empty>
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="allDiagnosisList.length === 0 && searchKey">
|
||||
<d-empty text="暂无相关诊断"></d-empty>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,50 +15,54 @@
|
||||
</view>
|
||||
|
||||
<view class="modal-content">
|
||||
<!-- 我的医嘱 -->
|
||||
<view class="section white radius-16 p-32 m-b-24 shadow-sm">
|
||||
<view class="section-header flex-row flex-jus-sp flex-ali-center m-b-24">
|
||||
<d-text text="我的医嘱" className="fs-30 font-bold color-title"></d-text>
|
||||
<view class="add-btn-modern" @click="openAddModal">
|
||||
<u-icon name="plus" size="24" color="#00A88A" class="m-r-8"></u-icon>
|
||||
<text class="add-text">添加医嘱</text>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="order-scroll" scroll-y>
|
||||
<view class="order-scroll-inner">
|
||||
<!-- 我的医嘱 -->
|
||||
<view class="section white radius-16 p-32 m-b-24 shadow-sm">
|
||||
<view class="section-header flex-row flex-jus-sp flex-ali-center m-b-24">
|
||||
<d-text text="我的医嘱" className="fs-30 font-bold color-title"></d-text>
|
||||
<view class="add-btn-modern" @click="openAddModal">
|
||||
<u-icon name="plus" size="24" color="#00A88A" class="m-r-8"></u-icon>
|
||||
<text class="add-text">添加医嘱</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的医嘱列表 -->
|
||||
<view class="modern-tag-container">
|
||||
<view
|
||||
v-for="item in doctorOrderMyList"
|
||||
:key="item.id"
|
||||
class="modern-tag-item"
|
||||
:class="{ 'is-selected': item.isSelect === 1 }"
|
||||
@click="selectDoctorOrder(item, 1)"
|
||||
>
|
||||
<text class="tag-text">{{ item.content }}</text>
|
||||
<view class="tag-action" @click.stop="deleteMyDoctorOrder(item)">
|
||||
<u-icon name="close" size="20"></u-icon>
|
||||
<!-- 我的医嘱列表 -->
|
||||
<view class="modern-tag-container">
|
||||
<view
|
||||
v-for="item in doctorOrderMyList"
|
||||
:key="item.id"
|
||||
class="modern-tag-item"
|
||||
:class="{ 'is-selected': item.isSelect === 1 }"
|
||||
@click="selectDoctorOrder(item, 1)"
|
||||
>
|
||||
<text class="tag-text">{{ item.content }}</text>
|
||||
<view class="tag-action" @click.stop="deleteMyDoctorOrder(item)">
|
||||
<u-icon name="close" size="20"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 公共医嘱 -->
|
||||
<view class="section white radius-16 p-32 shadow-sm" v-if="doctorOrderCommonList.length > 0">
|
||||
<view class="section-header m-b-24">
|
||||
<d-text text="系统公共医嘱" className="fs-30 font-bold color-title"></d-text>
|
||||
</view>
|
||||
<view class="modern-tag-container">
|
||||
<view
|
||||
v-for="item in doctorOrderCommonList"
|
||||
:key="item.id"
|
||||
class="modern-tag-item"
|
||||
:class="{ 'is-selected': item.isSelect === 1 }"
|
||||
@click="selectDoctorOrder(item, 2)"
|
||||
>
|
||||
<text class="tag-text">{{ item.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 公共医嘱 -->
|
||||
<view class="section white radius-16 p-32 shadow-sm" v-if="doctorOrderCommonList.length > 0">
|
||||
<view class="section-header m-b-24">
|
||||
<d-text text="系统公共医嘱" className="fs-30 font-bold color-title"></d-text>
|
||||
</view>
|
||||
<view class="modern-tag-container">
|
||||
<view
|
||||
v-for="item in doctorOrderCommonList"
|
||||
:key="item.id"
|
||||
class="modern-tag-item"
|
||||
:class="{ 'is-selected': item.isSelect === 1 }"
|
||||
@click="selectDoctorOrder(item, 2)"
|
||||
>
|
||||
<text class="tag-text">{{ item.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
@@ -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 {
|
||||
|
||||
@@ -27,43 +27,45 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 产品列表 -->
|
||||
<view class="product-list" v-if="productList.length > 0">
|
||||
<view
|
||||
class="medicine-card"
|
||||
v-for="item in productList"
|
||||
:key="item.id"
|
||||
>
|
||||
<view class="flex-row">
|
||||
<image
|
||||
v-if="(item.drug && item.drug.image) || item.image"
|
||||
:src="(item.drug && item.drug.image) || item.image"
|
||||
class="drug-image-modern"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="flex-1 m-l-24 flex-col">
|
||||
<view class="flex-row flex-jus-sp flex-ali-start">
|
||||
<text class="fs-30 font-bold color-title line-clamp-2 w-70">{{item.drug.drug_name || item.drug.name}}</text>
|
||||
<text class="fs-32 font-bold color-price">¥{{parseFloat(item.price || 0).toFixed(2)}}</text>
|
||||
</view>
|
||||
<view class="flex-row flex-jus-sp flex-ali-center m-t-12">
|
||||
<text class="fs-24 color-sub">规格:{{item.drug.specification || '--'}}</text>
|
||||
</view>
|
||||
<scroll-view class="list-scroll" scroll-y>
|
||||
<!-- 产品列表 -->
|
||||
<view class="product-list" v-if="productList.length > 0">
|
||||
<view
|
||||
class="medicine-card"
|
||||
v-for="item in productList"
|
||||
:key="item.id"
|
||||
>
|
||||
<view class="flex-row">
|
||||
<image
|
||||
v-if="(item.drug && item.drug.image) || item.image"
|
||||
:src="(item.drug && item.drug.image) || item.image"
|
||||
class="drug-image-modern"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="flex-1 m-l-24 flex-col">
|
||||
<view class="flex-row flex-jus-sp flex-ali-start">
|
||||
<text class="fs-30 font-bold color-title line-clamp-2 w-70">{{item.drug.drug_name || item.drug.name}}</text>
|
||||
<text class="fs-32 font-bold color-price">¥{{parseFloat(item.price || 0).toFixed(2)}}</text>
|
||||
</view>
|
||||
<view class="flex-row flex-jus-sp flex-ali-center m-t-12">
|
||||
<text class="fs-24 color-sub">规格:{{item.drug.specification || '--'}}</text>
|
||||
</view>
|
||||
|
||||
<view class="product-action m-t-24">
|
||||
<view class="action-btn-ghost primary" @click.stop="handleSelectProduct(item)">
|
||||
<u-icon name="plus" size="24" class="m-r-8"></u-icon> 选用此产品
|
||||
<view class="product-action m-t-24">
|
||||
<view class="action-btn-ghost primary" @click.stop="handleSelectProduct(item)">
|
||||
<u-icon name="plus" size="24" class="m-r-8"></u-icon> 选用此产品
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无产品"></d-empty>
|
||||
</view>
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无产品"></d-empty>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<u-popup
|
||||
v-model="show"
|
||||
mode="bottom"
|
||||
:closeable="true"
|
||||
@close="handleClose"
|
||||
:safe-area-inset-bottom="true"
|
||||
:mask-close-able="false"
|
||||
z-index="10078"
|
||||
height="80%"
|
||||
>
|
||||
<view class="tcm-term-modal page-bg">
|
||||
<view class="modal-header white">
|
||||
<d-text :text="titleText" className="fs-32 font-bold color-title"></d-text>
|
||||
</view>
|
||||
|
||||
<view class="modal-content">
|
||||
<view class="search-box m-b-24">
|
||||
<u-input
|
||||
v-model="searchKey"
|
||||
placeholder="请输入关键词搜索(名称/别名/拼音首字母)"
|
||||
:custom-style="searchStyle"
|
||||
@input="onSearchInput"
|
||||
@clear="onSearchClear"
|
||||
>
|
||||
<template slot="suffix">
|
||||
<u-icon name="search" size="32" color="#B0B6C2"></u-icon>
|
||||
</template>
|
||||
</u-input>
|
||||
</view>
|
||||
|
||||
<scroll-view class="list-scroll" scroll-y :scroll-top="scrollTop">
|
||||
<view v-if="!searchKeyTrim" class="hint-wrap">
|
||||
<text class="fs-26 color-sub">请输入关键词筛选术语(数据量较大)</text>
|
||||
</view>
|
||||
<view v-else-if="displayedList.length === 0" class="hint-wrap">
|
||||
<d-empty text="无匹配结果"></d-empty>
|
||||
</view>
|
||||
<view v-else class="tag-container">
|
||||
<view
|
||||
v-for="item in displayedList"
|
||||
:key="item.id"
|
||||
class="term-tag"
|
||||
:class="{ 'is-selected': Number(pendingId) === Number(item.id) }"
|
||||
@click="handlePick(item)"
|
||||
>
|
||||
<text class="tag-text">{{ item.name }}</text>
|
||||
<text v-if="item.alias" class="tag-alias">{{ item.alias }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="modal-footer flex-row flex-jus-between flex-ali-center white shadow-up">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
@click="handleClose"
|
||||
shape="circle"
|
||||
:custom-style="btnGhost"
|
||||
>取消</u-button>
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
@click="handleConfirm"
|
||||
shape="circle"
|
||||
:custom-style="btnPrimary"
|
||||
>确定</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TraditionalTermPickerModal',
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '请选择'
|
||||
},
|
||||
/** 当前字段已选 id(用于高亮) */
|
||||
selectedId: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
},
|
||||
/** 当前分类下的完整选项列表 */
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
searchKey: '',
|
||||
searchTimer: null,
|
||||
pendingId: null,
|
||||
scrollTop: 0,
|
||||
searchStyle: {
|
||||
fontSize: '28rpx',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '16rpx',
|
||||
height: '80rpx',
|
||||
padding: '0 32rpx',
|
||||
boxShadow: '0 4rpx 16rpx rgba(0,0,0,0.02)'
|
||||
},
|
||||
btnGhost: {
|
||||
backgroundColor: '#F4F6F8',
|
||||
color: '#2A2E35',
|
||||
height: '88rpx',
|
||||
width: '320rpx',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '30rpx',
|
||||
border: 'none'
|
||||
},
|
||||
btnPrimary: {
|
||||
backgroundColor: '#00A88A',
|
||||
color: '#fff',
|
||||
height: '88rpx',
|
||||
width: '320rpx',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '30rpx',
|
||||
boxShadow: '0 8rpx 20rpx rgba(0, 168, 138, 0.25)',
|
||||
border: 'none'
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
titleText() {
|
||||
return this.title || '请选择';
|
||||
},
|
||||
searchKeyTrim() {
|
||||
return (this.searchKey || '').trim();
|
||||
},
|
||||
displayedList() {
|
||||
const key = this.searchKeyTrim.toLowerCase();
|
||||
if (!key) return [];
|
||||
const all = this.options || [];
|
||||
const out = [];
|
||||
const max = 400;
|
||||
for (let i = 0; i < all.length && out.length < max; i++) {
|
||||
const it = all[i];
|
||||
if (!it) continue;
|
||||
const name = (it.name || '').toLowerCase();
|
||||
const alias = (it.alias || '').toLowerCase();
|
||||
const py = (it.initials_of_pinyin || '').toLowerCase();
|
||||
if (name.includes(key) || alias.includes(key) || py.includes(key)) {
|
||||
out.push(it);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
if (newVal) {
|
||||
this.searchKey = '';
|
||||
this.pendingId = this.selectedId != null && this.selectedId !== '' ? Number(this.selectedId) : null;
|
||||
this.scrollTop = 0;
|
||||
}
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSearchInput() {
|
||||
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.scrollTop = 0;
|
||||
}, 80);
|
||||
},
|
||||
onSearchClear() {
|
||||
this.searchKey = '';
|
||||
this.scrollTop = 0;
|
||||
},
|
||||
handlePick(item) {
|
||||
if (!item || item.id == null) return;
|
||||
this.pendingId = Number(item.id);
|
||||
},
|
||||
handleConfirm() {
|
||||
if (this.pendingId == null || Number.isNaN(this.pendingId)) {
|
||||
uni.showToast({ title: '请先选择一项', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.$emit('confirm', this.pendingId);
|
||||
this.show = false;
|
||||
},
|
||||
handleClose() {
|
||||
this.show = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tcm-term-modal {
|
||||
min-height: 60vh;
|
||||
}
|
||||
.modal-header {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #eef0f3;
|
||||
}
|
||||
.modal-content {
|
||||
padding: 24rpx 32rpx 0;
|
||||
}
|
||||
.list-scroll {
|
||||
height: 52vh;
|
||||
}
|
||||
.hint-wrap {
|
||||
padding: 80rpx 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.tag-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
.term-tag {
|
||||
margin: 0 16rpx 16rpx 0;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 40rpx;
|
||||
background: #f4f6f8;
|
||||
border: 2rpx solid transparent;
|
||||
max-width: 100%;
|
||||
}
|
||||
.term-tag.is-selected {
|
||||
background: #e8f6f4;
|
||||
border-color: #6acdbb;
|
||||
}
|
||||
.tag-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
.tag-alias {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.modal-footer {
|
||||
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
border-top: 1rpx solid #eef0f3;
|
||||
}
|
||||
</style>
|
||||
@@ -27,47 +27,49 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 药品列表 -->
|
||||
<view class="drug-list" v-if="drugList.length > 0">
|
||||
<view
|
||||
class="medicine-card"
|
||||
v-for="(item, index) in drugList"
|
||||
:key="index"
|
||||
:class="{ 'is-selected': isSelected(item) }"
|
||||
@click="!isSelected(item) && handleSelectDrug(item)"
|
||||
>
|
||||
<view class="flex-row">
|
||||
<image
|
||||
v-if="getDrugImage(item)"
|
||||
:src="getDrugImage(item)"
|
||||
class="drug-image-modern"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="flex-1 m-l-24 flex-col">
|
||||
<view class="flex-row flex-jus-sp flex-ali-start">
|
||||
<text class="fs-30 font-bold color-title line-clamp-2 w-70">{{item.drug.drug_name || item.drug.name}}</text>
|
||||
<text class="fs-32 font-bold color-price">¥{{parseFloat(item.price || 0).toFixed(2)}}</text>
|
||||
</view>
|
||||
<view class="flex-row flex-jus-sp flex-ali-center m-t-12">
|
||||
<text class="fs-24 color-sub">规格:{{item.drug.specification || '--'}}</text>
|
||||
</view>
|
||||
<view class="drug-status m-t-24">
|
||||
<view v-if="isSelected(item)" class="action-btn-ghost disabled">
|
||||
已添加处方
|
||||
<scroll-view class="list-scroll" scroll-y>
|
||||
<!-- 药品列表 -->
|
||||
<view class="drug-list" v-if="drugList.length > 0">
|
||||
<view
|
||||
class="medicine-card"
|
||||
v-for="(item, index) in drugList"
|
||||
:key="index"
|
||||
:class="{ 'is-selected': isSelected(item) }"
|
||||
@click="!isSelected(item) && handleSelectDrug(item)"
|
||||
>
|
||||
<view class="flex-row">
|
||||
<image
|
||||
v-if="getDrugImage(item)"
|
||||
:src="getDrugImage(item)"
|
||||
class="drug-image-modern"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="flex-1 m-l-24 flex-col">
|
||||
<view class="flex-row flex-jus-sp flex-ali-start">
|
||||
<text class="fs-30 font-bold color-title line-clamp-2 w-70">{{item.drug.drug_name || item.drug.name}}</text>
|
||||
<text class="fs-32 font-bold color-price">¥{{parseFloat(item.price || 0).toFixed(2)}}</text>
|
||||
</view>
|
||||
<view v-else class="action-btn-ghost primary">
|
||||
<u-icon name="plus" size="24" class="m-r-8"></u-icon> 选用此药
|
||||
<view class="flex-row flex-jus-sp flex-ali-center m-t-12">
|
||||
<text class="fs-24 color-sub">规格:{{item.drug.specification || '--'}}</text>
|
||||
</view>
|
||||
<view class="drug-status m-t-24">
|
||||
<view v-if="isSelected(item)" class="action-btn-ghost disabled">
|
||||
已添加处方
|
||||
</view>
|
||||
<view v-else class="action-btn-ghost primary">
|
||||
<u-icon name="plus" size="24" class="m-r-8"></u-icon> 选用此药
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无药品"></d-empty>
|
||||
</view>
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无药品"></d-empty>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<view class="safe-area-inset-bottom page-bg">
|
||||
<!-- 导航栏 -->
|
||||
<u-navbar class="navbar" :is-back="true" title="开方" :custom-back="handleCustomBack" title-color="#000" background="{ background: '#fff' }">
|
||||
<u-navbar class="navbar" :is-back="true" :title="navbarTitle" :custom-back="handleCustomBack" title-color="#000" background="{ background: '#fff' }">
|
||||
</u-navbar>
|
||||
|
||||
<view v-if="registerModeHint" class="register-mode-hint mx-32 m-t-12">
|
||||
<text class="register-mode-hint__text">{{ registerModeHint }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 处方类型Tab切换 -->
|
||||
<view class="tabs flex p-32 flex-ali-center flex-jus-sp shadow-sm" style="overflow-x: auto; position: relative; z-index: 10;">
|
||||
<u-button
|
||||
@@ -55,6 +59,44 @@
|
||||
<!-- 滚动内容区域 -->
|
||||
<scroll-view :style="scrollStyle" scroll-y="true">
|
||||
|
||||
<!-- 在线复诊 + 中药:证候/治法/中医疾病(接口 diseases_id、method_id、syndrome_id,与 PC 在线问诊处方一致) -->
|
||||
<view v-if="activeCategory === 1 && isOnlineRevisit" class="white p-32 radius-12 mx-32 m-t-24 flex-col">
|
||||
<view class="top_title flex-row flex-ali-center m-b-16">
|
||||
<image :src="require('@/static/image/js.png')" class="size-32"></image>
|
||||
<d-text text="中医辨证" className="fs-32 m-l-16 font-bold" color="#333"></d-text>
|
||||
</view>
|
||||
<view class="m-t-8">
|
||||
<d-text text="中医证候" className="fs-26 color-sub"></d-text>
|
||||
<u-input
|
||||
disabled
|
||||
:value="onlineTcmDiseasesLabel"
|
||||
placeholder="请点击选择中医证候"
|
||||
:custom-style="illnessStyle"
|
||||
class="m-t-8"
|
||||
@click="handleOpenTcmPicker('diseases')" />
|
||||
</view>
|
||||
<view class="m-t-24">
|
||||
<d-text text="中医治法" className="fs-26 color-sub"></d-text>
|
||||
<u-input
|
||||
disabled
|
||||
:value="onlineTcmMethodLabel"
|
||||
placeholder="请点击选择中医治法"
|
||||
:custom-style="illnessStyle"
|
||||
class="m-t-8"
|
||||
@click="handleOpenTcmPicker('method')" />
|
||||
</view>
|
||||
<view class="m-t-24">
|
||||
<d-text text="中医疾病" className="fs-26 color-sub"></d-text>
|
||||
<u-input
|
||||
disabled
|
||||
:value="onlineTcmSyndromeLabel"
|
||||
placeholder="请点击选择中医疾病"
|
||||
:custom-style="illnessStyle"
|
||||
class="m-t-8"
|
||||
@click="handleOpenTcmPicker('syndrome')" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 西药列表 -->
|
||||
<view class="mx-32 m-t-24" v-if="activeCategory === 2">
|
||||
<view class="flex-row flex-jus-sp flex-ali-center m-b-24">
|
||||
@@ -331,6 +373,14 @@
|
||||
</u-button>
|
||||
</view>
|
||||
|
||||
<TraditionalTermPickerModal
|
||||
v-model="showTcmPicker"
|
||||
:title="tcmPickerTitle"
|
||||
:options="tcmPickerList"
|
||||
:selected-id="tcmPickerCurrentSelectedId"
|
||||
@confirm="handleTcmPickerConfirm"
|
||||
/>
|
||||
|
||||
<!-- 诊断选择弹窗 -->
|
||||
<DiagnosisModal
|
||||
v-model="showDiagnosisModal"
|
||||
@@ -356,6 +406,7 @@
|
||||
<WesternMedicineModal
|
||||
v-model="showWesternMedicineModal"
|
||||
:currentDrugs="currentDrugs"
|
||||
:initial-search-key="westernModalInitialKeyword"
|
||||
@select="handleSelectWesternDrug"
|
||||
/>
|
||||
|
||||
@@ -408,13 +459,16 @@ import {
|
||||
getDrugUseList,
|
||||
getProductListDoctorReception,
|
||||
getPatientInfoByPatientId,
|
||||
getPatientItem,
|
||||
getProcessRuleList,
|
||||
getCommonPrescriptionListApi,
|
||||
saveWestCommonPrescriptionApi,
|
||||
saveChineseCommonPrescriptionApi,
|
||||
getCurrentStoreTypeApi,
|
||||
getPrescriptionInfoApi
|
||||
getPrescriptionInfoApi,
|
||||
getTraditionalChineseMedicineJson
|
||||
} from '@/api/reception.js';
|
||||
import { getChatRoomByRegisterApi, sendToUserHttpApi } from '@/api/chat.js';
|
||||
import { PrescriptionStorage } from './utils/prescriptionStorage.js';
|
||||
import { PrescriptionCalculator } from './utils/prescriptionCalculator.js';
|
||||
import { PrescriptionValidator } from './utils/prescriptionValidator.js';
|
||||
@@ -426,6 +480,9 @@ import ChineseMedicineModal from './components/modals/ChineseMedicineModal.vue';
|
||||
import SimpleProductModal from './components/modals/SimpleProductModal.vue';
|
||||
import WesternMedicineUsageModal from './components/modals/WesternMedicineUsageModal.vue';
|
||||
import ChineseMedicineConfig from './components/ChineseMedicineConfig.vue';
|
||||
import TraditionalTermPickerModal from './components/modals/TraditionalTermPickerModal.vue';
|
||||
|
||||
const PENDING_RX_FROM_CHAT = 'xk_pending_rx_from_chat';
|
||||
|
||||
export default {
|
||||
name: 'PrescriptionV2',
|
||||
@@ -437,7 +494,8 @@ export default {
|
||||
ChineseMedicineModal,
|
||||
SimpleProductModal,
|
||||
WesternMedicineUsageModal,
|
||||
ChineseMedicineConfig
|
||||
ChineseMedicineConfig,
|
||||
TraditionalTermPickerModal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -483,6 +541,7 @@ export default {
|
||||
showDoctorOrderModal: false,
|
||||
showCommonPrescriptionModal: false,
|
||||
showWesternMedicineModal: false,
|
||||
westernModalInitialKeyword: '',
|
||||
showChineseMedicineModal: false,
|
||||
showSimpleProductModal: false,
|
||||
showWesternUsageModal: false,
|
||||
@@ -497,6 +556,19 @@ export default {
|
||||
registerStoreInfo: null,
|
||||
sendMode: 0,
|
||||
selectedStoreId: null,
|
||||
registerOrderType: null,
|
||||
|
||||
/** 在线复诊中药:术语表(HTTP 拉取,内存缓存) */
|
||||
traditionalTcmData: null,
|
||||
traditionalTcmLoadPromise: null,
|
||||
showTcmPicker: false,
|
||||
tcmPickerTitle: '',
|
||||
tcmPickerField: '',
|
||||
tcmPickerList: [],
|
||||
/** 与 PC prescriptionStore 提交字段一致:diseases_id←证候(diseases);method_id←治法;syndrome_id←中医疾病(syndrome) */
|
||||
onlineTcmDiseasesId: null,
|
||||
onlineTcmMethodId: null,
|
||||
onlineTcmSyndromeId: null,
|
||||
|
||||
// UI Styles
|
||||
tabsBtnStyle: {
|
||||
@@ -567,6 +639,52 @@ export default {
|
||||
},
|
||||
supportsCommonPrescriptionTemplate() {
|
||||
return [1, 2].includes(this.activeCategory);
|
||||
},
|
||||
isOnlineRevisit() {
|
||||
const t = Number(this.registerOrderType);
|
||||
return t === 2 || t === 3;
|
||||
},
|
||||
registerModeHint() {
|
||||
if (this.registerOrderType === null || this.registerOrderType === undefined || this.registerOrderType === '') return '';
|
||||
return this.isOnlineRevisit ? '当前:在线复诊开方(提交后将向患者发送处方消息)' : '当前:线下开方';
|
||||
},
|
||||
navbarTitle() {
|
||||
if (this.registerOrderType === null || this.registerOrderType === undefined || this.registerOrderType === '') {
|
||||
return '开方';
|
||||
}
|
||||
return this.isOnlineRevisit ? '开方 · 在线复诊' : '开方 · 线下';
|
||||
},
|
||||
onlineTcmDiseasesLabel() {
|
||||
const data = this.traditionalTcmData;
|
||||
const id = this.onlineTcmDiseasesId;
|
||||
if (!data || id == null || id === '') return '';
|
||||
const row = (data.diseases || []).find((x) => Number(x.id) === Number(id));
|
||||
return row ? row.name : '';
|
||||
},
|
||||
onlineTcmMethodLabel() {
|
||||
const data = this.traditionalTcmData;
|
||||
const id = this.onlineTcmMethodId;
|
||||
if (!data || id == null || id === '') return '';
|
||||
const row = (data.method || []).find((x) => Number(x.id) === Number(id));
|
||||
return row ? row.name : '';
|
||||
},
|
||||
onlineTcmSyndromeLabel() {
|
||||
const data = this.traditionalTcmData;
|
||||
const id = this.onlineTcmSyndromeId;
|
||||
if (!data || id == null || id === '') return '';
|
||||
const row = (data.syndrome || []).find((x) => Number(x.id) === Number(id));
|
||||
return row ? row.name : '';
|
||||
},
|
||||
tcmPickerCurrentSelectedId() {
|
||||
if (this.tcmPickerField === 'diseases') return this.onlineTcmDiseasesId;
|
||||
if (this.tcmPickerField === 'method') return this.onlineTcmMethodId;
|
||||
if (this.tcmPickerField === 'syndrome') return this.onlineTcmSyndromeId;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showWesternMedicineModal(val) {
|
||||
if (!val) this.westernModalInitialKeyword = '';
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
@@ -589,11 +707,13 @@ export default {
|
||||
try {
|
||||
this.restoreFromLocalStorage();
|
||||
await this.loadPatientInfo();
|
||||
await this.loadRegisterOrderType();
|
||||
await this.loadBasicConfigData();
|
||||
await this.loadRegisterStoreInfo();
|
||||
this.loadCurrentCategoryDrugs();
|
||||
await this.maybeApplyReusePrescription();
|
||||
if (this.activeCategory === 1) await this.checkAndShowTransferTip();
|
||||
this.applyPendingExperienceDrugFromChat();
|
||||
} catch (error) {
|
||||
console.error('初始化页面数据失败:', error);
|
||||
this.$toast('初始化失败,请重试');
|
||||
@@ -628,8 +748,23 @@ export default {
|
||||
this.medicalAdvice = data.medicalAdvice || '';
|
||||
this.treatmentPrice = data.treatmentPrice || '';
|
||||
if (data.chineseConfig) this.chineseConfig = { ...this.chineseConfig, ...data.chineseConfig };
|
||||
if (this.activeCategory === 1) {
|
||||
if (data.onlineTcmDiseasesId !== undefined && data.onlineTcmDiseasesId !== null) this.onlineTcmDiseasesId = data.onlineTcmDiseasesId;
|
||||
else this.onlineTcmDiseasesId = null;
|
||||
if (data.onlineTcmMethodId !== undefined && data.onlineTcmMethodId !== null) this.onlineTcmMethodId = data.onlineTcmMethodId;
|
||||
else this.onlineTcmMethodId = null;
|
||||
if (data.onlineTcmSyndromeId !== undefined && data.onlineTcmSyndromeId !== null) this.onlineTcmSyndromeId = data.onlineTcmSyndromeId;
|
||||
else this.onlineTcmSyndromeId = null;
|
||||
} else {
|
||||
this.onlineTcmDiseasesId = null;
|
||||
this.onlineTcmMethodId = null;
|
||||
this.onlineTcmSyndromeId = null;
|
||||
}
|
||||
} else {
|
||||
this.currentDrugs = [];
|
||||
this.onlineTcmDiseasesId = null;
|
||||
this.onlineTcmMethodId = null;
|
||||
this.onlineTcmSyndromeId = null;
|
||||
}
|
||||
},
|
||||
saveToLocalStorage() {
|
||||
@@ -639,7 +774,10 @@ export default {
|
||||
diagnosisText: this.diagnosisText,
|
||||
medicalAdvice: this.medicalAdvice,
|
||||
treatmentPrice: this.treatmentPrice,
|
||||
chineseConfig: this.activeCategory === 1 ? this.chineseConfig : null
|
||||
chineseConfig: this.activeCategory === 1 ? this.chineseConfig : null,
|
||||
onlineTcmDiseasesId: this.activeCategory === 1 ? this.onlineTcmDiseasesId : undefined,
|
||||
onlineTcmMethodId: this.activeCategory === 1 ? this.onlineTcmMethodId : undefined,
|
||||
onlineTcmSyndromeId: this.activeCategory === 1 ? this.onlineTcmSyndromeId : undefined
|
||||
};
|
||||
PrescriptionStorage.savePrescriptionData(this.activeCategory, this.registerId, data);
|
||||
PrescriptionStorage.saveActiveCategory(this.activeCategory, this.registerId);
|
||||
@@ -662,6 +800,24 @@ export default {
|
||||
console.error('获取患者信息失败:', error);
|
||||
}
|
||||
},
|
||||
async loadRegisterOrderType() {
|
||||
if (!this.registerId) {
|
||||
this.registerOrderType = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await getPatientItem(this.registerId);
|
||||
const data = res.result || res.data || res;
|
||||
if (data && data.type !== undefined && data.type !== null) {
|
||||
this.registerOrderType = data.type;
|
||||
} else {
|
||||
this.registerOrderType = null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
this.registerOrderType = null;
|
||||
}
|
||||
},
|
||||
async loadBasicConfigData() {
|
||||
try {
|
||||
const drugUseRes = await getDrugUseList();
|
||||
@@ -724,6 +880,44 @@ export default {
|
||||
}
|
||||
},
|
||||
handleOpenDiagnosisModal() { this.showDiagnosisModal = true; },
|
||||
async ensureTraditionalTcmData() {
|
||||
if (this.traditionalTcmData) return this.traditionalTcmData;
|
||||
if (this.traditionalTcmLoadPromise) return this.traditionalTcmLoadPromise;
|
||||
this.traditionalTcmLoadPromise = getTraditionalChineseMedicineJson()
|
||||
.then((d) => {
|
||||
this.traditionalTcmData = d;
|
||||
return d;
|
||||
})
|
||||
.finally(() => {
|
||||
this.traditionalTcmLoadPromise = null;
|
||||
});
|
||||
return this.traditionalTcmLoadPromise;
|
||||
},
|
||||
async handleOpenTcmPicker(field) {
|
||||
if (!this.isOnlineRevisit || this.activeCategory !== 1) return;
|
||||
try {
|
||||
uni.showLoading({ title: '加载中', mask: true });
|
||||
await this.ensureTraditionalTcmData();
|
||||
uni.hideLoading();
|
||||
this.tcmPickerField = field;
|
||||
this.tcmPickerTitle = field === 'diseases' ? '中医证候' : field === 'method' ? '中医治法' : '中医疾病';
|
||||
const d = this.traditionalTcmData;
|
||||
this.tcmPickerList = field === 'diseases' ? (d.diseases || []) : field === 'method' ? (d.method || []) : (d.syndrome || []);
|
||||
this.showTcmPicker = true;
|
||||
} catch (e) {
|
||||
uni.hideLoading();
|
||||
console.error(e);
|
||||
uni.showToast({ title: '术语表加载失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
handleTcmPickerConfirm(id) {
|
||||
const nid = id != null ? Number(id) : null;
|
||||
if (this.tcmPickerField === 'diseases') this.onlineTcmDiseasesId = nid;
|
||||
else if (this.tcmPickerField === 'method') this.onlineTcmMethodId = nid;
|
||||
else if (this.tcmPickerField === 'syndrome') this.onlineTcmSyndromeId = nid;
|
||||
this.saveToLocalStorage();
|
||||
this.showTcmPicker = false;
|
||||
},
|
||||
handleDiagnosisConfirm(diagnosisText) {
|
||||
this.diagnosisText = diagnosisText;
|
||||
if (diagnosisText) {
|
||||
@@ -776,7 +970,144 @@ export default {
|
||||
else if (category === 2) return 'west';
|
||||
else return 'granular';
|
||||
},
|
||||
handleOpenWesternMedicineModal() { this.showWesternMedicineModal = true; },
|
||||
applyPendingExperienceDrugFromChat() {
|
||||
let raw;
|
||||
try {
|
||||
raw = uni.getStorageSync(PENDING_RX_FROM_CHAT);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (!raw) return;
|
||||
let p;
|
||||
try {
|
||||
p = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
} catch (e2) {
|
||||
try {
|
||||
uni.removeStorageSync(PENDING_RX_FROM_CHAT);
|
||||
} catch (e3) {}
|
||||
return;
|
||||
}
|
||||
if (!p || String(p.register_id) !== String(this.registerId)) return;
|
||||
|
||||
if (p.source === 'register_drugs' && Array.isArray(p.drugs) && p.drugs.length > 0) {
|
||||
try {
|
||||
uni.removeStorageSync(PENDING_RX_FROM_CHAT);
|
||||
} catch (e4) {}
|
||||
this.activeCategory = 2;
|
||||
this.saveToLocalStorage();
|
||||
this.loadCurrentCategoryDrugs();
|
||||
this.mergeWesternRowsFromRegisterDrugs(p.drugs);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.source === 'transfer_import' && Array.isArray(p.drugs) && p.drugs.length > 0) {
|
||||
try {
|
||||
uni.removeStorageSync(PENDING_RX_FROM_CHAT);
|
||||
} catch (e5) {}
|
||||
this.activeCategory = 1;
|
||||
this.saveToLocalStorage();
|
||||
this.loadCurrentCategoryDrugs();
|
||||
this.mergeTransferChineseRows(p.drugs);
|
||||
if (p.diagnosisText) {
|
||||
this.diagnosisText = String(p.diagnosisText);
|
||||
const names = this.diagnosisText.split(',').filter(Boolean);
|
||||
this.diagnoses = names.map((name, index) => ({ id: `temp_${index}`, name }));
|
||||
}
|
||||
if (p.medicalAdvice != null && String(p.medicalAdvice).trim() !== '') {
|
||||
this.medicalAdvice = String(p.medicalAdvice);
|
||||
}
|
||||
this.saveToLocalStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uni.removeStorageSync(PENDING_RX_FROM_CHAT);
|
||||
} catch (e4) {}
|
||||
this.activeCategory = 2;
|
||||
this.westernModalInitialKeyword = String(p.drug_name || '').trim();
|
||||
this.saveToLocalStorage();
|
||||
this.loadCurrentCategoryDrugs();
|
||||
this.$nextTick(() => {
|
||||
this.showWesternMedicineModal = true;
|
||||
});
|
||||
},
|
||||
mergeWesternRowsFromRegisterDrugs(rows) {
|
||||
let added = 0;
|
||||
for (const row of rows) {
|
||||
const d = row.drug || {};
|
||||
const qtyRaw = row.purchase_quantity != null ? Number(row.purchase_quantity) : null;
|
||||
const qty =
|
||||
qtyRaw != null && !Number.isNaN(qtyRaw) && qtyRaw >= 1 ? Math.min(99, qtyRaw) : Number(d.number || 1) || 1;
|
||||
const drugId = row.drug_id || d.id;
|
||||
const synthetic = {
|
||||
id: drugId,
|
||||
drug_id: drugId,
|
||||
drug_name: d.drug_name,
|
||||
number: qty,
|
||||
select_number: qty,
|
||||
price: parseFloat(row.price) || 0,
|
||||
specification: d.specification || '',
|
||||
image: d.image || '',
|
||||
instruction: d.instruction || '',
|
||||
time_id: d.time_id || 0,
|
||||
type_id: d.type_id || 0,
|
||||
frequency_id: d.frequency_id || 0,
|
||||
unit_id: d.unit_id || 0,
|
||||
drug: d
|
||||
};
|
||||
if (PrescriptionValidator.isDrugExists(this.currentDrugs, synthetic)) {
|
||||
continue;
|
||||
}
|
||||
const newDrug = {
|
||||
index_id: row.id,
|
||||
id: drugId,
|
||||
drug_id: drugId,
|
||||
drug_name: d.drug_name,
|
||||
number: qty,
|
||||
select_number: qty,
|
||||
price: parseFloat(row.price) || 0,
|
||||
specification: d.specification || '',
|
||||
image: d.image || '',
|
||||
instruction: d.instruction || '',
|
||||
time_id: d.time_id || 0,
|
||||
type_id: d.type_id || 0,
|
||||
frequency_id: d.frequency_id || 0,
|
||||
unit_id: d.unit_id || 0,
|
||||
use_num: this.drugUseList.drug_time?.find((item) => item.id === (d.time_id || 0)),
|
||||
use_type: this.drugUseList.drug_use_type?.find((item) => item.id === (d.type_id || 0)),
|
||||
use_frequency: this.drugUseList.drug_use_frequency?.find((item) => item.id === (d.frequency_id || 0)),
|
||||
unit: this.drugUseList.drug_unit?.find((item) => item.id === (d.unit_id || 0))
|
||||
};
|
||||
this.currentDrugs.push(newDrug);
|
||||
added++;
|
||||
}
|
||||
this.saveToLocalStorage();
|
||||
if (added > 0) {
|
||||
this.$toast(`已添加 ${added} 个药品`);
|
||||
} else {
|
||||
this.$toast('药品已在列表中或未能匹配');
|
||||
}
|
||||
},
|
||||
mergeTransferChineseRows(rows) {
|
||||
let added = 0;
|
||||
for (const product of rows) {
|
||||
if (PrescriptionValidator.isDrugExists(this.currentDrugs, product)) {
|
||||
continue;
|
||||
}
|
||||
this.currentDrugs.push({ ...product });
|
||||
added++;
|
||||
}
|
||||
this.saveToLocalStorage();
|
||||
if (added > 0) {
|
||||
this.$toast(`已添加 ${added} 味药`);
|
||||
} else {
|
||||
this.$toast('药品已在列表中');
|
||||
}
|
||||
},
|
||||
handleOpenWesternMedicineModal() {
|
||||
this.westernModalInitialKeyword = '';
|
||||
this.showWesternMedicineModal = true;
|
||||
},
|
||||
handleSelectWesternDrug(drug) {
|
||||
if (PrescriptionValidator.isDrugExists(this.currentDrugs, drug)) {
|
||||
this.$toast('该药品已在列表中');
|
||||
@@ -1120,8 +1451,15 @@ export default {
|
||||
},
|
||||
validatePrescriptionData() {
|
||||
return PrescriptionValidator.validatePrescriptionData({
|
||||
category: this.activeCategory, drugs: this.currentDrugs, diagnoses: this.diagnoses,
|
||||
medicalAdvice: this.medicalAdvice, chineseConfig: this.activeCategory === 1 ? this.chineseConfig : null
|
||||
category: this.activeCategory,
|
||||
drugs: this.currentDrugs,
|
||||
diagnoses: this.diagnoses,
|
||||
medicalAdvice: this.medicalAdvice,
|
||||
chineseConfig: this.activeCategory === 1 ? this.chineseConfig : null,
|
||||
requireOnlineTcm: this.activeCategory === 1 && this.isOnlineRevisit,
|
||||
onlineTcmDiseasesId: this.onlineTcmDiseasesId,
|
||||
onlineTcmMethodId: this.onlineTcmMethodId,
|
||||
onlineTcmSyndromeId: this.onlineTcmSyndromeId
|
||||
});
|
||||
},
|
||||
async checkChineseMedicineConflict() {
|
||||
@@ -1245,6 +1583,11 @@ export default {
|
||||
};
|
||||
if (this.activeCategory === 1) {
|
||||
params.package_method_id = this.chineseConfig.packageMethodId || null; params.process_rule_id = this.chineseConfig.processRuleId || null; params.process_rule_note_id = this.chineseConfig.processRuleNoteId || null; params.child_process_rule_id = this.chineseConfig.childProcessRuleId || null; params.process_rule_type = this.chineseConfig.ruleType || 1; params.processing_fee = this.processingFee; params.dosage = this.chineseConfig.dosage || 7; params.day_dosage = this.chineseConfig.dayDosage || 2;
|
||||
if (this.isOnlineRevisit) {
|
||||
params.diseases_id = this.onlineTcmDiseasesId;
|
||||
params.method_id = this.onlineTcmMethodId;
|
||||
params.syndrome_id = this.onlineTcmSyndromeId;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await addWestPrescription(params);
|
||||
@@ -1261,6 +1604,14 @@ export default {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.isOnlineRevisit) {
|
||||
try {
|
||||
await this.trySendPrescriptionChatMessage(res);
|
||||
} catch (e) {
|
||||
console.error('处方消息同步失败', e);
|
||||
uni.showToast({ title: '处方消息同步失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
this.clearPrescriptionData();
|
||||
PrescriptionStorage.clearAllPrescriptionData(this.registerId);
|
||||
setTimeout(() => { uni.navigateBack(); }, 1500);
|
||||
@@ -1275,9 +1626,37 @@ export default {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
},
|
||||
async trySendPrescriptionChatMessage(res) {
|
||||
const inner = res && (res.result !== undefined ? res.result : res.data !== undefined ? res.data : null);
|
||||
if (!inner || typeof inner !== 'object') return;
|
||||
if (!this.registerId) return;
|
||||
const roomRes = await getChatRoomByRegisterApi(this.registerId);
|
||||
const roomPayload = roomRes && (roomRes.result || roomRes.data || roomRes);
|
||||
const roomId = roomPayload && roomPayload.room_id;
|
||||
const receiverUserId = roomPayload && roomPayload.receiver_user_id;
|
||||
if (!roomId || !receiverUserId) return;
|
||||
const doctorId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
|
||||
if (!doctorId) return;
|
||||
const senderUserId = String(doctorId).startsWith('doctor-') ? String(doctorId) : `doctor-${doctorId}`;
|
||||
try {
|
||||
await sendToUserHttpApi({
|
||||
room_id: roomId,
|
||||
sender_user_id: senderUserId,
|
||||
receiver_user_id: receiverUserId,
|
||||
message_type: 4,
|
||||
message_content: JSON.stringify(inner),
|
||||
duration: 0
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('trySendPrescriptionChatMessage', e);
|
||||
}
|
||||
},
|
||||
clearPrescriptionData() {
|
||||
this.currentDrugs = []; this.diagnoses = []; this.diagnosisText = ''; this.medicalAdvice = ''; this.treatmentPrice = '';
|
||||
this.chineseConfig = { ruleType: 1, packageMethodId: null, processRuleId: null, childProcessRuleId: null, processRuleNoteId: null, dosage: 7, dayDosage: 2 };
|
||||
this.onlineTcmDiseasesId = null;
|
||||
this.onlineTcmMethodId = null;
|
||||
this.onlineTcmSyndromeId = null;
|
||||
},
|
||||
handleSaveAsCommonPrescription() {
|
||||
if (this.currentDrugs.length === 0) { this.$toast('请先添加药品后再保存为常用方'); return; }
|
||||
@@ -1298,7 +1677,7 @@ export default {
|
||||
const resCn = await saveChineseCommonPrescriptionApi(payload);
|
||||
if (!resCn || resCn.code !== 0) { this.$toast(resCn.msg || resCn.message || '保存失败'); return; }
|
||||
} else {
|
||||
const drugs = this.currentDrugs.map((drug) => ({ drug_id: drug.id || drug.drug_id, drug_name: drug.drug_name || drug.name, number: drug.number || 1, select_number: drug.select_number || 1, price: drug.price || 0, time_id: drug.time_id || 0, type_id: drug.type_id || 0, frequency_id: drug.frequency_id || 0, unit_id: drug.unit_id || 0, image: drug.image || '', instruction: drug.instruction || '' }));
|
||||
const drugs = this.currentDrugs.map((drug) => ({ drug_id: drug.id || drug.drug_id, drug_name: drug.drug_name || drug.name, number: drug.number || 1, select_number: drug.select_number || 1, price: drug.price || 0, time_id: drug.time_id || 0, type_id: drug.type_id || 0, frequency_id: drug.frequency_id || 0, unit_id: drug.unit_id || 0, image: drug.image || '', instruction: drug.instruction || '', specification: drug.specification || '' }));
|
||||
const resW = await saveWestCommonPrescriptionApi({ name: this.commonPrescriptionName.trim(), drugs, store_id: storeId, clinical_diagnose: this.diagnosisText || '', doctor_order: this.medicalAdvice || '', category: '1' });
|
||||
if (!resW || resW.code !== 0) { this.$toast(resW.msg || resW.message || '保存失败'); return; }
|
||||
}
|
||||
@@ -1502,6 +1881,17 @@ export default {
|
||||
}
|
||||
|
||||
/* 底部操作栏(恢复原状) */
|
||||
.register-mode-hint {
|
||||
padding: 16rpx 24rpx;
|
||||
background: #f0faf8;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #d4efe8;
|
||||
}
|
||||
.register-mode-hint__text {
|
||||
font-size: 24rpx;
|
||||
color: #008771;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
||||
@@ -14,7 +14,17 @@ export class PrescriptionValidator {
|
||||
* @returns {Object} { valid: boolean, message: string }
|
||||
*/
|
||||
static validatePrescriptionData(params) {
|
||||
const { category, drugs, diagnoses, medicalAdvice, chineseConfig } = params;
|
||||
const {
|
||||
category,
|
||||
drugs,
|
||||
diagnoses,
|
||||
medicalAdvice,
|
||||
chineseConfig,
|
||||
requireOnlineTcm,
|
||||
onlineTcmDiseasesId,
|
||||
onlineTcmMethodId,
|
||||
onlineTcmSyndromeId
|
||||
} = params;
|
||||
|
||||
// 验证药品
|
||||
if (!drugs || drugs.length === 0) {
|
||||
@@ -48,6 +58,19 @@ export class PrescriptionValidator {
|
||||
}
|
||||
}
|
||||
|
||||
// 在线复诊 + 中药:证候/治法/中医疾病(与 PC diseases_id、method_id、syndrome_id 一致)
|
||||
if (requireOnlineTcm) {
|
||||
if (onlineTcmDiseasesId == null || onlineTcmDiseasesId === '') {
|
||||
return { valid: false, message: '请选择中医证候' };
|
||||
}
|
||||
if (onlineTcmMethodId == null || onlineTcmMethodId === '') {
|
||||
return { valid: false, message: '请选择中医治法' };
|
||||
}
|
||||
if (onlineTcmSyndromeId == null || onlineTcmSyndromeId === '') {
|
||||
return { valid: false, message: '请选择中医疾病' };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: '验证通过'
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/** 中医术语表本地缓存(与 doctor-reception-wx traditional-chinese-medicine-all 配合) */
|
||||
|
||||
export const STORAGE_KEY = 'xk_traditional_tcm_v1';
|
||||
|
||||
/** 默认 3 天(秒),与后端 TRADITIONAL_TCM_CACHE_TTL_SECONDS 默认一致 */
|
||||
export const DEFAULT_TTL_MS = 259200 * 1000;
|
||||
|
||||
function isPayloadValid(obj) {
|
||||
return (
|
||||
obj &&
|
||||
Array.isArray(obj.diseases) &&
|
||||
Array.isArray(obj.method) &&
|
||||
Array.isArray(obj.syndrome)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未过期则返回 { diseases, method, syndrome },否则 null
|
||||
* @returns {{ diseases: any[], method: any[], syndrome: any[] } | null}
|
||||
*/
|
||||
export function loadValid() {
|
||||
try {
|
||||
const raw = uni.getStorageSync(STORAGE_KEY);
|
||||
if (raw == null || raw === '') return null;
|
||||
const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
if (!obj || typeof obj !== 'object') return null;
|
||||
const { expiresAt, diseases, method, syndrome } = obj;
|
||||
if (typeof expiresAt !== 'number' || expiresAt <= Date.now()) return null;
|
||||
const p = { diseases, method, syndrome };
|
||||
return isPayloadValid(p) ? p : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ diseases: any[], method: any[], syndrome: any[] }} payload
|
||||
* @param {number} expiresAtMs
|
||||
* @returns {boolean} 是否成功写入 storage
|
||||
*/
|
||||
export function save(payload, expiresAtMs) {
|
||||
if (!isPayloadValid(payload) || typeof expiresAtMs !== 'number') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const toStore = {
|
||||
diseases: payload.diseases,
|
||||
method: payload.method,
|
||||
syndrome: payload.syndrome,
|
||||
expiresAt: expiresAtMs,
|
||||
};
|
||||
JSON.stringify(toStore);
|
||||
uni.setStorageSync(STORAGE_KEY, JSON.stringify(toStore));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('traditionalTcm local save failed', e);
|
||||
uni.showToast({
|
||||
title: '本地缓存写入失败,本次仍可使用',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,32 @@
|
||||
*/
|
||||
import { chatConfig } from '@/config/chat.js';
|
||||
import { sendWebSocketMessage, isWebSocketConnected } from '@/utils/ws/initWebSocket.js';
|
||||
import { getChatRegisterInfoApi, getMessagesByRoomIdApi } from '@/request/api/im.js';
|
||||
import { getChatRegisterInfoApi, getMessagesByRoomIdApi } from '@/api/chat.js';
|
||||
import { getMessagePreview } from '@/utils/chat/messagePreview.js';
|
||||
|
||||
function unwrapMessagesPayload(res) {
|
||||
if (res == null) return null;
|
||||
if (res.result !== undefined && res.result !== null) return res.result;
|
||||
if (res.data && res.data.result !== undefined && res.data.result !== null) return res.data.result;
|
||||
if (res.data && typeof res.data === 'object') return res.data;
|
||||
return res;
|
||||
}
|
||||
|
||||
/** 房间 ID 归一化,便于 WS 与列表接口字段比较 */
|
||||
export function normalizeRoomId(id) {
|
||||
return String(id == null ? '' : id).trim();
|
||||
}
|
||||
|
||||
/** 仅在工作台 / 在线接诊列表为栈顶页时派发列表增量,减少后台页无意义合并 */
|
||||
function shouldEmitDoctorImListUpdate() {
|
||||
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : [];
|
||||
const cur = pages && pages.length ? pages[pages.length - 1] : null;
|
||||
const route = cur && cur.route ? cur.route : '';
|
||||
return (
|
||||
route === 'pages/workbench/index' ||
|
||||
route === 'subPackages/sub_online_reception/pages/list'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天室管理器类
|
||||
@@ -33,16 +58,17 @@ class ChatRoomManager {
|
||||
* 职责:设置当前房间,重置未读数,保存到本地存储
|
||||
*/
|
||||
enterRoom(roomId) {
|
||||
if (!roomId) {
|
||||
const id = normalizeRoomId(roomId);
|
||||
if (!id) {
|
||||
console.warn('房间ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentRoomId = roomId;
|
||||
uni.setStorageSync('currentRoomId', roomId);
|
||||
this.resetUnreadCount(roomId);
|
||||
|
||||
console.log('进入聊天室:', roomId);
|
||||
|
||||
this.currentRoomId = id;
|
||||
uni.setStorageSync('currentRoomId', id);
|
||||
this.resetUnreadCount(id);
|
||||
|
||||
console.log('进入聊天室:', id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,14 +95,14 @@ class ChatRoomManager {
|
||||
* 职责:如果房间不是当前房间,则增加未读数并通知
|
||||
*/
|
||||
increaseUnreadCount(roomId) {
|
||||
if (!roomId) return;
|
||||
|
||||
// 如果是当前房间,不增加未读数
|
||||
if (this.getCurrentRoomId() === roomId) {
|
||||
const key = normalizeRoomId(roomId);
|
||||
if (!key) return;
|
||||
|
||||
if (normalizeRoomId(this.getCurrentRoomId()) === key) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unreadCounts[roomId] = (this.unreadCounts[roomId] || 0) + 1;
|
||||
|
||||
this.unreadCounts[key] = (this.unreadCounts[key] || 0) + 1;
|
||||
this.notifyUnreadChange();
|
||||
}
|
||||
|
||||
@@ -86,8 +112,9 @@ class ChatRoomManager {
|
||||
* 职责:将指定房间的未读数重置为0并通知
|
||||
*/
|
||||
resetUnreadCount(roomId) {
|
||||
if (this.unreadCounts[roomId]) {
|
||||
this.unreadCounts[roomId] = 0;
|
||||
const key = normalizeRoomId(roomId);
|
||||
if (this.unreadCounts[key]) {
|
||||
this.unreadCounts[key] = 0;
|
||||
this.notifyUnreadChange();
|
||||
}
|
||||
}
|
||||
@@ -98,7 +125,8 @@ class ChatRoomManager {
|
||||
* @returns {number} 未读数
|
||||
*/
|
||||
getUnreadCount(roomId) {
|
||||
return this.unreadCounts[roomId] || 0;
|
||||
const key = normalizeRoomId(roomId);
|
||||
return this.unreadCounts[key] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,18 +165,19 @@ class ChatRoomManager {
|
||||
* 职责:将消息添加到指定房间的消息列表
|
||||
*/
|
||||
addMessageToRoom(roomId, message) {
|
||||
if (!roomId || !message) return;
|
||||
|
||||
if (!this.roomMessages[roomId]) {
|
||||
this.roomMessages[roomId] = [];
|
||||
const key = normalizeRoomId(roomId);
|
||||
if (!key || !message) return;
|
||||
|
||||
if (!this.roomMessages[key]) {
|
||||
this.roomMessages[key] = [];
|
||||
}
|
||||
|
||||
|
||||
// 检查消息是否已存在(避免重复)
|
||||
const existingIndex = this.roomMessages[roomId].findIndex(m => m.id === message.id);
|
||||
const existingIndex = this.roomMessages[key].findIndex(m => m.id === message.id);
|
||||
if (existingIndex !== -1) {
|
||||
this.roomMessages[roomId][existingIndex] = message;
|
||||
this.roomMessages[key][existingIndex] = message;
|
||||
} else {
|
||||
this.roomMessages[roomId].push(message);
|
||||
this.roomMessages[key].push(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +187,8 @@ class ChatRoomManager {
|
||||
* @returns {Array} 消息列表
|
||||
*/
|
||||
getRoomMessages(roomId) {
|
||||
return this.roomMessages[roomId] || [];
|
||||
const key = normalizeRoomId(roomId);
|
||||
return this.roomMessages[key] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,28 +198,32 @@ class ChatRoomManager {
|
||||
* @returns {Promise<Array>} 消息列表
|
||||
*/
|
||||
async loadRoomMessages(roomId, lastMessageId = 0) {
|
||||
if (!roomId) {
|
||||
const key = normalizeRoomId(roomId);
|
||||
if (!key) {
|
||||
console.warn('房间ID不能为空');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const res = await getMessagesByRoomIdApi({
|
||||
room_id: roomId,
|
||||
room_id: key,
|
||||
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;
|
||||
|
||||
|
||||
const payload = unwrapMessagesPayload(res);
|
||||
if (payload && typeof payload === 'object') {
|
||||
const rawList = payload.list;
|
||||
const list = Array.isArray(rawList) ? rawList : rawList != null ? [rawList] : [];
|
||||
const messages = this.processMessages(list);
|
||||
this.lastMessageId = payload.last_message_id || 0;
|
||||
|
||||
// 如果是首次加载,替换消息列表;否则追加到前面
|
||||
if (lastMessageId === 0) {
|
||||
this.roomMessages[roomId] = messages;
|
||||
this.roomMessages[key] = messages;
|
||||
} else {
|
||||
this.roomMessages[roomId] = [...messages, ...(this.roomMessages[roomId] || [])];
|
||||
this.roomMessages[key] = [...messages, ...(this.roomMessages[key] || [])];
|
||||
}
|
||||
|
||||
|
||||
uni.$emit('load-room-message', lastMessageId === 0 ? 1 : 0);
|
||||
return messages;
|
||||
}
|
||||
@@ -255,13 +289,16 @@ class ChatRoomManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {number} timestamp - 时间戳(秒)
|
||||
* @returns {string} 格式化后的时间(HH:mm)
|
||||
* 格式化时间(HH:mm),时间戳支持秒或毫秒
|
||||
* @param {number} timestamp
|
||||
* @returns {string}
|
||||
*/
|
||||
formatTime(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp * 1000);
|
||||
if (timestamp === undefined || timestamp === null || timestamp === '') return '';
|
||||
const n = Number(timestamp);
|
||||
if (Number.isNaN(n)) return '';
|
||||
const ms = n > 1e12 ? n : n * 1000;
|
||||
const date = new Date(ms);
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
@@ -280,8 +317,9 @@ class ChatRoomManager {
|
||||
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;
|
||||
const roomMessages = this.roomMessages[roomId];
|
||||
|
||||
@@ -312,7 +350,9 @@ class ChatRoomManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* 通过 WebSocket 发送消息(request_type: send_message)。
|
||||
* 在线接诊聊天页(sub_online_reception/pages/chat.vue)主路径已改为 Go IM `POST /api/send-to-user`(与 websocket 配置同基址),
|
||||
* 此处保留供其它仍走 WS 直发的场景;新页面发聊天消息请优先使用 HTTP API。
|
||||
* @param {Object} messageData - 消息数据
|
||||
* @param {string} messageData.room_id - 房间ID
|
||||
* @param {string} messageData.sender_user_id - 发送者ID
|
||||
@@ -382,21 +422,26 @@ class ChatRoomManager {
|
||||
if (data.request_type !== 'receive_message' && !data.room_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roomId = data.room_id;
|
||||
|
||||
const roomId = normalizeRoomId(data.room_id);
|
||||
if (!roomId) return;
|
||||
|
||||
|
||||
const dataNorm = { ...data, room_id: roomId };
|
||||
|
||||
// 处理消息
|
||||
const message = this.processSingleMessage(data);
|
||||
|
||||
const message = this.processSingleMessage(dataNorm);
|
||||
|
||||
// 添加到房间消息列表
|
||||
this.addMessageToRoom(roomId, message);
|
||||
|
||||
|
||||
const currentNorm = normalizeRoomId(this.getCurrentRoomId());
|
||||
const incrementUnread = currentNorm !== roomId;
|
||||
|
||||
// 如果不是当前房间,增加未读数
|
||||
if (this.getCurrentRoomId() !== roomId) {
|
||||
if (incrementUnread) {
|
||||
this.increaseUnreadCount(roomId);
|
||||
}
|
||||
|
||||
|
||||
// 调用所有注册的处理器
|
||||
this.messageHandlers.forEach(handler => {
|
||||
try {
|
||||
@@ -405,6 +450,27 @@ class ChatRoomManager {
|
||||
console.error('消息处理器执行失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const lastMessageTime =
|
||||
dataNorm.send_time != null && dataNorm.send_time !== ''
|
||||
? dataNorm.send_time
|
||||
: dataNorm.created_at != null && dataNorm.created_at !== ''
|
||||
? dataNorm.created_at
|
||||
: message.timestamp != null && message.timestamp !== ''
|
||||
? message.timestamp
|
||||
: message.created_at;
|
||||
|
||||
const unreadForRoom = this.getUnreadCount(roomId);
|
||||
|
||||
if (shouldEmitDoctorImListUpdate()) {
|
||||
uni.$emit('doctor-im-room-update', {
|
||||
roomId,
|
||||
last_message_preview: getMessagePreview(dataNorm),
|
||||
last_message_time: lastMessageTime,
|
||||
incrementUnread,
|
||||
unreadForRoom
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -413,7 +479,8 @@ class ChatRoomManager {
|
||||
*/
|
||||
clearRoomCache(roomId = null) {
|
||||
if (roomId) {
|
||||
delete this.roomMessages[roomId];
|
||||
const key = normalizeRoomId(roomId);
|
||||
if (key) delete this.roomMessages[key];
|
||||
} else {
|
||||
this.roomMessages = {};
|
||||
}
|
||||
|
||||
54
utils/chat/messagePreview.js
Normal file
54
utils/chat/messagePreview.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 会话列表「最后一条消息」预览文案(与患者端 ChatManager.getMessagePreview 对齐)
|
||||
* @param {Object} message
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getMessagePreview(message) {
|
||||
if (!message || message.message_type === undefined) {
|
||||
return '您有一条新消息';
|
||||
}
|
||||
const messageType = message.message_type;
|
||||
const content = message.message_content;
|
||||
|
||||
switch (messageType) {
|
||||
case 0:
|
||||
return content != null ? String(content) : '';
|
||||
case 1:
|
||||
return '[图片]';
|
||||
case 2:
|
||||
return '[语音消息]';
|
||||
case 3:
|
||||
return '[视频]';
|
||||
case 4:
|
||||
return '[处方单]';
|
||||
case 5:
|
||||
return '[文件]';
|
||||
case 6:
|
||||
return '[视频通话]';
|
||||
case 7:
|
||||
return '[语音通话]';
|
||||
case 9:
|
||||
try {
|
||||
const parsed = typeof content === 'object' ? content : JSON.parse(content);
|
||||
if (parsed.type === 'price_update') {
|
||||
return parsed.message || '[价格更新]';
|
||||
}
|
||||
if (parsed.type === 'price_adjust') {
|
||||
return parsed.message || '[价格调整]';
|
||||
}
|
||||
return '[系统消息]';
|
||||
} catch (e) {
|
||||
return '[系统消息]';
|
||||
}
|
||||
case 10:
|
||||
return '[挂号信息]';
|
||||
case 11:
|
||||
return '[就诊信息]';
|
||||
case 12:
|
||||
return '[商品推荐]';
|
||||
case 13:
|
||||
return '[问诊已结束]';
|
||||
default:
|
||||
return '您有一条新消息';
|
||||
}
|
||||
}
|
||||
25
utils/chat/sessionListFormat.js
Normal file
25
utils/chat/sessionListFormat.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 会话列表卡片右上角相对时间(工作台「正在接诊」、在线接诊列表等共用)
|
||||
* @param {Object} item
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatSessionRelativeTime(item) {
|
||||
if (!item) return '';
|
||||
const t =
|
||||
item.last_message_time ||
|
||||
item.updated_at ||
|
||||
item.created_at ||
|
||||
item.register_time;
|
||||
if (t == null || t === '') return '';
|
||||
const ms = typeof t === 'number' ? (t > 1e12 ? t : t * 1000) : new Date(t).getTime();
|
||||
if (Number.isNaN(ms)) return '';
|
||||
const d = new Date(ms);
|
||||
const now = Date.now();
|
||||
const diff = Math.floor((now - ms) / 60000);
|
||||
if (diff < 1) return '刚刚';
|
||||
if (diff < 60) return `${diff}分钟前`;
|
||||
const h = Math.floor(diff / 60);
|
||||
if (h < 24) return `${h}小时前`;
|
||||
const pad = (n) => (n < 10 ? '0' + n : '' + n);
|
||||
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
Reference in New Issue
Block a user