fix: 首页UI重构设计

This commit is contained in:
2026-01-07 14:21:29 +08:00
parent 43a87ccc87
commit 95c1146570
68 changed files with 1292 additions and 571 deletions

70
App.vue
View File

@@ -3,6 +3,7 @@
getVersion
} from './request/api/api'
import { webSocketManager } from '@/utils/ws/websocket.js';
import { ChatManager } from '@/store/chat/chat.js';
import {checkDev} from "@/utils/utils";
import {bindUserApi} from "@/request/api/salesperson";
export default {
@@ -151,7 +152,76 @@
methods: {
initWebSocket() {
webSocketManager.connect();
// 注册全局消息处理器
webSocketManager.addMessageHandler((data) => {
// 只处理聊天消息(包含 room_id 的消息)
if (data.room_id) {
// 获取当前页面路径
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentPath = currentPage ? currentPage.route : '';
// 如果用户在聊天页面,不调用 ChatManagerchat.vue 会自己处理)
// 避免消息重复推送
if (currentPath && currentPath.includes('chat/chat')) {
return;
}
ChatManager.handleIncomingMessage(data);
}
});
// 监听全局通知事件
uni.$on('show-global-notification', this.handleShowNotification);
},
/**
* 处理显示通知
* @param {Object} message 消息对象
*/
handleShowNotification(message) {
// 获取当前页面路径
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentPath = currentPage ? currentPage.route : '';
// 如果在聊天页面,不显示顶部通知
if (currentPath && currentPath.includes('chat/chat')) {
return;
}
// 获取消息预览文本
const messagePreview = ChatManager.getMessagePreview(message);
// 从消息中提取发送者信息
let senderName = '新消息';
let senderAvatar = '/static/xx/ysxx.png';
// 尝试解析发送者ID获取更多信息
const senderId = message.sender_user_id || '';
if (senderId.startsWith('doctor-')) {
senderName = '医生消息';
}
// 触发通知组件显示
uni.$emit('show-message-notification', {
avatar: senderAvatar,
title: senderName,
message: messagePreview,
roomId: message.room_id,
roomData: {
nick_name: senderName,
user_id: senderId,
avatar: senderAvatar
}
});
}
},
onUnload() {
// 移除全局通知事件监听
uni.$off('show-global-notification', this.handleShowNotification);
}
}
</script>

View File

@@ -0,0 +1,236 @@
<template>
<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" />
<!-- 内容区域 -->
<view class="content">
<text class="title">{{ title || '新消息' }}</text>
<text class="message">{{ displayMessage }}</text>
</view>
<!-- 关闭按钮 -->
<view class="close" @click.stop="handleClose">
<text class="close-icon">×</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'MessageNotification',
data() {
return {
visible: false,
avatar: '',
title: '',
message: '',
roomId: '',
roomData: null,
autoHideTimer: null,
notificationTop: 50 // 默认值
}
},
created() {
// 获取胶囊按钮位置,计算通知的安全位置
this.calculateSafeTop();
},
computed: {
displayMessage() {
// 限制消息长度超过30个字符显示省略号
if (this.message && this.message.length > 30) {
return this.message.substring(0, 30) + '...';
}
return this.message || '您有一条新消息';
}
},
mounted() {
// 监听显示通知事件
uni.$on('show-message-notification', this.showNotification);
// 监听隐藏通知事件
uni.$on('hide-message-notification', this.hideNotification);
},
beforeDestroy() {
// 移除事件监听
uni.$off('show-message-notification', this.showNotification);
uni.$off('hide-message-notification', this.hideNotification);
// 清除定时器
if (this.autoHideTimer) {
clearTimeout(this.autoHideTimer);
}
},
methods: {
/**
* 计算通知的安全顶部位置(避开胶囊按钮)
*/
calculateSafeTop() {
try {
// #ifdef MP-WEIXIN
// 获取胶囊按钮的位置信息
const menuButtonInfo = wx.getMenuButtonBoundingClientRect();
// 通知顶部 = 胶囊底部 + 10px 安全边距
this.notificationTop = menuButtonInfo.bottom + 10;
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境,使用状态栏高度 + 安全边距
const systemInfo = uni.getSystemInfoSync();
this.notificationTop = (systemInfo.statusBarHeight || 44) + 10;
// #endif
} catch (e) {
// 获取失败时使用默认值
this.notificationTop = 50;
}
},
/**
* 显示通知
* @param {Object} data 通知数据
* @param {string} data.avatar 头像
* @param {string} data.title 标题(发送者昵称)
* @param {string} data.message 消息内容
* @param {string} data.roomId 房间ID
* @param {Object} data.roomData 房间数据(用于跳转)
*/
showNotification(data) {
// 清除之前的定时器
if (this.autoHideTimer) {
clearTimeout(this.autoHideTimer);
}
// 设置通知数据
this.avatar = data.avatar || '';
this.title = data.title || '新消息';
this.message = data.message || '您有一条新消息';
this.roomId = data.roomId || '';
this.roomData = data.roomData || null;
// 显示通知
this.visible = true;
// 3秒后自动隐藏
this.autoHideTimer = setTimeout(() => {
this.hideNotification();
}, 3000);
},
/**
* 隐藏通知
*/
hideNotification() {
this.visible = false;
if (this.autoHideTimer) {
clearTimeout(this.autoHideTimer);
this.autoHideTimer = null;
}
},
/**
* 点击通知,跳转到聊天页面
*/
handleClick() {
this.hideNotification();
if (this.roomId && this.roomData) {
uni.navigateTo({
url: `/subPackages/chat/chat?room_id=${this.roomId}&nick_name=${this.roomData.nick_name || ''}&user_id=${this.roomData.user_id || ''}&avatar=${encodeURIComponent(this.roomData.avatar || '')}`
});
} else if (this.roomId) {
// 如果只有 roomId也尝试跳转
uni.navigateTo({
url: `/subPackages/chat/chat?room_id=${this.roomId}`
});
}
},
/**
* 关闭通知
*/
handleClose() {
this.hideNotification();
}
}
}
</script>
<style lang="scss" scoped>
.notification-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
padding-left: 20rpx;
padding-right: 20rpx;
padding-bottom: 20rpx;
// padding-top 通过 :style 动态设置,避开胶囊按钮
transform: translateY(-100%);
transition: transform 0.3s ease-out;
&.slide-in {
transform: translateY(0);
}
}
.notification {
display: flex;
align-items: center;
background: #ffffff;
border-radius: 20rpx;
padding: 24rpx;
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.15);
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
flex-shrink: 0;
background-color: #f5f5f5;
}
.content {
flex: 1;
margin-left: 20rpx;
overflow: hidden;
.title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #333333;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message {
display: block;
font-size: 26rpx;
color: #666666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.close {
flex-shrink: 0;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 16rpx;
.close-icon {
font-size: 40rpx;
color: #999999;
line-height: 1;
}
}
}
</style>

View File

@@ -3,7 +3,9 @@
// 下载安装方式
// "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
// npm安装方式
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
// 消息通知组件
"^MessageNotification": "@/components/MessageNotification/MessageNotification.vue"
},
"pages": [
//pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 搜索医生 -->
<view class="search">
<u-search placeholder="请输入关键字" bg-color="#F5f5f5" color="#999" border-color="#F5f5f5" :show-action="false"
@@ -234,16 +235,19 @@
},
// 医生id
goLookDoctor(id) {
// type=3 (在线复诊) 跳转医生详情页走挂号流程
if (this.type === '3') {
return uni.navigateTo({
url: '/subPackages/doctor/doctor-detail?id=' + id + '&r_type=3'
})
}
// type=2 保持原有逻辑
if (this.type === '2') {
return uni.navigateTo({
url: '/subPackages/product/product?doctor_id=' + id + '&register_type=2'
})
} else if (this.type === '3') {
console.log(this.type, 'sssssssssssssssssssss')
return uni.navigateTo({
url: '/subPackages/product/product?doctor_id=' + id + '&register_type=3'
})
}
// 默认跳转医生详情
uni.navigateTo({
url: '/subPackages/doctor/doctor-detail?id=' + id
})

View File

@@ -476,10 +476,10 @@
height: 100%;
border-radius: 8rpx;
// 处方药图片模糊处理
// 处方药图片模糊处理(暂时禁用)
&.prescription-blur {
filter: blur(10px);
-webkit-filter: blur(10px);
/* filter: blur(10px);
-webkit-filter: blur(10px); */
}
}
}

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 搜索 -->
<view class="search">
<u-search placeholder="输入文章标题" bg-color="#F5f5f5" color="#DDDDDD" border-color="#F5f5f5"

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="_html" v-if="content">
<u-parse :html="content"></u-parse>

View File

@@ -1,5 +1,8 @@
<template>
<view class="content safe-area-inset-bottom">
<!-- 全局消息通知组件 -->
<MessageNotification />
<!-- 1. 自定义吸顶导航栏 -->
<view class="custom-nav"
:class="[
@@ -69,7 +72,7 @@
</view>
<image src="../../static/home/bg1.png" class="nav-icon-img-64" mode="aspectFit"></image>
</view>
<view class="nav-card" @click="golist(3)">
<view class="nav-card" @click="goYard(3)">
<view class="nav-content">
<view class="nav-title" style="color:#00B578">在线复诊</view>
<view class="nav-desc">在线选药 极速达</view>
@@ -208,8 +211,12 @@ import {
import { getPlatformQualificationsApi } from '../../request/api/platform'
import { getStoreDrugListBySalespersonApi, getHomeTopProductsApi, getHomeZonesApi, checkOnlineConsultationConfigApi } from "@/request/api/product";
import request from "@/request/api/request";
import MessageNotification from "@/components/MessageNotification/MessageNotification.vue";
export default {
components: {
MessageNotification
},
data() {
return {
storeId: 0,
@@ -293,27 +300,10 @@ export default {
},
async golist(type) {
if (type === 3) {
try {
const storeId = uni.getStorageSync('store_id');
const res = await checkOnlineConsultationConfigApi({ store_id: storeId });
if (!res.data?.result?.can_use) {
uni.showToast({
title: res.data?.result?.message || '该药店暂不支持在线复诊功能',
icon: 'none',
duration: 2000
});
return;
}
const doctorId = res.data.result.doctor_id || this.defaultDoctorId;
const delegateStoreId = res.data.result.delegate_store_id || storeId;
return uni.navigateTo({
url: '/subPackages/product/product?doctor_id=' + doctorId + '&register_type=3&delegate_store_id=' + delegateStoreId
});
} catch (error) {
console.error('检查在线复诊配置失败:', error);
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
return;
}
// 药店模式直接进入商品页,不检查配置
return uni.navigateTo({
url: '/subPackages/product/product?register_type=3'
});
}
uni.navigateTo({
url: '/pages/Inquiries/Inquiries?type=' + type
@@ -365,6 +355,7 @@ export default {
}).then((res) => {
if (res.data.errcode == 0) {
this.storeType = res.data.data.store['type']
uni.setStorageSync('store_type', res.data.data.store['type']) // 存储门店类型
this.infoList = res.data.data
this.headerTitle = res.data.data.store['name']
this.getBannered();
@@ -669,19 +660,18 @@ $card-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04);
/* --- 5. 药品卡片 (双列瀑布流) --- */
.product-list-container {
padding: 0 20rpx; /* 减小左右边距 */
padding: 0 24rpx; /* 左右边距 */
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 16rpx; /* 卡片间隙 */
}
.product-card {
background: #fff;
/* 动态计算宽度,保留中间20rpx间隙 (750 - 20*2 - 20) / 2 = 345rpx */
width: calc((100% - 20rpx) / 2);
margin-bottom: 24rpx;
/* 动态计算宽度,保留中间16rpx间隙 */
width: calc((100% - 16rpx) / 2);
padding: 0;
border-radius: $card-radius;
border-radius: 16rpx;
display: flex;
flex-direction: column;
box-shadow: $card-shadow;
@@ -690,7 +680,7 @@ $card-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04);
.prod-img-box {
width: 100%;
height: 345rpx; /* 保持正方形 */
height: 280rpx; /* 缩小图片高度原345rpx减小约19% */
background: #f8f8f8;
border-radius: 0;
margin: 0;
@@ -698,21 +688,21 @@ $card-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04);
align-items: center;
justify-content: center;
.prod-img { width: 100%; height: 100%; border-radius: 0; &.prescription-blur { filter: blur(4px); } }
.prod-img { width: 100%; height: 100%; border-radius: 0; /* &.prescription-blur { filter: blur(4px); } 暂时禁用模糊 */ }
}
.prod-info {
flex: 1;
display: flex; flex-direction: column; justify-content: space-between;
padding: 16rpx;
padding: 12rpx; /* 减小内边距原16rpx */
.prod-title {
font-size: 28rpx;
font-size: 26rpx; /* 减小字号原28rpx */
font-weight: 600;
color: #333;
line-height: 1.4;
margin-bottom: 12rpx;
height: 80rpx; /* 限制两行高度 */
margin-bottom: 8rpx;
height: 72rpx; /* 减小高度原80rpx */
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
@@ -720,19 +710,19 @@ $card-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04);
-webkit-box-orient: vertical;
}
.tag-row { display: flex; margin-bottom: 12rpx; .otc-tag { font-size: 18rpx; padding: 0rpx 6rpx; border-radius: 4rpx; font-weight: 700; border: 1rpx solid transparent; &.otc-green { color: $secondary-color; background: #E6F7F0; border-color: #A3E6CD; } &.otc-red { color: #FF4D4F; background: #FFF0F0; border-color: #FFCDCD; } } }
.prod-spec { font-size: 20rpx; color: #999; margin-bottom: 16rpx;}
.tag-row { display: flex; margin-bottom: 8rpx; .otc-tag { font-size: 18rpx; padding: 0rpx 6rpx; border-radius: 4rpx; font-weight: 700; border: 1rpx solid transparent; &.otc-green { color: $secondary-color; background: #E6F7F0; border-color: #A3E6CD; } &.otc-red { color: #FF4D4F; background: #FFF0F0; border-color: #FFCDCD; } } }
.prod-spec { font-size: 18rpx; color: #999; margin-bottom: 12rpx;} /* 减小字号原20rpx */
.prod-bottom {
display: flex; justify-content: space-between; align-items: flex-end;
.price-box { color: #FF4D4F; font-weight: 800; font-size: 34rpx; .price-symbol { font-size: 22rpx; margin-right: 2rpx; } }
.buy-btn { background: linear-gradient(135deg, #4175EE 0%, #298DFF 100%); color: #fff; width: 56rpx; height: 56rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 12rpx rgba(65, 117, 238, 0.3); }
.price-box { color: #FF4D4F; font-weight: 800; font-size: 30rpx; .price-symbol { font-size: 20rpx; margin-right: 2rpx; } } /* 减小字号原34rpx/22rpx */
.buy-btn { background: linear-gradient(135deg, #4175EE 0%, #298DFF 100%); color: #fff; width: 52rpx; height: 52rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 12rpx rgba(65, 117, 238, 0.3); } /* 减小尺寸原56rpx */
}
}
}
.qualifications-section {
padding: 0 32rpx 40rpx;
padding: 40rpx 32rpx 40rpx;
.qualifications-row {
display: flex;
flex-wrap: wrap;

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom" @click="onclick">
<MessageNotification />
<view class="imgs">
<image class="img"
:src="id==1?'https://p3.maiyaole.com/img/971/971752/org_org.jpg?v=1':'https://p2.maiyaole.com/img/item/202210/24/202210241042513.jpg'"

View File

@@ -1,5 +1,6 @@
<template>
<view class="content safe-area-inset-bottom">
<MessageNotification />
<view class="head" @click="gologin">
<!-- 门店 -->
<view class="store">

View File

@@ -1,5 +1,6 @@
<template>
<view class="content safe-area-inset-bottom">
<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>

View File

@@ -1,5 +1,8 @@
<template>
<view class="content safe-area-inset-bottom">
<!-- 全局消息通知组件 -->
<MessageNotification />
<!-- 标题 -->
<view class="title">
<text>全部消息</text>
@@ -155,8 +158,12 @@ import {
import {getSystemNoticeIndexApi} from '../../request/api/systemNotice'
import {getChatFriendsListApi} from "@/request/api/im";
import {checkDev} from "@/utils/utils";
import MessageNotification from "@/components/MessageNotification/MessageNotification.vue";
export default {
components: {
MessageNotification
},
data() {
return {
order: [],
@@ -169,10 +176,25 @@ export default {
}
},
onLoad() {
// 监听聊天列表更新事件
uni.$on('chat-list-update', this.handleChatListUpdate);
},
onUnload() {
// 移除事件监听
uni.$off('chat-list-update', this.handleChatListUpdate);
},
methods: {
checkDev,
/**
* 处理聊天列表更新事件
* 收到新消息时自动刷新列表
*/
handleChatListUpdate() {
this.getMyChatFriendsList();
},
// 清除未读信息
getClearInfo() {
getseesionRead({
@@ -237,11 +259,8 @@ export default {
})
},
goChat(item, index) {
console.log(this.imChat, 'ssssssssss')
console.log(item, 'ssssssssss')
console.log(index, 'ssssssssss')
uni.navigateTo({
url: `/subPackages/chat/chat?room_id=${item.room_id}&nick_name=${item.nick_name}&user_id=${item.id}&avatar=${item.avatar}`
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",

View File

@@ -1,5 +1,6 @@
<template>
<view class="content safe-area-inset-bottom">
<MessageNotification />
<!-- 描述 -->
<view class="card" v-for="item in list" :key="item.id">
<image src="/static/xx/cpdd.png" mode=""></image>

View File

@@ -1,5 +1,6 @@
<template>
<view class="content safe-area-inset-bottom">
<MessageNotification />
<!-- 资质证明 -->
<view class="section">
<view class="section-title">{{ storeName || (isPlatform ? '萧康云医' : '店铺') }} - 资质证明</view>

View File

@@ -1,5 +1,6 @@
<template>
<view class="content safe-area-inset-bottom">
<MessageNotification />
<!-- 描述 -->
<view class="info" v-for="item in list" :key="item.id" @click="next(item.base_type,item.id,item.data)">
<view class="infos_title">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="none">
<!-- 授权登录 -->
<view class="conter">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="title">
欢迎注册新账号

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 头部 -->
<view class="head">
<view class="head_img">

View File

@@ -69,4 +69,13 @@ export function upLoadChatFileApi(params) {
*/
export async function getDoctorInfoApi(params) {
return await get('/chat-friends/get-doctor-info', params, 3)
}
/**
* 获取房间状态
* @param params { room_id: string }
* @returns {Promise<*>}
*/
export async function getRoomStatusApi(params) {
return await get('/chat-friends/get-room-status', params, 3)
}

View File

@@ -56,4 +56,13 @@ export async function updateIntroductionImagesApi(params) {
*/
export async function checkOnlineConsultationConfigApi(params) {
return await get('/store/check-online-consultation-config', params, 3)
}
/**
* 获取药品分类列表
* @param params { zone_type: string }
* @returns {Promise<*>}
*/
export async function getDrugCategoriesApi(params) {
return await get('/product/drug-categories', params, 3)
}

View File

@@ -23,7 +23,6 @@ const ChatManager = {
enterRoom(roomId) {
this.currentRoomId = roomId;
uni.setStorageSync('currentRoomId', roomId);
console.log(`进入房间`);
// 重置当前房间的未读消息
this.resetUnreadCount(roomId);
},
@@ -32,7 +31,6 @@ const ChatManager = {
leaveRoom() {
this.currentRoomId = null;
uni.removeStorageSync('currentRoomId');
console.log('离开当前房间');
},
// 获取当前房间ID
@@ -46,7 +44,6 @@ const ChatManager = {
if (this.getCurrentRoomId() !== roomId) {
this.unreadCounts[roomId] = (this.unreadCounts[roomId] || 0) + 1;
this.notifyUnreadChange();
console.log(`房间 ${roomId} 未读消息 +1, 当前: ${this.unreadCounts[roomId]}`);
}
},
@@ -55,7 +52,6 @@ const ChatManager = {
if (this.unreadCounts[roomId]) {
this.unreadCounts[roomId] = 0;
this.notifyUnreadChange();
console.log(`重置房间 ${roomId} 未读消息数`);
}
},
@@ -83,7 +79,6 @@ const ChatManager = {
this.roomMessages[roomId] = [];
}
this.roomMessages[roomId].push(message);
console.log('addMessageToRoom', this.roomMessages[roomId])
},
// 获取房间消息
@@ -96,7 +91,6 @@ const ChatManager = {
}).then((res) => {
that.roomMessages[roomId] = that.checkMessage(res.data.result.list);
that.lastMessageId = res.data.result.last_message_id;
console.log(that.roomMessages[roomId], 'roomMessages')
uni.$emit('load-room-message', 1);
return this.roomMessages[roomId] || [];
@@ -190,13 +184,7 @@ const ChatManager = {
getChatRegisterInfoApi({
id: item.id
}).then((res) => {
console.log(that.roomMessages, 'ssssssssssss1')
console.log(that.currentRoomId, 'ssssssssssss2')
console.log(index, 'ssssssssssss3')
console.log(that.roomMessages[that.currentRoomId][index], 'ssssssssssss4')
that.roomMessages[that.currentRoomId][index].content = res.data.result;
console.log('getChatRegisterInfo', that.roomMessages[that.currentRoomId][index].content)
// return res.data.result
})
},
@@ -297,6 +285,61 @@ const ChatManager = {
} else {
// 不在当前房间,缓存消息并增加未读消息数
this.increaseUnreadCount(roomId);
// 触发顶部通知(传递原始消息,由 App.vue 处理通知显示)
uni.$emit('show-global-notification', message);
}
// 通知聊天列表页刷新
uni.$emit('chat-list-update');
},
/**
* 获取消息预览文本
* @param {Object} message 消息对象
* @returns {string} 预览文本
*/
getMessagePreview(message) {
const messageType = message.message_type;
const content = message.message_content;
switch (messageType) {
case 0: // 文本
return content;
case 1: // 图片
return '[图片]';
case 2: // 音频
return '[语音消息]';
case 3: // 视频
return '[视频]';
case 4: // 处方
return '[处方单]';
case 5: // 文件
return '[文件]';
case 6: // 视频通话
return '[视频通话]';
case 7: // 音频通话
return '[语音通话]';
case 9: // 系统消息
try {
const parsed = JSON.parse(content);
if (parsed.type === 'price_update') {
return parsed.message || '[价格更新]';
}
return '[系统消息]';
} catch (e) {
return '[系统消息]';
}
case 10: // 挂号
return '[挂号信息]';
case 11: // 患者就诊经历
return '[就诊信息]';
case 12: // 产品卡片
return '[商品推荐]';
case 13: // 结束问诊
return '[问诊已结束]';
default:
return '您有一条新消息';
}
}
};

View File

@@ -108,7 +108,7 @@
</view>
<!-- 复杂卡片类消息容器 (挂号处方档案等) -->
<view v-else-if="[4, 10, 11, 12, 13].includes(msg.message_type)" class="card-bubble">
<view v-else-if="[4, 9, 10, 11, 12, 13].includes(msg.message_type)" class="card-bubble">
<!-- 类型10: 挂号信息卡片 -->
<block v-if="msg.message_type === 10">
@@ -117,9 +117,6 @@
<u-icon name="calendar-fill" size="18" color="#059669"></u-icon>
<text class="card-title">预约挂号</text>
</view>
<text class="status-tag" :class="'status-' + getParsedContent(msg.message_type, msg.message_content).status">
{{ getRegisterStatusText(getParsedContent(msg.message_type, msg.message_content).status) }}
</text>
</view>
<view class="card-body">
<view class="info-row">
@@ -225,6 +222,23 @@
</view>
</block>
<!-- 类型9: 系统消息 - 价格变更通知 -->
<block v-else-if="msg.message_type === 9">
<view class="system-message-card" v-if="getParsedContent(msg.message_type, msg.message_content).type === 'price_update'">
<view class="price-update-notice">
<u-icon name="info-circle-fill" size="18" color="#fa8c16"></u-icon>
<text class="notice-text">{{ getParsedContent(msg.message_type, msg.message_content).message }}</text>
</view>
<view class="price-detail">
<text>订单号{{ getParsedContent(msg.message_type, msg.message_content).order_no }}</text>
<text>应付金额¥{{ getParsedContent(msg.message_type, msg.message_content).total_pay_price }}</text>
</view>
</view>
<view v-else class="system-message">
<text>{{ msg.message_content }}</text>
</view>
</block>
<!-- 类型13: 结束问诊卡片 -->
<block v-else-if="msg.message_type === 13">
<view class="card-header">
@@ -276,7 +290,7 @@
</view>
<!-- 卡片: 我发送的卡片 -->
<view v-else-if="[4, 10, 11, 12, 13].includes(msg.message_type)" class="card-bubble">
<view v-else-if="[4, 9, 10, 11, 12, 13].includes(msg.message_type)" class="card-bubble">
<!-- 类型10: 挂号 -->
<block v-if="msg.message_type === 10">
@@ -285,9 +299,6 @@
<u-icon name="calendar-fill" size="18" color="#059669"></u-icon>
<text class="card-title">预约挂号</text>
</view>
<text class="status-tag" :class="'status-' + getParsedContent(msg.message_type, msg.message_content).status">
{{ getRegisterStatusText(getParsedContent(msg.message_type, msg.message_content).status) }}
</text>
</view>
<view class="card-body">
<view class="info-row">
@@ -434,71 +445,80 @@
<!-- ================= 底部输入与工具栏 (重构版) ================= -->
<view class="chat-footer safe-area-inset-bottom">
<!-- 上半部分快捷工具栏 -->
<view class="toolbar-area">
<view class="action-grid">
<!-- 相册按钮 -->
<view class="action-item" @click="openMediaPicker('image')">
<view class="icon-circle"><u-icon name="photo-fill" size="20" color="#606266"></u-icon></view>
<text>相册</text>
</view>
<!-- &lt;!&ndash; 拍照按钮 &ndash;&gt;-->
<!-- <view class="action-item" @click="openCamera">-->
<!-- <view class="icon-circle"><u-icon name="camera-fill" size="20" color="#606266"></u-icon></view>-->
<!-- <text>拍摄</text>-->
<!-- </view>-->
<!-- &lt;!&ndash; 视频按钮 &ndash;&gt;-->
<!-- <view class="action-item" @click="openMediaPicker('video')">-->
<!-- <view class="icon-circle"><u-icon name="play-circle-fill" size="20" color="#606266"></u-icon></view>-->
<!-- <text>视频</text>-->
<!-- </view>-->
</view>
<!-- 问诊已结束提示 -->
<view v-if="isConsultationEnded" class="consultation-ended-notice">
<u-icon name="checkmark-circle-fill" size="20" color="#999"></u-icon>
<text>本次问诊已结束</text>
</view>
<!-- 下半部分输入框行 -->
<view class="input-bar">
<!-- 语音/键盘切换 -->
<!-- <view class="voice-switch" @click="toggleVoiceInput">-->
<!-- <u-icon :name="voiceInputActive ? 'keyboard' : 'mic'" size="28" color="#333"></u-icon>-->
<!-- </view>-->
<!-- 输入区域容器 -->
<view class="input-field-wrapper">
<!-- 文本输入框 -->
<input
v-if="!voiceInputActive"
v-model="newMessage"
class="main-input"
placeholder="描述您的症状或问题..."
confirm-type="send"
@confirm="sendTextMessage"
:adjust-position="false"
cursor-spacing="20"
/>
<!-- 语音按住按钮 -->
<view v-else class="voice-btn"
:class="{ 'recording': recording }"
@touchstart="startVoiceRecording"
@touchend="stopVoiceRecording"
@touchcancel="stopVoiceRecording">
<text>{{ recording ? '松开 发送' : '按住 说话' }}</text>
<!-- 正常输入区域 -->
<template v-else>
<!-- 上半部分快捷工具栏 -->
<view class="toolbar-area">
<view class="action-grid">
<!-- 相册按钮 -->
<view class="action-item" @click="openMediaPicker('image')">
<view class="icon-circle"><u-icon name="photo-fill" size="20" color="#606266"></u-icon></view>
<text>相册</text>
</view>
<!-- &lt;!&ndash; 拍照按钮 &ndash;&gt;-->
<!-- <view class="action-item" @click="openCamera">-->
<!-- <view class="icon-circle"><u-icon name="camera-fill" size="20" color="#606266"></u-icon></view>-->
<!-- <text>拍摄</text>-->
<!-- </view>-->
<!-- &lt;!&ndash; 视频按钮 &ndash;&gt;-->
<!-- <view class="action-item" @click="openMediaPicker('video')">-->
<!-- <view class="icon-circle"><u-icon name="play-circle-fill" size="20" color="#606266"></u-icon></view>-->
<!-- <text>视频</text>-->
<!-- </view>-->
</view>
</view>
<!-- 发送按钮 (仅在有内容或语音模式下显示) -->
<view class="send-btn-wrapper" v-if="newMessage.trim() || voiceInputActive">
<view class="send-btn" @click="sendTextMessage">
<!-- 文本发送图标 -->
<image src="/static/chat/send2.png" style="width: 30rpx; height: 30rpx;" />
<!-- <uni-icons type="paperplane-filled"></uni-icons>-->
<!-- <u-icon name="arrow-up" size="18" color="#fff" v-if="!voiceInputActive"></u-icon>-->
<!-- &lt;!&ndash; 语音状态图标 &ndash;&gt;-->
<!-- <view class="voice-wave" v-else>-->
<!-- <u-icon name="mic-fill" size="16" color="#fff"></u-icon>-->
<!-- </view>-->
<!-- 下半部分输入框行 -->
<view class="input-bar">
<!-- 语音/键盘切换 -->
<!-- <view class="voice-switch" @click="toggleVoiceInput">-->
<!-- <u-icon :name="voiceInputActive ? 'keyboard' : 'mic'" size="28" color="#333"></u-icon>-->
<!-- </view>-->
<!-- 输入区域容器 -->
<view class="input-field-wrapper">
<!-- 文本输入框 -->
<input
v-if="!voiceInputActive"
v-model="newMessage"
class="main-input"
placeholder="描述您的症状或问题..."
confirm-type="send"
@confirm="sendTextMessage"
:adjust-position="false"
cursor-spacing="20"
/>
<!-- 语音按住按钮 -->
<view v-else class="voice-btn"
:class="{ 'recording': recording }"
@touchstart="startVoiceRecording"
@touchend="stopVoiceRecording"
@touchcancel="stopVoiceRecording">
<text>{{ recording ? '松开 发送' : '按住 说话' }}</text>
</view>
</view>
<!-- 发送按钮 (仅在有内容或语音模式下显示) -->
<view class="send-btn-wrapper" v-if="newMessage.trim() || voiceInputActive">
<view class="send-btn" @click="sendTextMessage">
<!-- 文本发送图标 -->
<image src="/static/chat/send2.png" style="width: 30rpx; height: 30rpx;" />
<!-- <uni-icons type="paperplane-filled"></uni-icons>-->
<!-- <u-icon name="arrow-up" size="18" color="#fff" v-if="!voiceInputActive"></u-icon>-->
<!-- &lt;!&ndash; 语音状态图标 &ndash;&gt;-->
<!-- <view class="voice-wave" v-else>-->
<!-- <u-icon name="mic-fill" size="16" color="#fff"></u-icon>-->
<!-- </view>-->
</view>
</view>
</view>
</view>
</template>
</view>
<!-- ================= 录音全屏蒙层 ================= -->
@@ -524,7 +544,7 @@
*/
import { webSocketManager } from '@/utils/ws/websocket';
import { ChatManager } from '@/store/chat/chat.js';
import {getChatRegisterInfoApi, sendToUserApi, upLoadChatFileApi, getDoctorInfoApi} from "@/request/api/im";
import {getChatRegisterInfoApi, sendToUserApi, upLoadChatFileApi, getDoctorInfoApi, getRoomStatusApi} from "@/request/api/im";
import { checkDev } from '@/utils/utils';
// 根据环境获取上传URL
@@ -537,7 +557,7 @@ const uploadBaseUrl = isDev ? 'http://127.0.0.1:18001/api/mobile' : 'https://api
*/
function getParsedContent(messageType, messageContent) {
// 需要解析的类型:处方(4), 挂号(10), 经历(11), 商品(12), 结束(13)
if ([4, 10, 11, 12, 13].includes(messageType)) {
if ([4, 9, 10, 11, 12, 13].includes(messageType)) {
try {
// 如果已经是对象则直接返回,否则解析字符串
return typeof messageContent === 'object' ? messageContent : JSON.parse(messageContent);
@@ -627,7 +647,10 @@ export default {
express_name: "李测试",
total_pay_price: "11.94"
},
registerData: {}
registerData: {},
// 问诊是否已结束
isConsultationEnded: false
};
},
@@ -655,9 +678,22 @@ export default {
ChatManager.resetUnreadCount(option.room_id);
// 获取缓存的房间消息
this.messages = ChatManager.getRoomMessages(option.room_id);
// 检查房间状态是否为结束问诊(优先使用 option 参数)
if (option.room_status === '1' || option.room_status === 1) {
this.isConsultationEnded = true;
} else if (this.messages.some(msg => msg.message_type === 13)) {
// 兼容:检查历史消息中是否有结束问诊消息
this.isConsultationEnded = true;
}
}
// 5. 检查医生信息是否完整,如果不完整则从API获取
// 5. 从 API 获取最新房间状态(确保状态最新)
if (option.room_id) {
this.fetchRoomStatus(option.room_id);
}
// 6. 检查医生信息是否完整如果不完整则从API获取
const nickName = this.doctorUserInfo.nick_name;
if (!nickName || nickName === 'undefined' || nickName === 'null') {
this.fetchDoctorInfo(doctorId);
@@ -665,13 +701,11 @@ export default {
// 设置导航栏标题
uni.setNavigationBarTitle({
title: `${nickName}医生的对话`,
success: () => {
console.log('修改标题成功');
}
success: () => {}
});
}
// 6. 初次渲染滚动到底部
// 7. 初次渲染滚动到底部
// Fix: 页面加载时延时触发滚动到锚点
this.$nextTick(() => {
this.scrollToBottom();
@@ -753,9 +787,7 @@ export default {
// 更新导航栏标题
uni.setNavigationBarTitle({
title: `${this.doctorUserInfo.nick_name}医生的对话`,
success: () => {
console.log('修改标题成功');
}
success: () => {}
});
}
} catch (error) {
@@ -767,6 +799,22 @@ export default {
}
},
/**
* 获取房间状态
* 判断问诊是否已结束
*/
async fetchRoomStatus(roomId) {
try {
const res = await getRoomStatusApi({ room_id: roomId });
if (res.data && res.data.result) {
// 根据房间状态设置问诊结束状态status=1结束status=0正常
this.isConsultationEnded = res.data.result.status === 1;
}
} catch (error) {
console.error('获取房间状态失败:', error);
}
},
/**
* 检查隐私授权
* 微信小程序从2023年9月起要求在使用敏感API前获取用户隐私授权
@@ -819,13 +867,6 @@ export default {
// 设置样式(使用 px 单位,因为系统信息返回的是 px
this.scrollViewStyle = `height: ${scrollHeight}px;`;
console.log('Scroll-view height calculated:', {
windowHeight,
headerHeight,
footerHeight,
scrollHeight
});
}).exec();
}).exec();
});
@@ -890,17 +931,93 @@ export default {
},
/**
* 异步获取挂号详情并更新消息内容 (保留原逻辑)
* 批量加载挂号卡片信息
* 遍历消息列表,找出需要加载的挂号卡片并请求后端
*/
loadRegisterCards() {
let that = this;
this.messages.forEach((msg, index) => {
if (msg.message_type === 10) {
const parsedContent = getParsedContent(msg.message_type, msg.message_content);
// 如果已加载或没有 id跳过
if (parsedContent._loaded || !parsedContent.id) {
return;
}
// 调用 API 获取完整信息
that.fetchRegisterCardInfo(msg, index, parsedContent.id);
}
});
},
/**
* 获取单个挂号卡片的详细信息
* @param {Object} msg - 消息对象
* @param {Number} index - 消息在列表中的索引(仅作为备用)
* @param {Number} registerId - 挂号 ID
*/
fetchRegisterCardInfo(msg, index, registerId) {
let that = this;
// 保存消息 ID可能是数字或字符串
const messageId = msg.id;
// 调试日志:确认 msg.id 的实际值
console.log('=== fetchRegisterCardInfo ===');
console.log('msg:', msg);
console.log('msg.id:', msg.id, typeof msg.id);
console.log('registerId:', registerId);
getChatRegisterInfoApi({
id: registerId,
message_id: messageId || '' // 确保传递,即使为空
}).then((res) => {
if (res.data && res.data.result) {
// 使用消息 ID 重新查找索引,而不是使用传入的 index
// 原因异步请求期间消息列表可能发生变化index 可能已失效
let targetIndex = index;
if (messageId) {
const foundIndex = that.messages.findIndex(m =>
m.id === messageId || String(m.id) === String(messageId)
);
if (foundIndex !== -1) {
targetIndex = foundIndex;
}
}
// 添加 _loaded 标识,避免重复请求
const resultWithFlag = {
...res.data.result,
_loaded: true
};
// 使用 $set 确保响应式更新
that.$set(that.messages, targetIndex, {
...that.messages[targetIndex],
message_content: JSON.stringify(resultWithFlag)
});
}
}).catch((err) => {
console.error('获取挂号信息失败:', err);
});
},
/**
* 异步获取挂号详情并更新消息内容(兼容旧调用方式)
* 后端会更新数据库中的 message_content下次直接使用缓存
*/
getChatRegisterInfo(item, index) {
let that = this;
const parsedContent = getParsedContent(item.message_type, item.message_content);
getChatRegisterInfoApi({
id: parsedContent.id
}).then((res) => {
that.messages[index].message_content = JSON.stringify(res.data.result);
console.log('getChatRegisterInfo', that.messages[index]);
})
// 如果已经加载过完整信息,直接返回不再请求
if (parsedContent._loaded) {
return item;
}
// 检查是否有 id
if (!parsedContent.id) {
return item;
}
this.fetchRegisterCardInfo(item, index, parsedContent.id);
return item;
},
@@ -923,6 +1040,9 @@ export default {
this.messages = ChatManager.roomMessages[this.doctorUserInfo.room_id] || [];
}
// 加载完消息后,检查是否有需要动态获取的挂号卡片
this.loadRegisterCards();
// 修复如果是触顶加载isLoadHistory=true强制 type 为 0保持滚动位置
// 只有在初始化加载时才滚动到底部type=1 且 isLoadHistory=false
const isHistoryLoad = this.isLoadHistory;
@@ -956,10 +1076,13 @@ export default {
/**
* WebSocket 消息处理
* 直接处理当前房间消息,不走 ChatManager 避免重复
*/
handleSocketMessage(data) {
if (typeof ChatManager !== 'undefined') {
ChatManager.handleIncomingMessage(data);
// 只处理当前房间的消息
if (data.room_id === this.doctorUserInfo.room_id) {
this.addMessage(data);
this.scrollToBottom();
}
},
@@ -967,7 +1090,6 @@ export default {
* 监听新消息事件
*/
handleNewRoomMessage(message) {
console.log('handle', message);
this.addMessage(message);
this.scrollToBottom();
},
@@ -995,7 +1117,6 @@ export default {
if (existingIndexById !== -1) {
// 如果 ID 相同,更新现有消息(服务器返回的消息替换临时消息)
this.messages[existingIndexById] = message;
console.log('update message by id', message.id);
return;
}
@@ -1041,7 +1162,6 @@ export default {
if (tempMessageIndex !== -1) {
// 找到临时消息,用服务器返回的消息替换
this.messages[tempMessageIndex] = message;
console.log('replace temporary message with server message', message.id);
return;
}
}
@@ -1066,13 +1186,30 @@ export default {
if (existingIndexByContent !== -1) {
// 如果找到相似消息,用服务器返回的消息替换临时消息(保留服务器 ID
this.messages[existingIndexByContent] = message;
console.log('update message by content', message.id);
return;
}
// 新消息,添加到列表
console.log('push new message', message.id);
this.messages.push(message);
// 如果是挂号卡片消息,检查是否需要加载完整信息
if (message.message_type === 10) {
const parsedContent = getParsedContent(message.message_type, message.message_content);
if (!parsedContent._loaded && parsedContent.id) {
const newIndex = this.messages.length - 1;
this.fetchRegisterCardInfo(message, newIndex, parsedContent.id);
}
}
// 检测结束问诊消息,立即设置问诊结束状态
if (message.message_type === 13) {
this.isConsultationEnded = true;
}
// 收到任何消息后,都从 API 确认房间状态
if (this.doctorUserInfo.room_id) {
this.fetchRoomStatus(this.doctorUserInfo.room_id);
}
},
toggleDoctorDetail() {
@@ -1134,7 +1271,7 @@ export default {
});
},
fail: (res) => {
console.log(res, '上传失败');
console.error('上传失败', res);
}
});
} catch (error) {
@@ -1195,7 +1332,7 @@ export default {
// 构造请求数据 (发送给服务器)
const requestData = {
room_id: this.doctorUserInfo.room_id,
sender_user_id: this.currentUserId,
sender_user_id: `user-${this.currentUserId}`,
receiver_user_id: receiverUserId,
message_type: messageType,
message_content: message.message_content || message.content || '',
@@ -1289,7 +1426,6 @@ export default {
},
playAudio(msg) {
console.log('播放音频:', getParsedContent(msg.message_type, msg.message_content));
// TODO: 实际音频播放逻辑,可使用 uni.createInnerAudioContext
// 设置 this.currentPlayingAudio = msg.id 实现动画效果
},
@@ -1317,7 +1453,7 @@ export default {
this.uploadAndSendFile(file, 'image');
});
},
fail: (e) => { console.log('错误', e); },
fail: (e) => { console.error('选择图片错误', e); },
});
} else if (type === 'video') {
// 选择视频
@@ -1328,7 +1464,7 @@ export default {
success: res => {
this.uploadAndSendFile(res.tempFilePath, 'video');
},
fail: (e) => { console.log('选择视频错误', e); },
fail: (e) => { console.error('选择视频错误', e); },
});
}
},
@@ -1363,7 +1499,7 @@ export default {
},
fail: (res) => {
uni.hideLoading();
console.log(res, '上传失败');
console.error('上传失败', res);
uni.showToast({ title: '上传失败', icon: 'none' });
}
});
@@ -1780,7 +1916,7 @@ $radius-bubble: 20rpx; // 气泡圆角
// 图片、视频、卡片类型去除默认气泡样式 (背景、阴影、内边距)
&.type-1, &.type-3,
&.type-4, &.type-10, &.type-11, &.type-12, &.type-13 {
&.type-4, &.type-9, &.type-10, &.type-11, &.type-12, &.type-13 {
padding: 0;
overflow: visible; // 允许卡片阴影溢出
background: transparent;
@@ -1932,6 +2068,17 @@ $radius-bubble: 20rpx; // 气泡圆角
right: 0;
z-index: 99;
// 问诊已结束提示
.consultation-ended-notice {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx;
color: #999;
font-size: 28rpx;
gap: 12rpx;
}
.toolbar-area {
margin-bottom: 20rpx;
@@ -2082,4 +2229,43 @@ $radius-bubble: 20rpx; // 气泡圆角
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
// ================= 系统消息卡片样式 =================
.system-message-card {
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 12rpx;
padding: 20rpx;
margin: 20rpx auto;
max-width: 80%;
.price-update-notice {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 10rpx;
.notice-text {
color: #d48806;
font-size: 28rpx;
font-weight: 500;
}
}
.price-detail {
display: flex;
flex-direction: column;
gap: 6rpx;
font-size: 24rpx;
color: #666;
padding-left: 28rpx;
}
}
.system-message {
text-align: center;
color: #999;
font-size: 24rpx;
padding: 10rpx 0;
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="score">
<u-rate :count="count" v-model="value" active-color="#FB8C00" inactive-color="#E2E8F0" size="40">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 名片 -->
<view class="card">
<!-- 姓名 -->
@@ -118,6 +119,7 @@
userList: [],
registList: [],
peopleList: [],
rType: '0', // 挂号类型
}
},
onLoad(e) {
@@ -153,6 +155,7 @@
} else {
// 点击详情
this.doctorID = e.id
this.rType = e.r_type || '0' // 接收挂号类型参数
const id = e.sts_id || uni.getStorageSync('store_id')
if (id!='undefined' && id) {
console.log('idddd');
@@ -187,7 +190,7 @@
}
uni.navigateTo({
url: "/subPackages/doctor/doctor-userinfo?id=" + this.doctorID,
url: "/subPackages/doctor/doctor-userinfo?id=" + this.doctorID + "&r_type=" + this.rType,
})
},

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 按钮 -->
<view class="head">
<view class="head_left">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 个人介绍 -->
<view class="card">
<view class="card_left">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="title">
请描述您的病情
</view>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 标题 -->
<view class="title">
您需要为谁{{ registerType === '3'? '购药' : '挂号' }}
@@ -25,23 +26,15 @@
</view>
</view>
<!-- 新增挂号类型选择 -->
<view class="card" style="padding-bottom: 0" v-if="registerType !== '3'">
<!-- 挂号类型展示只读 -->
<view class="card" style="padding-bottom: 0">
<view class="card_title">
挂号类型
</view>
<view class="card_names">
<u-radio-group
v-model="registerType"
size="44rpx"
active-color="#2979FF"
@change="handleTypeChange"
>
<u-radio v-if="registerType === '0'" name="0" label="线下问诊">线下问诊</u-radio>
<u-radio v-if="checkDev('open-im')" name="1" label="线上咨询">线上咨询</u-radio>
<u-radio v-if="checkDev('open-im')" name="2" label="线上咨询">在线复诊</u-radio>
<u-radio v-if="checkDev('open-im')" name="3" label="线上咨询">预约购药</u-radio>
</u-radio-group>
<view class="register-type-display">
<text class="type-label">{{ registerTypeLabel }}</text>
</view>
</view>
</view>
@@ -229,6 +222,16 @@ export default {
}
})
return list
},
// 挂号类型标签
registerTypeLabel() {
const typeMap = {
'0': '线下就诊',
'1': '在线问诊',
'2': '预约购药',
'3': '在线复诊'
};
return typeMap[this.registerType] || '未知类型';
}
},
methods: {
@@ -637,6 +640,21 @@ export default {
::v-deep .u-input {
width: 380rpx;
}
.register-type-display {
display: flex;
align-items: center;
.type-label {
display: inline-block;
padding: 8rpx 24rpx;
background: linear-gradient(135deg, #4175EE 0%, #298dff 100%);
color: #FFFFFF;
font-size: 28rpx;
font-weight: 500;
border-radius: 8rpx;
}
}
}
.card_history {

View File

@@ -1,5 +1,6 @@
<template>
<view class="content safe-area-inset-bottom">
<MessageNotification />
<!-- 描述 -->
<view class="card">
{{list.content['content']}}

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 物流提示信息 -->
<view class="order-text-tip" v-if="orderText">
<text>{{ orderText }}</text>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 搜索框 -->
<!-- <view class="search">
<u-search placeholder="请输入药品名称" bg-color="#F1F5F9" border-color="#F1F5F9" :show-action="false"

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 中转页 -->
<view class="none">
<!-- 订单未生成 -->

View File

@@ -1,5 +1,6 @@
<template>
<view class="container">
<MessageNotification />
<!-- 头部 -->
<view class="head">
<image src="@/static/empty/login.png" mode=""></image>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="head" @click="toChoose">
<view class="head_name">{{infoUserList[0].name || '请选择就诊人'}}</view>
<view class="head_qh">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 头部 -->
<view class="head">
<view class="title">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 基础信息 -->
<view class="card">
<view class="card_title">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="card-box">
<!-- 基础信息 -->
<view class="card">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="none" v-if="showList==false">
<!-- 暂未添加就诊人 -->
<view class="conter">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="all" v-if="status==1||status==4">
<!-- 处方 -->
<view class="head">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="head" @click="toChoose">
<view class="head_name">{{infoUserList[0].name || '请选择就诊人'}}</view>
<view class="head_qh">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 头部 -->
<view class="head" @click="show = true">
<view class="head_sign">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="card">
<image src="/static/empty/yhq.png" mode=""></image>
<view class="store">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 快递自提 -->
<view class="_choose">
<text>配送方式</text>

View File

@@ -1,5 +1,6 @@
<template>
<view class="content safe-area-inset-bottom">
<MessageNotification />
<!-- 待支付 -->
<view class="unpaid" v-if="orderInfo.order_status.status==1">
<view class="title">

View File

@@ -1,123 +1,119 @@
<template>
<view class="container">
<!-- 顶部吸顶区域自定义紧凑布局 -->
<view class="header-sticky-box">
<view class="header-row">
<!-- 1. 左侧自定义下拉筛选触发器 -->
<view class="category-trigger" @click.stop="toggleCategoryMenu">
<text class="trigger-text">{{ currentCategoryLabel }}</text>
<u-icon
:name="showCategoryMenu ? 'arrow-up' : 'arrow-down'"
size="24"
color="#333"
style="margin-left: 6rpx;"
></u-icon>
</view>
<!-- 2. 右侧搜索栏 (带搜索按钮) -->
<view class="search-box">
<u-search
v-model="searchKeyword"
placeholder="搜索药品/症状"
:show-action="true"
action-text="搜索"
bg-color="#F5F7FA"
placeholder-color="#9CA3AF"
shape="round"
height="64"
:input-style="{fontSize: '26rpx'}"
@custom="handleSearch"
@search="handleSearch"
@clear="clearSearch"
></u-search>
</view>
<MessageNotification />
<!-- 1. 顶部搜索栏 -->
<view class="header-section">
<view class="search-box">
<u-search
v-model="searchKeyword"
placeholder="搜索药品/症状"
:show-action="true"
action-text="搜索"
bg-color="#F5F7FA"
placeholder-color="#9CA3AF"
shape="round"
height="64"
:input-style="{fontSize: '26rpx'}"
@custom="handleSearch"
@search="handleSearch"
@clear="clearSearch"
></u-search>
</view>
<!-- 自定义下拉菜单面板 (绝对定位) -->
<view
class="category-popup"
v-if="showCategoryMenu"
@click.stop=""
>
<view
class="cat-item"
v-for="(opt, index) in categoryOptions"
:key="opt.value"
:class="{ active: selectedCategory === opt.value }"
@click="selectCategory(opt)"
>
<text>{{ opt.label }}</text>
<u-icon v-if="selectedCategory === opt.value" name="checkmark" color="#2B85E4" size="28"></u-icon>
</view>
</view>
<!-- 下拉菜单遮罩层 -->
<view class="mask-layer" v-if="showCategoryMenu" @click="closeCategoryMenu"></view>
</view>
<!-- 商品列表区域 -->
<view class="list-section">
<!-- 列表项卡片 -->
<view
v-for="(item, index) in products"
:key="item.id"
class="list-item"
@click="handClickProduct(item)"
>
<!-- 左侧药品图片 -->
<view class="image-wrapper">
<image
class="item-image"
:class="{ 'prescription-blur': item.drug.is_otc === 0 && item.drug.type == 2 }"
:src="item.drug.image || '/static/mine/avatar_1.png'"
mode="aspectFill"
lazy-load
/>
</view>
<!-- 右侧详细内容 -->
<view class="item-content">
<view class="top-info">
<!-- 标题 -->
<view class="item-title">{{ item.drug.drug_name }}</view>
<!-- 标签行 -->
<view class="tags-row">
<!-- 动态标签保健食品/OTC/Rx -->
<text
class="tag-label"
:class="getProductTag(item.drug).className"
>
{{ getProductTag(item.drug).label }}
</text>
<!-- 规格 -->
<text class="tag-spec">{{ item.drug.specification || '标准规格' }}</text>
</view>
<!-- 主治功能 -->
<view class="item-function" v-if="item.drug.function">
主治{{ item.drug.function }}
</view>
</view>
<!-- 底部价格与操作 -->
<view class="item-bottom">
<view class="price-section">
<text class="currency">¥</text>
<text class="price-val">{{ item.price }}</text>
</view>
<view class="cart-btn" @click.stop="showCartPopup(item)">
<u-icon name="plus" color="#ffffff" size="30"></u-icon>
</view>
<!-- 2. 顶部专区Tab -->
<view class="zone-tabs-wrapper">
<scroll-view scroll-x class="zone-tabs-scroll">
<view class="zone-tabs">
<view
v-for="(zone, index) in homeZones"
:key="zone.id || index"
class="zone-tab-item"
:class="{ active: selectedZoneIndex === index }"
@click="onZoneChange(index)"
>
<text>{{ zone.title }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 3. 主体内容区左侧分类 + 右侧商品 -->
<view class="main-content">
<!-- 左侧分类栏 -->
<view class="category-sidebar">
<scroll-view scroll-y class="category-scroll">
<view
class="category-item"
:class="{ active: selectedCategoryId === 0 }"
@click="selectCategory(0)"
>
<text>全部</text>
</view>
<view
v-for="cat in categoryList"
:key="cat.id"
class="category-item"
:class="{ active: selectedCategoryId === cat.id }"
@click="selectCategory(cat.id)"
>
<text class="u-line-1">{{ cat.category_name }}</text>
</view>
</scroll-view>
</view>
<!-- 空状态 -->
<view v-if="products.length === 0" class="empty-state">
<u-empty text="暂无相关药品" mode="search" margin-top="100"></u-empty>
<!-- 右侧商品列表区 -->
<view class="product-area">
<scroll-view scroll-y class="product-scroll" @scrolltolower="loadMore">
<!-- 商品列表 - 两列布局 -->
<view class="product-grid" v-if="products.length > 0">
<view
v-for="(item, index) in products"
:key="item.id"
class="product-card"
@click="handClickProduct(item)"
>
<!-- 商品图片 -->
<view class="product-img-box">
<image
class="product-img"
:class="{ 'prescription-blur': item.drug && item.drug.is_otc === 0 && item.drug.type == 2 }"
:src="(item.drug && item.drug.image) ? item.drug.image : '/static/mine/avatar_1.png'"
mode="aspectFill"
lazy-load
/>
</view>
<!-- 商品信息 -->
<view class="product-info">
<view class="product-title u-line-2">{{ item.drug ? item.drug.drug_name : '' }}</view>
<view class="product-tags">
<text
class="tag-label"
:class="getProductTag(item.drug).className"
>
{{ getProductTag(item.drug).label }}
</text>
<text class="tag-spec u-line-1">{{ (item.drug && item.drug.specification) ? item.drug.specification : '标准规格' }}</text>
</view>
<view class="product-bottom">
<view class="price-box">
<text class="price-symbol">¥</text>
<text class="price-val">{{ item.price }}</text>
</view>
<view class="cart-btn" @click.stop="showCartPopup(item)">
<u-icon name="plus" color="#ffffff" size="26"></u-icon>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="products.length === 0" class="empty-state">
<u-empty text="暂无相关药品" mode="search" margin-top="100"></u-empty>
</view>
</scroll-view>
</view>
</view>
@@ -176,7 +172,7 @@
</template>
<script>
import { getStoreDrugListBySalespersonApi, getHomeZonesApi } from "@/request/api/product";
import { getStoreDrugListBySalespersonApi, getHomeZonesApi, getDrugCategoriesApi } from "@/request/api/product";
import { getCartCountsApi, saveCartApi } from "@/request/api/cart";
export default {
@@ -185,14 +181,18 @@ export default {
products: [],
searchKeyword: "",
// 自定义下拉菜单相关
selectedCategory: 'otc',
showCategoryMenu: false, // 控制下拉菜单显示
categoryOptions: [], // 从后端获取分类选项
// 专区相关
homeZones: [],
selectedZoneIndex: 0,
selectedZoneType: 'otc',
// 分类相关
categoryList: [],
selectedCategoryId: 0,
doctorId: 0,
r_type: 0,
delegateStoreId: '', // 委托诊所ID
delegateStoreId: '',
cartCount: 0,
cartPosition: { x: 0, y: 0 },
cartMove: { startX: 0, startY: 0, moving: false },
@@ -202,93 +202,113 @@ export default {
};
},
computed: {
// 获取当前选中的分类标签
currentCategoryLabel() {
const target = this.categoryOptions.find(item => item.value === this.selectedCategory);
return target ? target.label : '分类';
},
getProductImage() {
return this.currentProduct.drug && this.currentProduct.drug.image ? this.currentProduct.drug.image : '/static/mine/avatar_1.png';
return (this.currentProduct.drug && this.currentProduct.drug.image) ? this.currentProduct.drug.image : '/static/mine/avatar_1.png';
},
getProductName() {
return this.currentProduct.drug && this.currentProduct.drug.drug_name ? this.currentProduct.drug.drug_name : '';
return (this.currentProduct.drug && this.currentProduct.drug.drug_name) ? this.currentProduct.drug.drug_name : '';
},
getProductSpec() {
return this.currentProduct.drug && this.currentProduct.drug.specification ? this.currentProduct.drug.specification : '标准规格';
return (this.currentProduct.drug && this.currentProduct.drug.specification) ? this.currentProduct.drug.specification : '标准规格';
}
},
onLoad(e) {
this.doctorId = e.doctor_id;
this.r_type = e.register_type;
// 获取委托诊所ID如果有
this.delegateStoreId = e.delegate_store_id || '';
// 根据首页传入的专区类型初始化分类默认OTC
// 根据首页传入的专区类型初始化
if (e.zone_type) {
this.selectedCategory = e.zone_type;
this.selectedZoneType = e.zone_type;
} else {
this.selectedCategory = 'otc';
this.selectedZoneType = 'otc';
}
// 获取分类选项
this.getCategoryOptions();
this.getList();
// 获取专区列表
this.getHomeZones();
},
methods: {
// 获取分类选项(从后端获取专区配置)
getCategoryOptions() {
// 获取专区列表
getHomeZones() {
getHomeZonesApi({
store_id: uni.getStorageSync('store_id') || '11001',
}).then((res) => {
if (res.data.code == 0) {
const zones = res.data.result || res.data.data || [];
// 将专区数据转换为分类选项格式
this.categoryOptions = zones.map(zone => ({
label: zone.title || this.getCategoryLabelByType(zone.type),
value: zone.type
}));
// 如果当前选中的分类不在选项中,重置为第一个选项
if (this.categoryOptions.length > 0) {
const hasCurrentCategory = this.categoryOptions.some(opt => opt.value === this.selectedCategory);
if (!hasCurrentCategory) {
this.selectedCategory = this.categoryOptions[0].value;
}
this.homeZones = res.data.result || res.data.data || [];
// 根据传入的zone_type找到对应的索引
if (this.homeZones.length > 0) {
const index = this.homeZones.findIndex(z => z.type === this.selectedZoneType);
this.selectedZoneIndex = index >= 0 ? index : 0;
this.selectedZoneType = this.homeZones[this.selectedZoneIndex]?.type || 'otc';
}
// 获取分类列表
this.getCategoryList();
}
}).catch(() => {
// 如果接口失败,使用默认分类选项
this.categoryOptions = [
{ label: 'OTC', value: 'otc' },
{ label: '处方药', value: 'prescription' },
{ label: '保健食品', value: 'health_food' }
// 使用默认专区
this.homeZones = [
{ id: 1, title: 'OTC', type: 'otc' },
{ id: 2, title: '保健食品', type: 'health_food' }
];
this.getCategoryList();
});
},
// 根据type获取分类标签备用
getCategoryLabelByType(type) {
const typeMap = {
'otc': 'OTC',
'prescription': '处方药',
'health_food': '保健食品',
'service_package': '产品服务包'
};
return typeMap[type] || type;
// 获取分类列表
getCategoryList() {
getDrugCategoriesApi({
zone_type: this.selectedZoneType
}).then((res) => {
if (res.data.code == 0) {
// 将树形结构扁平化为一级分类列表
const categories = res.data.result || [];
this.categoryList = this.flattenCategories(categories);
}
}).catch(() => {
this.categoryList = [];
}).finally(() => {
// 重置分类选择并获取商品列表
this.selectedCategoryId = 0;
this.getList();
});
},
// 切换下拉菜单显示
toggleCategoryMenu() {
this.showCategoryMenu = !this.showCategoryMenu;
// 扁平化分类树(只取一级分类,或展开所有层级)
flattenCategories(categories, result = []) {
for (const cat of categories) {
result.push({
id: cat.id,
category_name: cat.category_name,
level: cat.level,
parent_id: cat.parent_id
});
if (cat.children && cat.children.length > 0) {
this.flattenCategories(cat.children, result);
}
}
return result;
},
// 关闭下拉菜单
closeCategoryMenu() {
this.showCategoryMenu = false;
// 专区切换
onZoneChange(index) {
this.selectedZoneIndex = index;
this.selectedZoneType = this.homeZones[index]?.type || 'otc';
this.selectedCategoryId = 0;
// 重新获取分类和商品
this.getCategoryList();
},
// 选择分类
selectCategory(item) {
this.selectedCategory = item.value;
this.closeCategoryMenu();
this.getList(); // 触发列表更新
// 分类选择
selectCategory(categoryId) {
this.selectedCategoryId = categoryId;
this.getList();
},
// 核心标签判断逻辑
getProductTag(drug) {
if (!drug) return { label: 'OTC', className: 'tag-green' };
if (drug.type === 3) {
return { label: '保健食品', className: 'tag-orange' };
}
@@ -303,34 +323,36 @@ export default {
const params = {
store_id: uni.getStorageSync('store_id') || '11001',
page: 1,
limit: 100
limit: 100,
category: this.selectedZoneType
};
if (this.selectedCategory !== 'all') {
params.category = this.selectedCategory;
if (this.selectedCategoryId > 0) {
params.category_id = this.selectedCategoryId;
}
if (this.searchKeyword) {
params.keyword = this.searchKeyword;
}
getStoreDrugListBySalespersonApi(params).then((res) => {
if (res.data.code == 0) {
this.products = res.data.result.records || [];
this.cartCount = res.data.result.cart_count || 0;
}
})
});
},
// 执行搜索(点击按钮或回车)
handleSearch() {
this.getList();
},
// 清空搜索
clearSearch() {
this.searchKeyword = "";
this.getList();
},
handClickProduct(item) {
// 传递委托诊所ID如果有
let url = `/subPackages/product/symptoms?id=${item.drug.id}&drug_name=${item.drug.drug_name}&is_otc=${item.drug.is_otc}&image=${item.drug.image}&price=${item.price}`;
if (this.delegateStoreId) {
url += `&delegate_store_id=${this.delegateStoreId}`;
@@ -397,27 +419,29 @@ export default {
this.updateCartCount();
this.closePopup();
this.$refs.uToast.show({ title: '已加入购物车', type: 'success' });
})
});
},
updateCartCount() {
let that = this;
getCartCountsApi({
store_id: uni.getStorageSync('store_id') || '11001',
page: 1,
limit: 100
}).then((res) => {
if (res.data.code == 0) {
that.cartCount = res.data.result.cart_count;
this.cartCount = res.data.result.cart_count;
}
})
});
},
loadMore() {
// 可扩展加载更多功能
}
},
onShow() {
// 如果分类选项为空,重新获取
if (this.categoryOptions.length === 0) {
this.getCategoryOptions();
if (this.homeZones.length === 0) {
this.getHomeZones();
} else {
this.getList();
}
this.getList();
this.initCartPosition();
this.updateCartCount();
},
@@ -434,210 +458,229 @@ export default {
<style lang="scss" scoped>
.container {
background: linear-gradient(180deg, #f0f5ff 0%, #ffffff 100%);
min-height: 100vh;
box-sizing: border-box;
padding-bottom: 40rpx;
display: flex;
flex-direction: column;
height: 100vh;
background: #F5F7FA;
overflow: hidden;
}
/* 顶部吸顶盒子 */
.header-sticky-box {
position: sticky;
top: 0;
z-index: 100;
background: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
padding: 20rpx 24rpx;
.header-row {
display: flex;
align-items: center;
}
/* 自定义分类触发器 */
.category-trigger {
display: flex;
align-items: center;
margin-right: 20rpx;
padding: 10rpx 0;
.trigger-text {
font-size: 30rpx;
font-weight: 600;
color: #333;
max-width: 140rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
/* 搜索框容器 */
/* 1. 顶部搜索栏 */
.header-section {
background: #fff;
padding: 16rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03);
.search-box {
flex: 1;
width: 100%;
}
}
/* 自定义下拉弹窗 */
.category-popup {
position: absolute;
top: 100%; /* 吸附在 header 下方 */
left: 24rpx; /* 对齐左边距 */
width: 240rpx;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.1);
z-index: 101;
padding: 10rpx 0;
.cat-item {
display: flex;
justify-content: space-between;
/* 2. 顶部专区Tab */
.zone-tabs-wrapper {
background: #fff;
border-bottom: 2rpx solid #f0f0f0;
.zone-tabs-scroll {
white-space: nowrap;
}
.zone-tabs {
display: inline-flex;
padding: 0 24rpx;
.zone-tab-item {
display: inline-flex;
align-items: center;
padding: 20rpx 30rpx;
justify-content: center;
padding: 24rpx 32rpx;
font-size: 28rpx;
color: #333;
&:active {
background: #f8f8f8;
}
color: #666;
position: relative;
transition: all 0.2s;
&.active {
color: #2B85E4;
font-weight: bold;
background: #F0F7FF;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
background: linear-gradient(90deg, #5CADFF, #2B85E4);
border-radius: 3rpx;
}
}
}
}
/* 遮罩层 */
.mask-layer {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0); /* 透明遮罩,只用于点击关闭 */
z-index: 99;
}
}
.list-section {
padding: 24rpx;
}
/* 商品卡片 */
.list-item {
display: flex;
background: #ffffff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(65, 117, 238, 0.08);
transition: transform 0.1s;
&:active {
transform: scale(0.99);
}
}
.image-wrapper {
position: relative;
margin-right: 24rpx;
.item-image {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
background-color: #f8f8f8;
// 处方药图片模糊处理
&.prescription-blur {
filter: blur(10px);
-webkit-filter: blur(10px);
}
}
}
.item-content {
/* 3. 主体内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.top-info {
.item-title {
font-size: 30rpx;
font-weight: 700;
color: #1F2937;
margin-bottom: 12rpx;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
/* 左侧分类栏 */
.category-sidebar {
width: 180rpx;
background: #fff;
flex-shrink: 0;
.category-scroll {
height: 100%;
}
.category-item {
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #666;
position: relative;
padding: 0 12rpx;
text {
text-align: center;
max-width: 100%;
}
&.active {
background: #F5F7FA;
color: #2B85E4;
font-weight: bold;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 48rpx;
background: #2B85E4;
border-radius: 0 3rpx 3rpx 0;
}
}
}
}
.tags-row {
/* 右侧商品区域 */
.product-area {
flex: 1;
overflow: hidden;
.product-scroll {
height: 100%;
padding: 16rpx;
box-sizing: border-box;
}
}
/* 商品网格 - 两列布局 */
.product-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.product-card {
width: calc((100% - 16rpx) / 2);
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04);
&:active {
transform: scale(0.98);
}
.product-img-box {
width: 100%;
height: 240rpx;
background: #f8f8f8;
.product-img {
width: 100%;
height: 100%;
&.prescription-blur {
/* 暂时禁用模糊
filter: blur(8px);
-webkit-filter: blur(8px);
*/
}
}
}
.product-info {
padding: 16rpx;
.product-title {
font-size: 26rpx;
font-weight: 600;
color: #333;
line-height: 1.4;
height: 72rpx;
margin-bottom: 12rpx;
}
.product-tags {
display: flex;
align-items: center;
margin-bottom: 12rpx;
flex-wrap: wrap;
/* 通用标签基础样式 */
gap: 8rpx;
.tag-label {
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 6rpx;
margin-right: 12rpx;
border: 1px solid;
font-size: 18rpx;
padding: 2rpx 8rpx;
border-radius: 4rpx;
font-weight: 500;
/* 颜色变种 */
border: 1px solid;
flex-shrink: 0;
&.tag-green { color: #10B981; border-color: #10B981; background: #ECFDF5; }
&.tag-red { color: #EF4444; border-color: #EF4444; background: #FEF2F2; }
&.tag-orange { color: #F59E0B; border-color: #F59E0B; background: #FFFBEB; }
}
.tag-spec {
font-size: 22rpx;
color: #9CA3AF;
background: #F3F4F6;
padding: 2rpx 8rpx;
border-radius: 6rpx;
font-size: 20rpx;
color: #999;
flex: 1;
min-width: 0;
}
}
.item-function {
font-size: 22rpx;
color: #9CA3AF;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.item-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
.price-section {
color: #EF4444;
font-weight: bold;
.currency { font-size: 24rpx; margin-right: 2rpx; }
.price-val { font-size: 36rpx; font-family: 'DIN Alternate', sans-serif; }
}
.cart-btn {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: linear-gradient(135deg, #4A90E2, #5B8FF9);
.product-bottom {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 10rpx rgba(74, 144, 226, 0.3);
.price-box {
color: #EF4444;
font-weight: bold;
.price-symbol { font-size: 22rpx; }
.price-val { font-size: 32rpx; font-family: 'DIN Alternate', sans-serif; }
}
.cart-btn {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: linear-gradient(135deg, #4A90E2, #5B8FF9);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 8rpx rgba(74, 144, 226, 0.3);
}
}
}
}
@@ -646,6 +689,7 @@ export default {
.cart-float {
position: fixed;
z-index: 999;
.cart-icon-inner {
width: 100rpx;
height: 100rpx;
@@ -656,6 +700,7 @@ export default {
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(74, 144, 226, 0.4);
position: relative;
.badge {
position: absolute;
top: -6rpx;
@@ -679,6 +724,7 @@ export default {
.product-header-row {
display: flex;
margin-bottom: 40rpx;
.popup-img {
width: 180rpx;
height: 180rpx;
@@ -686,11 +732,13 @@ export default {
margin-right: 24rpx;
background: #f8f8f8;
}
.popup-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
.price-row {
color: #EF4444;
font-size: 28rpx;
@@ -719,6 +767,7 @@ export default {
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
&.add-cart {
background: linear-gradient(90deg, #4A90E2, #5B8FF9);
color: #fff;
@@ -728,5 +777,20 @@ export default {
}
}
.empty-state { margin-top: 100rpx; }
</style>
.empty-state {
margin-top: 100rpx;
}
.u-line-1 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.u-line-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container">
<MessageNotification />
<!-- 1. 顶部轮播图 -->
<view class="swiper-section">
<swiper

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom" v-if="registList.doctor">
<MessageNotification />
<u-navbar class="navbar" :is-back="true" title="挂号详情" :custom-back="customBack" title-color="#000">
</u-navbar>
<!-- 已预约 -->

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 预约成功 -->
<view class="paided">
<text class="iconfont icon-emichenggong"></text>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 预约信息 -->
<view class="title">
预约信息
@@ -158,8 +159,8 @@
registerMap: {
0: '挂号',
1: '咨询',
2: '复诊',
3: '购药',
2: '购药',
3: '复诊',
},
rInfo: {},
}
@@ -227,29 +228,43 @@
return;
}
// 4. 准备患者就诊经历数据
const patientExperienceData = {
symptom_description: that.rInfo.illness_info || '',
has_visited: that.rInfo.has_visited || '0',
has_used_drug: that.rInfo.has_used_drug || '0',
drug_name: that.rInfo.drug_name || '',
illness_info: that.rInfo.illness_info || '',
doctor_id: doctorId,
user_id: patientId,
register_id: that.register_id,
created_at: new Date().toLocaleString('zh-CN')
};
// 5. 发送患者就诊经历消息消息类型11
// 使用纯数字ID格式与房间创建时的格式保持一致
await sendToUserApi({
room_id: roomId,
sender_user_id: patientId, // 纯数字ID'252'
receiver_user_id: doctorId, // 纯数字ID'39'
message_type: 11, // 患者就诊经历卡片
message_content: JSON.stringify(patientExperienceData),
duration: 0
});
// 4. 仅药店模式(store_type='1')发送患者就诊经历卡片
const storeType = uni.getStorageSync('store_type')
if (storeType === '1' || storeType === 1) {
const patientExperienceData = {
symptom_description: that.rInfo.illness_info || '',
has_visited: that.rInfo.has_visited || '0',
has_used_drug: that.rInfo.has_used_drug || '0',
drug_name: that.rInfo.drug_name || '',
illness_info: that.rInfo.illness_info || '',
doctor_id: doctorId,
user_id: patientId,
register_id: that.register_id,
created_at: new Date().toLocaleString('zh-CN')
};
// 5. 发送患者就诊经历消息消息类型11
await sendToUserApi({
room_id: roomId,
sender_user_id: `user-${patientId}`,
receiver_user_id: `doctor-${doctorId}`,
message_type: 11, // 患者就诊经历卡片
message_content: JSON.stringify(patientExperienceData),
duration: 0
});
} else {
// 诊所模式(store_type='0')发送挂号信息卡片message_type=10
await sendToUserApi({
room_id: roomId,
sender_user_id: `user-${patientId}`,
receiver_user_id: `doctor-${doctorId}`,
message_type: 10, // 挂号信息卡片
message_content: JSON.stringify({
id: that.register_id,
}),
duration: 0
});
}
// 6. 生成订单(如果需要)
genOrderApi({
@@ -306,8 +321,8 @@
}).then(result => {
sendToUserApi({
room_id: result.data.result.room_id,
sender_user_id: that.currentUserId,
receiver_user_id: that.currentDoctorId,
sender_user_id: `user-${that.currentUserId}`,
receiver_user_id: `doctor-${that.currentDoctorId}`,
message_type: 10,
message_content: JSON.stringify({
id: that.register_id,
@@ -358,7 +373,7 @@
that.flag = false
uni.hideLoading();
setTimeout(() => {
this.$refs.uToast.show({
that.$refs.uToast.show({
title: res.data.msg || '支付失败',
type: 'default',
icon: false

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="none" v-if="actionList.length == 0">
<!-- 暂无收货地址 -->
<view class="conter">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="card" v-if="editShow==false">
<view class="title">
<u-field v-model="name" label="收货人" placeholder="请填写收货人名字" :field-style="{ background: '#F6F6F6',fontSize: '28rpx',paddingLeft:'30rpx',

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 隐私内容 -->
<view class="card">
<view class="box">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container">
<MessageNotification />
<view class="header">
<text class="title">开发者选项</text>
<text class="subtitle">仅供开发测试使用</text>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 头部 -->
<view class="head">
<image src="/static/empty/login.png" mode=""></image>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 隐私内容 -->
<view class="card">
<view class="box">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 隐私内容 -->
<view class="card">
<view class="box">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 隐私内容 -->
<view class="card">
<view class="box">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 退款原因 -->
<view class="row-box">
<text>退款原因</text>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 收货地址 -->
<view class="head" v-if="logisticsList.express_name!=''">
<image src="/static/mine/mr_tx.png" mode=""></image>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="imgs">
<image class="img" :src="drugDetailsList.image" mode=""></image>
<text class="cf">{{drugDetailsList.is_otc=='0'?'处方药':'非处方药'}}</text>

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 搜索 -->
<view class="search">
<u-search placeholder="输入药品名称" bg-color="#F6F6F6" color="#999" border-color="#F6F6F6" :show-action="false"

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 待付款 -->
<view class="p_state">
<view class="state">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 付款成功 -->
<view class="head">
<view class="paided">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 搜索 -->
<view class="search">
<u-search placeholder="输入商品名称" bg-color="#F5f5f5" color="#999999" border-color="#F5f5f5"

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 搜索 -->
<view class="search">
<u-search placeholder="输入商品名称" bg-color="#F5f5f5" color="#999999" border-color="#F5f5f5"

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 头部区域显示数量和清空按钮 -->
<view class="head">
<view class="num">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<!-- 收货地址 未添加地址 -->
<view class="area" @click="toArea" v-if="addList.length==0">
<view class="state">

View File

@@ -1,5 +1,6 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="detail">
<u-field v-model="txt" :focus="true" label-width="0" placeholder="请在此填写备注信息"
placeholder-style="color:#94A3B8" type="textarea" @input="sumfontnum" :auto-height="false">