1. 医生端开方2.0测试
This commit is contained in:
52
App.vue
52
App.vue
@@ -8,8 +8,14 @@
|
||||
leadInfo,
|
||||
servInfo,
|
||||
} from "@/api/all.js"
|
||||
import { initWebSocket, disconnectWebSocket, rebindUser, addMessageHandler, isWebSocketConnected } from '@/utils/ws/initWebSocket.js';
|
||||
import { chatRoomManager } from '@/utils/chat/chatRoomManager.js';
|
||||
|
||||
export default {
|
||||
onLaunch() {
|
||||
// 初始化WebSocket(如果已登录)
|
||||
// 参考患者端的方式,直接初始化,不延迟
|
||||
this.initWebSocketConnection();
|
||||
// 获取小程序更新机制兼容
|
||||
if (uni.canIUse('getUpdateManager')) {
|
||||
const updateManager = uni.getUpdateManager()
|
||||
@@ -68,12 +74,54 @@
|
||||
if (uni.getStorageSync('token')) {
|
||||
this.getInfo()
|
||||
}
|
||||
|
||||
// 监听登录事件
|
||||
uni.$on('user-login', () => {
|
||||
this.initWebSocketConnection();
|
||||
});
|
||||
|
||||
// 监听登出事件
|
||||
uni.$on('user-logout', () => {
|
||||
disconnectWebSocket();
|
||||
});
|
||||
|
||||
// 监听token刷新事件
|
||||
uni.$on('token-refreshed', () => {
|
||||
rebindUser();
|
||||
});
|
||||
|
||||
// 注册聊天室消息处理器
|
||||
addMessageHandler((data) => {
|
||||
chatRoomManager.handleWebSocketMessage(data);
|
||||
});
|
||||
},
|
||||
onShow: function() {
|
||||
// 应用显示时,如果已登录但WebSocket未连接,尝试连接
|
||||
if (uni.getStorageSync('token') && !isWebSocketConnected()) {
|
||||
this.initWebSocketConnection();
|
||||
}
|
||||
},
|
||||
onShow: function() {},
|
||||
onHide: function() {
|
||||
// this.$store.dispatch('closeWebSocket')
|
||||
// 应用隐藏时不断开WebSocket,保持连接以便接收消息
|
||||
// 如果需要断开,可以调用 disconnectWebSocket()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 初始化WebSocket连接
|
||||
* 职责:检查登录状态,如果已登录则初始化WebSocket
|
||||
* 参考患者端的方式,直接连接,不延迟
|
||||
*/
|
||||
initWebSocketConnection() {
|
||||
const token = uni.getStorageSync('token');
|
||||
const doctorId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
|
||||
|
||||
if (token && doctorId) {
|
||||
// 直接初始化,不延迟(参考患者端的方式)
|
||||
initWebSocket();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async getInfo() {
|
||||
let role = uni.getStorageSync('identity')
|
||||
let req = {};
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
*/
|
||||
export function doctorOrderCommonList(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-order/common-list',
|
||||
url: '/newApi/doctor-reception-wx/common-list',
|
||||
method: 'get',
|
||||
data
|
||||
})
|
||||
|
||||
273
api/reception.js
Normal file
273
api/reception.js
Normal file
@@ -0,0 +1,273 @@
|
||||
import {
|
||||
req
|
||||
} from '@/common/js/index.js';
|
||||
|
||||
// 转接方患者列表
|
||||
export function getPatientList(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/patient-list',
|
||||
method: 'GET',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取患者详情
|
||||
export function getPatientItem(id) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/patient-item',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据患者ID获取患者信息
|
||||
* @param {number} patientId - 患者ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getPatientInfoByPatientId(patientId) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/get-patient-info-by-patient-id',
|
||||
method: 'GET',
|
||||
data: { patient_id: patientId }
|
||||
})
|
||||
}
|
||||
|
||||
// 接诊
|
||||
export function receptionApi(id) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/reception',
|
||||
method: 'POST',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
// 拒诊
|
||||
export function refuseReceptionApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/refuse-reception',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取拒诊原因列表
|
||||
export function refuseReceptionListApi() {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/refuse-reception-list',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
// 结束问诊
|
||||
export function endOfDiagnosisApi(id) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/end-of-diagnosis',
|
||||
method: 'POST',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取商品列表
|
||||
export function getProductListDoctorReception(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/product-list',
|
||||
method: 'GET',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取药品使用方式列表
|
||||
export function getDrugUseList() {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/drug-use-list',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取药品使用方式及默认用法用量
|
||||
export function getDrugUseWithDefault(drugId) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/drug-use-with-default',
|
||||
method: 'GET',
|
||||
data: { drug_id: drugId }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取疾病列表
|
||||
export function getDiseaseList(name, page = 1, pageSize = 20) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/disease-list',
|
||||
// url: '/doctor-reception-wx/disease-list',
|
||||
method: 'GET',
|
||||
data: { name, page, page_size: pageSize }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取医生的常用诊断列表
|
||||
export function getMyDiseaseList() {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/my-disease-list',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
// 添加常用诊断
|
||||
export function addMyDisease(diseaseId) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/add-my-disease',
|
||||
method: 'POST',
|
||||
data: { disease_id: diseaseId }
|
||||
})
|
||||
}
|
||||
|
||||
// 删除常用诊断
|
||||
export function deleteMyDisease(id) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/delete-my-disease',
|
||||
method: 'POST',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取医嘱列表
|
||||
export function getDoctorOrderList() {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/doctor-order-list',
|
||||
// url: '/doctor-reception-wx/doctor-order-list',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
// 创建医嘱
|
||||
export function createDoctorOrder(content) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/create-doctor-order',
|
||||
method: 'POST',
|
||||
data: { content }
|
||||
})
|
||||
}
|
||||
|
||||
// 删除医嘱
|
||||
export function deleteDoctorOrder(id) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/delete-doctor-order',
|
||||
method: 'POST',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
// 添加西药处方
|
||||
export function addWestPrescription(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/add-west-prescription',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 检查中药相冲
|
||||
export function checkChineseMedicineConflictApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/check-chinese-medicine-conflict',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取我的诊所列表
|
||||
export function getMyStoreListApi() {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/get-my-store-list',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前诊所类型
|
||||
export function getCurrentStoreTypeApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/get-current-store-type',
|
||||
method: 'GET',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 切换诊所
|
||||
export function switchStoreApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/switch-store',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取处方详情
|
||||
export function getPrescriptionInfoApi(id) {
|
||||
return req.request({
|
||||
url: '/newApi/prescription/detail',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取加工规则列表
|
||||
export function getProcessRuleList(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/process-rule-list',
|
||||
method: 'GET',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取煎煮方式列表
|
||||
export function getUseWayList() {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/drug-use-way-list',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取常用方列表
|
||||
export function getCommonPrescriptionListApi(storeId) {
|
||||
return req.request({
|
||||
url: '/newApi/common-prescription/list',
|
||||
method: 'GET',
|
||||
data: { store_id: storeId }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取常用方详情
|
||||
export function getCommonPrescriptionDetailApi(id, type) {
|
||||
return req.request({
|
||||
url: '/newApi/common-prescription/detail',
|
||||
method: 'GET',
|
||||
data: { id, type }
|
||||
})
|
||||
}
|
||||
|
||||
// 保存西药常用方
|
||||
export function saveWestCommonPrescriptionApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/common-prescription/save-west',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 保存中药常用方
|
||||
export function saveChineseCommonPrescriptionApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/common-prescription/save-chinese',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取公共医嘱列表
|
||||
export function getDoctorOrderCommonList(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/common-list',
|
||||
method: 'GET',
|
||||
data
|
||||
})
|
||||
}
|
||||
@@ -3,9 +3,10 @@ import {config} from "@/common/js/index";
|
||||
export async function requestConfig(ins, options, successHandler = null, failHandler = null, completeHandler = null){
|
||||
//原来是:ins.baseUrl = options.baseUrl || ins.baseUrl
|
||||
let baseUrl = options.baseUrl || ins.baseUrl;
|
||||
// TODO 这里写dev还是生产环境
|
||||
if (options.url.split('/').indexOf('newApi') !== -1) {
|
||||
// baseUrl = "http://127.0.0.1:18001/api/doctor/"
|
||||
baseUrl = "https://api.xiaokang88.com/api/doctor/"
|
||||
baseUrl = "http://127.0.0.1:18001/api/doctor/"
|
||||
// baseUrl = "https://api.xiaokang88.com/api/doctor/"
|
||||
}
|
||||
options.url = options.url.replace('/oldApi/', '');
|
||||
options.url = options.url.replace('/newApi/', '');
|
||||
@@ -41,6 +42,20 @@ export async function requestConfig(ins, options, successHandler = null, failHan
|
||||
config["dataType"] = options.dataType || "json"
|
||||
config["responseType"] = options.responseType || "text"
|
||||
config['sslVerify'] = false
|
||||
|
||||
// 如果是需要提交复杂对象/数组的写操作(POST/PUT/PATCH),强制使用 JSON 提交
|
||||
const method = (config["method"] || "GET").toUpperCase()
|
||||
if (["POST", "PUT", "PATCH"].includes(method)) {
|
||||
const data = config["data"]
|
||||
if (data && typeof data === 'object') {
|
||||
const hasComplexValue = Object.values(data).some((v) => {
|
||||
return v && typeof v === 'object'
|
||||
})
|
||||
if (hasComplexValue) {
|
||||
config.header["Content-Type"] = "application/json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}else if(type === "upload"){
|
||||
config["filePath"] = options.filePath
|
||||
config["name"] = options.name
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Request from './request.js';
|
||||
import errorCode from './errorCode.js';
|
||||
import {Base64} from "js-base64";
|
||||
let baseUrl = 'https://app.xiaokang88.com/service/v1/'; // 域名
|
||||
// let baseUrl = 'http://127.0.0.1:18000/service/v1/'; // 域名
|
||||
// TODO 这里写dev还是生产环境
|
||||
// let baseUrl = 'https://app.xiaokang88.com/service/v1/'; // 域名
|
||||
let baseUrl = 'http://127.0.0.1:18000/service/v1/'; // 域名
|
||||
// let baseUrl = 'http://www.xk888.com/service/v1/'; // 域名
|
||||
export const config = {
|
||||
baseUrl: baseUrl
|
||||
|
||||
74
config/chat.js
Normal file
74
config/chat.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 聊天室配置文件
|
||||
* 统一管理聊天室相关配置
|
||||
*/
|
||||
|
||||
/**
|
||||
* 聊天室配置
|
||||
*/
|
||||
export const chatConfig = {
|
||||
/**
|
||||
* 消息类型映射
|
||||
* 将消息类型名称映射到数字类型
|
||||
*/
|
||||
messageTypes: {
|
||||
text: 0, // 文本消息
|
||||
image: 1, // 图片消息
|
||||
audio: 2, // 语音消息
|
||||
video: 3, // 视频消息
|
||||
prescription: 4, // 处方消息
|
||||
file: 5, // 文件消息
|
||||
register: 10, // 挂号消息
|
||||
'patient-experience': 11, // 患者体验消息
|
||||
'product-card': 12, // 产品卡片消息
|
||||
'end-consultation': 13 // 结束问诊消息
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据消息类型名称获取数字类型
|
||||
* @param {string} typeName - 消息类型名称
|
||||
* @returns {number} 消息类型数字
|
||||
*/
|
||||
getMessageType(typeName) {
|
||||
return this.messageTypes[typeName] || 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据数字类型获取消息类型名称
|
||||
* @param {number} type - 消息类型数字
|
||||
* @returns {string} 消息类型名称
|
||||
*/
|
||||
getMessageTypeName(type) {
|
||||
const entries = Object.entries(this.messageTypes);
|
||||
const found = entries.find(([name, value]) => value === type);
|
||||
return found ? found[0] : 'text';
|
||||
},
|
||||
|
||||
/**
|
||||
* 房间配置
|
||||
*/
|
||||
room: {
|
||||
messagePageSize: 20, // 每页消息数量
|
||||
maxCacheSize: 100 // 最大缓存消息数
|
||||
},
|
||||
|
||||
/**
|
||||
* 未读数配置
|
||||
*/
|
||||
unread: {
|
||||
maxCount: 99 // 最大未读数显示(超过显示99+)
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化未读数显示
|
||||
* @param {number} count - 未读数
|
||||
* @returns {string} 格式化后的未读数
|
||||
*/
|
||||
formatUnreadCount(count) {
|
||||
if (count <= 0) return '';
|
||||
if (count > this.unread.maxCount) {
|
||||
return `${this.unread.maxCount}+`;
|
||||
}
|
||||
return count.toString();
|
||||
}
|
||||
};
|
||||
55
config/websocket.js
Normal file
55
config/websocket.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* WebSocket配置文件
|
||||
* 统一管理WebSocket相关配置
|
||||
*/
|
||||
|
||||
// 检查是否为开发环境
|
||||
// 在uni-app中,可以通过条件编译或manifest.json判断
|
||||
const isDev = true; // TODO: 根据实际环境配置
|
||||
|
||||
/**
|
||||
* WebSocket配置
|
||||
*/
|
||||
export const websocketConfig = {
|
||||
/**
|
||||
* WebSocket URL配置
|
||||
* dev: 开发环境WebSocket地址
|
||||
* prod: 生产环境WebSocket地址
|
||||
*/
|
||||
url: {
|
||||
dev: 'ws://127.0.0.1:12080/ws',
|
||||
prod: 'wss://api.ws.g.xiaokang88.com/ws'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前环境的WebSocket URL
|
||||
* @returns {string} WebSocket URL
|
||||
*/
|
||||
getUrl() {
|
||||
return isDev ? this.url.dev : this.url.prod;
|
||||
},
|
||||
|
||||
/**
|
||||
* 重连配置
|
||||
*/
|
||||
reconnect: {
|
||||
maxAttempts: 5, // 最大重连次数
|
||||
initialDelay: 1000, // 初始重连延迟(毫秒)
|
||||
maxDelay: 30000 // 最大重连延迟(毫秒)
|
||||
},
|
||||
|
||||
/**
|
||||
* 心跳配置
|
||||
*/
|
||||
heartbeat: {
|
||||
interval: 300000 // 心跳间隔(毫秒),5分钟
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户标识配置
|
||||
*/
|
||||
user: {
|
||||
prefix: 'doctor-', // 医生小程序使用doctor-前缀
|
||||
platform: 'doctor-miniprogram' // 平台标识(医生小程序)
|
||||
}
|
||||
};
|
||||
29
pages.json
29
pages.json
@@ -187,6 +187,13 @@
|
||||
"navigationBarTitleText": "常用方详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "prescription_v2/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "开方",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "workbench_basicInfo",
|
||||
"style": {
|
||||
@@ -394,6 +401,28 @@
|
||||
"navigationBarTitleText": "视频"
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
"root": "subPackages/sub_reception",
|
||||
"pages": [{
|
||||
"path": "reception_list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "转接方",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}, {
|
||||
"path": "reception_chat",
|
||||
"style": {
|
||||
"navigationBarTitleText": "转接方聊天",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
}, {
|
||||
"path": "reception_prescription",
|
||||
"style": {
|
||||
"navigationBarTitleText": "转接方开方",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}]
|
||||
}
|
||||
],
|
||||
"tabBar": {
|
||||
|
||||
@@ -149,6 +149,15 @@
|
||||
// 记录店铺id
|
||||
uni.setStorageSync('store_id', res.data['StoreDoctor']['store_id'])
|
||||
|
||||
// 存储doctor_id或user_id,用于WebSocket绑定
|
||||
if (res.data['ServiceUser'] && res.data['ServiceUser']['id']) {
|
||||
uni.setStorageSync('doctor_id', res.data['ServiceUser']['id']);
|
||||
uni.setStorageSync('user_id', res.data['ServiceUser']['id']);
|
||||
}
|
||||
|
||||
// 触发WebSocket初始化(登录成功后)
|
||||
uni.$emit('user-login');
|
||||
|
||||
const data = {
|
||||
store_id: uni.getStorageSync('store_id') || res.data['StoreDoctor']['store_id'] || '11001',
|
||||
}
|
||||
|
||||
@@ -121,6 +121,15 @@
|
||||
uni.setStorageSync('token', res.data['token'])
|
||||
uni.setStorageSync('logInfo', res.data['user'])
|
||||
|
||||
// 存储doctor_id或user_id,用于WebSocket绑定
|
||||
if (res.data['user'] && res.data['user']['id']) {
|
||||
uni.setStorageSync('doctor_id', res.data['user']['id']);
|
||||
uni.setStorageSync('user_id', res.data['user']['id']);
|
||||
}
|
||||
|
||||
// 触发WebSocket初始化(注册成功后)
|
||||
uni.$emit('user-login');
|
||||
|
||||
setTimeout(() => {
|
||||
uni.showToast({
|
||||
title: '开始注册',
|
||||
|
||||
60
request/api/im.js
Normal file
60
request/api/im.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { req } from '@/common/js/index.js';
|
||||
|
||||
// 发送消息
|
||||
export async function sendToUserApi(params) {
|
||||
return req.request({
|
||||
url: 'imApi/send-to-user',
|
||||
method: 'POST',
|
||||
data: params,
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取房间聊天记录
|
||||
export async function getMessagesByRoomIdApi(params) {
|
||||
return req.request({
|
||||
url: 'chat-friends/messages-by-room-id',
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
}
|
||||
|
||||
// 获取聊天注册信息
|
||||
export async function getChatRegisterInfoApi(params) {
|
||||
return req.request({
|
||||
url: 'chat-friends/chat-register-info',
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
}
|
||||
|
||||
// 上传聊天文件
|
||||
export function upLoadChatFileApi(params) {
|
||||
return req.request({
|
||||
url: 'chat-friends/upload-chat-file',
|
||||
method: 'UPLOAD',
|
||||
filePath: params.filePath,
|
||||
name: params.name || 'file',
|
||||
formData: params.formData || {}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取医生信息
|
||||
export async function getDoctorInfoApi(params) {
|
||||
return req.request({
|
||||
url: 'chat-friends/get-doctor-info',
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
}
|
||||
|
||||
// 获取房间状态
|
||||
export async function getRoomStatusApi(params) {
|
||||
return req.request({
|
||||
url: 'chat-friends/get-room-status',
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
}
|
||||
173
store/chat/chat.js
Normal file
173
store/chat/chat.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import { getChatRegisterInfoApi, getMessagesByRoomIdApi } from "@/request/api/im";
|
||||
|
||||
/**
|
||||
* 聊天消息管理器(简化版)
|
||||
*/
|
||||
const ChatManager = {
|
||||
currentRoomId: null,
|
||||
lastMessageId: 0,
|
||||
unreadCounts: {},
|
||||
messageCache: {},
|
||||
roomMessages: {},
|
||||
|
||||
enterRoom(roomId) {
|
||||
this.currentRoomId = roomId;
|
||||
uni.setStorageSync('currentRoomId', roomId);
|
||||
this.resetUnreadCount(roomId);
|
||||
},
|
||||
|
||||
leaveRoom() {
|
||||
this.currentRoomId = null;
|
||||
uni.removeStorageSync('currentRoomId');
|
||||
},
|
||||
|
||||
getCurrentRoomId() {
|
||||
return this.currentRoomId || uni.getStorageSync('currentRoomId');
|
||||
},
|
||||
|
||||
increaseUnreadCount(roomId) {
|
||||
if (this.getCurrentRoomId() !== roomId) {
|
||||
this.unreadCounts[roomId] = (this.unreadCounts[roomId] || 0) + 1;
|
||||
this.notifyUnreadChange();
|
||||
}
|
||||
},
|
||||
|
||||
resetUnreadCount(roomId) {
|
||||
if (this.unreadCounts[roomId]) {
|
||||
this.unreadCounts[roomId] = 0;
|
||||
this.notifyUnreadChange();
|
||||
}
|
||||
},
|
||||
|
||||
getUnreadCount(roomId) {
|
||||
return this.unreadCounts[roomId] || 0;
|
||||
},
|
||||
|
||||
getAllUnreadCount() {
|
||||
return Object.values(this.unreadCounts).reduce((sum, count) => sum + count, 0);
|
||||
},
|
||||
|
||||
notifyUnreadChange() {
|
||||
uni.$emit('unread-message-update', {
|
||||
total: this.getAllUnreadCount(),
|
||||
byRoom: { ...this.unreadCounts }
|
||||
});
|
||||
},
|
||||
|
||||
addMessageToRoom(roomId, message) {
|
||||
if (!this.roomMessages[roomId]) {
|
||||
this.roomMessages[roomId] = [];
|
||||
}
|
||||
this.roomMessages[roomId].push(message);
|
||||
},
|
||||
|
||||
getRoomMessages(roomId) {
|
||||
let that = this;
|
||||
this.roomMessages[roomId] = [];
|
||||
getMessagesByRoomIdApi({
|
||||
room_id: roomId,
|
||||
last_message_id: 0,
|
||||
}).then((res) => {
|
||||
if (res && res.data && res.data.result) {
|
||||
that.roomMessages[roomId] = that.checkMessage(res.data.result.list || []);
|
||||
that.lastMessageId = res.data.result.last_message_id || 0;
|
||||
uni.$emit('load-room-message', 1);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取消息失败:', err);
|
||||
});
|
||||
return this.roomMessages[roomId] || [];
|
||||
},
|
||||
|
||||
checkMessage(message, type = 0) {
|
||||
if (type === 1) {
|
||||
return {
|
||||
id: message.id,
|
||||
sender_user_id: message.sender_user_id,
|
||||
receiver_user_id: message.receiver_user_id,
|
||||
message_type: message.message_type,
|
||||
message_content: message.message_content,
|
||||
duration: message.duration,
|
||||
room_id: message.room_id,
|
||||
created_at: message.created_at || message.send_time,
|
||||
created_at_text: message.created_at_text || this.formatTime(message.send_time || message.created_at),
|
||||
timestamp: message.send_time || message.created_at
|
||||
};
|
||||
}
|
||||
return message.map((item) => {
|
||||
const result = {
|
||||
id: item.id,
|
||||
sender_user_id: item.sender_user_id,
|
||||
receiver_user_id: item.receiver_user_id,
|
||||
message_type: item.message_type,
|
||||
message_content: item.message_content,
|
||||
duration: item.duration,
|
||||
room_id: item.room_id,
|
||||
created_at: item.created_at || item.send_time,
|
||||
created_at_text: item.created_at_text || this.formatTime(item.send_time || item.created_at),
|
||||
timestamp: item.send_time || item.created_at
|
||||
};
|
||||
|
||||
if (item.message_type === 10) {
|
||||
this.getChatRegisterInfo(result, message.indexOf(item));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
},
|
||||
|
||||
formatTime(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp * 1000);
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
},
|
||||
|
||||
getChatRegisterInfo(message, index) {
|
||||
const parsedContent = this.parseMessageContent(message.message_content);
|
||||
if (parsedContent._loaded || !parsedContent.id) return;
|
||||
|
||||
getChatRegisterInfoApi({ id: parsedContent.id, message_id: message.id || '' }).then((res) => {
|
||||
if (res && res.data && res.data.result) {
|
||||
const resultWithFlag = { ...res.data.result, _loaded: true };
|
||||
const roomId = message.room_id;
|
||||
if (this.roomMessages[roomId] && this.roomMessages[roomId][index]) {
|
||||
this.roomMessages[roomId][index].message_content = JSON.stringify(resultWithFlag);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
parseMessageContent(content) {
|
||||
try {
|
||||
return typeof content === 'object' ? content : JSON.parse(content);
|
||||
} catch (e) {
|
||||
return content;
|
||||
}
|
||||
},
|
||||
|
||||
loadHistory(roomId) {
|
||||
getMessagesByRoomIdApi({
|
||||
room_id: roomId,
|
||||
last_message_id: this.lastMessageId,
|
||||
}).then((res) => {
|
||||
if (res && res.data && res.data.result) {
|
||||
const newMessages = this.checkMessage(res.data.result.list || []);
|
||||
if (newMessages.length > 0) {
|
||||
this.roomMessages[roomId] = [...newMessages, ...(this.roomMessages[roomId] || [])];
|
||||
this.lastMessageId = res.data.result.last_message_id || 0;
|
||||
uni.$emit('load-room-message', 0);
|
||||
} else {
|
||||
uni.$emit('no-more-history');
|
||||
}
|
||||
} else {
|
||||
uni.$emit('no-more-history');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('加载历史消息失败:', err);
|
||||
uni.$emit('no-more-history');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export { ChatManager };
|
||||
@@ -1,7 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import WebSocket from '@/common/common.js';
|
||||
import Utils from '@/common/utils.js';
|
||||
import { disconnectWebSocket } from '@/utils/ws/initWebSocket.js';
|
||||
|
||||
import {
|
||||
registerList
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
Vue.use(Vuex)
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
// url: 'wss://wss.nbxlmy.com/ws',
|
||||
webSocket: null,
|
||||
utils: null,
|
||||
consult: [], //咨询会话列表
|
||||
docSession: [], //医生首页消息会话列表
|
||||
@@ -25,37 +23,37 @@ const store = new Vuex.Store({
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* 登出
|
||||
* 职责:断开WebSocket连接,清理状态
|
||||
*/
|
||||
logout({
|
||||
state,
|
||||
dispatch
|
||||
}) {
|
||||
dispatch('closeWebSocket')
|
||||
// state.webSocket.logout()
|
||||
dispatch('closeWebSocket');
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化(保留用于其他初始化逻辑)
|
||||
* 注意:WebSocket初始化已在App.vue中统一处理
|
||||
*/
|
||||
init({
|
||||
state,
|
||||
dispatch
|
||||
}) {
|
||||
console.log('这里是websocket init!!!!')
|
||||
// 拿到存储
|
||||
let userInfo = uni.getStorageSync('userInfo')
|
||||
if (userInfo) {
|
||||
console.log(1);
|
||||
// 连接socket
|
||||
state.webSocket = new WebSocket({
|
||||
url: state.url
|
||||
})
|
||||
// state.utils = new Utils()
|
||||
// 获取会话列表
|
||||
// dispatch('getSessionList')
|
||||
}
|
||||
// WebSocket初始化已移至App.vue,这里保留其他初始化逻辑
|
||||
// state.utils = new Utils()
|
||||
// 获取会话列表
|
||||
// dispatch('getSessionList')
|
||||
},
|
||||
closeWebSocket({
|
||||
state
|
||||
}) {
|
||||
if (state.webSocket) {
|
||||
state.webSocket.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭WebSocket
|
||||
* 职责:断开WebSocket连接
|
||||
*/
|
||||
closeWebSocket() {
|
||||
disconnectWebSocket();
|
||||
},
|
||||
// 获取挂号信息
|
||||
getConsult({
|
||||
|
||||
408
subPackages/sub_reception/components/DiagnosisModal.vue
Normal file
408
subPackages/sub_reception/components/DiagnosisModal.vue
Normal file
@@ -0,0 +1,408 @@
|
||||
<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>
|
||||
392
subPackages/sub_reception/components/DoctorOrderModal.vue
Normal file
392
subPackages/sub_reception/components/DoctorOrderModal.vue
Normal file
@@ -0,0 +1,392 @@
|
||||
<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>
|
||||
615
subPackages/sub_reception/components/MessageBubble.vue
Normal file
615
subPackages/sub_reception/components/MessageBubble.vue
Normal file
@@ -0,0 +1,615 @@
|
||||
<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>
|
||||
<view class="card-footer"><button class="action-btn" @click="$emit('viewRegister', parsedContent.id)">查看详情</button></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;
|
||||
}
|
||||
},
|
||||
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>
|
||||
@@ -0,0 +1,382 @@
|
||||
<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>
|
||||
1135
subPackages/sub_reception/reception_chat.vue
Normal file
1135
subPackages/sub_reception/reception_chat.vue
Normal file
File diff suppressed because it is too large
Load Diff
168
subPackages/sub_reception/reception_list.vue
Normal file
168
subPackages/sub_reception/reception_list.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<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>
|
||||
980
subPackages/sub_reception/reception_prescription.vue
Normal file
980
subPackages/sub_reception/reception_prescription.vue
Normal file
@@ -0,0 +1,980 @@
|
||||
<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">
|
||||
<view class="btnText" @click="saveAsCommonPrescription">另存常用方</view>
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
@click="sendPrescription"
|
||||
shape="circle"
|
||||
:custom-style="{backgroundColor:buttonLodging?'#A5DCD2':'#6ACDBB',color:'#fff',height:'86rpx', width: '448rpx'}"
|
||||
: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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 另存常用方
|
||||
*/
|
||||
async saveAsCommonPrescription() {
|
||||
if (this.currentDrugs.length === 0) {
|
||||
uni.showToast({ title: '请先添加药品后再保存为常用方', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.showToast({ title: '另存常用方功能待实现', icon: 'none' });
|
||||
// TODO: 实现另存常用方功能
|
||||
},
|
||||
|
||||
/**
|
||||
* 返回
|
||||
*/
|
||||
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>
|
||||
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<view class="chinese-medicine-config">
|
||||
<view class="config-section">
|
||||
<view class="section-title">
|
||||
<d-text text="制剂方式" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="section-content">
|
||||
<view class="tag-group">
|
||||
<view
|
||||
class="tag-item"
|
||||
:class="{ active: localConfig.ruleType === 1 }"
|
||||
@click="handleRuleTypeChange(1)"
|
||||
>
|
||||
自制剂
|
||||
</view>
|
||||
<view
|
||||
class="tag-item"
|
||||
:class="{ active: localConfig.ruleType === 2 }"
|
||||
@click="handleRuleTypeChange(2)"
|
||||
>
|
||||
委托调剂
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自制剂配置 -->
|
||||
<view class="config-section" v-if="localConfig.ruleType === 1">
|
||||
<view class="section-title">
|
||||
<d-text text="包法" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="section-content">
|
||||
<view class="tag-group">
|
||||
<view
|
||||
v-for="item in packageMethodList"
|
||||
:key="item.id"
|
||||
class="tag-item"
|
||||
:class="{ active: localConfig.packageMethodId === item.id }"
|
||||
@click="handlePackageMethodTagClick(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 委托调剂配置 -->
|
||||
<view class="config-section" v-if="localConfig.ruleType === 2">
|
||||
<view class="section-title">
|
||||
<d-text text="制剂" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="section-content">
|
||||
<view class="tag-group">
|
||||
<view
|
||||
v-for="item in processRuleList"
|
||||
:key="item.id"
|
||||
class="tag-item"
|
||||
:class="{ active: localConfig.processRuleId === item.id }"
|
||||
@click="handleProcessRuleTagClick(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-title m-t-16">
|
||||
<d-text text="煎法" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="section-content">
|
||||
<view class="tag-group">
|
||||
<view
|
||||
v-for="item in childProcessRuleList"
|
||||
:key="item.id"
|
||||
class="tag-item"
|
||||
:class="{ active: localConfig.childProcessRuleId === item.id }"
|
||||
@click="handleChildProcessRuleTagClick(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-title m-t-16">
|
||||
<d-text text="备注" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="section-content">
|
||||
<view class="tag-group">
|
||||
<view
|
||||
v-for="item in processRuleNoteList"
|
||||
:key="item.id"
|
||||
class="tag-item"
|
||||
:class="{ active: localConfig.processRuleNoteId === item.id }"
|
||||
@click="handleProcessRuleNoteTagClick(item)"
|
||||
>
|
||||
{{ item.note || item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用量和频次 -->
|
||||
<view class="config-section">
|
||||
<view class="section-title">
|
||||
<d-text text="用量" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="section-content">
|
||||
<u-number-box v-model="localConfig.dosage" :min="1" />
|
||||
<d-text text="天" className="fs-3 color9 m-l-16"></d-text>
|
||||
<u-input disabled class="input" placeholder="剂数" v-model="localConfig.dosage" :clearable="false" type="number" />
|
||||
<d-text text="剂" className="fs-3 color9"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="config-section">
|
||||
<view class="section-title">
|
||||
<d-text text="频次" className="fs-3 main-c"></d-text>
|
||||
</view>
|
||||
<view class="section-content">
|
||||
<u-number-box v-model="localConfig.dayDosage" :min="1" />
|
||||
<d-text text="次/天" className="fs-3 color9 m-l-16"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChineseMedicineConfig',
|
||||
props: {
|
||||
/**
|
||||
* 中药配置
|
||||
*/
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
ruleType: 1,
|
||||
packageMethodId: null,
|
||||
processRuleId: null,
|
||||
childProcessRuleId: null,
|
||||
processRuleNoteId: null,
|
||||
dosage: 7,
|
||||
dayDosage: 2
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 制剂列表
|
||||
*/
|
||||
processRuleList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 煎法列表
|
||||
*/
|
||||
childProcessRuleList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 备注列表
|
||||
*/
|
||||
processRuleNoteList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localConfig: {},
|
||||
packageMethodList: [
|
||||
{ id: 1, name: '味包' },
|
||||
{ id: 2, name: '剂包' }
|
||||
]
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
config: {
|
||||
handler(newVal) {
|
||||
this.localConfig = { ...newVal };
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
},
|
||||
localConfig: {
|
||||
handler(newVal) {
|
||||
this.$emit('update:config', { ...newVal });
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 处理制剂方式改变
|
||||
* @param {number} value - 制剂方式:1=自制剂,2=委托调剂
|
||||
* 职责:更新制剂方式,触发配置更新
|
||||
*/
|
||||
handleRuleTypeChange(value) {
|
||||
this.localConfig.ruleType = value;
|
||||
// 切换时清空相关配置
|
||||
if (value === 1) {
|
||||
// 自制剂:清空委托调剂相关配置,默认选择第一个包法
|
||||
this.localConfig.processRuleId = null;
|
||||
this.localConfig.childProcessRuleId = null;
|
||||
this.localConfig.processRuleNoteId = null;
|
||||
// 如果包法未选择,默认选择第一个
|
||||
if (!this.localConfig.packageMethodId && this.packageMethodList.length > 0) {
|
||||
this.localConfig.packageMethodId = this.packageMethodList[0].id;
|
||||
}
|
||||
} else {
|
||||
// 委托调剂:清空自制剂相关配置
|
||||
this.localConfig.packageMethodId = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理包法改变(标签点击)
|
||||
* @param {Object} item - 包法项
|
||||
* 职责:更新包法ID
|
||||
*/
|
||||
handlePackageMethodTagClick(item) {
|
||||
this.localConfig.packageMethodId = item.id;
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理制剂改变(标签点击)
|
||||
* @param {Object} item - 制剂项
|
||||
* 职责:更新制剂ID,触发加载煎法列表
|
||||
*/
|
||||
handleProcessRuleTagClick(item) {
|
||||
// 清空煎法和备注ID(级联清空)
|
||||
this.localConfig.processRuleId = item.id;
|
||||
this.localConfig.childProcessRuleId = null;
|
||||
this.localConfig.processRuleNoteId = null;
|
||||
// localConfig 的变化会通过 watch 自动触发 @update:config
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理煎法改变(标签点击)
|
||||
* @param {Object} item - 煎法项
|
||||
* 职责:更新煎法ID,触发加载备注列表
|
||||
*/
|
||||
handleChildProcessRuleTagClick(item) {
|
||||
// 清空备注ID(级联清空)
|
||||
this.localConfig.childProcessRuleId = item.id;
|
||||
this.localConfig.processRuleNoteId = null;
|
||||
// localConfig 的变化会通过 watch 自动触发 @update:config
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理备注改变(标签点击)
|
||||
* @param {Object} item - 备注项
|
||||
* 职责:更新备注ID
|
||||
*/
|
||||
handleProcessRuleNoteTagClick(item) {
|
||||
this.localConfig.processRuleNoteId = item.id;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取包法名称
|
||||
* @returns {string} 包法名称
|
||||
*/
|
||||
getPackageMethodName() {
|
||||
const item = this.packageMethodList.find(item => item.id === this.localConfig.packageMethodId);
|
||||
return item?.name || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取制剂名称
|
||||
* @returns {string} 制剂名称
|
||||
*/
|
||||
getProcessRuleName() {
|
||||
const item = this.processRuleList.find(item => item.id === this.localConfig.processRuleId);
|
||||
return item?.name || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取煎法名称
|
||||
* @returns {string} 煎法名称
|
||||
*/
|
||||
getChildProcessRuleName() {
|
||||
const item = this.childProcessRuleList.find(item => item.id === this.localConfig.childProcessRuleId);
|
||||
return item?.name || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取备注名称
|
||||
* @returns {string} 备注名称
|
||||
*/
|
||||
getProcessRuleNoteName() {
|
||||
const item = this.processRuleNoteList.find(item => item.id === this.localConfig.processRuleNoteId);
|
||||
return item?.name || '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chinese-medicine-config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.tag-group {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
padding: 12rpx 32rpx;
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
border-radius: 32rpx;
|
||||
font-size: 28rpx;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tag-item.active {
|
||||
background-color: #6ACDBB;
|
||||
color: #fff;
|
||||
border-color: #6ACDBB;
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
padding: 16rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8rpx;
|
||||
min-width: 200rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 200rpx;
|
||||
}
|
||||
|
||||
.m-t-16 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-l-16 {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.color9 {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,729 @@
|
||||
<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="chinese-medicine-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-search
|
||||
v-model="searchKey"
|
||||
placeholder="请输入药品名称搜索"
|
||||
@input="handleSearch"
|
||||
@custom="searchKey = ''; handleSearch()"
|
||||
:show-action="false"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 药品列表(可滚动,支持触底加载) -->
|
||||
<scroll-view
|
||||
class="drug-list-scroll"
|
||||
scroll-y
|
||||
@scrolltolower="loadMore"
|
||||
:lower-threshold="100"
|
||||
>
|
||||
<view class="drug-list" v-if="drugList.length > 0">
|
||||
<view
|
||||
class="drug-item white b-r-8 p-32 m-t-2"
|
||||
v-for="(item, index) in drugList"
|
||||
:key="item.id || index"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
>
|
||||
<view class="drug-header flex-row flex-jus-sp flex-ali-center">
|
||||
<d-text :text="getDrugName(item)" className="fs-3 color0 bold"></d-text>
|
||||
<d-text :text="`¥${parseFloat(item.price || 0).toFixed(2)}/${getUnitName(item)}`" className="fs-3 error-c"></d-text>
|
||||
</view>
|
||||
<view class="drug-info m-t-16">
|
||||
<view class="specification-row flex-row flex-jus-sp flex-ali-center m-b-16">
|
||||
<d-text :text="`规格:${getSpecification(item)}`" className="fs-24 color9"></d-text>
|
||||
<view class="selected-tag" v-if="isSelected(item)">已选</view>
|
||||
</view>
|
||||
<view class="common-quantity-tags m-b-16" v-if="commonQuantities.length > 0">
|
||||
<view
|
||||
class="quantity-tag"
|
||||
v-for="qty in commonQuantities"
|
||||
:key="qty"
|
||||
@click.stop="handleSelectCommonQuantity"
|
||||
:data-index="index"
|
||||
:data-quantity="qty"
|
||||
>
|
||||
{{ qty }}g
|
||||
</view>
|
||||
</view>
|
||||
<view class="quantity-input-row flex-row flex-ali-center">
|
||||
<view class="quantity-label">用量:</view>
|
||||
<view class="quantity-input-wrapper flex-row flex-ali-center">
|
||||
<view
|
||||
class="quantity-btn"
|
||||
@click.stop="handleDecreaseQuantity"
|
||||
:data-index="index"
|
||||
>-</view>
|
||||
<input
|
||||
class="quantity-input"
|
||||
type="number"
|
||||
:value="getQuantity(item)"
|
||||
@input="handleQuantityInput"
|
||||
:data-index="index"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
<view
|
||||
class="quantity-btn"
|
||||
@click.stop="handleIncreaseQuantity"
|
||||
:data-index="index"
|
||||
>+</view>
|
||||
</view>
|
||||
<text class="unit-text">{{ getUnitName(item) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" v-if="loading">
|
||||
<text class="load-more-text">加载中...</text>
|
||||
</view>
|
||||
<view class="load-more" v-else-if="!hasMore && drugList.length > 0">
|
||||
<text class="load-more-text">没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无药品"></d-empty>
|
||||
</view>
|
||||
</scroll-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 { getProductListDoctorReception } from '@/api/reception.js';
|
||||
|
||||
export default {
|
||||
name: 'ChineseMedicineModal',
|
||||
props: {
|
||||
/**
|
||||
* 弹窗显示状态
|
||||
*/
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 当前已选中的药品列表
|
||||
*/
|
||||
currentDrugs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
searchKey: '',
|
||||
drugList: [],
|
||||
selectedDrugs: [],
|
||||
loading: false,
|
||||
searchTimer: null,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: false,
|
||||
// 常用克数(后端返回,暂时硬编码)
|
||||
commonQuantities: [5, 8, 10]
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
if (newVal) {
|
||||
this.selectedDrugs = JSON.parse(JSON.stringify(this.currentDrugs));
|
||||
this.resetList();
|
||||
}
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetList() {
|
||||
this.searchKey = '';
|
||||
this.drugList = [];
|
||||
this.page = 1;
|
||||
this.hasMore = false;
|
||||
},
|
||||
/**
|
||||
* 加载药品列表
|
||||
* @param {string} keyword - 搜索关键词
|
||||
* 职责:调用接口获取中药列表
|
||||
*/
|
||||
async loadDrugList(keyword = '') {
|
||||
const name = (keyword || '').trim();
|
||||
if (!name) {
|
||||
this.drugList = [];
|
||||
this.loading = false;
|
||||
this.hasMore = false;
|
||||
return;
|
||||
}
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const storeId = uni.getStorageSync('store_id') || 11001;
|
||||
const res = await getProductListDoctorReception({
|
||||
store_id: storeId,
|
||||
type: 1, // 中药
|
||||
name: name,
|
||||
page: this.page,
|
||||
page_size: this.pageSize
|
||||
});
|
||||
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
const list = res.data || res.result || [];
|
||||
this.hasMore = list.length >= this.pageSize;
|
||||
// 合并已选中的药品数量,使用 $set 确保响应式
|
||||
const merged = list.map(drug => {
|
||||
const selected = this.selectedDrugs.find(item => (item.id || item.drug_id) === drug.id);
|
||||
// 直接使用原对象,使用 $set 添加响应式属性
|
||||
this.$set(drug, '_quantity', selected ? selected.number : 0);
|
||||
return drug;
|
||||
});
|
||||
if (this.page > 1) {
|
||||
this.drugList = this.drugList.concat(merged);
|
||||
} else {
|
||||
this.drugList = merged;
|
||||
}
|
||||
// 确保所有药品都有 _quantity 属性
|
||||
this.drugList.forEach(drug => {
|
||||
if (typeof drug._quantity === 'undefined') {
|
||||
this.$set(drug, '_quantity', 0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (this.page === 1) {
|
||||
this.drugList = [];
|
||||
}
|
||||
this.hasMore = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取药品列表失败:', error);
|
||||
this.$toast('获取药品列表失败');
|
||||
if (this.page === 1) {
|
||||
this.drugList = [];
|
||||
}
|
||||
this.hasMore = false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索药品
|
||||
* 职责:防抖搜索,调用加载药品列表
|
||||
*/
|
||||
handleSearch() {
|
||||
clearTimeout(this.searchTimer);
|
||||
this.searchTimer = setTimeout(() => {
|
||||
const name = (this.searchKey || '').trim();
|
||||
if (!name) {
|
||||
this.page = 1;
|
||||
this.hasMore = false;
|
||||
this.drugList = [];
|
||||
return;
|
||||
}
|
||||
this.page = 1;
|
||||
this.hasMore = true;
|
||||
this.loadDrugList(this.searchKey);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取药品数量
|
||||
* @param {Object} drug - 药品数据
|
||||
* @returns {number} 数量
|
||||
*/
|
||||
getQuantity(drug) {
|
||||
return drug._quantity || 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取药品名称
|
||||
* @param {Object} drug - 药品数据
|
||||
* @returns {string} 药品名称
|
||||
*/
|
||||
getDrugName(drug) {
|
||||
if (drug.drug && drug.drug.drug_name) {
|
||||
return drug.drug.drug_name;
|
||||
}
|
||||
if (drug.drug_name) {
|
||||
return drug.drug_name;
|
||||
}
|
||||
if (drug.name) {
|
||||
return drug.name;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取单位名称
|
||||
* @param {Object} drug - 药品数据
|
||||
* @returns {string} 单位名称
|
||||
*/
|
||||
getUnitName(drug) {
|
||||
if (drug.drug && drug.drug.unit && drug.drug.unit.name) {
|
||||
return drug.drug.unit.name;
|
||||
}
|
||||
if (drug.unit && drug.unit.name) {
|
||||
return drug.unit.name;
|
||||
}
|
||||
return 'g';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取规格
|
||||
* @param {Object} drug - 药品数据
|
||||
* @returns {string} 规格
|
||||
*/
|
||||
getSpecification(drug) {
|
||||
if (drug.drug && drug.drug.specification) {
|
||||
return drug.drug.specification;
|
||||
}
|
||||
if (drug.specification) {
|
||||
return drug.specification;
|
||||
}
|
||||
return '--';
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断是否已选中该中药
|
||||
* @param {Object} drug
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSelected(drug) {
|
||||
if (!this.currentDrugs || !drug) return false;
|
||||
const id = drug.id || drug.drug_id;
|
||||
return this.currentDrugs.some(item => (item.id || item.drug_id) === id);
|
||||
},
|
||||
|
||||
/**
|
||||
* 增加数量
|
||||
* @param {Event} event - 点击事件对象
|
||||
* 职责:增加药品数量
|
||||
*/
|
||||
handleIncreaseQuantity(event) {
|
||||
const index = event.currentTarget.dataset.index;
|
||||
console.log('handleIncreaseQuantity called', { index, drugListLength: this.drugList.length });
|
||||
|
||||
if (index === undefined || index === null) {
|
||||
console.error('handleIncreaseQuantity: index is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const drug = this.drugList[index];
|
||||
console.log('drug found', drug);
|
||||
|
||||
if (!drug) {
|
||||
console.error('handleIncreaseQuantity: drug not found at index', index);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保 _quantity 属性存在
|
||||
if (typeof drug._quantity === 'undefined') {
|
||||
this.$set(drug, '_quantity', 0);
|
||||
}
|
||||
this.$set(drug, '_quantity', (drug._quantity || 0) + 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* 减少数量
|
||||
* @param {Event} event - 点击事件对象
|
||||
* 职责:减少药品数量,如果为0则从选中列表移除
|
||||
*/
|
||||
handleDecreaseQuantity(event) {
|
||||
const index = event.currentTarget.dataset.index;
|
||||
console.log('handleDecreaseQuantity called', { index, drugListLength: this.drugList.length });
|
||||
|
||||
if (index === undefined || index === null) {
|
||||
console.error('handleDecreaseQuantity: index is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const drug = this.drugList[index];
|
||||
console.log('drug found', drug);
|
||||
|
||||
if (!drug) {
|
||||
console.error('handleDecreaseQuantity: drug not found at index', index);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保 _quantity 属性存在
|
||||
if (typeof drug._quantity === 'undefined') {
|
||||
this.$set(drug, '_quantity', 0);
|
||||
return;
|
||||
}
|
||||
if (drug._quantity > 0) {
|
||||
this.$set(drug, '_quantity', drug._quantity - 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理数量输入
|
||||
* @param {Event} event - 输入事件对象
|
||||
* 职责:更新药品数量
|
||||
*/
|
||||
handleQuantityInput(event) {
|
||||
const index = event.currentTarget.dataset.index;
|
||||
console.log('handleQuantityInput called', { index, drugListLength: this.drugList.length });
|
||||
|
||||
if (index === undefined || index === null) {
|
||||
console.error('handleQuantityInput: index is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const drug = this.drugList[index];
|
||||
console.log('drug found', drug);
|
||||
|
||||
if (!drug) {
|
||||
console.error('handleQuantityInput: drug not found at index', index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event || !event.detail) {
|
||||
console.error('handleQuantityInput: event or event.detail is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const value = event.detail.value;
|
||||
const numValue = parseFloat(value);
|
||||
|
||||
// 确保 _quantity 属性存在
|
||||
if (typeof drug._quantity === 'undefined') {
|
||||
this.$set(drug, '_quantity', 0);
|
||||
}
|
||||
|
||||
if (!isNaN(numValue) && numValue >= 0) {
|
||||
this.$set(drug, '_quantity', numValue);
|
||||
} else if (value === '') {
|
||||
this.$set(drug, '_quantity', 0);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择常用克数
|
||||
* @param {Event} event - 点击事件对象
|
||||
* 职责:将常用克数写入到药品数量
|
||||
*/
|
||||
handleSelectCommonQuantity(event) {
|
||||
const index = event.currentTarget.dataset.index;
|
||||
const quantity = parseFloat(event.currentTarget.dataset.quantity);
|
||||
console.log('handleSelectCommonQuantity called', { index, quantity, drugListLength: this.drugList.length });
|
||||
|
||||
if (index === undefined || index === null) {
|
||||
console.error('handleSelectCommonQuantity: index is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(quantity)) {
|
||||
console.error('handleSelectCommonQuantity: quantity is NaN', event.currentTarget.dataset.quantity);
|
||||
return;
|
||||
}
|
||||
|
||||
const drug = this.drugList[index];
|
||||
console.log('drug found', drug);
|
||||
|
||||
if (!drug) {
|
||||
console.error('handleSelectCommonQuantity: drug not found at index', index);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保 _quantity 属性存在
|
||||
if (typeof drug._quantity === 'undefined') {
|
||||
this.$set(drug, '_quantity', 0);
|
||||
}
|
||||
// 设置数量
|
||||
this.$set(drug, '_quantity', quantity);
|
||||
},
|
||||
|
||||
/**
|
||||
* 触底加载更多
|
||||
*/
|
||||
loadMore() {
|
||||
const name = (this.searchKey || '').trim();
|
||||
if (this.loading || !this.hasMore || !name) {
|
||||
return;
|
||||
}
|
||||
this.page += 1;
|
||||
this.loadDrugList(this.searchKey);
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认选择
|
||||
* 职责:将选中的药品传递给父组件
|
||||
*/
|
||||
handleConfirm() {
|
||||
// 过滤出数量大于0的药品
|
||||
// 参考PC端:index_id 使用 drug.id(列表项ID),id 使用 drug.drug.id(药品本身的ID)
|
||||
const selected = this.drugList
|
||||
.filter(drug => drug._quantity > 0)
|
||||
.map(drug => {
|
||||
// 获取药品名称
|
||||
let drugName = '';
|
||||
if (drug.drug && drug.drug.drug_name) {
|
||||
drugName = drug.drug.drug_name;
|
||||
} else if (drug.drug_name) {
|
||||
drugName = drug.drug_name;
|
||||
} else if (drug.name) {
|
||||
drugName = drug.name;
|
||||
}
|
||||
|
||||
// 参考PC端数据结构:index_id, id, drug_name, number, price, way_id, select_number
|
||||
return {
|
||||
index_id: drug.id, // 列表项ID(对应PC端的 data.id)
|
||||
id: drug.drug?.id || drug.id, // 药品本身的ID(对应PC端的 data.drug.id)
|
||||
drug_name: drugName,
|
||||
number: drug._quantity,
|
||||
price: drug.price || 0,
|
||||
way_id: drug.drug?.way_id || drug.way_id || 0, // 用法ID(对应PC端的 data.drug?.way_id)
|
||||
select_number: 1 // 默认为1(与PC端一致)
|
||||
};
|
||||
});
|
||||
|
||||
this.$emit('select', selected);
|
||||
this.handleClose();
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
* 职责:关闭弹窗,触发input事件
|
||||
*/
|
||||
handleClose() {
|
||||
this.show = false;
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chinese-medicine-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
overflow: hidden; // 防止整体溢出
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
flex: 1;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; // 防止内容溢出
|
||||
}
|
||||
|
||||
.drug-list-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto; // 确保可以滚动
|
||||
min-height: 0; // 重要:允许 flex 子元素缩小
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.drug-list {
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.drug-item {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.drug-item.selected {
|
||||
border: 2rpx solid #6ACDBB;
|
||||
}
|
||||
|
||||
.drug-header {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.drug-info {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.specification-row {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.selected-tag {
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 32rpx;
|
||||
border: 1rpx solid #6ACDBB;
|
||||
color: #6ACDBB;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.m-b-16 {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.common-quantity-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.quantity-tag {
|
||||
padding: 8rpx 24rpx;
|
||||
background-color: #f0f5ff;
|
||||
color: #6ACDBB;
|
||||
border: 1rpx solid #6ACDBB;
|
||||
border-radius: 32rpx;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quantity-tag:active {
|
||||
background-color: #6ACDBB;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.quantity-input-row {
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quantity-label {
|
||||
font-size: 28rpx;
|
||||
color: #6C7380;
|
||||
}
|
||||
|
||||
.quantity-input-wrapper {
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quantity-btn {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
line-height: 48rpx;
|
||||
text-align: center;
|
||||
border: 1rpx solid #6ACDBB;
|
||||
color: #6ACDBB;
|
||||
border-radius: 8rpx;
|
||||
font-size: 32rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
width: 120rpx;
|
||||
height: 48rpx;
|
||||
line-height: 48rpx;
|
||||
text-align: center;
|
||||
border: 1rpx solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
padding: 0 16rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.unit-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.white {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.b-r-8 {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.p-32 {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.m-t-2 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-t-16 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-jus-sp {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-ali-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color0 {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.color9 {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,487 @@
|
||||
<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="common-prescription-modal">
|
||||
<view class="modal-header">
|
||||
<d-text text="常用方" className="fs-32 main-c"></d-text>
|
||||
</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="getItemKey(item, index)"
|
||||
@click="handleSelectPrescription(item)"
|
||||
>
|
||||
<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)"
|
||||
: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)" class="btn right">使用</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无常用方"></d-empty>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="modal-footer" v-if="!isSelectMode">
|
||||
<u-button
|
||||
@click="handleClose"
|
||||
:custom-style="{ backgroundColor: '#f5f5f5', color: '#333' }"
|
||||
>取消</u-button>
|
||||
<u-button
|
||||
@click="handleAddNew"
|
||||
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff' }"
|
||||
>新建常用方</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCommonPrescriptionListApi } from '@/api/reception.js';
|
||||
|
||||
export default {
|
||||
name: 'CommonPrescriptionModal',
|
||||
props: {
|
||||
/**
|
||||
* 弹窗显示状态
|
||||
*/
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 处方类型:1=中药,2=西药,3=保健食品
|
||||
*/
|
||||
prescriptionType: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
/**
|
||||
* 是否为选择模式(选择模式不显示删除按钮)
|
||||
*/
|
||||
isSelectMode: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
prescriptionList: [],
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* 获取标签类名(计算属性)
|
||||
* @returns {string} 标签类名
|
||||
*/
|
||||
tagClass() {
|
||||
if (this.prescriptionType === 1) {
|
||||
return 'tag-chinese';
|
||||
} else if (this.prescriptionType === 2) {
|
||||
return 'tag-west';
|
||||
} else {
|
||||
return 'tag-granular';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取标签文本(计算属性)
|
||||
* @returns {string} 标签文本
|
||||
*/
|
||||
tagText() {
|
||||
if (this.prescriptionType === 1) {
|
||||
return '中药饮片';
|
||||
} else if (this.prescriptionType === 2) {
|
||||
return '西药(中成)';
|
||||
} else {
|
||||
return '保健食品';
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
if (newVal) {
|
||||
this.loadPrescriptionList();
|
||||
}
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载常用方列表
|
||||
* 职责:调用接口获取常用方列表
|
||||
*/
|
||||
async loadPrescriptionList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const storeId = uni.getStorageSync('store_id') || 11001;
|
||||
const type = this.getCommonPrescriptionType(this.prescriptionType);
|
||||
const res = await getCommonPrescriptionListApi(storeId);
|
||||
|
||||
if (res && res.code === 0) {
|
||||
// 根据类型过滤常用方
|
||||
const listKey = this.getListKey(type);
|
||||
this.prescriptionList = res.result?.[listKey] || [];
|
||||
} else {
|
||||
this.prescriptionList = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取常用方列表失败:', error);
|
||||
this.$toast('获取常用方列表失败');
|
||||
this.prescriptionList = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取常用方类型字符串
|
||||
* @param {number} type - 处方类型
|
||||
* @returns {string} 常用方类型:west、chinese、granular
|
||||
*/
|
||||
getCommonPrescriptionType(type) {
|
||||
if (type === 1) {
|
||||
return 'chinese';
|
||||
} else if (type === 2) {
|
||||
return 'west';
|
||||
} else {
|
||||
return 'granular';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取列表键名
|
||||
* @param {string} type - 常用方类型
|
||||
* @returns {string} 列表键名
|
||||
*/
|
||||
getListKey(type) {
|
||||
if (type === 'chinese') {
|
||||
return 'chin_prescription';
|
||||
} else if (type === 'west') {
|
||||
return 'west_prescription';
|
||||
} else {
|
||||
return 'granular_prescription';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取药品列表
|
||||
* @param {Object} prescription - 常用方数据
|
||||
* @returns {Array} 药品列表
|
||||
*/
|
||||
getDrugList(prescription) {
|
||||
// 根据常用方类型获取对应的药品列表
|
||||
const type = this.getCommonPrescriptionType(this.prescriptionType);
|
||||
const listKey = type === 'chinese' ? 'chinese' : type === 'west' ? 'west' : 'granular';
|
||||
|
||||
// 从常用方数据中获取药品列表
|
||||
// 这里需要根据实际数据结构调整
|
||||
return prescription.drugs || prescription[listKey] || [];
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化药品名称
|
||||
* @param {Object} drug - 药品数据
|
||||
* @returns {string} 格式化后的药品名称
|
||||
*/
|
||||
formatDrugName(drug) {
|
||||
if (typeof drug === 'string') {
|
||||
try {
|
||||
drug = JSON.parse(drug);
|
||||
} catch (e) {
|
||||
return drug;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.prescriptionType === 1) {
|
||||
// 中药
|
||||
const name = drug.drug_name || drug.name || '';
|
||||
const number = drug.number || 1;
|
||||
const unit = drug.unit?.name || 'g';
|
||||
return `${name} ${number}${unit}`;
|
||||
} else {
|
||||
// 西药或其他
|
||||
const name = drug.drug_name || drug.name || '';
|
||||
const number = drug.number || drug.select_number || 1;
|
||||
return `${name} *${number}盒`;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取列表项的唯一key
|
||||
* @param {Object} item - 列表项
|
||||
* @param {number} index - 索引
|
||||
* @returns {string|number} 唯一key
|
||||
*/
|
||||
getItemKey(item, index) {
|
||||
return item.id || `item_${index}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择常用方
|
||||
* @param {Object} prescription - 常用方数据
|
||||
* 职责:触发select事件,传递选中的常用方
|
||||
*/
|
||||
handleSelectPrescription(prescription) {
|
||||
this.$emit('select', prescription);
|
||||
this.handleClose();
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除常用方
|
||||
* @param {Object} prescription - 常用方数据
|
||||
* @param {number} index - 索引
|
||||
* 职责:调用接口删除常用方
|
||||
*/
|
||||
async handleDeletePrescription(prescription, index) {
|
||||
uni.showModal({
|
||||
title: '删除常用方',
|
||||
content: '是否要删除该常用方?',
|
||||
showCancel: true,
|
||||
success: async ({ confirm }) => {
|
||||
if (confirm) {
|
||||
try {
|
||||
// TODO: 调用删除接口
|
||||
// await deleteCommonPrescriptionApi(prescription.id);
|
||||
this.$toast('删除成功');
|
||||
this.prescriptionList.splice(index, 1);
|
||||
} catch (error) {
|
||||
console.error('删除常用方失败:', error);
|
||||
this.$toast('删除失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 新建常用方
|
||||
* 职责:关闭弹窗,触发新建事件
|
||||
*/
|
||||
handleAddNew() {
|
||||
this.handleClose();
|
||||
// 可以触发新建事件,让父组件处理
|
||||
// this.$emit('add-new');
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
* 职责:关闭弹窗,触发input事件
|
||||
*/
|
||||
handleClose() {
|
||||
this.show = false;
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.common-prescription-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;
|
||||
}
|
||||
|
||||
.prescription-list {
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.prescription-item {
|
||||
padding: 32rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
position: relative;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 48rpx;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
|
||||
&.tag-chinese {
|
||||
background: rgba(15, 190, 22, 0.55);
|
||||
}
|
||||
|
||||
&.tag-west {
|
||||
background: rgba(15, 163, 190, 0.55);
|
||||
}
|
||||
|
||||
&.tag-granular {
|
||||
background: #6ACDBB;
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #6C7380;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.drug-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.drug-item {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
padding: 8rpx 16rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.btnBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1rpx solid rgba(242, 242, 242, 1);
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 86rpx;
|
||||
line-height: 86rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
|
||||
&.left {
|
||||
border-right: 1rpx solid rgba(242, 242, 242, 1);
|
||||
color: #FC3636;
|
||||
}
|
||||
|
||||
&.right {
|
||||
color: #6ACDBB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.white {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.b-r-8 {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.m-t-2 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-t-16 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-t-24 {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-jus-sp {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-ali-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color9 {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,517 @@
|
||||
<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>
|
||||
<scroll-view
|
||||
class="tag-container-scroll"
|
||||
scroll-y
|
||||
@scrolltolower="loadMore"
|
||||
:lower-threshold="100"
|
||||
>
|
||||
<view class="tag-container">
|
||||
<view
|
||||
v-for="item in allDiagnosisList"
|
||||
: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="load-more" v-if="loading">
|
||||
<text class="load-more-text">加载中...</text>
|
||||
</view>
|
||||
<view class="load-more" v-else-if="!hasMore && allDiagnosisList.length > 0">
|
||||
<text class="load-more-text">没有更多了</text>
|
||||
</view>
|
||||
</scroll-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, getMyDiseaseList, addMyDisease, deleteMyDisease } from '@/api/reception.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: 20,
|
||||
hasMore: false,
|
||||
loading: false,
|
||||
searchTimer: null,
|
||||
searchStyle: {
|
||||
fontSize: '28rpx',
|
||||
backgroundColor: '#F3F4F5',
|
||||
borderRadius: '66rpx',
|
||||
height: '66rpx',
|
||||
padding: '8rpx 32rpx'
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
if (newVal) {
|
||||
this.selectDiagnosisList = this.selectedDiagnosis || '';
|
||||
this.searchKey = '';
|
||||
this.allDiagnosisList = [];
|
||||
this.currentPage = 1;
|
||||
this.hasMore = false;
|
||||
// 只加载常用诊断列表,不自动加载搜索列表
|
||||
this.loadDoctorMyDiseaseList();
|
||||
}
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载诊断列表
|
||||
* @param {string} searchKey - 搜索关键词
|
||||
* @param {number} page - 页码
|
||||
* @param {boolean} append - 是否追加数据
|
||||
* 职责:调用接口获取诊断列表
|
||||
*/
|
||||
async loadDiagnosisList(searchKey = '', page = 1, append = false) {
|
||||
try {
|
||||
const res = await getDiseaseList(searchKey, page, this.pageSize);
|
||||
let list = [];
|
||||
let hasMore = false;
|
||||
|
||||
// 正确解析响应数据
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
// 优先使用 data,然后是 result
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
list = res.data;
|
||||
} else if (res.result && Array.isArray(res.result)) {
|
||||
list = res.result;
|
||||
} else if (Array.isArray(res)) {
|
||||
list = res;
|
||||
}
|
||||
|
||||
// 从响应中获取分页信息
|
||||
if (res.pagination) {
|
||||
hasMore = res.pagination.has_more || false;
|
||||
} else if (res.has_more !== undefined) {
|
||||
hasMore = res.has_more;
|
||||
} else {
|
||||
// 如果没有分页信息,根据返回数量判断
|
||||
hasMore = list.length >= this.pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
const processedList = list.map((item) => {
|
||||
item.isSelect = 0;
|
||||
item.isInMyList = this.doctorMyDiseaseList.some(
|
||||
(myItem) => myItem.disease && 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;
|
||||
});
|
||||
|
||||
if (append) {
|
||||
this.allDiagnosisList = [...this.allDiagnosisList, ...processedList];
|
||||
} else {
|
||||
this.allDiagnosisList = processedList;
|
||||
this.currentPage = 1;
|
||||
}
|
||||
|
||||
this.hasMore = hasMore;
|
||||
this.loading = false;
|
||||
} catch (error) {
|
||||
console.error('获取诊断列表失败:', error);
|
||||
this.$toast('获取诊断列表失败');
|
||||
this.loading = false;
|
||||
this.hasMore = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载常用诊断列表
|
||||
* 职责:调用接口获取医生的常用诊断列表
|
||||
*/
|
||||
async loadDoctorMyDiseaseList() {
|
||||
try {
|
||||
const res = await getMyDiseaseList();
|
||||
let list = [];
|
||||
|
||||
// 正确解析响应数据
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
list = res.data;
|
||||
} else if (res.result && Array.isArray(res.result)) {
|
||||
list = res.result;
|
||||
} else if (Array.isArray(res)) {
|
||||
list = res;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保 list 是数组,避免 map 报错
|
||||
if (!Array.isArray(list)) {
|
||||
list = [];
|
||||
}
|
||||
|
||||
this.doctorMyDiseaseList = list.map((item) => {
|
||||
// 确保 item.disease 存在
|
||||
if (item.disease) {
|
||||
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);
|
||||
this.doctorMyDiseaseList = [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索诊断
|
||||
* 职责:防抖搜索,只在有输入时调用加载诊断列表
|
||||
*/
|
||||
handleSearch() {
|
||||
clearTimeout(this.searchTimer);
|
||||
this.searchTimer = setTimeout(() => {
|
||||
if (this.searchKey && this.searchKey.trim()) {
|
||||
this.loading = true;
|
||||
this.currentPage = 1;
|
||||
this.allDiagnosisList = [];
|
||||
this.loadDiagnosisList(this.searchKey.trim(), 1, false);
|
||||
} else {
|
||||
// 清空搜索时,清空列表
|
||||
this.allDiagnosisList = [];
|
||||
this.currentPage = 1;
|
||||
this.hasMore = false;
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
|
||||
/**
|
||||
* 触底加载更多
|
||||
* 职责:加载下一页数据
|
||||
*/
|
||||
loadMore() {
|
||||
if (this.loading || !this.hasMore || !this.searchKey || !this.searchKey.trim()) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.currentPage++;
|
||||
this.loadDiagnosisList(this.searchKey.trim(), this.currentPage, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择诊断
|
||||
* @param {Object} item - 诊断项
|
||||
* 职责:切换诊断的选中状态,更新选中列表
|
||||
*/
|
||||
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(',');
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加到常用诊断
|
||||
* @param {Object} item - 诊断项
|
||||
* 职责:调用接口添加到常用诊断
|
||||
*/
|
||||
async addToMyDiagnosis(item) {
|
||||
try {
|
||||
const res = await addMyDisease(item.id);
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
this.$toast('已添加到常用诊断');
|
||||
await this.loadDoctorMyDiseaseList();
|
||||
item.isInMyList = true;
|
||||
} else {
|
||||
this.$toast(res.msg || res.message || '添加失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加常用诊断失败:', error);
|
||||
this.$toast('添加失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 从常用诊断中删除
|
||||
* @param {Object} item - 诊断项
|
||||
* 职责:调用接口从常用诊断中删除
|
||||
*/
|
||||
async removeFromMyDiagnosis(item) {
|
||||
try {
|
||||
const res = await deleteMyDisease(item.id);
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
this.$toast('已从常用诊断中移除');
|
||||
await this.loadDoctorMyDiseaseList();
|
||||
const searchItem = this.allDiagnosisList.find(d => d.id === item.disease.id);
|
||||
if (searchItem) {
|
||||
searchItem.isInMyList = false;
|
||||
}
|
||||
} else {
|
||||
this.$toast(res.msg || res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除常用诊断失败:', error);
|
||||
this.$toast('删除失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认选择
|
||||
* 职责:触发确认事件,传递选中的诊断文本
|
||||
*/
|
||||
handleConfirm() {
|
||||
this.$emit('confirm', this.selectDiagnosisList);
|
||||
this.handleClose();
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
* 职责:关闭弹窗,触发input事件
|
||||
*/
|
||||
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-scroll {
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.tag-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 32rpx 0;
|
||||
|
||||
.load-more-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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>
|
||||
@@ -0,0 +1,423 @@
|
||||
<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';
|
||||
|
||||
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.loadDoctorOrderListData();
|
||||
}
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载医嘱列表数据
|
||||
* 职责:调用接口获取我的医嘱和公共医嘱列表
|
||||
*/
|
||||
async loadDoctorOrderListData() {
|
||||
try {
|
||||
// 获取我的医嘱
|
||||
const myRes = await getDoctorOrderList();
|
||||
if (myRes && (myRes.code === 0 || myRes.errcode === 0)) {
|
||||
const list = myRes.data || myRes.result || myRes.my || [];
|
||||
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) {
|
||||
this.doctorOrderCommonList = (commonRes.result || []).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;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取医嘱列表失败:', error);
|
||||
this.$toast('获取医嘱列表失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择医嘱
|
||||
* @param {Object} item - 医嘱项
|
||||
* @param {number} type - 类型:1=我的医嘱,2=公共医嘱
|
||||
* 职责:切换医嘱的选中状态,更新选中列表
|
||||
*/
|
||||
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()) {
|
||||
this.$toast('请输入医嘱内容');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await createDoctorOrder(this.newOrderContent.trim());
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
this.$toast('添加成功');
|
||||
this.newOrderContent = '';
|
||||
this.showAddInput = false;
|
||||
await this.loadDoctorOrderListData();
|
||||
} else {
|
||||
this.$toast(res.msg || res.message || '添加失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加医嘱失败:', error);
|
||||
this.$toast('添加失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除我的医嘱
|
||||
* @param {Object} item - 医嘱项
|
||||
* 职责:调用接口删除我的医嘱
|
||||
*/
|
||||
async deleteMyDoctorOrder(item) {
|
||||
try {
|
||||
const res = await deleteDoctorOrder(item.id);
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
this.$toast('删除成功');
|
||||
await this.loadDoctorOrderListData();
|
||||
} else {
|
||||
this.$toast(res.msg || res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除医嘱失败:', error);
|
||||
this.$toast('删除失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消添加
|
||||
* 职责:关闭添加输入框
|
||||
*/
|
||||
cancelAdd() {
|
||||
this.newOrderContent = '';
|
||||
this.showAddInput = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认选择
|
||||
* 职责:触发确认事件,传递选中的医嘱文本
|
||||
*/
|
||||
handleConfirm() {
|
||||
this.$emit('confirm', this.selectDoctorOrderList);
|
||||
this.handleClose();
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
* 职责:关闭弹窗,触发input事件
|
||||
*/
|
||||
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>
|
||||
@@ -0,0 +1,286 @@
|
||||
<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="simple-product-modal">
|
||||
<view class="modal-header">
|
||||
<d-text :text="getModalTitle()" className="fs-32 main-c"></d-text>
|
||||
</view>
|
||||
|
||||
<view class="modal-content">
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-box">
|
||||
<u-search
|
||||
v-model="searchKey"
|
||||
placeholder="请输入产品名称搜索"
|
||||
@input="handleSearch"
|
||||
@custom="searchKey = ''; handleSearch()"
|
||||
:show-action="false"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 产品列表 -->
|
||||
<view class="product-list" v-if="productList.length > 0">
|
||||
<view
|
||||
class="product-item white b-r-8 p-32 m-t-2"
|
||||
v-for="item in productList"
|
||||
:key="item.id"
|
||||
@click="handleSelectProduct(item)"
|
||||
>
|
||||
<view class="product-header flex-row flex-jus-sp flex-ali-center">
|
||||
<d-text :text="item.drug.drug_name || item.drug.name" className="fs-3 color0 bold"></d-text>
|
||||
<d-text :text="`¥${parseFloat(item.price || 0).toFixed(2)}`" className="fs-3 error-c"></d-text>
|
||||
</view>
|
||||
<view class="product-info m-t-16">
|
||||
<d-text :text="`规格:${item.drug.specification || '--'}`" className="fs-24 color9"></d-text>
|
||||
</view>
|
||||
<view class="product-action m-t-16">
|
||||
<u-button
|
||||
size="mini"
|
||||
:custom-style="{ backgroundColor: '#6ACDBB', color: '#fff' }"
|
||||
@click.stop="handleSelectProduct(item)"
|
||||
>
|
||||
选择
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无产品"></d-empty>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getProductListDoctorReception } from '@/api/reception.js';
|
||||
|
||||
export default {
|
||||
name: 'SimpleProductModal',
|
||||
props: {
|
||||
/**
|
||||
* 弹窗显示状态
|
||||
*/
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 产品类型:3=保健食品,5=产品服务包,6=非药品,7=医疗器械
|
||||
*/
|
||||
productType: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
/**
|
||||
* 当前已选中的产品列表
|
||||
*/
|
||||
currentProducts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
searchKey: '',
|
||||
productList: [],
|
||||
loading: false,
|
||||
searchTimer: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
if (newVal) {
|
||||
this.loadProductList();
|
||||
}
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 获取弹窗标题
|
||||
* @returns {string} 弹窗标题
|
||||
*/
|
||||
getModalTitle() {
|
||||
const titles = {
|
||||
3: '选择保健食品',
|
||||
5: '选择产品服务包',
|
||||
6: '选择非药品',
|
||||
7: '选择医疗器械'
|
||||
};
|
||||
return titles[this.productType] || '选择产品';
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载产品列表
|
||||
* @param {string} keyword - 搜索关键词
|
||||
* 职责:调用接口获取产品列表
|
||||
*/
|
||||
async loadProductList(keyword = '') {
|
||||
this.loading = true;
|
||||
try {
|
||||
const storeId = uni.getStorageSync('store_id') || 11001;
|
||||
const res = await getProductListDoctorReception({
|
||||
store_id: storeId,
|
||||
type: this.productType,
|
||||
name: keyword
|
||||
});
|
||||
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
this.productList = res.data || res.result || [];
|
||||
} else {
|
||||
this.productList = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取产品列表失败:', error);
|
||||
this.$toast('获取产品列表失败');
|
||||
this.productList = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索产品
|
||||
* 职责:防抖搜索,调用加载产品列表
|
||||
*/
|
||||
handleSearch() {
|
||||
clearTimeout(this.searchTimer);
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.loadProductList(this.searchKey);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择产品
|
||||
* @param {Object} product - 产品数据
|
||||
* 职责:触发select事件,传递选中的产品
|
||||
*/
|
||||
handleSelectProduct(product) {
|
||||
this.$emit('select', product);
|
||||
this.handleClose();
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
* 职责:关闭弹窗,触发input事件
|
||||
*/
|
||||
handleClose() {
|
||||
this.show = false;
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.simple-product-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;
|
||||
}
|
||||
|
||||
.product-list {
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.product-header {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.product-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.white {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.b-r-8 {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.p-32 {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.m-t-2 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-t-16 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-jus-sp {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-ali-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color0 {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.color9 {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,345 @@
|
||||
<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-medicine-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-search
|
||||
v-model="searchKey"
|
||||
placeholder="请输入药品名称搜索"
|
||||
@input="handleSearch"
|
||||
@custom="searchKey = ''; handleSearch()"
|
||||
:show-action="false"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 药品列表 -->
|
||||
<view class="drug-list" v-if="drugList.length > 0">
|
||||
<view
|
||||
class="drug-card white b-r-8 p-32 m-t-2"
|
||||
v-for="(item, index) in drugList"
|
||||
:key="index"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
@click="!isSelected(item) && handleSelectDrug(item)"
|
||||
>
|
||||
<view class="drug-content flex-row">
|
||||
<image
|
||||
v-if="getDrugImage(item)"
|
||||
:src="getDrugImage(item)"
|
||||
class="drug-image"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="drug-info-wrapper flex-1">
|
||||
<view class="drug-header flex-row flex-jus-sp flex-ali-center">
|
||||
<d-text
|
||||
:text="`${index + 1}、${item.drug.drug_name || item.drug.name}`"
|
||||
className="fs-3 color0 bold"
|
||||
></d-text>
|
||||
<d-text
|
||||
:text="`¥${parseFloat(item.price || 0).toFixed(2)}`"
|
||||
className="fs-3 error-c"
|
||||
></d-text>
|
||||
</view>
|
||||
<view class="drug-info m-t-16 flex-row flex-ali-center flex-jus-sp">
|
||||
<d-text
|
||||
:text="`规格:${item.drug.specification || '--'}`"
|
||||
className="fs-24 color9"
|
||||
></d-text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="drug-status">
|
||||
<u-button
|
||||
size="mini"
|
||||
:disabled="isSelected(item)"
|
||||
:custom-style="isSelected(item) ? selectedButtonStyle : addButtonStyle"
|
||||
@click.stop="handleSelectDrug(item)"
|
||||
>
|
||||
{{ isSelected(item) ? '已选' : '添加' }}
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<d-empty text="暂无药品"></d-empty>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getProductListDoctorReception } from '@/api/reception.js';
|
||||
|
||||
export default {
|
||||
name: 'WesternMedicineModal',
|
||||
props: {
|
||||
/**
|
||||
* 弹窗显示状态
|
||||
*/
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 当前已选中的药品列表
|
||||
*/
|
||||
currentDrugs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
searchKey: '',
|
||||
drugList: [],
|
||||
loading: false,
|
||||
searchTimer: null,
|
||||
addButtonStyle: {
|
||||
backgroundColor: '#6ACDBB',
|
||||
color: '#fff'
|
||||
},
|
||||
selectedButtonStyle: {
|
||||
backgroundColor: '#F5F7FA',
|
||||
color: '#6ACDBB',
|
||||
borderColor: '#6ACDBB'
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
if (newVal) {
|
||||
this.loadDrugList();
|
||||
}
|
||||
},
|
||||
show(newVal) {
|
||||
if (!newVal) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载药品列表
|
||||
* @param {string} keyword - 搜索关键词
|
||||
* 职责:调用接口获取西药列表
|
||||
*/
|
||||
async loadDrugList(keyword = '') {
|
||||
this.loading = true;
|
||||
try {
|
||||
const storeId = uni.getStorageSync('store_id') || 11001;
|
||||
const res = await getProductListDoctorReception({
|
||||
store_id: storeId,
|
||||
type: 2, // 西药
|
||||
name: keyword
|
||||
});
|
||||
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
this.drugList = res.data || res.result || [];
|
||||
} else {
|
||||
this.drugList = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取药品列表失败:', error);
|
||||
this.$toast('获取药品列表失败');
|
||||
this.drugList = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索药品
|
||||
* 职责:防抖搜索,调用加载药品列表
|
||||
*/
|
||||
handleSearch() {
|
||||
clearTimeout(this.searchTimer);
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.loadDrugList(this.searchKey);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取药品图片
|
||||
* @param {Object} item - 药品数据
|
||||
* @returns {string} 图片URL
|
||||
*/
|
||||
getDrugImage(item) {
|
||||
if (item.drug && item.drug.image) {
|
||||
return item.drug.image;
|
||||
}
|
||||
if (item.image) {
|
||||
return item.image;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断药品是否已在当前处方中
|
||||
* @param {Object} drug
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSelected(drug) {
|
||||
if (!this.currentDrugs || !drug) return false;
|
||||
const drugId = drug.drug_id || (drug.drug && drug.drug.id) || drug.id;
|
||||
return this.currentDrugs.some(item => (item.id || item.drug_id) === drugId);
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择药品
|
||||
* @param {Object} drug - 药品数据
|
||||
* 职责:触发select事件,传递选中的药品
|
||||
*/
|
||||
handleSelectDrug(drug) {
|
||||
if (this.isSelected(drug)) {
|
||||
return;
|
||||
}
|
||||
this.$emit('select', drug);
|
||||
this.handleClose();
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
* 职责:关闭弹窗,触发input事件
|
||||
*/
|
||||
handleClose() {
|
||||
this.show = false;
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.western-medicine-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;
|
||||
}
|
||||
|
||||
.drug-list {
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.drug-item {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.drug-content {
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.drug-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 8rpx;
|
||||
flex-shrink: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.drug-info-wrapper {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.drug-header {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.drug-info {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.drug-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.white {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.b-r-8 {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.p-32 {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.m-t-2 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-t-16 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-jus-sp {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-ali-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.color0 {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.color9 {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,455 @@
|
||||
<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: 'WesternMedicineUsageModal',
|
||||
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) {
|
||||
// 打开时先加载用法用量列表和默认值,再根据 default/drug 初始化 usageData
|
||||
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;
|
||||
|
||||
// 仅在当前 usageData 还没有配置时使用默认值
|
||||
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('获取用法用量失败');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 改变用法类型
|
||||
* @param {Object} e - 事件对象
|
||||
* 职责:更新用法类型
|
||||
*/
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 改变频次
|
||||
* @param {Object} e - 事件对象
|
||||
* 职责:更新频次
|
||||
*/
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 改变时间
|
||||
* @param {Object} e - 事件对象
|
||||
* 职责:更新时间
|
||||
*/
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 改变单位
|
||||
* @param {Object} e - 事件对象
|
||||
* 职责:更新单位
|
||||
*/
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* 职责:触发confirm事件,传递用法用量数据
|
||||
*/
|
||||
handleConfirm() {
|
||||
this.$emit('confirm', { ...this.usageData });
|
||||
this.handleClose();
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
* 职责:关闭弹窗,触发input事件
|
||||
*/
|
||||
handleClose() {
|
||||
this.show = false;
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.western-usage-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;
|
||||
}
|
||||
|
||||
.drug-info {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.usage-item {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
padding: 16rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8rpx;
|
||||
min-width: 200rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
|
||||
.u-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.white {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.b-r-8 {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.p-32 {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.m-t-2 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-t-16 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-l-16 {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.color0 {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.color9 {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
2043
subPackages/sub_workbench/prescription_v2/index.vue
Normal file
2043
subPackages/sub_workbench/prescription_v2/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 处方价格计算工具类
|
||||
* 职责:统一管理处方价格计算逻辑
|
||||
*/
|
||||
export class PrescriptionCalculator {
|
||||
/**
|
||||
* 计算中药商品总价
|
||||
* @param {Array} drugs - 药品列表
|
||||
* @param {number} dosage - 剂数(天数)
|
||||
* @returns {number} 商品总价
|
||||
*/
|
||||
static calculateChineseMedicineProductPrice(drugs, dosage = 7) {
|
||||
if (!drugs || drugs.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return drugs.reduce((sum, drug) => {
|
||||
const price = parseFloat(drug.price || 0);
|
||||
const quantity = parseFloat(drug.number || 1);
|
||||
return sum + (price * quantity * dosage);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算西药商品总价
|
||||
* @param {Array} drugs - 药品列表
|
||||
* @returns {number} 商品总价
|
||||
*/
|
||||
static calculateWesternMedicineProductPrice(drugs) {
|
||||
if (!drugs || drugs.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return drugs.reduce((sum, drug) => {
|
||||
const price = parseFloat(drug.price || 0);
|
||||
const quantity = parseFloat(drug.select_number || drug.number || 1);
|
||||
return sum + (price * quantity);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算简单产品总价(保健食品、服务包、非药品、医疗器械)
|
||||
* @param {Array} products - 产品列表
|
||||
* @returns {number} 商品总价
|
||||
*/
|
||||
static calculateSimpleProductPrice(products) {
|
||||
if (!products || products.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return products.reduce((sum, product) => {
|
||||
const price = parseFloat(product.price || 0);
|
||||
const quantity = parseFloat(product.select_number || product.number || 1);
|
||||
return sum + (price * quantity);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算加工费(委托调剂)
|
||||
* @param {Object} processRule - 加工规则对象
|
||||
* @param {number} dosage - 剂数
|
||||
* @param {number} totalDrugQuantity - 总药品数量(用于按数量计算)
|
||||
* @returns {number} 加工费
|
||||
*/
|
||||
static calculateProcessingFee(processRule, dosage = 7, totalDrugQuantity = 0) {
|
||||
if (!processRule || !processRule.calc_method) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const calcMethod = processRule.calc_method; // 1=固定价格,2=按剂数,3=按数量
|
||||
const price = parseFloat(processRule.price || 0);
|
||||
|
||||
switch (calcMethod) {
|
||||
case 1:
|
||||
// 固定价格
|
||||
return price;
|
||||
case 2:
|
||||
// 按剂数计算
|
||||
return price * dosage;
|
||||
case 3:
|
||||
// 按数量计算
|
||||
return price * dosage * totalDrugQuantity;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算处方总价
|
||||
* @param {Object} params - 计算参数
|
||||
* @param {number} params.category - 处方类型
|
||||
* @param {Array} params.drugs - 药品列表
|
||||
* @param {number} params.dosage - 剂数(中药需要)
|
||||
* @param {number} params.processingFee - 加工费(委托调剂需要)
|
||||
* @param {number} params.treatmentPrice - 诊疗费
|
||||
* @returns {number} 总价
|
||||
*/
|
||||
static calculateTotalPrice(params) {
|
||||
const { category, drugs, dosage = 7, processingFee = 0, treatmentPrice = 0 } = params;
|
||||
|
||||
let productPrice = 0;
|
||||
|
||||
// 根据类型计算商品价格
|
||||
if (category === 1) {
|
||||
// 中药
|
||||
productPrice = this.calculateChineseMedicineProductPrice(drugs, dosage);
|
||||
} else if (category === 2) {
|
||||
// 西药
|
||||
productPrice = this.calculateWesternMedicineProductPrice(drugs);
|
||||
} else if ([3, 5, 6, 7].includes(category)) {
|
||||
// 简单产品
|
||||
productPrice = this.calculateSimpleProductPrice(drugs);
|
||||
}
|
||||
|
||||
// 总价 = 商品价格 + 加工费 + 诊疗费
|
||||
const total = productPrice + processingFee + treatmentPrice;
|
||||
return parseFloat(total.toFixed(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 处方数据存储工具类
|
||||
* 职责:统一管理处方数据的本地存储操作
|
||||
*/
|
||||
export class PrescriptionStorage {
|
||||
/**
|
||||
* 存储前缀
|
||||
*/
|
||||
static STORAGE_PREFIX = 'prescriptionV2-';
|
||||
|
||||
/**
|
||||
* 获取存储键名
|
||||
* @param {number} category - 处方类型:1=中药,2=西药,3=保健食品,5=产品服务包,6=非药品,7=医疗器械
|
||||
* @param {number} registerId - 挂号ID
|
||||
* @returns {string} 存储键名
|
||||
*/
|
||||
static getStorageKey(category, registerId) {
|
||||
return `${this.STORAGE_PREFIX}prescriptionData_${category}_${registerId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取激活分类的存储键名
|
||||
* @param {number} registerId - 挂号ID
|
||||
* @returns {string} 存储键名
|
||||
*/
|
||||
static getActiveCategoryKey(registerId) {
|
||||
return `${this.STORAGE_PREFIX}activeCategory_${registerId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存处方数据到本地存储
|
||||
* @param {number} category - 处方类型
|
||||
* @param {number} registerId - 挂号ID
|
||||
* @param {Object} data - 处方数据(药品列表、诊断、医嘱等)
|
||||
*/
|
||||
static savePrescriptionData(category, registerId, data) {
|
||||
try {
|
||||
const key = this.getStorageKey(category, registerId);
|
||||
uni.setStorageSync(key, JSON.stringify(data));
|
||||
console.log('保存处方数据成功:', key, data);
|
||||
} catch (error) {
|
||||
console.error('保存处方数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储加载处方数据
|
||||
* @param {number} category - 处方类型
|
||||
* @param {number} registerId - 挂号ID
|
||||
* @returns {Object|null} 处方数据
|
||||
*/
|
||||
static loadPrescriptionData(category, registerId) {
|
||||
try {
|
||||
const key = this.getStorageKey(category, registerId);
|
||||
const data = uni.getStorageSync(key);
|
||||
if (data) {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('加载处方数据失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定处方的本地存储数据
|
||||
* @param {number} category - 处方类型
|
||||
* @param {number} registerId - 挂号ID
|
||||
*/
|
||||
static clearPrescriptionData(category, registerId) {
|
||||
try {
|
||||
const key = this.getStorageKey(category, registerId);
|
||||
uni.removeStorageSync(key);
|
||||
} catch (error) {
|
||||
console.error('清除处方数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有处方的本地存储数据
|
||||
* @param {number} registerId - 挂号ID
|
||||
*/
|
||||
static clearAllPrescriptionData(registerId) {
|
||||
try {
|
||||
const categories = [1, 2, 3, 5, 6, 7];
|
||||
categories.forEach(category => {
|
||||
this.clearPrescriptionData(category, registerId);
|
||||
});
|
||||
// 清除激活分类
|
||||
uni.removeStorageSync(this.getActiveCategoryKey(registerId));
|
||||
} catch (error) {
|
||||
console.error('清除所有处方数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前激活的分类
|
||||
* @param {number} category - 处方类型
|
||||
* @param {number} registerId - 挂号ID
|
||||
*/
|
||||
static saveActiveCategory(category, registerId) {
|
||||
try {
|
||||
const key = this.getActiveCategoryKey(registerId);
|
||||
uni.setStorageSync(key, category.toString());
|
||||
} catch (error) {
|
||||
console.error('保存激活分类失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前激活的分类
|
||||
* @param {number} registerId - 挂号ID
|
||||
* @returns {number|null} 激活的分类,如果没有则返回null
|
||||
*/
|
||||
static getActiveCategory(registerId) {
|
||||
try {
|
||||
const key = this.getActiveCategoryKey(registerId);
|
||||
const category = uni.getStorageSync(key);
|
||||
if (category) {
|
||||
return parseInt(category);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('获取激活分类失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 处方数据验证工具类
|
||||
* 职责:统一管理处方数据验证逻辑
|
||||
*/
|
||||
export class PrescriptionValidator {
|
||||
/**
|
||||
* 验证处方数据
|
||||
* @param {Object} params - 验证参数
|
||||
* @param {number} params.category - 处方类型
|
||||
* @param {Array} params.drugs - 药品列表
|
||||
* @param {Array} params.diagnoses - 诊断列表
|
||||
* @param {string} params.medicalAdvice - 医嘱
|
||||
* @param {Object} params.chineseConfig - 中药配置(仅中药需要)
|
||||
* @returns {Object} { valid: boolean, message: string }
|
||||
*/
|
||||
static validatePrescriptionData(params) {
|
||||
const { category, drugs, diagnoses, medicalAdvice, chineseConfig } = params;
|
||||
|
||||
// 验证药品
|
||||
if (!drugs || drugs.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '请添加药品'
|
||||
};
|
||||
}
|
||||
|
||||
// 验证诊断
|
||||
if (!diagnoses || diagnoses.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '请添加诊断'
|
||||
};
|
||||
}
|
||||
|
||||
// 验证医嘱
|
||||
if (!medicalAdvice || medicalAdvice.trim() === '') {
|
||||
return {
|
||||
valid: false,
|
||||
message: '请输入医嘱'
|
||||
};
|
||||
}
|
||||
|
||||
// 验证中药配置
|
||||
if (category === 1 && chineseConfig) {
|
||||
const chineseValidation = this.validateChineseMedicineConfig(chineseConfig);
|
||||
if (!chineseValidation.valid) {
|
||||
return chineseValidation;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: '验证通过'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证中药配置
|
||||
* @param {Object} config - 中药配置
|
||||
* @param {number} config.ruleType - 规则类型:1=自制剂,2=委托调剂
|
||||
* @param {number} config.packageMethodId - 包法ID(自制剂需要)
|
||||
* @param {number} config.processRuleId - 制剂ID(委托调剂需要)
|
||||
* @param {number} config.childProcessRuleId - 煎法ID(委托调剂需要)
|
||||
* @param {number} config.processRuleNoteId - 备注ID(委托调剂需要)
|
||||
* @returns {Object} { valid: boolean, message: string }
|
||||
*/
|
||||
static validateChineseMedicineConfig(config) {
|
||||
const { ruleType, packageMethodId, processRuleId, childProcessRuleId, processRuleNoteId } = config;
|
||||
|
||||
if (ruleType === 1) {
|
||||
// 自制剂:需要包法
|
||||
if (!packageMethodId) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '请选择包法'
|
||||
};
|
||||
}
|
||||
} else if (ruleType === 2) {
|
||||
// 委托调剂:需要制剂、煎法、备注
|
||||
if (!processRuleId) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '请选择制剂'
|
||||
};
|
||||
}
|
||||
if (!childProcessRuleId) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '请选择煎法'
|
||||
};
|
||||
}
|
||||
if (!processRuleNoteId) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '请选择备注'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: '验证通过'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证药品是否已存在
|
||||
* @param {Array} drugs - 当前药品列表
|
||||
* @param {Object} newDrug - 新药品
|
||||
* @returns {boolean} 是否已存在
|
||||
*/
|
||||
static isDrugExists(drugs, newDrug) {
|
||||
if (!drugs || !newDrug) {
|
||||
return false;
|
||||
}
|
||||
// 通过药品ID判断
|
||||
const drugId = newDrug.id || newDrug.drug_id;
|
||||
return drugs.some(drug => (drug.id || drug.drug_id) === drugId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证诊断是否已存在
|
||||
* @param {Array} diagnoses - 当前诊断列表
|
||||
* @param {Object} newDiagnosis - 新诊断
|
||||
* @returns {boolean} 是否已存在
|
||||
*/
|
||||
static isDiagnosisExists(diagnoses, newDiagnosis) {
|
||||
if (!diagnoses || !newDiagnosis) {
|
||||
return false;
|
||||
}
|
||||
// 通过诊断ID或名称判断
|
||||
const diagnosisId = newDiagnosis.id || newDiagnosis.disease_id;
|
||||
const diagnosisName = newDiagnosis.name;
|
||||
return diagnoses.some(diagnosis =>
|
||||
(diagnosis.id || diagnosis.disease_id) === diagnosisId ||
|
||||
diagnosis.name === diagnosisName
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,15 @@
|
||||
<u-modal v-model="show" title="操作提示" :show-cancel-button="true" :content="content" @confirm="confirm"
|
||||
confirm-color="#6ACDBB"></u-modal>
|
||||
|
||||
<!-- 版本选择弹窗 -->
|
||||
<u-action-sheet
|
||||
v-model="showVersionModal"
|
||||
:list="versionList"
|
||||
@click="handleVersionSelect"
|
||||
:title="versionModalTitle"
|
||||
:close-on-click-action="true"
|
||||
></u-action-sheet>
|
||||
|
||||
<view class="empty"></view>
|
||||
<d-loading-page :loading="loading"></d-loading-page>
|
||||
<view class="fun" v-if="flag&&infoList.status!=3">
|
||||
@@ -187,7 +196,7 @@
|
||||
plain :ripple="true">结束问诊
|
||||
</u-button>
|
||||
<u-button v-if="infoList.status==2"
|
||||
@click="$go(`../../subPackages/sub_workbench/workbench_recipe/index?id=` + infoList.user_patient_id+'&r_id='+infoList.id)"
|
||||
@click="openPrescriptionVersionModal"
|
||||
shape="circle"
|
||||
:custom-style="{ heigth: '86rpx', width: '327rpx',backgroundColor:'#6ACDBB',color:'#fff' }">开处方
|
||||
</u-button>
|
||||
@@ -212,7 +221,21 @@
|
||||
id: "",
|
||||
re_id: "",
|
||||
show: false,
|
||||
content: '确认结束本次问诊吗?'
|
||||
content: '确认结束本次问诊吗?',
|
||||
showVersionModal: false,
|
||||
versionModalTitle: '选择开方版本',
|
||||
versionList: [
|
||||
{
|
||||
text: '开方2.0(新模块,推荐)',
|
||||
value: 'v2',
|
||||
color: '#6ACDBB'
|
||||
},
|
||||
{
|
||||
text: '旧版开方(原有模块)',
|
||||
value: 'old',
|
||||
color: '#333'
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -281,6 +304,31 @@
|
||||
}
|
||||
});
|
||||
},
|
||||
// 打开版本选择弹窗
|
||||
openPrescriptionVersionModal() {
|
||||
this.showVersionModal = true;
|
||||
},
|
||||
// 处理版本选择
|
||||
handleVersionSelect(index) {
|
||||
const version = this.versionList[index];
|
||||
if (version.value === 'v2') {
|
||||
this.goToPrescriptionV2();
|
||||
} else if (version.value === 'old') {
|
||||
this.goToOldPrescription();
|
||||
}
|
||||
},
|
||||
// 跳转到开方2.0页面
|
||||
goToPrescriptionV2() {
|
||||
const registerId = this.infoList.id;
|
||||
const patientId = this.infoList.user_patient_id;
|
||||
this.$go(`/subPackages/sub_workbench/prescription_v2/index?register_id=${registerId}&patient_id=${patientId}`);
|
||||
},
|
||||
// 跳转到旧版开方页面
|
||||
goToOldPrescription() {
|
||||
const patientId = this.infoList.user_patient_id;
|
||||
const registerId = this.infoList.id;
|
||||
this.$go(`../../subPackages/sub_workbench/workbench_recipe/index?id=${patientId}&r_id=${registerId}`);
|
||||
},
|
||||
},
|
||||
onShow() {
|
||||
this.getList();
|
||||
|
||||
424
utils/chat/chatRoomManager.js
Normal file
424
utils/chat/chatRoomManager.js
Normal file
@@ -0,0 +1,424 @@
|
||||
/**
|
||||
* 聊天室管理器
|
||||
* 封装聊天室相关操作,包括进入房间、离开房间、发送消息、接收消息、管理未读数等
|
||||
*/
|
||||
import { chatConfig } from '@/config/chat.js';
|
||||
import { sendWebSocketMessage, isWebSocketConnected } from '@/utils/ws/initWebSocket.js';
|
||||
import { getChatRegisterInfoApi, getMessagesByRoomIdApi } from '@/request/api/im.js';
|
||||
|
||||
/**
|
||||
* 聊天室管理器类
|
||||
*/
|
||||
class ChatRoomManager {
|
||||
constructor() {
|
||||
// 当前房间ID
|
||||
this.currentRoomId = null;
|
||||
|
||||
// 最后一条消息ID(用于分页加载)
|
||||
this.lastMessageId = 0;
|
||||
|
||||
// 未读数(按房间ID存储)
|
||||
this.unreadCounts = {};
|
||||
|
||||
// 消息缓存(按房间ID存储)
|
||||
this.roomMessages = {};
|
||||
|
||||
// 消息处理器列表
|
||||
this.messageHandlers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入聊天室
|
||||
* @param {string} roomId - 房间ID
|
||||
* 职责:设置当前房间,重置未读数,保存到本地存储
|
||||
*/
|
||||
enterRoom(roomId) {
|
||||
if (!roomId) {
|
||||
console.warn('房间ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentRoomId = roomId;
|
||||
uni.setStorageSync('currentRoomId', roomId);
|
||||
this.resetUnreadCount(roomId);
|
||||
|
||||
console.log('进入聊天室:', roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 离开聊天室
|
||||
* 职责:清空当前房间,移除本地存储
|
||||
*/
|
||||
leaveRoom() {
|
||||
this.currentRoomId = null;
|
||||
uni.removeStorageSync('currentRoomId');
|
||||
console.log('离开聊天室');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前房间ID
|
||||
* @returns {string|null} 当前房间ID
|
||||
*/
|
||||
getCurrentRoomId() {
|
||||
return this.currentRoomId || uni.getStorageSync('currentRoomId');
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加未读数
|
||||
* @param {string} roomId - 房间ID
|
||||
* 职责:如果房间不是当前房间,则增加未读数并通知
|
||||
*/
|
||||
increaseUnreadCount(roomId) {
|
||||
if (!roomId) return;
|
||||
|
||||
// 如果是当前房间,不增加未读数
|
||||
if (this.getCurrentRoomId() === roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unreadCounts[roomId] = (this.unreadCounts[roomId] || 0) + 1;
|
||||
this.notifyUnreadChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置未读数
|
||||
* @param {string} roomId - 房间ID
|
||||
* 职责:将指定房间的未读数重置为0并通知
|
||||
*/
|
||||
resetUnreadCount(roomId) {
|
||||
if (this.unreadCounts[roomId]) {
|
||||
this.unreadCounts[roomId] = 0;
|
||||
this.notifyUnreadChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读数
|
||||
* @param {string} roomId - 房间ID
|
||||
* @returns {number} 未读数
|
||||
*/
|
||||
getUnreadCount(roomId) {
|
||||
return this.unreadCounts[roomId] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有未读数总和
|
||||
* @returns {number} 总未读数
|
||||
*/
|
||||
getAllUnreadCount() {
|
||||
return Object.values(this.unreadCounts).reduce((sum, count) => sum + count, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化未读数显示
|
||||
* @param {string} roomId - 房间ID
|
||||
* @returns {string} 格式化后的未读数
|
||||
*/
|
||||
getFormattedUnreadCount(roomId) {
|
||||
const count = this.getUnreadCount(roomId);
|
||||
return chatConfig.formatUnreadCount(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知未读数变化
|
||||
* 职责:触发未读数更新事件
|
||||
*/
|
||||
notifyUnreadChange() {
|
||||
uni.$emit('unread-message-update', {
|
||||
total: this.getAllUnreadCount(),
|
||||
byRoom: { ...this.unreadCounts }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息到房间
|
||||
* @param {string} roomId - 房间ID
|
||||
* @param {Object} message - 消息对象
|
||||
* 职责:将消息添加到指定房间的消息列表
|
||||
*/
|
||||
addMessageToRoom(roomId, message) {
|
||||
if (!roomId || !message) return;
|
||||
|
||||
if (!this.roomMessages[roomId]) {
|
||||
this.roomMessages[roomId] = [];
|
||||
}
|
||||
|
||||
// 检查消息是否已存在(避免重复)
|
||||
const existingIndex = this.roomMessages[roomId].findIndex(m => m.id === message.id);
|
||||
if (existingIndex !== -1) {
|
||||
this.roomMessages[roomId][existingIndex] = message;
|
||||
} else {
|
||||
this.roomMessages[roomId].push(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间消息列表
|
||||
* @param {string} roomId - 房间ID
|
||||
* @returns {Array} 消息列表
|
||||
*/
|
||||
getRoomMessages(roomId) {
|
||||
return this.roomMessages[roomId] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载房间消息
|
||||
* @param {string} roomId - 房间ID
|
||||
* @param {number} lastMessageId - 最后一条消息ID(可选,用于分页)
|
||||
* @returns {Promise<Array>} 消息列表
|
||||
*/
|
||||
async loadRoomMessages(roomId, lastMessageId = 0) {
|
||||
if (!roomId) {
|
||||
console.warn('房间ID不能为空');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getMessagesByRoomIdApi({
|
||||
room_id: roomId,
|
||||
last_message_id: lastMessageId || 0
|
||||
});
|
||||
|
||||
if (res && res.data && res.data.result) {
|
||||
const messages = this.processMessages(res.data.result.list || []);
|
||||
this.lastMessageId = res.data.result.last_message_id || 0;
|
||||
|
||||
// 如果是首次加载,替换消息列表;否则追加到前面
|
||||
if (lastMessageId === 0) {
|
||||
this.roomMessages[roomId] = messages;
|
||||
} else {
|
||||
this.roomMessages[roomId] = [...messages, ...(this.roomMessages[roomId] || [])];
|
||||
}
|
||||
|
||||
uni.$emit('load-room-message', lastMessageId === 0 ? 1 : 0);
|
||||
return messages;
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('加载房间消息失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息数据
|
||||
* @param {Array|Object} messages - 消息数据(数组或单个消息)
|
||||
* @returns {Array|Object} 处理后的消息
|
||||
*/
|
||||
processMessages(messages) {
|
||||
if (!Array.isArray(messages)) {
|
||||
return this.processSingleMessage(messages);
|
||||
}
|
||||
|
||||
return messages.map((item) => {
|
||||
const result = {
|
||||
id: item.id,
|
||||
sender_user_id: item.sender_user_id,
|
||||
receiver_user_id: item.receiver_user_id,
|
||||
message_type: item.message_type,
|
||||
message_content: item.message_content,
|
||||
duration: item.duration,
|
||||
room_id: item.room_id,
|
||||
created_at: item.created_at || item.send_time,
|
||||
created_at_text: item.created_at_text || this.formatTime(item.send_time || item.created_at),
|
||||
timestamp: item.send_time || item.created_at
|
||||
};
|
||||
|
||||
// 如果是挂号消息(type=10),异步加载详细信息
|
||||
if (item.message_type === 10) {
|
||||
this.loadRegisterInfo(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个消息
|
||||
* @param {Object} message - 消息对象
|
||||
* @returns {Object} 处理后的消息
|
||||
*/
|
||||
processSingleMessage(message) {
|
||||
return {
|
||||
id: message.id,
|
||||
sender_user_id: message.sender_user_id,
|
||||
receiver_user_id: message.receiver_user_id,
|
||||
message_type: message.message_type,
|
||||
message_content: message.message_content,
|
||||
duration: message.duration,
|
||||
room_id: message.room_id,
|
||||
created_at: message.created_at || message.send_time,
|
||||
created_at_text: message.created_at_text || this.formatTime(message.send_time || message.created_at),
|
||||
timestamp: message.send_time || message.created_at
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {number} timestamp - 时间戳(秒)
|
||||
* @returns {string} 格式化后的时间(HH:mm)
|
||||
*/
|
||||
formatTime(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp * 1000);
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载挂号信息(异步)
|
||||
* @param {Object} message - 消息对象
|
||||
* 职责:为挂号消息加载详细信息
|
||||
*/
|
||||
loadRegisterInfo(message) {
|
||||
const parsedContent = this.parseMessageContent(message.message_content);
|
||||
if (parsedContent._loaded || !parsedContent.id) return;
|
||||
|
||||
getChatRegisterInfoApi({
|
||||
id: parsedContent.id,
|
||||
message_id: message.id || ''
|
||||
}).then((res) => {
|
||||
if (res && res.data && res.data.result) {
|
||||
const resultWithFlag = { ...res.data.result, _loaded: true };
|
||||
const roomId = message.room_id;
|
||||
const roomMessages = this.roomMessages[roomId];
|
||||
|
||||
if (roomMessages) {
|
||||
const index = roomMessages.findIndex(m => m.id === message.id);
|
||||
if (index !== -1) {
|
||||
roomMessages[index].message_content = JSON.stringify(resultWithFlag);
|
||||
uni.$emit('message-updated', { roomId, messageId: message.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('加载挂号信息失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析消息内容
|
||||
* @param {string|Object} content - 消息内容
|
||||
* @returns {Object|string} 解析后的内容
|
||||
*/
|
||||
parseMessageContent(content) {
|
||||
try {
|
||||
return typeof content === 'object' ? content : JSON.parse(content);
|
||||
} catch (e) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* @param {Object} messageData - 消息数据
|
||||
* @param {string} messageData.room_id - 房间ID
|
||||
* @param {string} messageData.sender_user_id - 发送者ID
|
||||
* @param {string} messageData.receiver_user_id - 接收者ID
|
||||
* @param {number|string} messageData.message_type - 消息类型
|
||||
* @param {string} messageData.message_content - 消息内容
|
||||
* @param {number} messageData.duration - 语音时长(可选)
|
||||
* @returns {boolean} 是否发送成功
|
||||
*/
|
||||
sendMessage(messageData) {
|
||||
if (!isWebSocketConnected()) {
|
||||
console.error('WebSocket未连接,无法发送消息');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 确保消息类型是数字
|
||||
let messageType = messageData.message_type;
|
||||
if (typeof messageType === 'string') {
|
||||
messageType = chatConfig.getMessageType(messageType);
|
||||
}
|
||||
|
||||
const message = {
|
||||
request_type: 'send_message',
|
||||
room_id: messageData.room_id,
|
||||
sender_user_id: messageData.sender_user_id,
|
||||
receiver_user_id: messageData.receiver_user_id,
|
||||
message_type: messageType,
|
||||
message_content: messageData.message_content || '',
|
||||
duration: messageData.duration || 0
|
||||
};
|
||||
|
||||
return sendWebSocketMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册消息处理器
|
||||
* @param {Function} handler - 消息处理函数
|
||||
* 职责:注册消息处理器,当收到WebSocket消息时调用
|
||||
*/
|
||||
onMessage(handler) {
|
||||
if (typeof handler !== 'function') {
|
||||
console.warn('消息处理器必须是函数');
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除消息处理器
|
||||
* @param {Function} handler - 消息处理函数
|
||||
*/
|
||||
offMessage(handler) {
|
||||
const index = this.messageHandlers.indexOf(handler);
|
||||
if (index !== -1) {
|
||||
this.messageHandlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理收到的WebSocket消息
|
||||
* @param {Object} data - WebSocket消息数据
|
||||
* 职责:处理收到的消息,更新未读数,调用注册的处理器
|
||||
*/
|
||||
handleWebSocketMessage(data) {
|
||||
// 只处理聊天消息
|
||||
if (data.request_type !== 'receive_message' && !data.room_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roomId = data.room_id;
|
||||
if (!roomId) return;
|
||||
|
||||
// 处理消息
|
||||
const message = this.processSingleMessage(data);
|
||||
|
||||
// 添加到房间消息列表
|
||||
this.addMessageToRoom(roomId, message);
|
||||
|
||||
// 如果不是当前房间,增加未读数
|
||||
if (this.getCurrentRoomId() !== roomId) {
|
||||
this.increaseUnreadCount(roomId);
|
||||
}
|
||||
|
||||
// 调用所有注册的处理器
|
||||
this.messageHandlers.forEach(handler => {
|
||||
try {
|
||||
handler(message, roomId);
|
||||
} catch (error) {
|
||||
console.error('消息处理器执行失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空房间消息缓存
|
||||
* @param {string} roomId - 房间ID(可选,不传则清空所有)
|
||||
*/
|
||||
clearRoomCache(roomId = null) {
|
||||
if (roomId) {
|
||||
delete this.roomMessages[roomId];
|
||||
} else {
|
||||
this.roomMessages = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const chatRoomManager = new ChatRoomManager();
|
||||
118
utils/ws/initWebSocket.js
Normal file
118
utils/ws/initWebSocket.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* WebSocket初始化工具
|
||||
* 统一管理WebSocket的初始化、连接、断开等操作
|
||||
*/
|
||||
import { webSocketManager } from './websocket.js';
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
* @returns {boolean} 是否已登录
|
||||
*/
|
||||
function checkLoginStatus() {
|
||||
const token = uni.getStorageSync('token');
|
||||
const doctorId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
|
||||
return !!(token && doctorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化WebSocket
|
||||
* 职责:检查登录状态,如果已登录则连接WebSocket并绑定用户
|
||||
* @returns {boolean} 是否初始化成功
|
||||
*/
|
||||
export function initWebSocket() {
|
||||
// 检查登录状态
|
||||
if (!checkLoginStatus()) {
|
||||
console.log('未登录,跳过WebSocket初始化');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果已经连接,不需要重复连接
|
||||
if (webSocketManager.isConnected) {
|
||||
console.log('WebSocket已连接,跳过初始化');
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('开始初始化WebSocket...');
|
||||
webSocketManager.connect();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('WebSocket初始化失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开WebSocket连接
|
||||
* 职责:断开WebSocket连接,清理资源
|
||||
*/
|
||||
export function disconnectWebSocket() {
|
||||
try {
|
||||
console.log('断开WebSocket连接...');
|
||||
webSocketManager.disconnect();
|
||||
} catch (error) {
|
||||
console.error('断开WebSocket失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新绑定用户
|
||||
* 职责:在token刷新或用户信息变更后,重新绑定用户
|
||||
* @returns {boolean} 是否绑定成功
|
||||
*/
|
||||
export function rebindUser() {
|
||||
if (!checkLoginStatus()) {
|
||||
console.warn('未登录,无法重新绑定用户');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('重新绑定WebSocket用户...');
|
||||
return webSocketManager.rebindUser();
|
||||
} catch (error) {
|
||||
console.error('重新绑定用户失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取WebSocket连接状态
|
||||
* @returns {boolean} 是否已连接
|
||||
*/
|
||||
export function isWebSocketConnected() {
|
||||
return webSocketManager.isConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取WebSocket认证状态
|
||||
* @returns {boolean} 是否已认证
|
||||
*/
|
||||
export function isWebSocketAuthenticated() {
|
||||
return webSocketManager.isAuthenticated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息处理器
|
||||
* @param {Function} handler - 消息处理函数
|
||||
*/
|
||||
export function addMessageHandler(handler) {
|
||||
webSocketManager.addMessageHandler(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除消息处理器
|
||||
* @param {Function} handler - 消息处理函数
|
||||
*/
|
||||
export function removeMessageHandler(handler) {
|
||||
webSocketManager.removeMessageHandler(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送WebSocket消息
|
||||
* @param {Object} message - 消息对象
|
||||
* @param {boolean} requireAuth - 是否需要认证
|
||||
* @returns {boolean} 是否发送成功
|
||||
*/
|
||||
export function sendWebSocketMessage(message, requireAuth = true) {
|
||||
return webSocketManager.send(message, requireAuth);
|
||||
}
|
||||
314
utils/ws/websocket.js
Normal file
314
utils/ws/websocket.js
Normal file
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* WebSocket管理器
|
||||
* 负责WebSocket连接、消息收发、重连、心跳等功能
|
||||
*/
|
||||
import { websocketConfig } from '@/config/websocket.js';
|
||||
|
||||
export class WebSocketManager {
|
||||
constructor(url = null) {
|
||||
// 使用配置文件中的URL,如果传入了url则使用传入的
|
||||
this.url = url || websocketConfig.getUrl();
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
this.isAuthenticated = false;
|
||||
this.reconnectAttempts = 0;
|
||||
// 从配置文件读取重连配置
|
||||
this.maxReconnectAttempts = websocketConfig.reconnect.maxAttempts;
|
||||
this.reconnectDelay = websocketConfig.reconnect.initialDelay;
|
||||
this.maxReconnectDelay = websocketConfig.reconnect.maxDelay;
|
||||
this.messageHandlers = [];
|
||||
this.pingInterval = null;
|
||||
this.cachedToken = null;
|
||||
// 从配置文件读取用户配置
|
||||
this.userPrefix = websocketConfig.user.prefix;
|
||||
this.platform = websocketConfig.user.platform;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
let token = uni.getStorageSync('token');
|
||||
if (token) {
|
||||
token = token.replace('Bearer ', '');
|
||||
this.cachedToken = token;
|
||||
}
|
||||
return token || null;
|
||||
}
|
||||
|
||||
clearCachedToken() {
|
||||
this.cachedToken = null;
|
||||
}
|
||||
|
||||
rebindUser() {
|
||||
const userId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
|
||||
if (!userId) {
|
||||
console.warn('重新绑定用户失败:doctor_id不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.clearCachedToken();
|
||||
|
||||
if (this.isConnected) {
|
||||
return this.bindUser(userId, true);
|
||||
} else {
|
||||
this.connect();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.isConnected) return;
|
||||
|
||||
console.log('正在连接WebSocket...');
|
||||
this.socket = uni.connectSocket({
|
||||
url: this.url,
|
||||
success: () => {
|
||||
console.log('WebSocket连接成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('WebSocket连接失败:', err);
|
||||
this.handleReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.onOpen(() => {
|
||||
console.log('WebSocket连接已打开');
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
this.startHeartbeat();
|
||||
|
||||
// 绑定当前用户(医生小程序使用doctor-前缀)
|
||||
const userId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
|
||||
if (userId) {
|
||||
this.bindUser(userId, true);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.onMessage((res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data);
|
||||
console.log('收到消息:', data);
|
||||
this.handleMessage(data);
|
||||
} catch (e) {
|
||||
console.error('解析消息失败:', e);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.onClose((res) => {
|
||||
console.log('WebSocket连接关闭:', res);
|
||||
this.isConnected = false;
|
||||
this.stopHeartbeat();
|
||||
this.handleReconnect();
|
||||
});
|
||||
|
||||
this.socket.onError((err) => {
|
||||
console.error('WebSocket错误:', err);
|
||||
});
|
||||
}
|
||||
|
||||
unbindUser(userId) {
|
||||
const token = this.getToken();
|
||||
if (!token) {
|
||||
console.warn('解绑用户失败:token不存在');
|
||||
return false;
|
||||
}
|
||||
// 使用配置文件中的用户前缀
|
||||
const normalizedUserId = userId.toString().startsWith(this.userPrefix) ? userId : `${this.userPrefix}${userId}`;
|
||||
console.log('发送unbind请求,清理旧连接:', normalizedUserId);
|
||||
return this.send({
|
||||
request_type: 'unbind',
|
||||
user_type: 'doctor',
|
||||
sender_user_id: normalizedUserId,
|
||||
token: token
|
||||
}, false);
|
||||
}
|
||||
|
||||
bindUser(userId, cleanOldConnections = true) {
|
||||
const token = this.getToken();
|
||||
if (!token) {
|
||||
console.error('绑定用户失败:token不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使用配置文件中的用户前缀
|
||||
const normalizedUserId = userId.toString().startsWith(this.userPrefix) ? userId : `${this.userPrefix}${userId}`;
|
||||
|
||||
// 构建bind请求对象
|
||||
const bindMessage = {
|
||||
request_type: 'bind',
|
||||
user_type: 'doctor',
|
||||
sender_user_id: normalizedUserId,
|
||||
token: token,
|
||||
platform: this.platform, // 使用配置文件中的平台标识
|
||||
clean_old_connections: cleanOldConnections
|
||||
};
|
||||
|
||||
// 输出详细的调试日志
|
||||
console.log('========== WebSocket Bind 请求 ==========');
|
||||
console.log('用户ID:', normalizedUserId);
|
||||
console.log('用户类型:', 'doctor');
|
||||
console.log('平台标识:', this.platform);
|
||||
console.log('清理旧连接:', cleanOldConnections);
|
||||
console.log('Token存在:', !!token);
|
||||
console.log('完整请求内容:', JSON.stringify(bindMessage, null, 2));
|
||||
console.log('==========================================');
|
||||
|
||||
return this.send(bindMessage);
|
||||
}
|
||||
|
||||
send(message, requireAuth = true) {
|
||||
if (!this.isConnected) {
|
||||
console.error('WebSocket未连接');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (requireAuth && !message.token && message.request_type !== 'ping') {
|
||||
const token = this.getToken();
|
||||
if (token) {
|
||||
message.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是发送消息,确保添加platform标识
|
||||
if (message.request_type === 'send_message' && !message.platform) {
|
||||
message.platform = this.platform;
|
||||
}
|
||||
|
||||
const messageStr = JSON.stringify(message);
|
||||
this.socket.send({
|
||||
data: messageStr,
|
||||
success: () => {
|
||||
console.log('消息发送成功:', message);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('消息发送失败:', err);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('发送消息出错:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
addMessageHandler(handler) {
|
||||
this.messageHandlers.push(handler);
|
||||
}
|
||||
|
||||
removeMessageHandler(handler) {
|
||||
const index = this.messageHandlers.indexOf(handler);
|
||||
if (index !== -1) {
|
||||
this.messageHandlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(data) {
|
||||
if (data.auth_status !== undefined) {
|
||||
if (data.auth_status === 'success') {
|
||||
console.log('========== WebSocket 认证成功 ==========');
|
||||
console.log('认证状态:', data.auth_status);
|
||||
console.log('完整响应:', JSON.stringify(data, null, 2));
|
||||
console.log('========================================');
|
||||
this.isAuthenticated = true;
|
||||
} else if (data.auth_status === 'failed') {
|
||||
console.error('========== WebSocket 认证失败 ==========');
|
||||
console.error('认证状态:', data.auth_status);
|
||||
console.error('错误消息:', data.message || '未知错误');
|
||||
console.error('错误代码:', data.code || '无');
|
||||
console.error('完整响应:', JSON.stringify(data, null, 2));
|
||||
console.error('当前用户ID:', uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id'));
|
||||
console.error('Token存在:', !!this.getToken());
|
||||
console.error('平台标识:', this.platform);
|
||||
console.error('========================================');
|
||||
this.isAuthenticated = false;
|
||||
this.cachedToken = null;
|
||||
} else if (data.auth_status === 'token_expired') {
|
||||
console.warn('========== Token 已过期 ==========');
|
||||
console.warn('认证状态:', data.auth_status);
|
||||
console.warn('错误消息:', data.message || 'Token已过期');
|
||||
console.warn('完整响应:', JSON.stringify(data, null, 2));
|
||||
console.warn('==================================');
|
||||
this.isAuthenticated = false;
|
||||
this.cachedToken = null;
|
||||
uni.$emit('ws-token-expired');
|
||||
const userId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
|
||||
if (userId) {
|
||||
console.log('尝试重新绑定用户...');
|
||||
this.bindUser(userId, true);
|
||||
}
|
||||
} else {
|
||||
console.warn('========== 未知的认证状态 ==========');
|
||||
console.warn('认证状态:', data.auth_status);
|
||||
console.warn('完整响应:', JSON.stringify(data, null, 2));
|
||||
console.warn('==================================');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.clientId) {
|
||||
const userId = uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id');
|
||||
if (userId) {
|
||||
this.bindUser(userId, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.messageHandlers.forEach(handler => {
|
||||
try {
|
||||
handler(data);
|
||||
} catch (e) {
|
||||
console.error('消息处理出错:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳
|
||||
* 职责:按照配置的心跳间隔发送ping消息
|
||||
*/
|
||||
startHeartbeat() {
|
||||
this.pingInterval = setInterval(() => {
|
||||
if (this.isConnected) {
|
||||
this.send({ request_type: 'ping' }, false);
|
||||
}
|
||||
}, websocketConfig.heartbeat.interval);
|
||||
}
|
||||
|
||||
stopHeartbeat() {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理重连
|
||||
* 职责:按照配置的重连策略进行重连
|
||||
*/
|
||||
handleReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.log('已达到最大重连次数');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
// 使用指数退避策略,但不超过最大延迟
|
||||
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
||||
console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
||||
this.connect();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
this.isConnected = false;
|
||||
this.stopHeartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
// 全局WebSocket实例
|
||||
// 使用配置文件中的URL
|
||||
export const webSocketManager = new WebSocketManager();
|
||||
Reference in New Issue
Block a user