Compare commits

39 Commits
master ... main

Author SHA1 Message Date
李琦
69c3e6d30e feat: 病历模块优化、AI辅助问诊、接入deepseek 2026-08-06 18:08:51 +08:00
李琦
f9dd9ad3a2 feat: 诊所问题优化 2026-07-30 07:37:29 +08:00
李琦
5629955dd3 feat: 诊所问题优化 2026-07-28 14:22:56 +08:00
李琦
77a690a071 feat(订单页面): 开票 2026-07-28 12:45:56 +08:00
李琦
8b455b1920 feat(home/chat): 列表骨架屏、聊天室己方头像与协议/医生详情优化
- 新增分包组件 ListSkeleton(doctor/message/product/article/category/zone)
- 首页/消息/资讯首次加载改用骨架屏,刷新不再清空旧数据避免闪空
  · 去掉 home getList、消息 onShow 中的数组清空
  · 三页 pages.json 增加 easycom 与 componentPlaceholder 主包异步引用
- 聊天室己方头像改为读取本地 userinfo.avatarurl(登录后微信头像),
  mycollection 上传头像成功后同步写回 storage
- 修复协议抽屉点不开:page-container 改为 v-if + :show;
  协议名单独可点节点,匹配放宽 name/desc 双向 includes
- 医生气泡头像可点击进入 doctor-detail?readonly=1 只读医生详情,
  隐藏预约挂号区
2026-07-21 16:05:09 +08:00
李琦
2d2a8abfd4 feat(home/chat): 列表骨架屏、聊天室己方头像与协议/医生详情优化
- 新增分包组件 ListSkeleton(doctor/message/product/article/category/zone)
- 首页/消息/资讯首次加载改用骨架屏,刷新不再清空旧数据避免闪空
  · 去掉 home getList、消息 onShow 中的数组清空
  · 三页 pages.json 增加 easycom 与 componentPlaceholder 主包异步引用
- 聊天室己方头像改为读取本地 userinfo.avatarurl(登录后微信头像),
  mycollection 上传头像成功后同步写回 storage
- 修复协议抽屉点不开:page-container 改为 v-if + :show;
  协议名单独可点节点,匹配放宽 name/desc 双向 includes
- 医生气泡头像可点击进入 doctor-detail?readonly=1 只读医生详情,
  隐藏预约挂号区
2026-07-19 16:08:04 +08:00
李琦
2c739324d6 fix:
1. 签收功能接口迁移到新的api
2. 选择药品抽屉优化
2026-07-16 07:52:05 +08:00
李琦
cae91485f6 1. 特色方功能迭代 2026-07-13 16:08:29 +08:00
李琦
b66f47dfbb 1. 特色方功能迭代 2026-07-07 08:50:17 +08:00
李琦
fb0df465a1 1. 特色方功能迭代 2026-07-03 16:48:46 +08:00
李琦
cef264c692 1. 特色方价格计算 2026-07-03 12:33:18 +08:00
李琦
fe7d56027b 1. 特色方 2026-07-03 07:54:03 +08:00
李琦
4ff6564ffb 1. 特色方 2026-07-02 14:11:24 +08:00
李琦
3f699fa2c6 1. 修复支付0元弹出报错 2026-06-24 10:28:47 +08:00
李琦
dcf6c37195 1. 处方修复未通过或者审核中依然能看到药品的问题 2026-06-15 15:57:12 +08:00
李琦
a2cb9fd8ba 1. 处方修复未通过或者审核中依然能看到药品的问题 2026-06-15 13:33:17 +08:00
李琦
aee0973a53 1. 处方修复未通过或者审核中依然能看到药品的问题 2026-06-05 14:41:09 +08:00
李琦
5564c92d33 1. 优化订单模块的UI、接口迁移到新的api 2026-06-01 12:36:08 +08:00
李琦
550332d963 1. 修复价格获取的bug,应该获取诊所|药店价格而不是总仓库的 2026-06-01 11:06:52 +08:00
李琦
105f9eafa2 页面UI更新 2026-06-01 09:52:26 +08:00
李琦
63ea420c03 页面UI更新 2026-05-30 16:54:01 +08:00
李琦
6ec1b72063 保留tabbar在本地 2026-05-15 16:47:46 +08:00
李琦
39843877fd 图片资源更新到云端 2026-05-15 16:42:32 +08:00
李琦
6294a3db0c 修复了一些问题 2026-05-15 16:28:11 +08:00
李琦
cd369ddf09 在线复诊选择药品、说明主诉 2026-05-06 16:41:19 +08:00
李琦
1693b0e236 挂号bug修复 2026-04-08 08:33:15 +08:00
李琦
ce5fc2355b 订单和预约购药页面优化 2026-02-07 23:22:28 +08:00
李琦
4f9ba9cf44 转诊方功能优化 2026-02-07 16:17:01 +08:00
李琦
da8ef2019f 转诊方功能优化 2026-02-07 14:17:30 +08:00
李琦
800aabf5ee 转诊方功能优化 2026-02-07 13:02:15 +08:00
李琦
184ff88bd2 转诊方功能优化 2026-02-07 11:12:18 +08:00
李琦
d233a42754 转诊方功能 2026-02-07 09:42:08 +08:00
李琦
a130f428b9 UI优化 2026-02-02 16:10:46 +08:00
李琦
ca03f17f28 在线问诊默认不能选择到店,聊天UI优化 2026-01-26 16:22:16 +08:00
李琦
e57fd125f7 在线问诊默认不能选择到店,聊天UI优化 2026-01-26 15:55:13 +08:00
李琦
696cb2e0fa 首页默认静默登陆 2026-01-15 12:33:46 +08:00
李琦
c4f322edd6 首页默认静默登陆 2026-01-14 20:59:32 +08:00
李琦
7ed22e2d58 首页默认静默登陆、聊天页面 2026-01-14 18:44:20 +08:00
李琦
11ade5d7ae 迁移一部分接口 2026-01-13 13:36:55 +08:00
155 changed files with 18131 additions and 5351 deletions

View File

@@ -0,0 +1,12 @@
---
description:
alwaysApply: true
---
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
2. 注意不要生成太多的空行上一部分代码和下一部分代码中间的空行不要大于2行
3. 有封装好的方法、组件需要复用,不要重复造轮子
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面记得使用v-if而不是v-show
5. 数据库的created_at、updated_at、deleted_at统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化
6. 微信小程序 `.vue` 模板(会编译成 WXML禁止使用 `??``?.` 等现代运算符,否则会报 `unexpected token '?'`;模板里用 `||` / 显式判断,脚本里可用 `??`/`?.`
7. 微信小程序模板里 `:class` / `:style` 禁止写 `fn(arg)` 方法调用(如 `:class="sexClass(p)"` 会编译失败);用对象/数组字面量(如 `:class="{ male: p.sex == 1 }"`),或把结果先算进 data/computed 再绑定

View File

@@ -0,0 +1,12 @@
---
description:
alwaysApply: true
---
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
2. 注意不要生成太多的空行上一部分代码和下一部分代码中间的空行不要大于2行
3. 有封装好的方法、组件需要复用,不要重复造轮子
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面记得使用v-if而不是v-show
5. 数据库的created_at、updated_at、deleted_at统一使用时间戳不要使用字符串并且我在查询器中一级格式化成字符串了无需再次格式化
6. 微信小程序 `.vue` 模板(会编译成 WXML禁止使用 `??`、`?.` 等现代运算符,否则会报 `unexpected token '?'`;模板里用 `||` / 显式判断,脚本里可用 `??`/`?.`
7. 微信小程序模板里 `:class` / `:style` 禁止写 `fn(arg)` 方法调用(如 `:class="sexClass(p)"` 会编译失败);用对象/数组字面量(如 `:class="{ male: p.sex == 1 }"`),或把结果先算进 data/computed 再绑定

2
.gitignore vendored
View File

@@ -117,3 +117,5 @@ dist
/.hbuilderx/
/.idea/
/database/
/utils/utils.js
/.cursor/skills/

View File

@@ -14,7 +14,7 @@
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="16">
<list size="17">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
@@ -31,6 +31,7 @@
<item index="13" class="java.lang.String" itemvalue="u-form-item" />
<item index="14" class="java.lang.String" itemvalue="u-checkbox-group" />
<item index="15" class="java.lang.String" itemvalue="u-checkbox" />
<item index="16" class="java.lang.String" itemvalue="u-parse" />
</list>
</value>
</option>

1
.mimocode/.cron-lock Normal file
View File

@@ -0,0 +1 @@
{"pid":36108,"startedAt":1783929676073}

115
App.vue
View File

@@ -12,6 +12,15 @@
export default {
// //初始化状态-如果登录过,保持登录状态
onLaunch: async function() {
silentLogin().then((loginResult) => {
if (loginResult.success) {
} else {
console.error('静默登录失败:', loginResult.message);
}
}).catch((error) => {
console.error('静默登录异常:', error);
});
const version = 4 // 如果当前版本和后台返回的版本一致,那就是审核中,审核通过后 后台设置加1如果后台的比本地的大 那就是生产版本
const res = await getVersion()
const val = Number(res.data.data.value)
@@ -34,54 +43,54 @@
// 存储当前版本,也可以使用globalData
uni.setStorageSync('isShow_envVersion', isShow)
// 检查是否已登录,如果未登录才检查配置并决定是否静默登录
const token = uni.getStorageSync('token');
const userInfo = uni.getStorageSync('userinfo');
// // 检查是否已登录,如果未登录才检查配置并决定是否静默登录
// const token = uni.getStorageSync('token');
// const userInfo = uni.getStorageSync('userinfo');
if (!token || !userInfo) {
// if (!token || !userInfo) {
// 未登录,获取系统配置,判断是否需要静默登录
try {
const configRes = await getSystemConfig({
method: "get",
data: {
store_id: uni.getStorageSync('store_id') || '11001'
}
});
// try {
// const configRes = getSystemConfig({
// method: "get",
// data: {
// store_id: uni.getStorageSync('store_id') || '11001'
// }
// });
//
// 根据配置决定是否静默登录
// 注意:需要根据实际接口返回格式解析 need_login 字段
const isGuest = isGuestMode();
let needLogin = true; // 默认需要登录页
if (isGuest && (configRes.data.code === 0 || configRes.data.code === '0')) {
// 新格式:从 result 中获取
const configData = configRes.data.result || {};
needLogin = configData.need_login !== false; // false 表示静默登录
} else if (!isGuest && (configRes.data.errcode === 0 || configRes.data.errcode === '0')) {
// 旧格式:从 data 中获取
const configData = configRes.data.data || {};
needLogin = configData.need_login !== false; // false 表示静默登录
}
// 如果配置为静默登录need_login: false直接执行静默登录
if (!needLogin) {
// 执行静默登录(不显示提示,不跳转)
silentLogin().then((loginResult) => {
if (loginResult.success) {
console.log('静默登录成功');
} else {
console.error('静默登录失败:', loginResult.message);
}
}).catch((error) => {
console.error('静默登录异常:', error);
});
}
// 如果配置为需要登录页need_login: true不执行任何操作等待用户手动登录
} catch (error) {
// 配置接口调用失败,不影响应用启动,仅记录日志
console.error('获取系统配置失败:', error);
}
}
// const isGuest = isGuestMode();
// let needLogin = false; // 默认需要登录页
//
// if (isGuest && (configRes.data.code === 0 || configRes.data.code === '0')) {
// // 新格式:从 result 中获取
// const configData = configRes.data.result || {};
// needLogin = configData.need_login !== false; // false 表示静默登录
// } else if (!isGuest && (configRes.data?.errcode === 0 || configRes.data?.errcode === '0')) {
// // 旧格式:从 data 中获取
// const configData = configRes.data.data || {};
// needLogin = configData.need_login !== false; // false 表示静默登录
// }
//
// // 如果配置为静默登录need_login: false直接执行静默登录
// if (!needLogin) {
// // 执行静默登录(不显示提示,不跳转)
// silentLogin().then((loginResult) => {
// if (loginResult.success) {
// console.log('静默登录成功');
// } else {
// console.error('静默登录失败:', loginResult.message);
// }
// }).catch((error) => {
// console.error('静默登录异常:', error);
// });
// }
// // 如果配置为需要登录页need_login: true不执行任何操作等待用户手动登录
// } catch (error) {
// // 配置接口调用失败,不影响应用启动,仅记录日志
// console.error('获取系统配置失败:', error);
// }
// }
console.log('获取更新机制', uni.canIUse('getUpdateManager'))
// 获取小程序更新机制兼容
@@ -145,8 +154,6 @@
},
onShow: function(e) {
// console.log('App Show')
// this.$store.dispatch('initLogin')
// 解析scene里面的参数
if (e === undefined)
return {};
@@ -185,6 +192,20 @@
console.log('userId', userId, st_id[0].split('su_id=')[1], res);
})
}
// 5.3 处理门店切换/设置
// const storeId = uni.getStorageSync('store_id');
// if (storeId) {
// storeChange({
// method: "post",
// data: {
// store_id: storeId
// }
// }).catch((err) => {
// console.error('切换门店失败:', err);
// });
// } else {
// uni.setStorageSync('store_id', res.data.data.data.store_id);
// }
// console.log(st_id, '4444111111111');
// console.log(scene, '4444');
}
@@ -246,7 +267,7 @@
// 从消息中提取发送者信息
let senderName = '新消息';
let senderAvatar = '/static/xx/ysxx.png';
let senderAvatar = 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/xx/ysxx.png';
// 尝试解析发送者ID获取更多信息
const senderId = message.sender_user_id || '';

View File

@@ -21,7 +21,7 @@ export function isGuestMode() {
export function setGuestMode() {
uni.setStorageSync(GUEST_MODE_KEY, true);
// 同时设置store_id为11001
uni.setStorageSync('store_id', GUEST_STORE_ID);
// uni.setStorageSync('store_id', GUEST_STORE_ID);
}
/**

30
common/utils/drug.js Normal file
View File

@@ -0,0 +1,30 @@
export function isWesternRxDrug(drug) {
return drug && drug.type == 2 && (drug.is_otc === 0 || drug.is_otc === '0');
}
export function isHealthFood(drug) {
return drug && (drug.type == 3 || drug.category_type === 'health_food');
}
export function canShowFunction(drug, prescriptionApproved) {
if (isHealthFood(drug)) return false;
if (isWesternRxDrug(drug)) return !!prescriptionApproved;
return true;
}
export function canShowUsage(drug, prescriptionApproved) {
if (isHealthFood(drug)) return false;
if (isWesternRxDrug(drug)) return !!prescriptionApproved;
return drug && drug.type != 3;
}
export function canShowInstruction(drug, prescriptionApproved) {
if (isWesternRxDrug(drug)) return !!prescriptionApproved;
return true;
}
export function isPrescriptionApprovedStatus(data) {
if (!data) return false;
const ps = data.prescription_status ?? data.status;
return data.status === true || ps === 1 || ps === '1' || ps === 3 || ps === '3' || ps === 4 || ps === '4';
}

View File

@@ -2,7 +2,7 @@
<view v-if="visible" class="notification-wrapper" :class="{ 'slide-in': visible }" :style="{ paddingTop: notificationTop + 'px' }">
<view class="notification" @click="handleClick">
<!-- 头像 -->
<image class="avatar" :src="avatar || '/static/xx/ysxx.png'" mode="aspectFill" />
<image class="avatar" :src="avatar || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/xx/ysxx.png'" mode="aspectFill" />
<!-- 内容区域 -->
<view class="content">

View File

@@ -0,0 +1,539 @@
<template>
<view class="u-upload" v-if="!disabled">
<view
v-if="showUploadList"
class="u-list-item u-preview-wrap"
v-for="(item, index) in lists"
:key="index"
:style="{
width: $u.addUnit(width),
height: $u.addUnit(height)
}"
>
<view
v-if="deletable"
class="u-delete-icon"
@tap.stop="deleteItem(index)"
:style="{
background: delBgColor
}"
>
<u-icon class="u-icon" :name="delIcon" size="20" :color="delColor"></u-icon>
</view>
<u-line-progress
v-if="showProgress && item.progress > 0 && item.progress != 100 && !item.error"
:show-percent="false"
height="16"
class="u-progress"
:percent="item.progress"
></u-line-progress>
<view @tap.stop="retry(index)" v-if="item.error" class="u-error-btn">点击重试</view>
<image @tap.stop="doPreviewImage(item.url || item.path, index)" class="u-preview-image" v-if="!item.isImage" :src="item.url || item.path" :mode="imageMode"></image>
</view>
<slot name="file" :file="lists"></slot>
<view style="display: inline-block;" @tap="selectFile" v-if="maxCount > lists.length">
<slot name="addBtn"></slot>
<view
v-if="!customBtn"
class="u-list-item u-add-wrap"
hover-class="u-add-wrap__hover"
hover-stay-time="150"
:style="{
width: $u.addUnit(width),
height: $u.addUnit(height)
}"
>
<u-icon name="plus" class="u-add-btn" size="40"></u-icon>
<view class="u-add-tips">{{ uploadText }}</view>
</view>
</view>
</view>
</template>
<script>
import { prepareImagePath } from '@/utils/image-compress.js';
export default {
name: 'c-upload',
props: {
showUploadList: {
type: Boolean,
default: true
},
action: {
type: String,
default: ''
},
maxCount: {
type: [String, Number],
default: 52
},
showProgress: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
},
imageMode: {
type: String,
default: 'aspectFill'
},
header: {
type: Object,
default() {
return {};
}
},
formData: {
type: Object,
default() {
return {};
}
},
name: {
type: String,
default: 'file'
},
sizeType: {
type: Array,
default() {
return ['original', 'compressed'];
}
},
sourceType: {
type: Array,
default() {
return ['album', 'camera'];
}
},
previewFullImage: {
type: Boolean,
default: true
},
multiple: {
type: Boolean,
default: true
},
deletable: {
type: Boolean,
default: true
},
maxSize: {
type: [String, Number],
default: Number.MAX_VALUE
},
fileList: {
type: Array,
default() {
return [];
}
},
uploadText: {
type: String,
default: '选择图片'
},
autoUpload: {
type: Boolean,
default: true
},
showTips: {
type: Boolean,
default: true
},
customBtn: {
type: Boolean,
default: false
},
width: {
type: [String, Number],
default: 200
},
height: {
type: [String, Number],
default: 200
},
delBgColor: {
type: String,
default: '#fa3534'
},
delColor: {
type: String,
default: '#ffffff'
},
delIcon: {
type: String,
default: 'close'
},
toJson: {
type: Boolean,
default: true
},
beforeUpload: {
type: Function,
default: null
},
beforeRemove: {
type: Function,
default: null
},
limitType: {
type: Array,
default() {
return ['png', 'jpg', 'jpeg', 'webp', 'gif', 'image'];
}
},
index: {
type: [Number, String],
default: ''
}
},
data() {
return {
lists: [],
isInCount: true,
uploading: false
};
},
watch: {
fileList: {
immediate: true,
handler(val) {
val.map(value => {
let tmp = this.lists.some(val => {
return val.url == value.url;
})
!tmp && this.lists.push({ url: value.url, error: false, progress: 100 });
});
}
},
lists(n) {
this.$emit('on-list-change', n, this.index);
}
},
methods: {
clear() {
this.lists = [];
},
reUpload() {
this.uploadFile();
},
selectFile() {
if (this.disabled) return;
const { maxCount, multiple, maxSize, sizeType, lists, sourceType } = this;
const newMaxCount = maxCount - lists.length;
uni.chooseImage({
count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
sourceType: sourceType,
sizeType,
success: async (res) => {
const listOldLength = this.lists.length;
for (let index = 0; index < res.tempFiles.length; index++) {
const val = res.tempFiles[index];
if (!this.checkFileExt(val)) continue;
if (!multiple && index >= 1) continue;
if (val.size > maxSize) {
this.$emit('on-oversize', val, this.lists, this.index);
this.showToast('超出允许的文件大小');
continue;
}
if (maxCount <= lists.length) {
this.$emit('on-exceed', val, this.lists, this.index);
this.showToast('超出最大允许的文件个数');
break;
}
try {
const prepared = await prepareImagePath({
path: val.path,
size: val.size,
});
if (!prepared) continue;
lists.push({
url: prepared.path,
progress: 0,
error: false,
file: {
...val,
path: prepared.path,
size: prepared.size,
},
});
} catch (error) {
this.$emit('on-choose-fail', error);
this.showToast('图片处理失败');
}
}
this.$emit('on-choose-complete', this.lists, this.index);
if (this.autoUpload) this.uploadFile(listOldLength);
},
fail: (error) => {
this.$emit('on-choose-fail', error);
}
});
},
showToast(message, force = false) {
if (this.showTips || force) {
uni.showToast({
title: message,
icon: 'none'
});
}
},
upload() {
this.uploadFile();
},
retry(index) {
this.lists[index].progress = 0;
this.lists[index].error = false;
this.lists[index].response = null;
uni.showLoading({
title: '重新上传'
});
this.uploadFile(index);
},
async uploadFile(index = 0) {
if (this.disabled) return;
if (this.uploading) return;
if (index >= this.lists.length) {
this.$emit('on-uploaded', this.lists, this.index);
return;
}
if (this.lists[index].progress == 100) {
if (this.autoUpload == false) this.uploadFile(index + 1);
return;
}
if (this.beforeUpload && typeof(this.beforeUpload) === 'function') {
let beforeResponse = this.beforeUpload.bind(this.$u.$parent.call(this))(index, this.lists);
if (!!beforeResponse && typeof beforeResponse.then === 'function') {
await beforeResponse.then(res => {
}).catch(err => {
return this.uploadFile(index + 1);
})
} else if (beforeResponse === false) {
return this.uploadFile(index + 1);
}
}
if (!this.action) {
this.showToast('请配置上传地址', true);
return;
}
this.lists[index].error = false;
this.uploading = true;
const task = uni.uploadFile({
url: this.action,
filePath: this.lists[index].url,
name: this.name,
formData: this.formData,
header: this.header,
// #ifdef MP-ALIPAY
fileType:'image',
// #endif
success: res => {
let data = this.toJson && this.$u.test.jsonString(res.data) ? JSON.parse(res.data) : res.data;
if (![200, 201, 204].includes(res.statusCode)) {
this.uploadError(index, data);
} else {
this.lists[index].response = data;
this.lists[index].progress = 100;
this.lists[index].error = false;
this.$emit('on-success', data, index, this.lists, this.index);
}
},
fail: e => {
this.uploadError(index, e);
},
complete: res => {
uni.hideLoading();
this.uploading = false;
this.uploadFile(index + 1);
this.$emit('on-change', res, index, this.lists, this.index);
}
});
task.onProgressUpdate(res => {
if (res.progress > 0) {
this.lists[index].progress = res.progress;
this.$emit('on-progress', res, index, this.lists, this.index);
}
});
},
uploadError(index, err) {
this.lists[index].progress = 0;
this.lists[index].error = true;
this.lists[index].response = null;
this.$emit('on-error', err, index, this.lists, this.index);
this.showToast('上传失败,请重试');
},
deleteItem(index) {
uni.showModal({
title: '提示',
content: '您确定要删除此项吗?',
success: async (res) => {
if (res.confirm) {
if (this.beforeRemove && typeof(this.beforeRemove) === 'function') {
let beforeResponse = this.beforeRemove.bind(this.$u.$parent.call(this))(index, this.lists);
if (!!beforeResponse && typeof beforeResponse.then === 'function') {
await beforeResponse.then(res => {
this.handlerDeleteItem(index);
}).catch(err => {
this.showToast('已终止移除');
})
} else if (beforeResponse === false) {
this.showToast('已终止移除');
} else {
this.handlerDeleteItem(index);
}
} else {
this.handlerDeleteItem(index);
}
}
}
});
},
handlerDeleteItem(index) {
if (this.lists[index].progress < 100 && this.lists[index].progress > 0) {
typeof this.lists[index].uploadTask != 'undefined' && this.lists[index].uploadTask.abort();
}
this.lists.splice(index, 1);
this.$forceUpdate();
this.$emit('on-remove', index, this.lists, this.index);
this.showToast('移除成功');
},
remove(index) {
if (index >= 0 && index < this.lists.length) {
this.lists.splice(index, 1);
this.$emit('on-list-change', this.lists, this.index);
}
},
doPreviewImage(url, index) {
if (!this.previewFullImage) return;
const images = this.lists.map(item => item.url || item.path);
uni.previewImage({
urls: images,
current: url,
success: () => {
this.$emit('on-preview', url, this.lists, this.index);
},
fail: () => {
uni.showToast({
title: '预览图片失败',
icon: 'none'
});
}
});
},
checkFileExt(file) {
let noArrowExt = false;
let fileExt = '';
const reg = /.+\./;
// #ifdef H5
fileExt = file.name.replace(reg, "").toLowerCase();
// #endif
// #ifndef H5
fileExt = file.path.replace(reg, "").toLowerCase();
// #endif
noArrowExt = this.limitType.some(ext => {
return ext.toLowerCase() === fileExt;
})
if (!noArrowExt) this.showToast(`不允许选择${fileExt}格式的文件`);
return noArrowExt;
}
}
};
</script>
<style lang="scss" scoped>
@import 'uview-ui/libs/css/style.components.scss';
.u-upload {
@include vue-flex;
flex-wrap: wrap;
align-items: center;
}
.u-list-item {
width: 200rpx;
height: 200rpx;
overflow: hidden;
margin: 10rpx;
background: rgb(244, 245, 246);
position: relative;
border-radius: 10rpx;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
}
.u-preview-wrap {
border: 1px solid rgb(235, 236, 238);
}
.u-add-wrap {
flex-direction: column;
color: $u-content-color;
font-size: 26rpx;
}
.u-add-tips {
margin-top: 20rpx;
line-height: 40rpx;
}
.u-add-wrap__hover {
background-color: rgb(235, 236, 238);
}
.u-preview-image {
display: block;
width: 100%;
height: 100%;
border-radius: 10rpx;
}
.u-delete-icon {
position: absolute;
top: 10rpx;
right: 10rpx;
z-index: 10;
background-color: $u-type-error;
border-radius: 100rpx;
width: 44rpx;
height: 44rpx;
@include vue-flex;
align-items: center;
justify-content: center;
}
.u-icon {
@include vue-flex;
align-items: center;
justify-content: center;
}
.u-progress {
position: absolute;
bottom: 10rpx;
left: 8rpx;
right: 8rpx;
z-index: 9;
width: auto;
}
.u-error-btn {
color: #ffffff;
background-color: $u-type-error;
font-size: 20rpx;
padding: 4px 0;
text-align: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 9;
line-height: 1;
}
</style>

View File

@@ -0,0 +1,676 @@
<template>
<u-popup v-model="visible" mode="center" width="85%" border-radius="16" :mask-close-able="false">
<view class="popup-wrap">
<view class="popup-title">图片较大</view>
<view v-if="sizeText" class="size-text">{{ sizeText }}</view>
<view v-if="status === 'compressing'" class="compressing-panel">
<image
v-if="originalPath"
:src="originalPath"
mode="aspectFit"
class="preview-image single"
@click="previewImage(originalPath)"
/>
<text v-if="originalPath" class="preview-hint">点击预览</text>
<view class="compressing-status">
<u-loading mode="circle" />
<text class="progress-text">{{ progressText }}</text>
</view>
</view>
<view v-else-if="status === 'ready'" class="compare-panel">
<view class="preview-column">
<text class="preview-label">原图</text>
<image
:src="originalPath"
mode="aspectFit"
class="preview-image"
@click="previewImage(originalPath)"
/>
<text class="preview-size">{{ formatFileSize(originalSize) }}</text>
<text class="preview-hint">点击预览</text>
</view>
<view class="preview-column">
<text class="preview-label">压缩后</text>
<image
:src="compressedPath"
mode="aspectFit"
class="preview-image"
@click="previewImage(compressedPath)"
/>
<text class="preview-size">{{ formatFileSize(compressedSize) }}</text>
<text class="preview-hint">点击预览</text>
</view>
</view>
<view v-else-if="status === 'gifSkipped'" class="gif-panel">
<image
v-if="originalPath"
:src="originalPath"
mode="aspectFit"
class="preview-image single"
@click="previewImage(originalPath)"
/>
<text v-if="originalPath" class="preview-hint">点击预览</text>
<text class="hint-text">GIF 不支持压缩将上传原图</text>
</view>
<view v-else class="error-panel">
<image
v-if="originalPath"
:src="originalPath"
mode="aspectFit"
class="preview-image single"
@click="previewImage(originalPath)"
/>
<text v-if="originalPath" class="preview-hint">点击预览</text>
<text class="error-text">{{ errorMessage }}</text>
</view>
<view class="footer-actions">
<view class="btn btn-default" @click="finish('cancel')">取消</view>
<view
v-if="status === 'ready' || status === 'error'"
class="btn btn-default"
@click="finish('original')"
>
{{ originalButtonLabel }}
</view>
<view
v-if="status === 'ready'"
class="btn btn-primary"
@click="finish('compressed')"
>
{{ compressedButtonLabel }}
</view>
<view
v-if="status === 'gifSkipped'"
class="btn btn-primary"
@click="finish('original')"
>
{{ originalButtonLabel }}
</view>
</view>
</view>
</u-popup>
</template>
<script>
import {
formatFileSize,
formatSavingsPercent,
isGifPath,
tryCompressImage,
} from '@/utils/image-compress.js';
export default {
name: 'ImageCompressPopup',
data() {
return {
visible: false,
status: 'compressing',
progressText: '正在压缩,请稍候…',
errorMessage: '',
originalPath: '',
compressedPath: '',
originalSize: 0,
compressedSize: 0,
skipped: false,
resolver: null,
};
},
computed: {
sizeText() {
if (!this.originalSize) return '';
const originalText = formatFileSize(this.originalSize);
if (this.status === 'gifSkipped') {
return `原图 ${originalText}`;
}
if (this.compressedSize && this.status === 'ready') {
const savings = formatSavingsPercent(this.originalSize, this.compressedSize);
return `原图 ${originalText} → 压缩后 ${formatFileSize(this.compressedSize)}${savings}`;
}
if (this.compressedSize) {
return `原图 ${originalText} → 压缩后 ${formatFileSize(this.compressedSize)}`;
}
return `原图 ${originalText}`;
},
originalButtonLabel() {
if (!this.originalSize) return '使用原图';
return `使用原图 (${formatFileSize(this.originalSize)})`;
},
compressedButtonLabel() {
if (!this.compressedSize) return '使用压缩图';
return `使用压缩图 (${formatFileSize(this.compressedSize)})`;
},
},
methods: {
formatFileSize,
previewImage(current) {
if (!this.originalPath) return;
const urls =
this.status === 'ready' && this.compressedPath
? [this.originalPath, this.compressedPath]
: [this.originalPath];
uni.previewImage({
urls,
current,
});
},
open({ path, size }) {
return new Promise((resolve) => {
this.resolver = resolve;
this.originalPath = path;
this.originalSize = size;
this.compressedPath = '';
this.compressedSize = 0;
this.skipped = false;
this.errorMessage = '';
this.progressText = '正在压缩,请稍候…';
this.visible = true;
this.startCompress(path, size);
});
},
async startCompress(path, size) {
if (isGifPath(path)) {
this.skipped = true;
this.status = 'gifSkipped';
return;
}
this.status = 'compressing';
this.progressText = '正在压缩(质量 80%)…';
try {
const compressed = await tryCompressImage(path, [80, 60, 40], (quality) => {
this.progressText = `正在压缩(质量 ${quality}%)…`;
});
this.compressedPath = compressed.path;
this.compressedSize = compressed.size;
this.status = 'ready';
this.progressText = '';
} catch (error) {
this.status = 'error';
this.errorMessage = '图片压缩失败,请使用原图或取消';
this.progressText = '';
}
},
finish(choice) {
const result = { choice, path: null, size: 0, usedCompress: false };
if (choice === 'cancel') {
this.visible = false;
this.resolver && this.resolver(null);
this.resolver = null;
return;
}
if (choice === 'compressed' && this.status === 'ready') {
result.path = this.compressedPath;
result.size = this.compressedSize;
result.usedCompress = true;
} else {
result.path = this.originalPath;
result.size = this.originalSize;
result.usedCompress = false;
}
this.visible = false;
this.resolver && this.resolver(result);
this.resolver = null;
},
},
};
</script>
<style lang="scss" scoped>
.popup-wrap {
padding: 32rpx;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
text-align: center;
margin-bottom: 16rpx;
}
.size-text {
font-size: 26rpx;
color: #666;
text-align: center;
margin-bottom: 24rpx;
}
.preview-image {
width: 100%;
height: 280rpx;
background: #fafafa;
border-radius: 12rpx;
}
.preview-image.single {
width: 100%;
}
.compare-panel {
display: flex;
gap: 16rpx;
}
.preview-column {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.preview-label {
font-size: 26rpx;
font-weight: 500;
margin-bottom: 12rpx;
}
.preview-size {
margin-top: 8rpx;
font-size: 22rpx;
color: #999;
}
.preview-hint {
margin-top: 4rpx;
font-size: 22rpx;
color: #bbb;
}
.compressing-panel,
.gif-panel,
.error-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.compressing-status {
display: flex;
align-items: center;
gap: 16rpx;
}
.progress-text,
.hint-text {
font-size: 26rpx;
color: #666;
}
.error-text {
font-size: 26rpx;
color: #fa3534;
}
.footer-actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 32rpx;
}
.btn {
min-width: 160rpx;
padding: 16rpx 24rpx;
border-radius: 8rpx;
font-size: 26rpx;
text-align: center;
}
.btn-default {
background: #f5f5f5;
color: #333;
}
.btn-primary {
background: #2979ff;
color: #fff;
}
</style>

View File

@@ -0,0 +1,181 @@
<template>
<u-popup
v-model="popupVisible"
mode="bottom"
border-radius="24"
:safe-area-inset-bottom="true"
:closeable="true"
@close="onPopupClose"
>
<view class="drawer-inner">
<view class="drawer-header">
<text class="drawer-title">选择待支付挂号</text>
</view>
<view v-if="!list.length" class="drawer-empty">暂无待支付挂号订单</view>
<scroll-view v-else scroll-y class="drawer-scroll">
<view
v-for="item in list"
:key="item.id"
class="drawer-item"
@click="onSelect(item)"
>
<view class="item-main">
<view class="item-row">
<text class="item-label">就诊人</text>
<text class="item-value">{{ displayText(item.patient) }}</text>
</view>
<view class="item-row">
<text class="item-label">医生</text>
<text class="item-value">{{ displayText(item.doctor) }}</text>
</view>
<view class="item-row">
<text class="item-label">诊所</text>
<text class="item-value">{{ displayText(item.store) }}</text>
</view>
<view class="item-row">
<text class="item-label">挂号时间</text>
<text class="item-value">{{ displayText(item.created_at) }}</text>
</view>
</view>
<u-icon name="arrow-right" color="#94A3B8" size="28"></u-icon>
</view>
</scroll-view>
</view>
</u-popup>
</template>
<script>
/**
* 待支付挂号订单选择抽屉
* 首页多条待付款挂号时,供用户选择具体订单前往支付
*/
export default {
name: 'UnpaidRegisterDrawer',
props: {
// v-model 控制抽屉显示隐藏
value: {
type: Boolean,
default: false,
},
// 待支付挂号列表,字段来自 fetchUnpaidRegisterListApi
list: {
type: Array,
default: () => [],
},
},
computed: {
popupVisible: {
get() {
return this.value;
},
set(val) {
this.$emit('input', val);
},
},
},
methods: {
/**
* 字段为空时显示占位符
* @param {string} val 展示值
*/
displayText(val) {
const text = val != null ? String(val).trim() : '';
return text || '--';
},
/**
* 关闭抽屉
*/
onPopupClose() {
this.$emit('input', false);
},
/**
* 选中某条挂号订单,通知父组件跳转支付
* @param {Object} item 挂号订单
*/
onSelect(item) {
if (!item || item.id == null) return;
this.$emit('select', item.id);
this.$emit('input', false);
},
},
};
</script>
<style lang="scss" scoped>
.drawer-inner {
display: flex;
flex-direction: column;
max-height: 70vh;
box-sizing: border-box;
padding: 24rpx 24rpx 0;
}
.drawer-header {
padding-bottom: 20rpx;
text-align: center;
}
.drawer-title {
font-size: 32rpx;
font-weight: 600;
color: #0f172a;
}
.drawer-empty {
padding: 48rpx 0 64rpx;
text-align: center;
color: #94a3b8;
font-size: 28rpx;
}
.drawer-scroll {
max-height: 60vh;
min-height: 200rpx;
}
.drawer-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 20rpx;
margin-bottom: 16rpx;
background: linear-gradient(90deg, rgba(65, 117, 238, 0.06) 0%, rgba(65, 117, 238, 0.02) 100%);
border: 1rpx solid rgba(65, 117, 238, 0.15);
border-radius: 16rpx;
&:active {
opacity: 0.85;
}
}
.item-main {
flex: 1;
min-width: 0;
margin-right: 16rpx;
}
.item-row {
display: flex;
align-items: flex-start;
margin-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
}
.item-label {
flex-shrink: 0;
width: 140rpx;
font-size: 26rpx;
color: #64748b;
}
.item-value {
flex: 1;
font-size: 26rpx;
color: #0f172a;
line-height: 1.5;
word-break: break-all;
}
</style>

View File

@@ -6,12 +6,20 @@ import Vue from 'vue'
// main.js
import uView from "uview-ui";
import share from '@/common/share.js'
import {setGuestMode} from "@/common/utils/auth";
Vue.mixin(share)
Vue.use(uView);
//防抖函数
import common from './common/common.js'
Vue.prototype.$noMultipleClicks = common.noMultipleClicks;
const token = uni.getStorageSync('token');
const userInfo = uni.getStorageSync('userinfo');
if (!token || !userInfo) {
console.log('未登录')
setGuestMode();
}
//时间戳的处理
Vue.filter("formatDate", function(value) {
var date = new Date(value * 1000); //时间戳为10位需*1000时间戳为13位的话不需乘1000

View File

@@ -3,19 +3,30 @@
// 下载安装方式
// "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
// npm安装方式
// "^u-upload$": "@/components/c-upload/c-upload.vue",
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
// 消息通知组件
"^MessageNotification": "@/components/MessageNotification/MessageNotification.vue"
"^MessageNotification": "@/components/MessageNotification/MessageNotification.vue",
"^FollowUpDrugDrawer$": "@/subPackages/doctor/components/FollowUpDrugDrawer.vue",
// 列表骨架屏(分包组件,主包页需配合 componentPlaceholder
"^ListSkeleton$": "@/subPackages/common/components/ListSkeleton.vue",
// 问诊助手壳(仅分包页使用,放分包以减小主包体积)
"^AssistantConsultationShell$": "@/subPackages/common/components/AssistantConsultationShell.vue"
},
"pages": [
{
"path": "pages/home/home",
"style": {
// "navigationBarTitleText": "custom",
"navigationStyle": "custom"
"navigationStyle": "custom",
"enablePullDownRefresh": true,
// "navigationStyle": "default",
// "navigationBarBackgroundColor": "#298DFF",
// "navigationBarTextStyle": "white"
// 主包异步引用分包 ListSkeleton
"componentPlaceholder": {
"list-skeleton": "view"
}
}
},
{
@@ -51,7 +62,12 @@
{
"path": "pages/cate/index",
"style": {
"navigationBarTitleText": "健康资讯"
"navigationBarTitleText": "健康资讯",
"enablePullDownRefresh": true,
// 主包异步引用分包 ListSkeleton
"componentPlaceholder": {
"list-skeleton": "view"
}
}
// "needLogin": true
},
@@ -65,7 +81,12 @@
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "消息"
"navigationBarTitleText": "消息",
"enablePullDownRefresh": true,
// 主包异步引用分包 ListSkeleton
"componentPlaceholder": {
"list-skeleton": "view"
}
}
// "needLogin": true
},
@@ -185,6 +206,28 @@
"navigationBarTitleText": "商品详情",
"enablePullDownRefresh": false
}
},
{
"path": "follow-up/medication-info",
"style": {
"navigationBarTitleText": "用药信息",
"enablePullDownRefresh": false
}
},
{
"path": "special-prescription/list",
"style": {
"navigationBarTitleText": "特色药方",
"enablePullDownRefresh": false,
"disableScroll": true
}
},
{
"path": "special-prescription/detail",
"style": {
"navigationBarTitleText": "药方详情",
"enablePullDownRefresh": false
}
}
]
},
@@ -312,6 +355,20 @@
"navigationBarTitleText": "订单详情",
"enablePullDownRefresh": false
}
},
{
"path": "my/my-drug/invoice-apply",
"style": {
"navigationBarTitleText": "申请开票",
"enablePullDownRefresh": true
}
},
{
"path": "my/my-drug/invoice-detail",
"style": {
"navigationBarTitleText": "开票详情",
"enablePullDownRefresh": false
}
}
]
},
@@ -466,6 +523,21 @@
"path": "chat/chat",
"style": {
"navigationBarTitleText": "在线问诊",
"navigationStyle": "custom",
"enablePullDownRefresh": false
}
},
{
"path": "transfer/transfer-message",
"style": {
"navigationBarTitleText": "转诊消息",
"enablePullDownRefresh": true
}
},
{
"path": "transfer/transfer-consultation",
"style": {
"navigationBarTitleText": "转诊咨询",
"enablePullDownRefresh": false
}
}

View File

@@ -43,11 +43,11 @@
<!-- 选择坐诊医生 -->
<view class="doctor_list">选择坐诊医生</view>
<!-- 列表 -->
<view class="list" v-for="(item,index) in doctorList" :key="item.id" @click="goLookDoctor(item.id)">
<view class="list" v-for="(item,index) in doctorList" :key="item.id">
<!-- 图片部分 -->
<view class="list_info">
<view class="list_left">
<image :src="item.avatar || '/static/mine/avatar_1.png'" mode="医生头像"></image>
<view class="list_left" @click="goLookDoctor(item.id)">
<image :src="item.avatar || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png'" mode="医生头像"></image>
<!-- 医生 -->
<view class="list_title">
<view class="titles_name">
@@ -61,10 +61,10 @@
</view>
</view>
<view class="list_right">{{ typeMap[type] }}</view>
<view class="list_right" @click.stop="goRegister(item)">{{ typeMap[type] }}</view>
</view>
<!-- 文字部分 -->
<view class="card">
<view class="card" @click="goLookDoctor(item.id)">
<!-- 擅长 -->
<view class="list_going">
擅长{{item.good_at}}
@@ -129,7 +129,7 @@
import {
dDropdown
} from '@/components/d-dropdown/d-dropdown'
import { isGuestMode } from '@/common/utils/auth.js'
import { isGuestMode, isLoggedIn, saveRedirectInfo, buildUrlWithParams } from '@/common/utils/auth.js'
export default {
components: {
dDropdown
@@ -172,11 +172,71 @@
type: "",
}
},
onLoad(e) {
async onLoad(e) {
this.type = e.type
// console.log(e, 'idd');
// 当 type === 2 (诊所在线问诊) 时,直接获取委托诊所和医生信息并跳转到挂号页面
if (this.type === '2') {
try {
const storeId = uni.getStorageSync('store_id') || '11001';
const { getClinicOnlineConsultationInfoApi } = await import('@/request/api/product');
const configRes = await getClinicOnlineConsultationInfoApi({ store_id: storeId });
if (configRes.data?.code === 0 || configRes.data?.code === '0') {
const result = configRes.data?.result;
if (result?.can_use && result?.delegate_doctor_id) {
// 直接跳转到就诊人选择页面传递医生ID和挂号类型
uni.redirectTo({
url: `/subPackages/doctor/doctor-userinfo?id=${result.delegate_doctor_id}&r_type=2&delegate_store_id=${result.delegate_store_id || storeId}`
});
return;
} else {
uni.showToast({
title: result?.message || '该门店暂不支持在线问诊功能',
icon: 'none',
duration: 2000
});
// 返回上一页
setTimeout(() => {
uni.navigateBack();
}, 2000);
return;
}
} else {
uni.showToast({
title: configRes.data?.message || '获取在线问诊信息失败',
icon: 'none',
duration: 2000
});
// 返回上一页
setTimeout(() => {
uni.navigateBack();
}, 2000);
return;
}
} catch (error) {
console.error('获取在线问诊信息失败:', error);
uni.showToast({
title: '获取在线问诊信息失败',
icon: 'none',
duration: 2000
});
// 返回上一页
setTimeout(() => {
uni.navigateBack();
}, 2000);
return;
}
}
},
methods: {
resolveRegisterType(pageType) {
const t = String(pageType ?? '0');
if (['0', '1', '2', '3'].includes(t)) return t;
return '0';
},
closeDropdown() {
this.$refs.uDropdown.close();
this.getList()
@@ -254,6 +314,60 @@
})
},
async goRegister(item) {
if (!isLoggedIn()) {
const storeId = uni.getStorageSync('store_id') || '11001';
saveRedirectInfo('/subPackages/doctor/doctor-userinfo', {
id: item.id,
r_type: this.resolveRegisterType(this.type),
store_id: storeId,
});
uni.navigateTo({
url: buildUrlWithParams('/pages/login/login', {
redirect: 'doctor-userinfo',
doctor_id: item.id,
store_id: storeId,
})
});
return;
}
const rType = this.resolveRegisterType(this.type);
if (rType === '2') {
try {
const storeId = uni.getStorageSync('store_id') || '11001';
const { getClinicOnlineConsultationInfoApi } = await import('@/request/api/product');
const configRes = await getClinicOnlineConsultationInfoApi({ store_id: storeId });
if (configRes.data?.code === 0 || configRes.data?.code === '0') {
const result = configRes.data?.result;
if (result?.can_use && result?.delegate_doctor_id) {
uni.navigateTo({
url: `/subPackages/doctor/doctor-userinfo?id=${result.delegate_doctor_id}&r_type=2&delegate_store_id=${result.delegate_store_id || storeId}`
});
return;
}
uni.showToast({
title: result?.message || '该门店暂不支持在线问诊功能',
icon: 'none',
});
return;
}
uni.showToast({
title: configRes.data?.message || '获取在线问诊信息失败',
icon: 'none',
});
} catch (error) {
console.error('获取在线问诊信息失败:', error);
uni.showToast({ title: '获取在线问诊信息失败', icon: 'none' });
}
return;
}
uni.navigateTo({
url: `/subPackages/doctor/doctor-userinfo?id=${item.id}&r_type=${rType}`,
});
},
goProduct() {
uni.navigateTo({
url: '/subPackages/product/product?id=' + id

View File

@@ -47,7 +47,7 @@
<view class="it_info">{{item.small_info}}</view>
<view class="it_price">
<text>{{item.drugstore_drug.price}}</text>
<image src="/static/home/gwc.png" mode=""
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/home/gwc.png" mode=""
@tap.stop="toCartImg(item.drugstore_drug.drug_id)"></image>
</view>
</view>

View File

@@ -5,7 +5,8 @@
<view class="search">
<u-search placeholder="输入文章标题" bg-color="#F5f5f5" color="#DDDDDD" border-color="#F5f5f5"
:show-action="false" maxlength="21" height="66" :clearabled="true" shape="round" v-model="keyword"
@search="toSearch">
@search="toSearch"
@clear="clearSearch">
</u-search>
</view>
@@ -13,7 +14,9 @@
<!-- //药品分类左 -->
<view class="goods">
<view class="goods_left" @touchmove.stop>
<scroll-view scroll-y scroll-with-animation class="u-menu-scroll-view" :scroll-top="scrollTop"
<!-- 首次加载分类骨架 -->
<ListSkeleton v-if="categoryInitialLoading" type="category" :rows="8" />
<scroll-view v-else scroll-y scroll-with-animation class="u-menu-scroll-view" :scroll-top="scrollTop"
:scroll-into-view="itemId">
<view v-for="(item,index) in categoryLists" :key="item.id" class="u-tab-item"
:class="[num == item.id ? 'u-item-active' : '']" @tap.stop="changeStyle(item.id)"
@@ -26,15 +29,18 @@
<view class="goods_right">
<!-- 药品 -->
<view class="second">
<!-- 商品列表 -->
<view class="list" v-for="(item,index) in infoList" @click="toDetail(item.id)" :key="item.id">
<view class="list_">
<image :src="item.cover||'/static/xx/cp.png'" mode=""></image>
<view class="list_title">
{{item.title}}
<!-- 首次或切分类且当前无文章时展示骨架有旧文章则保留至新结果 -->
<ListSkeleton v-if="showArticleSkeleton" type="article" :rows="4" />
<template v-else>
<view class="list" v-for="(item,index) in infoList" @click="toDetail(item.id)" :key="item.id">
<view class="list_">
<image :src="item.cover||'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/xx/cp.png'" mode=""></image>
<view class="list_title">
{{item.title}}
</view>
</view>
</view>
</view>
</template>
</view>
</view>
</view>
@@ -65,16 +71,36 @@
selectIndex: 0,
arr: [], // 储存距离顶部高度的数组
scrollRightTop: 0, // 右边栏目scroll-view的滚动条高度
show: false
show: false,
// 首次加载骨架
categoryInitialLoading: true,
articleInitialLoading: true,
// 切分类且当前列表为空时拉文章期间展示骨架
articleFetchingEmpty: false
}
},
computed: {
// 是否展示右侧文章骨架:首次加载,或请求中且当前无文章可保留
showArticleSkeleton() {
return this.articleInitialLoading || this.articleFetchingEmpty
}
},
onLoad(op) {
},
methods: {
// 搜索
// 搜索:按当前分类 + 标题关键词拉文章(不再误调分类接口)
toSearch() {
this.getList()
this.infoList = []
this.articleFetchingEmpty = true
this.getArticleList()
},
// 清空搜索词后重新拉当前分类文章
clearSearch() {
this.keyword = ''
this.infoList = []
this.articleFetchingEmpty = true
this.getArticleList()
},
/*切换需要*/
changeStyle(id) {
@@ -86,48 +112,76 @@
url: '/pages/cate/info-detail?id=' + e
})
},
//点击药品一级分类e
// 切分类:立即清空文章出骨架
handleCategory1(e, index) {
this.num = e
this.cid = e
this.infoList = []
this.articleFetchingEmpty = true
this.getArticleList()
},
//获取药品分类
// 获取分类:返回 Promise 供下拉刷新等待
getList() {
categoriesList({
return categoriesList({
method: "post",
data: {
store_id: uni.getStorageSync('store_id') || '11001',
}
}).then((res) => {
// console.log(res, 'list');
if (res.data.errcode == 0) {
// console.log(res, 'res');
this.categoryLists = res.data.data.list
this.cid = res.data.data.list.map(x => x.id)
this.num = res.data.data.list[0].id //初始选中
// this.current = res.data.data[0].subs[0].id //初始选中2
if (res.data.code == 0) {
const list = (res.data.result && res.data.result.list) || []
this.categoryLists = list
if (!list.length) {
this.infoList = []
this.articleInitialLoading = false
this.articleFetchingEmpty = false
return
}
const firstId = list[0].id
this.num = firstId
this.cid = firstId
return this.getArticleList()
}
}).finally(() => {
this.categoryInitialLoading = false
})
this.getArticleList()
},
// 文章列表
// 文章列表:可选 keyword 标题搜索
getArticleList() {
articleList({
if (!this.infoList || this.infoList.length === 0) {
this.articleFetchingEmpty = true
}
const data = {
store_id: uni.getStorageSync('store_id') || '11001',
cid: this.cid
}
const kw = (this.keyword || '').trim()
if (kw) {
data.keyword = kw
}
return articleList({
method: "get",
data: {
store_id: uni.getStorageSync('store_id') || '11001',
cid: this.cid
}
data
}).then((res) => {
// console.log(res, 'list');
if (res.data.errcode == 0) {
this.infoList = res.data.data.list
// console.log(res, 'list')
if (res.data.code == 0) {
this.infoList = (res.data.result && res.data.result.list) || []
}
}).finally(() => {
this.articleInitialLoading = false
this.articleFetchingEmpty = false
})
},
/**
* 下拉刷新:清空分类/文章并重走开骨架
*/
onPullDownRefresh() {
this.categoryLists = []
this.infoList = []
this.categoryInitialLoading = true
this.articleInitialLoading = true
this.articleFetchingEmpty = false
Promise.resolve(this.getList()).finally(() => {
uni.stopPullDownRefresh()
})
}
},
@@ -135,6 +189,7 @@
// this.getList();
},
onShow() {
// 不清空 categoryLists / infoList成功后再替换
this.getList();
}
}
@@ -248,4 +303,4 @@
}
}
}
</style>
</style>

View File

@@ -44,9 +44,9 @@
}
}).then((res) => {
// console.log(res, 'list');
if (res.data.errcode == 0) {
this.infoList = res.data.data
this.content = res.data.data.content
if (res.data.code == 0 && res.data.result) {
this.infoList = res.data.result
this.content = res.data.result.content
}
})
},

File diff suppressed because it is too large Load Diff

View File

@@ -77,12 +77,12 @@
<!-- 底部 -->
<view class="footer">
<view class="box">
<image src="/static/shop/home.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/shop/home.png" mode=""></image>
<text>首页</text>
</view>
<view class="box" @click="toShop">
<u-badge type="error" :count="gwcNum" size="mini" :offset="[0,16]"></u-badge>
<image :src="gwcNum==0&&'/static/shop/gwc.png' || gwcNum>0&&'/static/shop/gwc_l.png'" mode=""></image>
<image :src="gwcNum==0&&'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/shop/gwc.png' || gwcNum>0&&'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/shop/gwc_l.png'" mode=""></image>
<text :class="gwcNum>0?'active':''">购物车</text>
</view>
<view class="to_btns" @click="toAdd(drugDetailsList.drug_store_relation.drug_id)">

View File

@@ -25,14 +25,14 @@
<!-- 图片展示 -->
<view class="section" @click="gologin">
<u-swiper :list="list" :img-mode="aspectFill" :height="300" :interval="4000"></u-swiper>
<!-- <image class="banner" mode="aspectFill" src="/static/home/banner.png"></image> -->
<!-- <image class="banner" mode="aspectFill" src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/home/banner.png"></image> -->
</view>
<!-- 导航栏 -->
<view class="nav" v-if="isShow_envVersion==false">
<view class="nav_service" @click="gologin">
<view class="nav_service_img">
<image src="../../static/home/bg1.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/home/bg1.png" mode=""></image>
</view>
<view class="nav_service_title">
到店挂号
@@ -41,7 +41,7 @@
<view class="nav_service" @click="gologin">
<view class="nav_service_img">
<image src="../../static/home/bg2.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/home/bg2.png" mode=""></image>
</view>
<view class="nav_service_title">
预约购药
@@ -105,7 +105,7 @@
<!-- 图片部分 -->
<view class="list_infos">
<view class="list_lefts">
<image :src="item.avatar || '/static/mine/avatar_1.png'" mode="医生头像"></image>
<image :src="item.avatar || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png'" mode="医生头像"></image>
<!-- 医生 -->
<view class="list_title">
<view class="titles_name">
@@ -153,9 +153,9 @@
show: true,
doctorList: [],
list: [
// '/static/home/banner.png',
// '/static/home/banner.png',
// '/static/home/banner.png'
// 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/home/banner.png',
// 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/home/banner.png',
// 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/home/banner.png'
],
info:[],
isShow_envVersion: true
@@ -467,7 +467,7 @@
display: flex;
align-items: center;
justify-content: space-around;
background: url('/static/home/bg01.png') no-repeat;
background: url('https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/home/bg01.png') no-repeat;
background-size: 100% 100%;
.nav_service_img {
@@ -490,7 +490,7 @@
}
.nav_service:last-child {
background: url('/static/home/bg02.png') no-repeat;
background: url('https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/home/bg02.png') no-repeat;
background-size: 100% 100%;
}
}

View File

@@ -3,7 +3,7 @@
<MessageNotification />
<!-- 描述 -->
<view class="card" v-for="item in list" :key="item.id" @click="toInfo(item.id)">
<image :src="item.content['avatar']||'/static/mine/avatar_1.png'" mode=""></image>
<image :src="item.content['avatar']||'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png'" mode=""></image>
<view class="info">
<view class="infos_title">
<text class="infos">{{item.content['doctor']}}医生</text>

View File

@@ -12,85 +12,118 @@
</view>
</view>
<!-- 消息列表 -->
<view class="list" @click="goSystem">
<!-- 头像 -->
<view class="avator">
<view class="img">
<image src="/static/xx/xt.png" mode=""></image>
<!-- 首次加载四条固定入口用骨架避免 num 未定义时闪暂无新消息 -->
<ListSkeleton v-if="noticeInitialLoading" type="message" :rows="4" />
<template v-else>
<!-- 消息列表 -->
<view class="list" @click="goSystem">
<!-- 头像 -->
<view class="avator">
<view class="img">
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/xx/xt.png" mode=""></image>
</view>
<u-badge size="default" v-if="system.num>=0" :count="system.num" :is-dot="system.num==0?true:false"
type="error" :offset="system.num>0?[-4,-8]:[0,0]"></u-badge>
</view>
<u-badge size="default" v-if="system.num>=0" :count="system.num" :is-dot="system.num==0?true:false"
type="error" :offset="system.num>0?[-4,-8]:[0,0]"></u-badge>
</view>
<!-- 描述 -->
<view class="info">
<view class="infos_title">
<text class="infos">系统消息</text>
<text class="time">{{ system.time }}</text>
</view>
<view class="preview" v-if="system.num >0">
您有{{ system.num }}条未读信
</view>
<view class="preview" v-if="system.num==undefined">
暂无新消息
<!-- 描述 -->
<view class="info">
<view class="infos_title">
<text class="infos">系统消息</text>
<text class="time">{{ system.time }}</text>
</view>
<view class="preview" v-if="system.num >0">
您有{{ system.num }}条未读信息
</view>
<view class="preview" v-if="system.num==undefined">
暂无新消
</view>
</view>
</view>
</view>
<view class="list" @click="goOrder">
<!-- 头像 -->
<view class="avator">
<view class="img">
<image src="/static/xx/cp.png" mode=""></image>
<view class="list" @click="goOrder">
<!-- 头像 -->
<view class="avator">
<view class="img">
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/xx/cp.png" mode=""></image>
</view>
<u-badge size="default" v-if="order.num>=0" :count="order.num" :is-dot="order.num==0?true:false"
type="error" :offset="order.num>0?[-4,-8]:[0,0]"></u-badge>
</view>
<u-badge size="default" v-if="order.num>=0" :count="order.num" :is-dot="order.num==0?true:false"
type="error" :offset="order.num>0?[-4,-8]:[0,0]"></u-badge>
</view>
<!-- 描述 -->
<view class="info">
<view class="infos_title">
<text class="infos">产品订单</text>
<text class="time">{{ order.time }}</text>
</view>
<view class="preview" v-if="order.num>0">
您有{{ order.num }}条未读信
</view>
<view class="preview" v-if="order.num==undefined">
暂无新消息
<!-- 描述 -->
<view class="info">
<view class="infos_title">
<text class="infos">产品订单</text>
<text class="time">{{ order.time }}</text>
</view>
<view class="preview" v-if="order.num>0">
您有{{ order.num }}条未读信息
</view>
<view class="preview" v-if="order.num==undefined">
暂无新消
</view>
</view>
</view>
</view>
<view class="list" @click="goDoctor">
<!-- 头像 -->
<view class="avator">
<view class="img">
<image src="/static/xx/ysxx.png" mode=""></image>
<view class="list" @click="goDoctor">
<!-- 头像 -->
<view class="avator">
<view class="img">
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/xx/ysxx.png" mode=""></image>
</view>
<u-badge size="default" v-if="doctor.num>=0" :count="doctor.num" :is-dot="doctor.num==0?true:false"
type="error" :offset="doctor.num>0?[-4,-8]:[0,0]"></u-badge>
</view>
<u-badge size="default" v-if="doctor.num>=0" :count="doctor.num" :is-dot="doctor.num==0?true:false"
type="error" :offset="doctor.num>0?[-4,-8]:[0,0]"></u-badge>
</view>
<!-- 描述 -->
<view class="info">
<view class="infos_title">
<text class="infos">医生消息</text>
<text class="time">{{ doctor.time }}</text>
</view>
<view class="preview" v-if="doctor.num>0">
您有{{ doctor.num }}条新的消息
</view>
<view class="preview" v-if="doctor.num==undefined">
暂无新消息
<!-- 描述 -->
<view class="info">
<view class="infos_title">
<text class="infos">医生消息</text>
<text class="time">{{ doctor.time }}</text>
</view>
<view class="preview" v-if="doctor.num>0">
您有{{ doctor.num }}条新的消息
</view>
<view class="preview" v-if="doctor.num==undefined">
暂无新消息
</view>
</view>
</view>
</view>
<!-- 模拟<u-divider>在线问诊</u-divider>效果-->
<!-- 转诊消息 -->
<view class="list" @click="goTransfer">
<!-- 头像 -->
<view class="avator">
<view class="img">
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/xx/ysxx.png" mode=""></image>
</view>
<u-badge size="default" v-if="transfer.num>=0" :count="transfer.num" :is-dot="transfer.num==0?true:false"
type="error" :offset="transfer.num>0?[-4,-8]:[0,0]"></u-badge>
</view>
<!-- 描述 -->
<view class="info">
<view class="infos_title">
<text class="infos">转诊消息</text>
<text class="time">{{ transfer.time }}</text>
</view>
<view class="preview" v-if="transfer.num>0">
您有{{ transfer.num }}条转诊消息待处理
</view>
<view class="preview" v-if="transfer.num==undefined || transfer.num==0">
暂无转诊消息
</view>
</view>
</view>
</template>
<!-- 标题 - 在线咨询 -->
<view v-if="checkDev('open-im') && onlineConsultationList.length > 0" class="title">
<text>在线咨询</text>
</view>
<!-- IM 会话首次骨架有旧数据时直接展示旧列表直至替换 -->
<ListSkeleton
v-if="checkDev('open-im') && chatInitialLoading && onlineConsultationList.length === 0 && onlineReconsultationList.length === 0"
type="message"
:rows="3"
/>
<!-- 在线咨询 - 用户 (type=1) -->
<view v-if="checkDev('open-im')" v-for="(item, index) in onlineConsultationList" :key="item.id" class="list" @click="goChat(item, index)">
<!-- 头像 -->
@@ -110,7 +143,6 @@
</view>
<view class="preview">
<p>{{ item.last_message }}</p>
<!-- <p v-if="item.un_read_count>0">您有{{item.un_read_count}}条新的消息</p>-->
</view>
<view class="preview" v-if="item.last_message===''">
暂无新消息
@@ -141,7 +173,6 @@
</view>
<view class="preview">
<p>{{ item.last_message }}</p>
<!-- <p v-if="item.un_read_count>0">您有{{item.un_read_count}}条新的消息</p>-->
</view>
<view class="preview" v-if="item.last_message===''">
暂无新消息
@@ -157,6 +188,7 @@ import {
} from '../../request/api/api'
import {getSystemNoticeIndexApi} from '../../request/api/systemNotice'
import {getChatFriendsListApi} from "@/request/api/im";
import {getTransferPrescriptionListApi} from "@/request/api/transferPrescription";
import {checkDev} from "@/utils/utils";
import MessageNotification from "@/components/MessageNotification/MessageNotification.vue";
@@ -169,10 +201,17 @@ export default {
order: [],
system: [],
doctor: [],
transfer: {
num: 0,
time: ''
},
imChat: [],
onlineConsultationList: [], // 在线咨询列表 (type=1)
onlineReconsultationList: [], // 在线复诊列表 (type=3)
type: ""
type: "",
// 首次加载骨架:刷新时不再清空数据,故仅首次为 true
noticeInitialLoading: true,
chatInitialLoading: true
}
},
onLoad() {
@@ -204,7 +243,6 @@ export default {
type: "all"
}
}).then((res) => {
// console.log(res, 'res');
uni.showToast({
title: '清除成功',
duration: 2000,
@@ -226,7 +264,6 @@ export default {
type: "system"
}
}).then((res) => {
// console.log(res, 'res');
})
},
// 详情
@@ -241,7 +278,6 @@ export default {
type: "order"
}
}).then((res) => {
// console.log(res, 'res');
})
},
goDoctor() {
@@ -255,24 +291,20 @@ export default {
type: "doctor_news"
}
}).then((res) => {
// console.log(res, 'res');
})
},
// 转诊消息
goTransfer() {
uni.navigateTo({
url: '/subPackages/transfer/transfer-message'
})
},
goChat(item, index) {
uni.navigateTo({
url: `/subPackages/chat/chat?room_id=${item.room_id}&nick_name=${item.nick_name}&user_id=${item.id}&avatar=${encodeURIComponent(item.avatar || '')}&room_status=${item.room_status || 0}`
})
// getseesionRead({
// method: "post",
// data: {
// store_id: uni.getStorageSync('store_id') || 11001,
// type: "doctor_news"
// }
// }).then((res) => {
// // console.log(res, 'res');
// })
},
//
// 获取系统/订单/医生消息概览;成功才覆盖,失败保留旧数据
async getInfo() {
try {
const res = await getSystemNoticeIndexApi({
@@ -285,16 +317,73 @@ export default {
}
} catch (error) {
console.error('获取消息概览失败:', error)
} finally {
this.noticeInitialLoading = false
}
},
// 拉取 IM 好友会话列表,成功后整份替换,失败不写空
getMyChatFriendsList() {
getChatFriendsListApi().then((res) => {
return getChatFriendsListApi().then((res) => {
const allChatList = res.data.result || []
// 根据type字段区分在线咨询和在线复诊
this.onlineConsultationList = allChatList.filter(item => item.type === 1 || !item.type) // type=1或未设置type的为在线咨询
this.onlineReconsultationList = allChatList.filter(item => item.type === 3) // type=3的为在线复诊
this.onlineReconsultationList = allChatList.filter(item => (item.type === 3 || item.type === 2)) // type=3的为在线复诊
// 保持imChat兼容性包含所有
this.imChat = allChatList
}).catch((error) => {
console.error('获取聊天列表失败:', error)
}).finally(() => {
this.chatInitialLoading = false
})
},
// 获取转诊消息列表;失败不重置 transfer避免闪空
getTransferList() {
return getTransferPrescriptionListApi().then((res) => {
if (res.data && res.data.code == 0) {
const transferList = res.data.result || []
// 统计待确认的转诊消息数量
const pendingCount = transferList.filter(item => item.status === 0).length
this.transfer.num = pendingCount
// 获取最新的转诊时间
if (transferList.length > 0) {
const latestTransfer = transferList[0]
if (latestTransfer.transfer_time) {
this.transfer.time = latestTransfer.transfer_time
} else if (latestTransfer.created_at) {
this.transfer.time = latestTransfer.created_at
}
}
}
}).catch((error) => {
console.error('获取转诊消息列表失败:', error)
})
},
/**
* 下拉刷新:重置骨架并并行重拉消息概览 / IM / 转诊
*/
refreshMessagePage() {
this.noticeInitialLoading = true
if (checkDev('open-im')) {
this.chatInitialLoading = true
this.onlineConsultationList = []
this.onlineReconsultationList = []
this.imChat = []
}
const tasks = [
this.getInfo(),
this.getTransferList()
]
if (checkDev('open-im')) {
tasks.push(this.getMyChatFriendsList())
} else {
this.chatInitialLoading = false
}
return Promise.all(tasks)
},
onPullDownRefresh() {
this.refreshMessagePage().finally(() => {
uni.stopPullDownRefresh()
})
}
},
@@ -302,13 +391,10 @@ export default {
this.getInfo()
},
onShow() {
this.order = []
this.system = []
this.doctor = []
this.onlineConsultationList = []
this.onlineReconsultationList = []
// 不再清空列表:保留旧 UI接口成功后再覆盖
this.getInfo()
this.getMyChatFriendsList()
this.getTransferList()
}
}
</script>
@@ -428,4 +514,4 @@ export default {
}
}
</style>
</style>

View File

@@ -3,7 +3,7 @@
<MessageNotification />
<!-- 描述 -->
<view class="card" v-for="item in list" :key="item.id">
<image src="/static/xx/cpdd.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/xx/cpdd.png" mode=""></image>
<view class="info">
<view class="infos_title">
<text class="infos">订单</text>

View File

@@ -8,7 +8,7 @@
<view class="qualification-item" v-for="(item, index) in qualifications" :key="index" @click="previewImage(index)">
<view class="qualification-content">
<text class="qualification-name">{{ item.name }}</text>
<image :src="item.image || '/static/mine/avatar_1.png'" mode="aspectFit" class="qualification-img"></image>
<image :src="item.image || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png'" mode="aspectFit" class="qualification-img"></image>
</view>
</view>
</view>

View File

@@ -7,7 +7,7 @@
<view class="title">
欢迎登录
</view>
<image src="@/static/empty/login.png" mode="aspectFit"></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/empty/login.png" mode="aspectFit"></image>
<!-- 修改点1移除 disabled 属性和 bg 样式判断使其始终可点击且显示激活颜色 -->
<button class="btn" @click="handleLoginClick">一键登录</button>

View File

@@ -10,7 +10,7 @@
<view class="info">
<view class="pwd">
<d-input :maxlength="11" height="96" v-model="form['mobile']"
:prefixIcon="require('../../static/choose/act.png')" borderRadius="96rpx" prefixIconSize="40rpx"
prefixIcon="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/choose/act.png" borderRadius="96rpx" prefixIconSize="40rpx"
placeholder="请输入手机号" @blur="check" @input="is_d=false" :placeholder-style="{fontSize:'32rpx'}"
:custom-style="{fontSize:'32rpx',paddingLeft:'60rpx'}" />
</view>

View File

@@ -4,7 +4,7 @@
<!-- 头部 -->
<view class="head">
<view class="head_img">
<image :src="infoList.user.avatarurl || '/static/mine/mr_tx.png' " mode=""></image>
<image :src="infoList.user.avatarurl || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/mr_tx.png' " mode=""></image>
</view>
<view class="head_info">
<view class="head_name">
@@ -31,25 +31,25 @@
</view>
<view class="info">
<view class="item" @click="toAll(1)" style="position: relative;">
<image src="/static/mine/dfk.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/dfk.png" mode=""></image>
<text>待付款</text>
<u-badge type="error" :count="infoList.ProductOrder.unpay" :offset="[-2,20]"></u-badge>
</view>
<view class="item" @click="toAll(2)" style="position: relative;">
<image src="/static/mine/dfh.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/dfh.png" mode=""></image>
<text>待发货</text>
<u-badge type="error" :count="infoList.ProductOrder.wait_send" :offset="[-2,20]"></u-badge>
</view>
<view class="item" @click="toAll(3)" style="position: relative;">
<image src="/static/mine/dsh.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/dsh.png" mode=""></image>
<text>待收货</text>
<u-badge type="error" :count="infoList.ProductOrder.wait_accept" :offset="[-2,20]"></u-badge>
</view>
<view class="item" @click="toAll(4)">
<image src="/static/mine/tk.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/tk.png" mode=""></image>
<text>已完成</text>
</view>
</view>
@@ -64,15 +64,15 @@
<!-- 列表 -->
<view class="list">
<!-- <navigator url="../../subPackages/my/myorder" class="infos">
<image src="@/static/mine/01.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/01.png" mode=""></image>
<text>问诊记录</text>
</navigator> -->
<navigator url="../../subPackages/my/myrecord" class="infos">
<image src="@/static/mine/02.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/02.png" mode=""></image>
<text>处方记录</text>
</navigator>
<navigator url="../../subPackages/my/myappiont" class="infos">
<image src="@/static/mine/03.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/03.png" mode=""></image>
<text>挂号记录</text>
</navigator>
</view>
@@ -87,15 +87,15 @@
<!-- 列表 -->
<view class="list">
<navigator class="infos" url="../../subPackages/my/myinfo" v-if="isShow_envVersion==false">
<image src="@/static/mine/jzr.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/jzr.png" mode=""></image>
<text>就诊人管理</text>
</navigator>
<navigator url="../../subPackages/setup/area-list" class="infos">
<image src="@/static/mine/gy.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/gy.png" mode=""></image>
<text>收货地址</text>
</navigator>
<navigator url="../../subPackages/setup/customer-service" class="infos">
<image src="@/static/mine/kfzx.png" mode=""></image>
<image src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/kfzx.png" mode=""></image>
<text>客服中心</text>
</navigator>
</view>
@@ -106,8 +106,8 @@
<script>
import {
getuserInfo
} from '../../request/api/api'
getUserInfoApi
} from '../../request/api/user'
export default {
data() {
return {
@@ -137,15 +137,12 @@
// 用户信息
getInfo() {
getuserInfo({
method: "post",
data: {
store_id: uni.getStorageSync('store_id') || 11001
}
getUserInfoApi({
store_id: uni.getStorageSync('store_id') || 11001
}).then((res) => {
// console.log(res, 'ingo');
if (res.data.errcode == 0) {
this.infoList = res.data.data
if (res.data.code == 0) {
this.infoList = res.data.result
}
})
},

View File

@@ -1,4 +1,14 @@
import http from './request'
import { get } from './http'
/** 将 xk-api 的 code/result 格式转为旧版 errcode/data兼容现有页面 */
function normalizeXkApiResponse(res) {
if (res && res.data && (res.data.code === 0 || res.data.code === '0')) {
res.data.errcode = 0;
res.data.data = res.data.result;
}
return res;
}
// 登录 授权 其他手机号授权
export async function getsLogin(params) {
@@ -33,20 +43,20 @@ export async function getAgreem(params) {
// 首页 门店 门店切换
export async function storeChange(params) {
let data = await http('/oldApi/v1/store/store-change', params)
return data
let data = await http('/xkApi/store/store-change', params)
return normalizeXkApiResponse(data)
}
// 首页 门店 用户所有门店
export async function storeList(params) {
let data = await http('/oldApi/v1/store/list', params)
return data
let data = await http('/xkApi/store/list', params)
return normalizeXkApiResponse(data)
}
// 首页 门店 门店信息
export async function storeInfo(params) {
let data = await http('/oldApi/v1/store/info', params)
return data
let data = await http('/xkApi/store/info', params)
return normalizeXkApiResponse(data)
}
// 医生列表
@@ -106,7 +116,19 @@ export async function userInfo(params) {
// 就诊人保存
export async function userAdd(params) {
let data = await http('/oldApi/v1/patient/save', params)
let data = await http('/xkApi/patient/save', params)
return data
}
// 就诊人监护人信息(编辑回显)
export async function userGuardianInfo(params) {
const query = params.data || params
return await get('/patient/guardian-info', query, 3)
}
// 删除就诊人
export async function userPatientDel(params) {
let data = await http('/xkApi/patient/delete', params)
return data
}
@@ -317,9 +339,9 @@ export async function getUseWayPay(params) {
return data
}
// 更新配送方式 http://xiaokang.com/member/v1/product-order/update-delivery
// 更新配送方式Laravel单品包邮重算
export async function getDelivery(params) {
let data = await http('/oldApi/v1/product-order/update-delivery', params)
let data = await http('/xkApi/product-order/update-delivery', params)
return data
}
@@ -329,28 +351,51 @@ export async function getUpdateUseWay(params) {
return data
}
//资讯列表 /v1/info/article-list
//资讯列表Laravel guest-info列表不含正文
export async function articleList(params) {
let data = await http('/oldApi/v1/info/article-list', params)
let data = await http('/xkApi/guest/guest-info/article-list', params)
return data
}
//分类列表 /v1/info/categories
//分类列表
export async function categoriesList(params) {
let data = await http('/oldApi/v1/info/categories', params)
let data = await http('/xkApi/guest/guest-info/categories', params)
return data
}
//资讯详情 /v1/info/article-info
//资讯详情(含正文)
export async function articleInfo(params) {
let data = await http('/oldApi/v1/info/article-info', params)
let data = await http('/xkApi/guest/guest-info/article-info', params)
return data
}
// v1/product-order/list
export async function getOrdlist(params) {
let data = await http('/oldApi/v1/product-order/list', params)
return data
let data = await http('/xkApi/product-order/list', params)
return normalizeXkApiResponse(data)
}
/**
* 获取待支付商品订单列表(首页提醒用)
*/
export async function fetchUnpaidProductOrderListApi(params = {}) {
const storeId = params.store_id || uni.getStorageSync('store_id') || '11001';
const res = await getOrdlist({
method: 'post',
data: {
store_id: storeId,
type: 'unpaid',
page: 1,
...params,
},
});
if (res.data?.errcode != 0) {
return [];
}
const list = res.data?.data?.list || [];
return list.filter(
(item) => item.status === 0 && item.is_pay === 0 && item.cancel_status === 0
);
}
// v1/product-order/list
@@ -420,6 +465,7 @@ export async function PayList(params) {
export async function getPayConfig(params) {
let data = await http('/oldApi/v1/product-order/pay', params)
// let data = await http('/newApi/mobile/product-order/pay', params)
return data
}
// getOrdlist 获取订单列表 https://zjxk.app.ctkj88.com/member/order/lists
@@ -469,18 +515,17 @@ export async function postRefundBased(params) {
// return data
// }
/** 物流详情 → xk-api mobile含多包裹 packages */
export async function getLogistics(params) {
let data = await http('/oldApi/v1/product-order/express', params)
return data
let data = await http('/xkApi/product-order/express', params)
return normalizeXkApiResponse(data)
}
//确认收货 https://zjxk.app.ctkj88.com/member/order/confirm
// https://zjxk.app.ctkj88.com/member/order/confirm
// 确认收货(签收)→ xk-api product-order/receive改状态 + 解冻供应商/仓余额)
export async function confirmAccept(params) {
let data = await http('/newApi/order/confirm', params)
return data
let data = await http('/xkApi/product-order/receive', params)
return normalizeXkApiResponse(data)
}
//清空购物车
export async function clearAll(params) {
@@ -495,7 +540,8 @@ export async function getVersion(params) {
// 获取系统配置信息
export async function getSystemConfig(params) {
let data = await http('/xkApi/platform/config', params)
// let data = await http('/xkApi/platform/config', params)
let data = await http('/xkApi/platform/config', params, 3)
return data
}

33
request/api/invoice.js Normal file
View File

@@ -0,0 +1,33 @@
import { get, post } from './http'
/**
* 将 xk-api 的 code/result 规范为 errcode/data便于页面统一判断
*/
function normalizeXkApiResponse(res) {
if (res && res.data && (res.data.code === 0 || res.data.code === '0')) {
res.data.errcode = 0
res.data.data = res.data.result
}
return res
}
/**
* 申请开票
* @param {object} params order_id, title_type, title_name, tax_no, receive_type, email
*/
export async function applyInvoiceApi(params) {
const data = await post('/invoice/apply', params, 3)
return normalizeXkApiResponse(data)
}
/**
* 开票详情
* @param {object} params id 或 order_id
*/
export async function getInvoiceDetailApi(params) {
const data = await get('/invoice/detail', params, 3)
return normalizeXkApiResponse(data)
}
/** 与 config/xk.php invoice_notice 保持一致的订阅消息模板 ID */
export const INVOICE_NOTICE_TMPL_ID = '2FuQhhO74mABSqGx2cF7vRR2wGekrIMM9kZMvVI6h-Q'

View File

@@ -1,6 +1,20 @@
import {get, post, uploadFile} from './http'
const prefix = '/order';
/**
* 在线复诊:创建就诊信息(挂号前)
*/
export async function createRegisterInfoApi(params) {
return await post(`${prefix}/create-register-info`, params, 3)
}
/**
* 在线复诊:可选西药列表(双端上架)
*/
export async function getWesternDrugsForFollowUpApi(params) {
return await get(`${prefix}/western-drugs-for-follow-up`, params, 3)
}
/**
* 获取扫码诊所的商品详情
* @param params
@@ -26,6 +40,11 @@ export async function saveRegisterInfoApi(params) {
return await post(`${prefix}/save-register-info`, params, 3)
}
/** 诊所在线复诊:确认是否曾用过所选西药 */
export async function confirmFollowUpDrugUseApi(params) {
return await post(`${prefix}/confirm-follow-up-drug-use`, params, 3)
}
/**
* 查询订单的处方状态
* @param params

View File

@@ -0,0 +1,10 @@
import {post} from './http'
/**
* 获取处方记录列表
* @param params { up_id: number, store_id: number, page: number }
* @returns {Promise<*>}
*/
export async function getPrescriptionListApi(params) {
return await post('/prescription/list', params, 3)
}

View File

@@ -51,11 +51,30 @@ export async function updateIntroductionImagesApi(params) {
/**
* 检查药店在线复诊配置(委托诊所和医生)
* @param params { store_id: number }
* @param params { store_id: number, guest?: number } - guest: 0=普通接口, 1=Guest接口
* @returns {Promise<*>}
*/
export async function checkOnlineConsultationConfigApi(params) {
return await get('/store/check-online-consultation-config', params, 3)
const { guest = 0, ...restParams } = params;
// guest=1 时调用 Guest 接口guest=0 时调用普通接口
const url = guest === 1
? '/guest/guest-platform/check-online-consultation-config'
: '/store/check-online-consultation-config';
return await get(url, restParams, 3)
}
/**
* 获取诊所在线问诊信息(委托诊所和委托医生)
* @param params { store_id: number, guest?: number } - guest: 0=普通接口, 1=Guest接口
* @returns {Promise<*>}
*/
export async function getClinicOnlineConsultationInfoApi(params) {
const { guest = 0, ...restParams } = params;
// guest=1 时调用 Guest 接口guest=0 时调用普通接口
const url = guest === 1
? '/guest/guest-platform/get-clinic-online-consultation-info'
: '/store/get-clinic-online-consultation-info';
return await get(url, restParams, 3)
}
/**

29
request/api/register.js Normal file
View File

@@ -0,0 +1,29 @@
import {post} from './http'
/**
* 获取挂号记录列表
* @param params { patient_id: number, store_id: number, page: number }
* @returns {Promise<*>}
*/
export async function getRegisterListApi(params) {
return await post('/register/list', params, 3)
}
/**
* 获取待支付挂号列表(首页提醒用)
* @param params
* @returns {Promise<Array>}
*/
export async function fetchUnpaidRegisterListApi(params = {}) {
const storeId = params.store_id || uni.getStorageSync('store_id') || '11001';
const res = await post('/register/list?pageSize=100', {
store_id: storeId,
page: 1,
...params,
}, 3);
if (res.data?.code !== 0 && res.data?.code !== '0') {
return [];
}
const list = res.data?.result?.list || [];
return list.filter((item) => item.status === 0 && item.is_cancel !== 1);
}

View File

@@ -14,12 +14,66 @@ const arr = [
'password'
]
// 敏感数据
// 敏感数据:响应解密后保留明文原字段,脱敏值写入 {field}_tm
const sensitiveData = [
'id_card',
'idcard'
'idcard',
'guardian_id_card'
]
// 仅展示脱敏、编辑需明文的字段
const displayMaskFields = [
'mobile',
'express_mobile',
'patient_mobile',
]
/**
* 脱敏展示值(写入 field_tm
*/
function maskFieldValue(key, plainText) {
if (plainText == null || plainText === '') {
return plainText
}
const str = String(plainText)
switch (key) {
case 'express_name':
case 'express_region':
case 'accept_name':
case 'patient':
return str.slice(0, 1).padEnd(str.length, '*')
case 'mobile':
case 'express_mobile':
case 'patient_mobile':
case 'guardian_mobile':
if (str.length <= 7) {
return str
}
return str.slice(0, 3).padEnd(str.length - 4, '*') + str.slice(-4)
case 'id_card':
case 'idcard':
case 'guardian_id_card':
if (str.length <= 10) {
return str
}
return str.slice(0, 6).padEnd(str.length - 4, '*') + str.slice(-4)
default:
return str
}
}
/** 只读展示:优先 field_tm */
export function displayTm(obj, field) {
if (obj == null) {
return ''
}
const tm = obj[field + '_tm']
if (tm != null && tm !== '') {
return tm
}
return obj[field] ?? ''
}
const isDev = checkDev('dev');
// 环境URL配置
@@ -29,14 +83,14 @@ const ENV_URLS = {
main: "https://app.xiaokang88.com/member",
xkApi: "https://api.xiaokang88.com/api/mobile",
shop: "https://shop.xiaokang88.com/member",
im: "https://api.ws.g.xiaokang88.com/api"
im: "https://api.ws.g.xiaokang88.com/api",
},
// 测试环境
test: {
main: "https://test.xk.app.nailaoyun.cn/member",
xkApi: "https://test.xk.api.nailaoyun.cn/api/mobile",
shop: "https://test.xk.shop.nailaoyun.cn/member",
im: "https://api.ws.g.nailaoyun.cn/api"
main: "https://xk.test.saas.api-yii.nailaoyun.cn/member",
xkApi: "https://xk.test.saas.api.nailaoyun.cn/api/mobile",
shop: "https://test.xk.shop.nailaoyun.cn/member1",
im: "https://xk.ws.nailaoyun.cn/api"
},
// 本地开发环境
local: {
@@ -54,7 +108,7 @@ function getEnvUrls() {
return ENV_URLS.local;
}
// 检查是否开启了开发者测试模式
const devMode = uni.getStorageSync('dev_mode');
const devMode = checkDev('test');
return devMode ? ENV_URLS.test : ENV_URLS.prod;
}
@@ -87,6 +141,15 @@ function request(url, params, method = 0) {
} else if (url.indexOf('/xkApi/platform/qualifications') !== -1) {
url = url.replace('/xkApi/platform/qualifications', '/guest/guest-platform/qualifications');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/xkApi/store/info') !== -1) {
url = url.replace('/xkApi/store/info', '/guest/guest-home/store-info');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/xkApi/store/list') !== -1) {
url = url.replace('/xkApi/store/list', '/guest/guest-home/store-list');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/xkApi/special-prescription/') !== -1) {
url = url.replace('/xkApi/special-prescription/', '/guest/guest-special-prescription/');
baseURL = envUrls.xkApi;
}
}
@@ -112,12 +175,24 @@ function request(url, params, method = 0) {
} else if (url.indexOf('/oldApi/v1/store/detail') !== -1) {
url = url.replace('/oldApi/v1/store/detail', '/guest/guest-home/store-detail');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/oldApi/v1/store/list') !== -1) {
url = url.replace('/oldApi/v1/store/list', '/guest/guest-home/store-list');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/oldApi/v1/register/doc-register-info') !== -1) {
url = url.replace('/oldApi/v1/register/doc-register-info', '/guest/guest-doctor/doc-register-info');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/oldApi/v1/doctor/office-list') !== -1) {
url = url.replace('/oldApi/v1/doctor/office-list', '/guest/guest-doctor/office-list');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/oldApi/v1/info/categories') !== -1) {
url = url.replace('/oldApi/v1/info/categories', '/guest/guest-info/categories');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/oldApi/v1/info/article-list') !== -1) {
url = url.replace('/oldApi/v1/info/article-list', '/guest/guest-info/article-list');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/oldApi/v1/info/article-info') !== -1) {
url = url.replace('/oldApi/v1/info/article-info', '/guest/guest-info/article-info');
baseURL = envUrls.xkApi;
}
}
@@ -132,6 +207,12 @@ function request(url, params, method = 0) {
} else if (url.indexOf('/v1/register/doc-register-info') !== -1 || url.indexOf('/member/v1/register/doc-register-info') !== -1) {
url = url.replace(/\/member\/v1\/register\/doc-register-info|\/v1\/register\/doc-register-info/, '/guest/guest-doctor/doc-register-info');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/v1/store/info') !== -1 || url.indexOf('/member/v1/store/info') !== -1) {
url = url.replace(/\/member\/v1\/store\/info|\/v1\/store\/info/, '/guest/guest-home/store-info');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/v1/store/list') !== -1 || url.indexOf('/member/v1/store/list') !== -1) {
url = url.replace(/\/member\/v1\/store\/list|\/v1\/store\/list/, '/guest/guest-home/store-list');
baseURL = envUrls.xkApi;
}
}
@@ -143,8 +224,9 @@ function request(url, params, method = 0) {
}
if (url.split('/').indexOf('xkApi') !== -1) {
baseURL = envUrls.xkApi;
// 如果是guest模式且是xkApi请求添加/guest/前缀
if (isGuestRequest) {
// 如果是guest模式且是xkApi请求添加/guest/前缀(但避免重复添加)
// 转诊相关接口需要登录不转换为guest路由
if (isGuestRequest && url.indexOf('/guest/') === -1 && url.indexOf('/transfer-prescription') === -1) {
url = url.replace('/xkApi', '/guest/xkApi');
}
}
@@ -155,10 +237,29 @@ function request(url, params, method = 0) {
// 加密
if (params != null) {
if (!params.data) {
params.data = {};
}
params.data = getRes(params.data, false);
// 如果是guest模式强制设置store_id为11001
const isProductApi = url.indexOf('/product/') !== -1;
if (isProductApi && !params.data.store_id) {
const localStoreId = uni.getStorageSync('store_id');
params.data.store_id = localStoreId || getGuestStoreId();
}
// 如果是guest模式设置store_id优先使用用户传入或本地存储的值
if (isGuestRequest && params.data) {
params.data.store_id = getGuestStoreId();
// 优先使用用户传入的 store_id
if (!params.data.store_id) {
// 如果用户没传,尝试从本地存储获取
const localStoreId = uni.getStorageSync('store_id');
if (localStoreId) {
params.data.store_id = localStoreId;
} else {
// 如果本地存储也没有,使用默认值(保持向后兼容)
params.data.store_id = getGuestStoreId();
}
}
// 如果用户已经传了 store_id就使用用户传入的值不做任何修改
}
}
let num = 0;
@@ -224,56 +325,39 @@ function request(url, params, method = 0) {
function getRes(obj, isDecode = true) {
try {
if (obj == null || typeof obj !== 'object') {
return obj
}
for (const key in obj) {
if (key.endsWith('_tm')) {
continue
}
if (Array.isArray(obj[key])) {
obj[key] = getRes(obj[key])
} else if (typeof obj[key] === 'object') {
obj[key] = getRes(obj[key])
} else {
// 判断obj[key]是否在arr中
if (arr.indexOf(key) !== -1) {
let aseFile = ''
if (isDecode === true) {
aseFile = customBase64Decode(obj[key])
} else {
aseFile = customBase64Encode(obj[key])
obj[key] = getRes(obj[key], isDecode)
} else if (obj[key] !== null && typeof obj[key] === 'object') {
obj[key] = getRes(obj[key], isDecode)
} else if (arr.indexOf(key) !== -1) {
let plainValue = ''
if (isDecode === true) {
plainValue = customBase64Decode(obj[key])
} else {
plainValue = customBase64Encode(obj[key])
}
const isGarbled = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]|[\u{E000}-\u{F8FF}]/u.test(
typeof plainValue === 'string' ? plainValue : ''
)
if (isGarbled) {
plainValue = obj[key]
}
if (isDecode === true) {
const needTm = sensitiveData.indexOf(key) !== -1
|| displayMaskFields.indexOf(key) !== -1
obj[key] = plainValue
if (needTm) {
obj[key + '_tm'] = maskFieldValue(key, plainValue)
}
const isGarbled = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]|[\u{E000}-\u{F8FF}]/u.test(
typeof aseFile === 'string' ? aseFile : ''
)
if (isGarbled) {
aseFile = obj[key]
}
// 为了修改,暂时不脱敏
if (isDecode === true) {
if (sensitiveData.indexOf(key) !== -1) {
// 数据脱敏,自动匹配姓名、手机号、身份证
switch (key) {
case 'express_name':
case 'express_region':
case 'accept_name':
case 'patient':
// 保留前两个字符,其余用星号代替
aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*');
break;
case 'mobile':
case 'express_mobile':
// 保留前三位和后四位,中间用星号代替
aseFile = aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
break;
case 'id_card':
case 'idcard':
// 保留前六位和后四位,中间用星号代替
aseFile = aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
break;
default:
// 默认情况下,全部用星号代替
break;
}
}
}
obj[key] = aseFile
} else {
obj[key] = plainValue
}
}
}

View File

@@ -0,0 +1,105 @@
import { get, post } from './http'
export const SPECIAL_PRESCRIPTION_STORAGE_KEY = 'special_prescription_register_context'
/**
* 首页特色方配置
* @param params {{ store_id?: number|string }}
*/
export async function getSpecialPrescriptionHomeApi(params) {
return await get('/special-prescription/home', params, 3)
}
/**
* 特色方列表页配置
* @param params {{ store_id?: number|string }}
*/
export async function getSpecialPrescriptionListPageApi(params) {
return await get('/special-prescription/list-page', params, 3)
}
/**
* 特色方列表
* @param params {{ store_id?: number|string, category_id?: number, name?: string, page?: number, pageSize?: number }}
*/
export async function getSpecialPrescriptionListApi(params) {
return await get('/special-prescription/list', params, 3)
}
/**
* 特色方详情
* @param params {{ id: number|string, store_id?: number|string }}
*/
export async function getSpecialPrescriptionDetailApi(params) {
return await get('/special-prescription/detail', params, 3)
}
/**
* 特色方分类
*/
export async function getSpecialPrescriptionCategoriesApi(params = {}) {
return await get('/special-prescription/categories', params, 3)
}
/**
* 挂号成功后创建选方记录
* @param params {{ register_id, special_prescription_id, dose_count, store_id, doctor_id }}
*/
export async function createSpecialPrescriptionPatientRecordApi(params) {
return await post('/special-prescription/create-patient-record', params, 3)
}
/**
* 保存特色方挂号上下文merge 已有值,避免后续页面未传 sku_id 时覆盖为 0
* @param ctx {{ special_prescription_id, dose_count?, sku_id?, doctor_id? }}
*/
export function saveSpecialPrescriptionRegisterContext(ctx) {
if (!ctx || !ctx.special_prescription_id) return
const prev = getSpecialPrescriptionRegisterContext() || {}
const merged = {
special_prescription_id: ctx.special_prescription_id,
dose_count: ctx.dose_count != null ? ctx.dose_count : (prev.dose_count || 1),
doctor_id: ctx.doctor_id != null ? ctx.doctor_id : (prev.doctor_id || ''),
}
// 未传 sku_id 字段时保留 storage 已有值,防止中间页覆盖为 0
if (Object.prototype.hasOwnProperty.call(ctx, 'sku_id')) {
merged.sku_id = Number(ctx.sku_id) || 0
} else {
merged.sku_id = Number(prev.sku_id) || 0
}
uni.setStorageSync(SPECIAL_PRESCRIPTION_STORAGE_KEY, merged)
}
export function getSpecialPrescriptionRegisterContext() {
return uni.getStorageSync(SPECIAL_PRESCRIPTION_STORAGE_KEY) || null
}
export function clearSpecialPrescriptionRegisterContext() {
uni.removeStorageSync(SPECIAL_PRESCRIPTION_STORAGE_KEY)
}
/**
* 挂号成功后绑定特色方选方记录(幂等,失败不阻断流程)
* @param registerId
*/
export async function tryCreateSpecialPrescriptionPatientRecord(registerId) {
if (!registerId) return
const ctx = getSpecialPrescriptionRegisterContext()
if (!ctx || !ctx.special_prescription_id) return
try {
const res = await createSpecialPrescriptionPatientRecordApi({
register_id: registerId,
special_prescription_id: ctx.special_prescription_id,
dose_count: ctx.dose_count || 1,
sku_id: ctx.sku_id || 0,
store_id: uni.getStorageSync('store_id') || '11001',
doctor_id: ctx.doctor_id,
})
if (res.data?.code === 0 || res.data?.code === '0') {
clearSpecialPrescriptionRegisterContext()
}
} catch (e) {
console.error('tryCreateSpecialPrescriptionPatientRecord', e)
}
}

View File

@@ -0,0 +1,78 @@
import {post, get} from './http'
/**
* 获取转诊消息列表
* @returns {Promise<*>}
*/
export async function getTransferPrescriptionListApi() {
return await post('/transfer-prescription/list', {}, 3)
}
/**
* 获取转诊详情
* @param params { id: number }
* @returns {Promise<*>}
*/
export async function getTransferPrescriptionDetailApi(params) {
return await get('/transfer-prescription/detail', params, 3)
}
/**
* 同意转诊
* @param params { id: number }
* @returns {Promise<*>}
*/
export async function agreeTransferPrescriptionApi(params) {
return await post('/transfer-prescription/agree', params, 3)
}
/**
* 拒绝转诊
* @param params { id: number, reason?: string }
* @returns {Promise<*>}
*/
export async function rejectTransferPrescriptionApi(params) {
return await post('/transfer-prescription/reject', params, 3)
}
/**
* 获取转诊问题列表(用于咨询页面)
* @returns {Promise<*>}
*/
export async function getTransferQuestionsApi() {
return await get('/transfer-prescription/get-questions', {}, 3)
}
/**
* 提交转诊问题答案
* @param params { transfer_id: number, answers: Array<{question_id: number, answer: string}> }
* @returns {Promise<*>}
*/
export async function submitTransferQuestionsApi(params) {
return await post('/transfer-prescription/submit-questions', params, 3)
}
/**
* 获取转诊助理配置(移动端)
* @returns {Promise<*>}
*/
export async function getTransferAssistantConfigApi() {
return await get('/transfer-assistant-config/get-config', {}, 3)
}
/**
* 获取转诊咨询协议信息(固定文案和协议列表)
* @returns {Promise<*>}
*/
export async function getTransferConsultationAgreementInfoApi() {
return await get('/transfer-consultation/get-agreement-info', {}, 3)
}
/**
* 获取协议详情根据协议ID
* @param {number} agreementId 协议ID
* @returns {Promise<*>}
*/
export async function getAgreementDetailApi(agreementId) {
return await get(`/agreement/get-detail?id=${agreementId}`, {}, 3)
}

10
request/api/user.js Normal file
View File

@@ -0,0 +1,10 @@
import {post} from './http'
/**
* 获取用户信息(包含订单统计)
* @param params { store_id: number }
* @returns {Promise<*>}
*/
export async function getUserInfoApi(params) {
return await post('/user/info', params, 3)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -326,6 +326,9 @@ const ChatManager = {
if (parsed.type === 'price_update') {
return parsed.message || '[价格更新]';
}
if (parsed.type === 'price_adjust') {
return parsed.message || '[价格调整]';
}
return '[系统消息]';
} catch (e) {
return '[系统消息]';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,816 @@
<template>
<view class="bubble-container" :class="{ 'mine': isMine, 'other': !isMine }">
<!-- 头像非己方可点进医生详情 -->
<image class="avatar" :src="avatar" mode="aspectFill" @click.stop="onAvatarClick"></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="true">{{ 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">
<!-- 场景A: 运费/总价更新 -->
<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>
<!-- 场景B: 单品/明细调整 -->
<block v-else-if="parsedContent.type === 'price_adjust'">
<view class="sn-row">
<text class="sn-label">调整项目</text>
<text class="sn-value truncate">{{ parsedContent.drug_name || '医疗服务' }}</text>
</view>
<view class="sn-row">
<text class="sn-label">价格变动</text>
<view class="sn-value-group">
<text class="old">¥{{ parsedContent.old_price }}</text>
<u-icon name="arrow-right" size="10" color="#999" style="margin:0 4rpx;"></u-icon>
<text class="new">¥{{ parsedContent.new_price }}</text>
</view>
</view>
<view class="sn-row">
<text class="sn-label">变动金额</text>
<text class="sn-value" :class="parsedContent.adjust_amount >= 0 ? 'color-orange' : 'color-green'">
{{ parsedContent.adjust_amount >= 0 ? '增加' : '减少' }} ¥{{ Math.abs(parsedContent.adjust_amount) }}
</text>
</view>
<view class="sn-row">
<text class="sn-label">当前总额</text>
<text class="sn-value strong">¥{{ parsedContent.total_pay_price }}</text>
</view>
</block>
</view>
<!-- 底部操作栏 -->
<view class="sn-footer">
<text>查看详情</text>
<u-icon name="arrow-right" size="12" color="#ccc"></u-icon>
</view>
</view>
<!-- 普通系统文本 (保持原样) -->
<view v-else class="system-inner">
<text>{{ msg.message_content }}</text>
</view>
</block>
<!-- 卡片类消息 (4, 10, 11, 12, 13) -->
<view v-else-if="isCardType" class="card-content">
<!-- 类型10: 挂号 -->
<block v-if="msg.message_type === 10">
<view class="card-header bg-green">
<view class="title-group"><u-icon name="calendar-fill" size="16" color="#059669"></u-icon><text class="card-title">预约挂号</text></view>
<text class="status-tag" :class="'status-' + parsedContent.status">{{ getRegisterStatusText(parsedContent.status) }}</text>
</view>
<view class="card-body">
<view class="info-row"><text class="label">订单号</text><text class="value order-font">{{ parsedContent.order_no }}</text></view>
<view class="info-row"><text class="label">就诊人</text><text class="value">{{ (parsedContent.user_patient && parsedContent.user_patient.name) || '未知' }}</text></view>
<view class="info-row highlight"><text class="label">费用</text><text class="value price">¥{{ parsedContent.price }}</text></view>
<view class="text-block" v-if="parsedContent.chief_complaint">
<text class="block-label">主诉</text>
<text class="block-content">{{ parsedContent.chief_complaint }}</text>
</view>
<view class="text-block" v-if="parsedContent.illnessInfo">
<text class="block-label">适用症/症状</text>
<text class="block-content">{{ parsedContent.illnessInfo }}</text>
</view>
<view class="info-row" v-if="parsedContent.illnessInfo">
<text class="label">是否就诊过</text>
<text class="value">{{ parsedContent.has_visited === 1 || parsedContent.has_visited === '1' ? '是' : '否' }}</text>
</view>
<view class="info-row" v-if="parsedContent.illnessInfo">
<text class="label">是否使用过药品</text>
<text class="value">{{ parsedContent.has_used_drug === 1 || parsedContent.has_used_drug === '1' ? '是' : '否' }}</text>
</view>
<view v-if="registerCardDrugRows.length" class="drug-list-block">
<text class="block-label">所选药品</text>
<view v-for="row in registerCardDrugRows" :key="row._wxKey" class="info-row drug-spec-row">
<image
v-if="row.image"
class="drug-thumb"
:src="row.image"
mode="aspectFill"
@click.stop="$emit('previewImage', row.image)"
/>
<view class="drug-name-wrap">
<text class="label">{{ row.name || '药品' }}</text>
<text v-if="row.specification" class="spec-text">{{ row.specification }}</text>
</view>
<text class="value">×{{ row.quantity }}</text>
</view>
</view>
</view>
<view class="card-footer"><button class="action-btn" @click="$emit('viewRegister', parsedContent.id)">查看详情</button></view>
</block>
<!-- 类型11: 复诊用药确认扩展 flow与药店购药 type11 互斥展示 -->
<block v-else-if="msg.message_type === 11 && parsedContent && parsedContent.flow === 'follow_up_drug'">
<view class="card-header bg-gray">
<view class="title-group"><u-icon name="question-circle-fill" size="16" color="#64748B"></u-icon><text class="card-title">医生助理</text></view>
</view>
<view class="card-body">
<view class="text-block" v-if="parsedContent.question">
<text class="block-content">{{ parsedContent.question }}</text>
</view>
<view class="info-row" v-for="(row, idx) in (parsedContent.drugs || [])" :key="idx">
<text class="label">{{ row.name || '药品' }}</text>
<text class="value">×{{ row.quantity != null ? row.quantity : 1 }}</text>
</view>
<view v-if="followUpAnsweredLabel" class="info-row">
<text class="label">您的选择</text>
<text class="value">{{ followUpAnsweredLabel }}</text>
</view>
<view
v-if="parsedContent.has_visited === 1 || parsedContent.has_visited === '1' || parsedContent.has_visited === 0 || parsedContent.has_visited === '0'"
class="info-row"
>
<text class="label">是否就诊过</text>
<text class="value">{{ parsedContent.has_visited === 1 || parsedContent.has_visited === '1' ? '是' : '否' }}</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="text-block" v-if="parsedContent.supplement">
<text class="block-label">补充信息</text>
<text class="block-content">{{ parsedContent.supplement }}</text>
</view>
<view v-if="parsedContent.answer_status === 'pending'" class="follow-up-pending-readonly">
<text class="follow-up-pending-tip">请在挂号后的用药确认页完成选择此处不可更改</text>
</view>
</view>
</block>
<!-- 类型11: 患者就诊经历 -->
<block v-else-if="msg.message_type === 11">
<view class="card-header bg-gray">
<view class="title-group"><u-icon name="file-text-fill" size="16" color="#64748B"></u-icon><text class="card-title">患者就诊经历</text></view>
</view>
<view class="card-body">
<view 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.end_reason || parsedContent.reason || parsedContent.message || '本次服务已完成' }}</text>
</view>
</block>
<!-- 类型14: 转诊消息 -->
<block v-else-if="msg.message_type === 14">
<TransferMessageBubble
:message-data="parsedContent"
@agree="handleTransferAgree"
@reject="handleTransferReject"
/>
</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>
import TransferMessageBubble from './TransferMessageBubble.vue'
import {
agreeTransferPrescriptionApi,
rejectTransferPrescriptionApi
} from '@/request/api/transferPrescription'
export default {
name: "MessageBubble",
components: {
TransferMessageBubble
},
props: {
msg: {
type: Object,
required: true
},
isMine: {
type: Boolean,
default: false
},
avatar: {
type: String,
default: ''
},
isPlaying: {
type: Boolean,
default: false
}
},
computed: {
// 纯文本、图片、语音、视频以外的特殊类型,不需要默认的气泡背景/Padding
isMediaOrCard() {
// 1:图片, 3:视频, 4,10,11,12,13,14: 卡片 (9单独处理)
return [1, 3, 4, 10, 11, 12, 13, 14].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, 14].includes(this.msg.message_type);
},
parsedContent() {
const { message_type, message_content } = this.msg;
// 包含9(System)在内的复杂类型尝试解析 JSON
if ([4, 9, 10, 11, 12, 13, 14].includes(message_type)) {
try {
return typeof message_content === 'object' ? message_content : JSON.parse(message_content);
} catch (e) {
return message_content;
}
}
return message_content;
},
/**
* 预约挂号卡片药品行:药名 + 规格 + 数量(与医生端一致,去掉底部「各 X 盒」)
*/
registerCardDrugRows() {
if (this.msg.message_type !== 10) return [];
const pc = this.parsedContent;
if (!pc || typeof pc !== 'object') return [];
const fallbackQty = pc.number != null && pc.number !== '' ? Number(pc.number) : 1;
const arr = pc.selected_western_drugs;
if (Array.isArray(arr) && arr.length) {
return arr.map((d, idx) => {
const q = d && d.quantity != null ? Number(d.quantity) : fallbackQty;
const id = d && (d.drug_id || d.id);
return {
_wxKey: 'rd-' + (id != null ? id : idx),
name: (d && d.name) || '',
specification: (d && (d.specification || d.spec || d.drug_spec)) || '',
image: (d && (d.image || d.drug_image)) || '',
quantity: q >= 1 ? q : 1,
};
});
}
if (pc.drug && pc.drug.name) {
return [{
_wxKey: 'rd-' + (pc.drug.drug_id || pc.drug.id || 0),
name: pc.drug.name,
specification: pc.drug.specification || pc.drug.spec || '',
image: pc.drug.image || pc.drug.drug_image || '',
quantity: fallbackQty >= 1 ? fallbackQty : 1,
}];
}
return [];
},
followUpAnsweredLabel() {
const pc = this.parsedContent;
if (!pc || pc.flow !== 'follow_up_drug') return '';
const st = pc.answer_status;
if (st === 'yes' || st === '1') return '是,曾使用过';
if (st === 'no' || st === '0') return '否,未使用过';
return '';
}
},
methods: {
/**
* 非己方头像点击:通知父页跳转医生详情;己方头像不响应
*/
onAvatarClick() {
if (this.isMine) return;
this.$emit('avatarClick');
},
// 计算语音条宽度 (Min: 100rpx, Max: 350rpx, Unit: 10rpx/sec)
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] || '状态未知'; },
// 处理转诊同意
handleTransferAgree(messageData) {
if (!messageData || !messageData.transfer_id) {
uni.showToast({
title: '转诊信息不完整',
icon: 'none'
})
return
}
uni.showModal({
title: '确认转诊',
content: '确定同意转诊吗?',
success: (res) => {
if (res.confirm) {
agreeTransferPrescriptionApi({ id: messageData.transfer_id }).then((result) => {
if (result.data.code == 0) {
uni.showToast({
title: '同意转诊成功',
icon: 'success'
})
// 跳转到咨询页面
setTimeout(() => {
uni.redirectTo({
url: `/subPackages/transfer/transfer-consultation?transfer_id=${messageData.transfer_id}`,
fail: (err) => {
console.error('跳转失败:', err)
uni.showToast({ title: '跳转失败,请重试', icon: 'none' })
}
})
}, 1000)
} else {
uni.showToast({
title: result.data.message || '操作失败',
icon: 'none'
})
}
}).catch((error) => {
console.error('同意转诊失败:', error)
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
})
}
}
})
},
// 处理转诊拒绝
handleTransferReject(messageData) {
if (!messageData || !messageData.transfer_id) {
uni.showToast({
title: '转诊信息不完整',
icon: 'none'
})
return
}
uni.showModal({
title: '拒绝转诊',
content: '确定拒绝转诊吗?',
success: (res) => {
if (res.confirm) {
rejectTransferPrescriptionApi({ id: messageData.transfer_id }).then((result) => {
if (result.data.code == 0) {
uni.showToast({
title: '已拒绝转诊',
icon: 'success'
})
// 触发消息更新事件
uni.$emit('chat-message-update')
} else {
uni.showToast({
title: result.data.message || '操作失败',
icon: 'none'
})
}
}).catch((error) => {
console.error('拒绝转诊失败:', error)
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
})
})
}
}
})
}
}
}
</script>
<style lang="scss" scoped>
// 变量
$primary: #0A84FF;
$bg-mine: #0A84FF;
$bg-other: #FFFFFF;
$text-main: #333333;
$text-sub: #666666;
$radius-bubble: 12rpx;
$price-color: #FF4D4F; // 价格红
$success-color: #059669;
.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) ==========
.audio-content {
display: flex; align-items: center;
// 宽度由 style 动态控制,这里不要锁死 width
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;
}
// 我发的消息,图标在右,波纹需要转向左侧 (rotate 180)
&.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)); }
}
// 为了解决 rotate 和 scale 同时作用的问题,分别定义 mine 和 other
.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; }
}
.drug-spec-row {
align-items: flex-start;
.drug-thumb {
width: 64rpx;
height: 64rpx;
border-radius: 8rpx;
background: #f3f4f6;
flex-shrink: 0;
margin-right: 12rpx;
}
.drug-name-wrap {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4rpx;
flex: 1;
min-width: 0;
}
.spec-text {
display: block;
font-size: 22rpx;
color: #9ca3af;
font-weight: 400;
text-align: left;
}
}
.drug-list-block {
.block-label {
display: block;
font-size: 22rpx;
color: #888;
margin-bottom: 8rpx;
}
}
.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;
&.primary { background: $primary; color: #fff; border: none; }
&::after { 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; } }
}
.follow-up-pending-readonly {
margin-top: 12rpx;
padding: 16rpx 20rpx;
background: #f7f8fa;
border-radius: 12rpx;
.follow-up-pending-tip {
font-size: 24rpx;
color: #909399;
line-height: 1.5;
}
}
}
// ========== 原生风格服务通知 (Service Notify Card) ==========
// 这是一个非常经典、耐看的布局,模仿微信/支付宝的服务通知
.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;
}
}
// 内容区标准的左key右value列表
.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>

View File

@@ -0,0 +1,309 @@
<template>
<view class="transfer-message-bubble">
<view class="bubble-header">
<view class="header-icon">
<text class="iconfont icon-mendian"></text>
</view>
<view class="header-title">转诊消息</view>
</view>
<view class="bubble-body">
<view class="info-item" v-if="messageData.transfer_store">
<text class="label">转诊诊所</text>
<text class="value">{{ messageData.transfer_store.name || '未知诊所' }}</text>
</view>
<view class="info-item" v-if="messageData.delegate_store">
<text class="label">委托诊所</text>
<text class="value">{{ messageData.delegate_store.name || '未知诊所' }}</text>
</view>
<view class="info-item" v-if="messageData.prescription_no">
<text class="label">处方编号</text>
<text class="value">{{ messageData.prescription_no }}</text>
</view>
<view class="info-item" v-if="messageData.transfer_reason">
<text class="label">转诊原因</text>
<text class="value">{{ messageData.transfer_reason }}</text>
</view>
<!-- 咨询问题和答案 -->
<view class="questions-section" v-if="messageData.questions && messageData.questions.length > 0">
<view class="section-title">
<text class="section-icon"></text>
<text class="section-text">咨询问题</text>
</view>
<view class="question-item" v-for="(qa, index) in messageData.questions" :key="index">
<view class="question-text">
<text class="question-number">{{ index + 1 }}.</text>
<text class="question-content">{{ qa.question }}</text>
</view>
<view class="answer-text" :class="qa.answer === '是' ? 'answer-yes' : (qa.answer === '否' ? 'answer-no' : 'answer-other')">
<text class="answer-icon">{{ qa.answer === '是' ? '✓' : (qa.answer === '否' ? '✗' : '?') }}</text>
<text>{{ qa.answer }}</text>
</view>
</view>
</view>
</view>
<view class="bubble-footer" v-if="messageData.status === 0">
<button class="action-btn agree" @click="handleAgree">同意转诊</button>
<button class="action-btn reject" @click="handleReject">拒绝转诊</button>
</view>
<view class="bubble-footer" v-else-if="messageData.status === 1">
<text class="status-text confirmed">已同意转诊</text>
</view>
<view class="bubble-footer" v-else-if="messageData.status === 3">
<text class="status-text cancelled">已拒绝转诊</text>
</view>
</view>
</template>
<script>
export default {
name: 'TransferMessageBubble',
props: {
messageData: {
type: Object,
required: true,
default: () => ({})
}
},
methods: {
handleAgree() {
this.$emit('agree', this.messageData);
},
handleReject() {
this.$emit('reject', this.messageData);
},
// 获取答案的样式类:是=绿色,否=红色,其他=黄色
getAnswerClass(answer) {
if (answer === '是') {
return 'answer-yes';
} else if (answer === '否') {
return 'answer-no';
} else {
return 'answer-other';
}
},
// 获取答案的图标
getAnswerIcon(answer) {
if (answer === '是') {
return '✓';
} else if (answer === '否') {
return '✗';
} else {
return '?';
}
}
}
}
</script>
<style scoped>
.transfer-message-bubble {
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
border-radius: 16rpx;
border: 2rpx solid #fed7aa;
padding: 24rpx;
margin: 20rpx 0;
max-width: 600rpx;
}
.bubble-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 2rpx solid #fed7aa;
}
.header-icon {
width: 48rpx;
height: 48rpx;
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 28rpx;
}
.header-title {
font-size: 30rpx;
font-weight: 600;
color: #1e293b;
}
.bubble-body {
margin-bottom: 20rpx;
}
.info-item {
margin-bottom: 16rpx;
font-size: 26rpx;
line-height: 1.6;
}
.info-item:last-child {
margin-bottom: 0;
}
.label {
color: #64748b;
font-weight: 500;
}
.value {
color: #1e293b;
}
.bubble-footer {
display: flex;
gap: 20rpx;
justify-content: flex-end;
padding-top: 16rpx;
border-top: 2rpx solid #fed7aa;
}
.action-btn {
padding: 12rpx 32rpx;
border-radius: 8rpx;
font-size: 26rpx;
font-weight: 500;
border: none;
}
.action-btn.agree {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white;
}
.action-btn.reject {
background: white;
color: #64748b;
border: 2rpx solid #cbd5e1;
}
.status-text {
font-size: 26rpx;
font-weight: 500;
padding: 12rpx 32rpx;
border-radius: 8rpx;
}
.status-text.confirmed {
background: rgba(34, 197, 94, 0.1);
color: #16a34a;
}
.status-text.cancelled {
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
}
/* 咨询问题区域 */
.questions-section {
margin-top: 20rpx;
padding: 20rpx;
background: rgba(255, 255, 255, 0.6);
border-radius: 12rpx;
border: 2rpx solid #fed7aa;
}
.section-title {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 16rpx;
padding-bottom: 12rpx;
border-bottom: 2rpx solid #fed7aa;
}
.section-icon {
font-size: 28rpx;
}
.section-text {
font-size: 28rpx;
font-weight: 600;
color: #1e293b;
}
.question-item {
margin-bottom: 16rpx;
padding: 16rpx;
background: white;
border-radius: 8rpx;
border: 1rpx solid #e2e8f0;
}
.question-item:last-child {
margin-bottom: 0;
}
.question-text {
font-size: 26rpx;
color: #334155;
line-height: 1.6;
margin-bottom: 12rpx;
}
.question-number {
font-weight: 600;
color: #0A84FF;
margin-right: 8rpx;
}
.question-content {
color: #1e293b;
}
.answer-text {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
font-weight: 500;
padding: 8rpx 16rpx;
border-radius: 6rpx;
width: fit-content;
}
.answer-yes {
background: #dcfce7;
color: #16a34a;
}
.answer-yes .answer-icon {
color: #16a34a;
}
.answer-no {
background: #fee2e2;
color: #dc2626;
}
.answer-no .answer-icon {
color: #dc2626;
}
.answer-other {
background: #fef3c7;
color: #d97706;
}
.answer-other .answer-icon {
color: #d97706;
}
.answer-icon {
font-size: 24rpx;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,781 @@
<template>
<!--
问诊助手通用壳协议抽屉消息流选项按钮等
follow-up / transfer 分包页使用放在 subPackages/common 以减小主包体积
-->
<view class="consultation-container safe-area-inset-bottom">
<view class="consultation-header">
<view class="header-back" @click="handleHeaderBack">
<text class="iconfont icon-arrow-left"></text>
</view>
<view class="header-title">{{ headerTitle }}</view>
<view class="header-placeholder"></view>
</view>
<view class="notice-box" v-if="showNotice">
<view class="notice-text">
<text v-for="(part, index) in parsedNoticeParts" :key="index">
<text v-if="part.type === 'text'">{{ part.text }}</text>
<text v-else-if="part.type === 'agreement'" class="notice-link" @click="$emit('viewAgreement', part.agreementId)">
{{ part.text }}
</text>
</text>
</view>
</view>
<!-- 协议抽屉v-if 控制挂载:show 控制显隐 chat 页一致 -->
<page-container
v-if="agreementPopupVisible"
:show="agreementPopupVisible"
position="bottom"
round
overlay
custom-style="height:88%;"
@clickoverlay="closeAgreement"
@afterleave="closeAgreement"
>
<view class="agreement-popup">
<view class="agreement-header">
<text class="agreement-title">{{ currentAgreementName || '协议详情' }}</text>
<view class="agreement-close" @click="closeAgreement">×</view>
</view>
<scroll-view scroll-y class="agreement-content" :show-scrollbar="true">
<u-parse :html="agreementContent"></u-parse>
</scroll-view>
</view>
</page-container>
<scroll-view class="messages-scroll" scroll-y="true" :scroll-into-view="scrollIntoView" scroll-with-animation>
<view class="messages-container">
<view v-for="message in displayedMessages" :key="message.id">
<view v-if="message.type === 'welcome'" class="message-item other">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>您好! 我是医生助理, 已收到您的用药申请:{{ prescriptionInfo }}, 为了帮助医生判断准确, 请按照您的真实情况回答以下问题</text>
</view>
</view>
</view>
<view v-else-if="message.type === 'question'" class="message-item other" :id="'question-' + message.questionId">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>{{ message.content }}</text>
</view>
</view>
</view>
<view v-else-if="message.type === 'answer'" class="message-item mine">
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="message-bubble">
<text>{{ message.content }}</text>
</view>
</view>
</view>
<view v-else-if="message.type === 'waiting'" class="message-item other">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>请稍等, 正在为您转接医生进行复诊开方...</text>
</view>
</view>
</view>
<view v-else-if="message.type === 'hint'" class="message-item other">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>{{ message.content }}</text>
</view>
</view>
</view>
</view>
<view v-if="isTyping" class="message-item other typing-indicator">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble typing-bubble">
<view class="typing-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</view>
<view :id="bottomAnchorId" style="height: 1px;"></view>
</view>
</scroll-view>
<slot v-if="customFooterActive && $slots.footer" name="footer"></slot>
<view
v-else-if="showQuestionButtons && currentQuestion && !isSubmitting && !showWaiting && answerMode === 'multi'"
class="action-footer multi-footer safe-area-inset-bottom"
>
<!-- 多选适用症点选 toggling确认后再提交其他可填自定义症状 -->
<view class="multi-options">
<button
v-for="(answer, index) in currentQuestionAnswers"
:key="index"
hover-class="none"
:class="['multi-option-btn', { selected: !!multiSelectedFlag[index] }]"
@click="toggleMultiAnswer(index)"
>
{{ answer }}
</button>
</view>
<input
v-if="multiHasOtherSelected"
class="multi-other-input"
v-model="multiOtherText"
placeholder="请填写具体症状"
:maxlength="100"
/>
<!-- 不用原生 disabled避免微信小程序卡死无法点击未达标时 toast -->
<button
class="action-btn yes-btn confirm-btn"
hover-class="none"
:class="{ disabled: !multiConfirmEnabled }"
@click="confirmMultiAnswer"
>
确认
</button>
</view>
<view
v-else-if="showQuestionButtons && currentQuestion && !isSubmitting && !showWaiting"
class="action-footer safe-area-inset-bottom"
>
<button
v-for="(answer, index) in currentQuestionAnswers"
:key="index"
hover-class="none"
:class="['action-btn', answerButtonClass(answer, index)]"
@click="emitSingleAnswer(answer)"
>
{{ answer }}
</button>
</view>
<view class="submitting-overlay" v-if="isSubmitting">
<view class="submitting-content">
<view class="loading-spinner"></view>
<text class="submitting-text">{{ submittingText }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'AssistantConsultationShell',
props: {
headerTitle: {
type: String,
default: '转诊咨询',
},
showNotice: {
type: Boolean,
default: true,
},
parsedNoticeParts: {
type: Array,
default: () => [],
},
showAgreement: {
type: Boolean,
default: false,
},
agreementContent: {
type: String,
default: '',
},
currentAgreementName: {
type: String,
default: '',
},
displayedMessages: {
type: Array,
default: () => [],
},
assistantAvatar: {
type: String,
default: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/images/assistant-avatar.png',
},
assistantName: {
type: String,
default: '医生助理',
},
userAvatar: {
type: String,
default: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/images/user-avatar.png',
},
prescriptionInfo: {
type: String,
default: '',
},
isTyping: {
type: Boolean,
default: false,
},
scrollIntoView: {
type: String,
default: '',
},
bottomAnchorId: {
type: String,
default: 'bottom-anchor',
},
/** 为 true 时展示底部「是/否」等选项(与转诊一致) */
showQuestionButtons: {
type: Boolean,
default: true,
},
currentQuestion: {
type: Object,
default: null,
},
currentQuestionAnswers: {
type: Array,
default: () => ['是', '否'],
},
isSubmitting: {
type: Boolean,
default: false,
},
showWaiting: {
type: Boolean,
default: false,
},
submittingText: {
type: String,
default: '正在提交...',
},
/** 为 true 时使用 #footer 插槽,避免父组件始终占位导致无法显示默认答题按钮 */
customFooterActive: {
type: Boolean,
default: false,
},
/**
* 答题模式single 点一下即提交multi 可多选后点确认
* 诊所复诊按药选适用症用 multi
*/
answerMode: {
type: String,
default: 'single',
},
},
data() {
return {
/** 多选已选下标列表(整表替换,微信小程序下比 object+$set 更稳) */
multiSelectedIndexes: [],
/** 选「其他」时的自定义症状文案 */
multiOtherText: '',
/** 防双击emit 后立刻锁,题目切换解锁 */
answeringLock: false,
};
},
watch: {
// 题目切换时清空多选,避免上一题选中态残留
currentQuestion() {
this.multiSelectedIndexes = [];
this.multiOtherText = '';
this.answeringLock = false;
},
answerMode() {
this.multiSelectedIndexes = [];
this.multiOtherText = '';
this.answeringLock = false;
},
},
computed: {
agreementPopupVisible: {
get() {
return this.showAgreement
},
set(val) {
this.$emit('update:showAgreement', val)
},
},
/** 供模板判断选中,避免 :class 里写 indexOf */
multiSelectedFlag() {
const map = {};
const list = this.multiSelectedIndexes || [];
for (let i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return map;
},
multiSelectedCount() {
return (this.multiSelectedIndexes || []).length;
},
/** 是否选中了「其他」选项 */
multiHasOtherSelected() {
const answers = this.currentQuestionAnswers || [];
const list = this.multiSelectedIndexes || [];
for (let i = 0; i < list.length; i++) {
const idx = list[i];
if (answers[idx] === '其他') {
return true;
}
}
return false;
},
/** 多选确认是否可提交:至少选一项;选了其他则自定义文案非空 */
multiConfirmEnabled() {
if (this.multiSelectedCount === 0) return false;
if (this.multiHasOtherSelected) {
return !!(this.multiOtherText && String(this.multiOtherText).trim());
}
return true;
},
},
methods: {
handleHeaderBack() {
if (this.showAgreement) {
this.closeAgreement();
return;
}
this.$emit('back');
},
closeAgreement() {
this.$emit('update:showAgreement', false);
},
answerButtonClass(answer, index) {
const answers = this.currentQuestionAnswers
const totalAnswers = answers.length
if (totalAnswers === 2) {
if (answer === '是') return 'yes-btn'
if (answer === '否') return 'no-btn'
}
if (index === 0) return 'yes-btn'
return 'no-btn'
},
/** 单选:点一下即提交,防双击重复 emit */
emitSingleAnswer(answer) {
if (this.answeringLock || this.isSubmitting) return;
this.answeringLock = true;
this.$emit('answer', answer);
},
/** 多选:切换某个适用症的选中状态 */
toggleMultiAnswer(index) {
if (this.answeringLock || this.isSubmitting) return;
const list = this.multiSelectedIndexes || [];
const pos = list.indexOf(index);
if (pos === -1) {
this.multiSelectedIndexes = list.concat([index]);
} else {
this.multiSelectedIndexes = list.filter((i) => i !== index);
if (this.currentQuestionAnswers[index] === '其他') {
this.multiOtherText = '';
}
}
},
/**
* 多选确认:选中标签用顿号拼接;「其他」替换为自定义填写内容
*/
confirmMultiAnswer() {
if (this.answeringLock || this.isSubmitting) return;
if (!this.multiConfirmEnabled) {
if (this.multiSelectedCount === 0) {
uni.showToast({ title: '请至少选择一项', icon: 'none' });
} else if (this.multiHasOtherSelected) {
uni.showToast({ title: '请填写具体症状', icon: 'none' });
}
return;
}
const indexes = (this.multiSelectedIndexes || []).slice().sort((a, b) => a - b);
if (!indexes.length) return;
const otherText = (this.multiOtherText || '').trim();
const labels = indexes
.map((i) => {
const label = this.currentQuestionAnswers[i];
if (label === '其他') return otherText;
return label;
})
.filter(Boolean);
if (!labels.length) return;
this.answeringLock = true;
this.$emit('answer', labels.join('、'));
this.multiSelectedIndexes = [];
this.multiOtherText = '';
},
},
}
</script>
<style lang="scss" scoped>
.consultation-container {
width: 100%;
height: 100vh;
background-color: #f8fafc;
display: flex;
flex-direction: column;
}
.consultation-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
position: relative;
flex-shrink: 0;
.header-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 32rpx;
color: #333;
}
}
.header-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.header-placeholder {
width: 60rpx;
}
}
.notice-box {
margin: 20rpx 30rpx;
padding: 24rpx;
background: #f5f5f5;
border-radius: 12rpx;
font-size: 24rpx;
line-height: 1.6;
color: #666;
flex-shrink: 0;
.notice-text {
display: block;
word-break: break-all;
}
.notice-link {
color: #0a84ff;
text-decoration: underline;
}
}
.messages-scroll {
flex: 1;
min-height: 0;
overflow: hidden;
}
.messages-container {
padding: 30rpx;
min-height: 100%;
}
.message-item {
display: flex;
align-items: flex-start;
margin-bottom: 30rpx;
.avatar {
width: 72rpx;
height: 72rpx;
border-radius: 10rpx;
flex-shrink: 0;
background: #f2f2f2;
}
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
margin-left: 16rpx;
.assistant-label {
font-size: 22rpx;
color: #999;
margin-bottom: 8rpx;
}
.message-bubble {
padding: 18rpx 24rpx;
font-size: 30rpx;
line-height: 1.5;
border-radius: 4rpx 12rpx 12rpx 12rpx;
background: #fff;
color: #333;
word-break: break-all;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
}
}
&.mine {
flex-direction: row-reverse;
.message-content {
margin-left: 0;
margin-right: 16rpx;
align-items: flex-end;
.message-bubble {
background: #0a84ff;
color: #fff;
border-radius: 12rpx 4rpx 12rpx 12rpx;
}
}
}
&.typing-indicator {
.typing-bubble {
padding: 18rpx 24rpx;
min-width: 80rpx;
}
.typing-dots {
display: flex;
align-items: center;
gap: 8rpx;
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #999;
animation: typing 1.4s infinite ease-in-out;
}
.dot:nth-child(1) {
animation-delay: 0s;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
}
}
}
@keyframes typing {
0%,
60%,
100% {
transform: translateY(0);
opacity: 0.7;
}
30% {
transform: translateY(-10rpx);
opacity: 1;
}
}
.action-footer {
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
border-top: 1rpx solid #f0f0f0;
display: flex;
gap: 20rpx;
justify-content: space-between;
flex-shrink: 0;
.action-btn {
flex: 1;
min-height: 88rpx;
height: auto;
line-height: 1.35;
padding: 20rpx 16rpx;
border-radius: 44rpx;
font-size: 30rpx;
font-weight: 500;
border: none;
white-space: normal;
/* 覆盖微信 button 默认白底/按下灰底,避免按钮组背景错乱 */
background-color: #0a84ff !important;
color: #fff !important;
&::after {
border: none;
}
&.yes-btn {
background-color: #0a84ff !important;
color: #fff !important;
}
&.no-btn {
background-color: #fff !important;
color: #333 !important;
border: 2rpx solid #ddd;
}
&.disabled,
&[disabled] {
opacity: 0.45;
}
}
&.multi-footer {
flex-direction: column;
gap: 16rpx;
.multi-options {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.multi-other-input {
width: 100%;
box-sizing: border-box;
padding: 16rpx 24rpx;
min-height: 72rpx;
border-radius: 12rpx;
background: #f8fafc;
font-size: 28rpx;
color: #333;
border: 1rpx solid #e2e8f0;
}
.multi-option-btn {
min-width: 140rpx;
padding: 0 28rpx;
height: 64rpx;
line-height: 64rpx;
border-radius: 32rpx;
font-size: 26rpx;
background-color: rgba(10, 132, 255, 0.08) !important;
color: #475569 !important;
border: 2rpx solid transparent;
&::after {
border: none;
}
&.selected {
background-color: #0a84ff !important;
color: #fff !important;
}
}
.confirm-btn {
width: 100%;
flex: none;
background-color: #0a84ff !important;
color: #fff !important;
}
}
}
.submitting-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
.submitting-content {
background: #fff;
padding: 60rpx 80rpx;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #0a84ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.submitting-text {
font-size: 28rpx;
color: #666;
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.agreement-popup {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
}
.agreement-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 50rpx 20rpx;
border-bottom: 1rpx solid #e5e7eb;
background: #fff;
flex-shrink: 0;
}
.agreement-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
color: #64748b;
line-height: 1;
}
.agreement-title {
font-size: 32rpx;
font-weight: 600;
color: #1e293b;
line-height: 1.5;
flex: 1;
min-width: 0;
}
.agreement-content {
flex: 1;
height: 0;
min-height: 200rpx;
padding: 30rpx 50rpx 40rpx;
box-sizing: border-box;
}
</style>

Some files were not shown because too many files have changed in this diff Show More