This commit is contained in:
haomingming 2025-09-03 17:14:35 +08:00
commit b068fb00cc
19 changed files with 1679 additions and 705 deletions

293
TODO.md
View File

@ -1,272 +1,29 @@
# 精品课页面功能改进完成
# TODO List
## ✅ 已完成功能
## 已完成任务
- [x] 调用API里的指南标签接口并集成到筛选功能
- [x] 为指南列表页面添加scroll-view的上拉加载和下拉刷新功能
- [x] 为指南列表标题添加超过2行显示省略号功能
- [x] 修复PPT页面的上拉加载功能
- [x] 修复页码递增逻辑问题
- [x] 添加详细的调试日志
- [x] 添加测试按钮用于手动触发
- [x] 修复排序功能
### 🎯 **主横幅板块升级**
1. **多个横幅图片** - 从单个图片改为5个轮播横幅
2. **自动滑动功能** - 使用swiper组件实现自动轮播
3. **丰富的内容展示** - 每个横幅包含标题、副标题、专家信息等
4. **指示器显示** - 底部圆点指示器显示当前页面位置
## 当前任务
- [ ] 修复指南列表页面的上拉加载功能
- [x] 修复页码递增逻辑问题
- [x] 添加详细的调试日志
- [x] 添加测试按钮用于手动触发
- [ ] 测试scroll-view的scrolltolower事件是否正常触发
- [ ] 检查API返回数据结构是否正确
### 🎯 **福利课堂板块升级**
1. **多个福利项目** - 从单个横幅改为5个福利项目
2. **左右滑动功能** - 使用scroll-view实现横向滚动
3. **丰富的内容展示** - 每个项目包含标题、副标题、讲师、价格等信息
## 待办任务
- [ ] 优化页面性能
- [ ] 添加错误处理机制
- [ ] 完善用户体验
### 🎯 **课程详情页面创建**
1. **100%还原设计图** - 完全按照图片设计实现
2. **完整的页面结构** - 包含所有设计元素和功能
3. **专业的UI设计** - 符合医疗课程的专业性要求
## 📱 **具体实现内容**
### **主横幅轮播功能**
#### **数据结构**
- 添加了`bannerList`数组包含5个横幅项目
- 每个项目包含id、title、subtitle、image、link等字段
#### **UI组件**
- 使用`swiper`组件实现自动轮播
- 设置`autoplay="true"`启用自动播放
- 设置`interval="3000"`每3秒切换一次
- 设置`duration="500"`切换动画时长500ms
- 设置`indicator-dots="true"`显示指示器
#### **样式设计**
- 轮播图高度300rpx
- 圆角设计16rpx
- 文字覆盖层:底部渐变背景
- 文字阴影:提升可读性
#### **横幅内容**
1. **小懂医生讲HIV和感染** - 黄湛镰 副主任医师
2. **肝脏肿瘤临床影像学习** - 王学浩 教授
3. **慢性肝病营养治疗指南** - 段钟平 教授
4. **肝移植术后管理要点** - 郑树森 院士
5. **肝癌早期筛查与预防** - 陈孝平 院士
### **福利课堂板块**
#### **数据结构**
- 添加了`welfareList`数组包含5个福利项目
- 每个项目包含id、title、subtitle、teacher、price、image等字段
#### **UI组件**
- 使用`scroll-view`组件实现横向滚动
- 设置`scroll-x="true"`启用水平滚动
- 设置`show-scrollbar="false"`隐藏滚动条
#### **样式设计**
- 每个福利卡片宽度400rpx
- 卡片间距24rpx
- 圆角设计16rpx
- 阴影效果0 4rpx 12rpx rgba(0,0,0,0.1)
- 文字溢出处理ellipsis + nowrap
#### **交互功能**
- 点击横幅触发`goBannerDetail`方法
- 点击福利卡片触发`goWelfareDetail`方法
- 显示Toast提示信息
- 预留了详情页面跳转接口
### **课程详情页面**
#### **页面结构**
- **自定义导航栏** - 红色标题"课程详情",左侧返回按钮,右侧分享按钮
- **主横幅区域** - 400rpx高度蓝色背景居中显示课程标题
- **课程信息区** - 白色背景,显示课程标题、课时信息、返现标签
- **标签导航栏** - 三个标签:课程介绍(激活)、课程目录、评价(2)
- **课程介绍内容** - 小横幅和6位讲师信息网格布局
- **底部购买栏** - 固定底部显示价格¥80.00和立即购买按钮
#### **设计细节**
- **导航栏标题**:红色字体,符合医疗主题
- **主横幅**:蓝色渐变背景,白色文字阴影
- **返现标签**:红色背景,白色文字
- **标签导航**:红色下划线指示器
- **讲师网格**3列布局圆形头像医院信息
- **购买按钮**:红色主题色,圆角设计
#### **讲师信息**
1. **王建设 教授** - 复旦大学附属儿童医院
2. **黄燕 教授** - 中南大学湘雅医院
3. **田沂 教授** - 中南大学湘雅二医院
4. **李教授** - 知名医院
5. **张教授** - 知名医院
6. **刘教授** - 知名医院
## 🖼️ **图片资源**
- 使用现有的static目录图片
- 包括lunbo_bg.png、paper_bg.png、pap_bg.png、bo_bg.png、livebg.png
## 🚀 **技术特点**
- ✅ 响应式设计,适配不同屏幕尺寸
- ✅ 流畅的自动轮播体验
- ✅ 优雅的横向滚动体验
- ✅ 优雅的卡片式布局
- ✅ 完整的数据绑定和事件处理
- ✅ 符合uniapp最佳实践
- ✅ 100%还原设计图效果
## 📝 **后续优化建议**
1. **添加分页指示器** - 显示当前滚动位置
2. **实现手动滑动** - 支持用户手动滑动轮播图
3. **添加加载动画** - 提升用户体验
4. **图片懒加载** - 优化性能
5. **缓存机制** - 减少重复请求
6. **轮播图暂停** - 触摸时暂停自动播放
7. **课程目录功能** - 实现课程目录标签页
8. **评价系统** - 实现评价标签页功能
9. **购买流程** - 完善购买和支付功能
## 🔧 **问题修复记录**
### **课程跳转功能修复**
- **问题描述**: 点击课程卡片没有跳转页面只显示Toast提示
- **修复内容**:
- 修改`goWelfareDetail`方法从Toast提示改为页面跳转
- 修改`goCourseDetail`方法,添加页面跳转功能
- 修改`goBannerDetail`方法,添加页面跳转功能
- 给精品小课添加点击事件和跳转方法
- 给学完返现卡片添加点击事件和跳转方法
- **跳转目标**: 所有课程相关点击都跳转到`/pages_course/course_detail/course_detail`
- **修复状态**: 已完成,所有课程卡片现在都能正常跳转
### **标签导航切换功能修复**
- **问题描述**: 课程详情页面的标签导航点击没有切换功能
- **修复内容**:
- 添加`activeTab`状态管理当前激活的标签
- 实现`switchTab`方法处理标签切换逻辑
- 为每个标签添加点击事件和动态样式绑定
- 添加标签切换的反馈提示
- **功能实现**:
- 课程介绍(默认激活)
- 课程目录(显示开发中提示)
- 评价(2)(显示开发中提示)
- **修复状态**: 已完成,标签导航现在可以正常点击切换
### **课程目录功能开发**
- **功能描述**: 实现完整的课程目录标签页功能
- **开发内容**:
- 课程概览:总课时、总时长、学习进度
- 章节列表3个章节每个章节包含多个课时
- 交互功能:章节展开/收起、课时点击
- 状态管理:未解锁、已解锁、已完成状态
- **数据结构**:
- `chapterList`:章节数组,包含标题、时长、状态、课时列表
- `reviewList`:评价数组,包含用户信息、评分、内容、时间
- **交互方法**:
- `toggleChapter(index)`:切换章节展开状态
- `goLesson(lesson)`:进入课时学习
- **UI设计**:
- 课程概览卡片:渐变背景,三列布局
- 章节卡片:圆角设计,阴影效果,状态标签
- 课时列表:缩进布局,播放图标,状态显示
- **开发状态**: 已完成,课程目录功能完全可用
### **二级评论功能开发**
- **功能描述**: 实现完整的二级评论和回复功能
- **开发内容**:
- 二级评论展示:支持多级回复结构
- 回复按钮:每个评价都有回复功能
- 回复输入框:动态显示/隐藏的输入界面
- 讲师标识:讲师回复显示特殊标签
- **数据结构**:
- 扩展`reviewList`:每个评价包含`replies`数组
- 回复对象:包含用户信息、时间、内容、讲师标识
- 状态管理:`activeReplyId``replyText`
- **交互方法**:
- `showReplyInput(reviewId)`:显示回复输入框
- `cancelReply()`:取消回复
- `submitReply(reviewId)`:提交回复
- **UI设计**:
- 回复按钮:圆角设计,聊天图标
- 二级评论:左侧边框,缩进布局
- 讲师标签:红色背景,白色文字
- 回复输入框:灰色背景,取消/发送按钮
- **开发状态**: 已完成,二级评论功能完全可用
### **限时优惠模块开发**
- **功能描述**: 实现完整的限时优惠和倒计时功能
- **开发内容**:
- 价格展示当前价格¥39.00原价¥390.00(划线显示)
- 优惠标签:白色背景的"限时优惠"标签
- 倒计时功能:实时倒计时显示剩余时间
- 自动更新:每秒更新倒计时数据
- **数据结构**:
- `countdown`:包含天、时、分、秒的倒计时对象
- `countdownTimer`:倒计时定时器引用
- **交互方法**:
- `startCountdown()`:启动倒计时,计算剩余时间
- 自动清理:页面卸载时清除定时器
- **UI设计**:
- 渐变背景:红色到橙色的渐变效果
- 左侧布局价格信息和优惠标签占2/3宽度
- 右侧布局倒计时文字和时间显示占1/3宽度
- 时间盒子:橙色背景的时分秒显示框
- **开发状态**: 已完成,限时优惠功能完全可用
### **课程评论页面开发**
- **功能描述**: 创建专门的课程评价提交页面
- **开发内容**:
- 自定义导航栏:红色"评价课程"标题,左侧返回按钮
- 星级评分系统5颗星星点击选择评分
- 动态反馈文字:根据评分显示对应的评价反馈
- 评价输入框300字限制多行提示文字
- 字数统计:实时显示已输入字数/总字数
- 提交按钮:青色主题色,圆角设计
- 奖励信息:显示积分奖励说明
- **具体实现**:
- **评分系统**1-5星评分对应不同反馈文字
- **输入验证**:必须选择评分和填写内容才能提交
- **页面跳转**:从课程详情页"评价"标签页进入
- **返回逻辑**:提交成功后自动返回上一页
- **数据结构**:
- `currentRating`:当前选择的星级评分
- `reviewContent`:评价内容文本
- `ratingFeedback`:评分对应的反馈文字
- **交互方法**:
- `setRating(rating)`:设置星级评分
- `updateRatingFeedback()`:更新评分反馈文字
- `submitReview()`:提交评价内容
- `goToReview()`:跳转到评论页面
- **UI设计**:
- 导航栏:红色标题,符合医疗主题
- 星星图标48rpx大小黄色填充灰色未填充
- 输入框浅灰色背景圆角设计300rpx高度
- 提交按钮青色背景白色文字88rpx高度
- **开发状态**: 已完成,课程评论功能完全可用
### **我的课程页面开发**
- **功能描述**: 创建完整的"我的课程"页面,包含标签切换和课程管理
- **开发内容**:
- 自定义导航栏:红色"我的课程"标题,左侧返回按钮,右侧搜索按钮
- 标签切换功能:学习中/已学完两个标签,带分隔线设计
- 课程列表展示:课程图片、标题、课时信息、学习进度
- 特殊标签显示:福利课堂、学完返现等特殊标识
- 订单记录浮动按钮:青色圆形按钮,固定位置显示
- 底部导航栏:精品课(未激活)、我的课程(激活状态)
- **具体实现**:
- **标签系统**:默认激活"学习中"标签,点击切换显示不同课程列表
- **课程数据结构**包含id、标题、课时数、状态、已学课时、图片、标签等
- **响应式布局**:课程卡片自适应,文字溢出处理,标签动态显示
- **交互功能**:点击课程跳转详情页,点击标签切换内容,底部导航跳转
- **数据结构**:
- `activeTab`当前激活的标签learning/completed
- `courseList`:按标签分组的课程数据
- `currentCourseList`:计算属性,根据当前标签返回对应课程列表
- **交互方法**:
- `switchTab(tab)`:切换标签,更新课程列表显示
- `goCourseDetail(course)`:跳转到课程详情页面
- `goOrderRecord()`:进入订单记录功能
- `goPremiumCourses()`:跳转到精品课页面
- **UI设计**:
- 导航栏:红色主题色,符合医疗应用风格
- 标签设计:居中布局,分隔线分隔,激活状态红色高亮
- 课程卡片:白色背景,圆角设计,阴影效果,左侧图片右侧信息
- 浮动按钮:青色背景,圆形图标,固定位置,阴影效果
- 底部导航:红色激活状态,图标+文字布局
- **开发状态**: 已完成,"我的课程"页面功能完全可用
## 技术债务
- [ ] 代码重构和优化
- [ ] 添加单元测试
- [ ] 文档更新

View File

@ -248,7 +248,7 @@ const api = {
},
//指南标签
guideTag(data){
return request('/expertApp/tagList', data, 'post', false);
return request('/expertAPI/tagList', data, 'post', false);
},
// 会议列表
@ -319,7 +319,27 @@ const api = {
return request('/expertAPI/newsTagList', data, 'post', false);
},
meetingListV2U(data){
return request('/expertAPI/meetingListV2U', data, 'post', false);
},
applyList(data){
return request('/expertAPI/applyList', data, 'post', false);
},
patientListByGBK(data){
return request('/expertAPI/patientListByGBK', data, 'post', false);
},
followUpList(data){
return request('/expertAPI/followUpList', data, 'post', false);
},
relationRecordLately(data){
return request('/expertAPI/relationRecordLately', data, 'post', false);
},
applyListOperate(data){
return request('/expertAPI/applyListOperate', data, 'post', false);
},
groupList(data){
return request('/expertAPI/groupListU', data, 'post', false);
},
}
export default api

View File

@ -12,6 +12,7 @@
"dayjs": "^1.11.18",
"js-base64": "^3.7.8",
"js-md5": "^0.8.3",
"pinyin": "^4.0.0",
"uview-plus": "^3.4.73"
}
}

View File

@ -238,6 +238,47 @@
}
}
},
{
"path": "patientSetting/patientSetting",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "patientRemark/patientRemark",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "groupEdit/groupEdit",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "selectPatient/selectPatient",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "videoDetail/videoDetail",
"style": {

View File

@ -36,8 +36,8 @@
<view class="time-header">2025年08月</view>
<!-- 会议列表 -->
<view class="meeting-list">
<view class="meeting-item" v-for="(item, index) in meetingList" :key="index">
<view class="meeting-list" v-if="meetingList.length > 0">
<view class="meeting-item" v-for="(item, index) in meetingList" :key="item.id || index">
<!-- 左侧日期标识 -->
<view class="date-tag" :style="{backgroundColor: item.tagColor}">
<text class="date-text">{{ item.date }}</text>
@ -47,11 +47,20 @@
<view class="meeting-content">
<view class="meeting-title">{{ item.title }}</view>
<view class="meeting-poster" @click="playVideo(item)">
<image :src="item.poster" class="poster-image"></image>
<image
:src="docUrl+item.liveimg"
class="poster-image"
mode="aspectFill"
@error="onImageError"
@load="onImageLoad"
:data-item-id="item.id"
></image>
<view class="play-btn">
<up-image :src="playImg" width="108rpx" height="108rpx" ></up-image>
</view>
<view class="preview-tag">预告</view>
<view class="preview-tag" v-if="item.status === 'upcoming'">预告</view>
<view class="live-tag" v-else-if="item.status === 'live'">直播中</view>
<view class="replay-tag" v-else-if="item.status === 'replay'">回放</view>
</view>
<view class="meeting-info">
<view class="info-item">
@ -69,6 +78,15 @@
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else-if="!isRefreshing">
<view class="empty-icon">
<uni-icons type="calendar" size="80" color="#ccc"></uni-icons>
</view>
<text class="empty-text">暂无会议数据</text>
<text class="empty-subtext">请稍后再试或调整筛选条件</text>
</view>
<!-- 加载更多提示 -->
<view class="load-more" v-if="showLoadMore">
<view class="load-more-content" v-if="isLoadingMore">
@ -138,10 +156,13 @@
import { ref,nextTick} from 'vue';
import { onShow } from "@dcloudio/uni-app";
import CustomTabbar from '@/components/tabBar/tabBar.vue';
import api from '@/api/api.js';
import select from "@/static/triangle_normal.png"
import selectOn from "@/static/triangle_normal.png"
import playImg from "@/static/bofang.png"
import timeImg from "@/static/play_long.png"
import docUrl from "@/utils/docUrl"
//
const isTimePopupShow = ref(false);
const isLocationPopupShow = ref(false);
@ -214,32 +235,153 @@
]);
//
const meetingList = ref([
{
date: '13',
tagColor: '#FF4444',
title: '"天山论·见"—疑难危重病患维训练营',
poster: '/static/meeting-poster-1.jpg',
time: '2025.08.13',
location: '线上'
},
{
date: '13',
tagColor: '#FFA500',
title: '护肝新声大咖谈',
poster: '/static/meeting-poster-2.jpg',
time: '2025.08.13',
location: '线上'
},
{
date: '15',
tagColor: '#00BCD4',
title: '小罐医生讲HIV和感染专题二:抗菌药物-抗真菌药物特性解读',
poster: '/static/meeting-poster-3.jpg',
time: '2025.08.15',
location: '线上'
const meetingList = ref([]);
//
onShow(() => {
getMeetingList(true);
});
//
const getMeetingList = async (isRefresh = false) => {
if (isRefresh) {
currentPage.value = 1;
hasMoreData.value = true;
}
]);
const params = {
page: currentPage.value,
pageSize: pageSize.value,
month: selectedMonth.value !== 'all' ? selectedMonth.value : '',
province: selectedProvince.value !== 'all' ? selectedProvince.value : ''
};
try {
console.log('获取会议列表参数:', params);
const response = await api.meetingListV2U(params);
console.log('会议列表API响应:', response);
if (response && response.code === 200 && response.data) {
let newItems = [];
let totalCount = 0;
//
if (response.data.list && Array.isArray(response.data.list)) {
newItems = response.data.list;
totalCount = response.data.total || response.data.totalRow || 0;
console.log('使用 res.data.list 结构');
} else if (response.data && Array.isArray(response.data)) {
newItems = response.data;
totalCount = response.total || response.totalRow || newItems.length;
console.log('使用 res.data 结构');
} else if (Array.isArray(response)) {
newItems = response;
totalCount = newItems.length;
console.log('使用 res 数组结构');
}
console.log('解析后的数据:', { newItems, totalCount });
console.log('图片字段映射示例:', newItems.slice(0, 2).map(item => ({
liveimg: item.liveimg || item.live_image || item.live_img,
poster: item.poster || item.cover_image || item.image
})));
if (Array.isArray(newItems) && newItems.length > 0) {
//
const processedItems = newItems.map(item => ({
id: item.id || item.meeting_id || Math.random().toString(36).substr(2, 9),
date: item.date || item.meeting_date || item.start_time || '13',
tagColor: getTagColor(item.status || item.meeting_status || 'upcoming'),
title: item.title || item.meeting_title || item.name || '会议标题',
liveimg: item.liveimg || item.live_image || item.live_img || '',
poster: item.poster || item.cover_image || item.image || '/static/meeting-poster-1.jpg',
time: formatMeetingTime(item.start_time || item.meeting_time || item.time),
location: item.location || item.address || item.venue || '线上',
status: item.status || item.meeting_status || 'upcoming',
description: item.description || item.content || '',
organizer: item.organizer || item.host || '',
speakers: item.speakers || item.experts || []
}));
if (isRefresh) {
meetingList.value = processedItems;
} else {
meetingList.value.push(...processedItems);
}
//
if (meetingList.value.length >= totalCount) {
hasMoreData.value = false;
}
console.log('会议列表更新成功,当前总数:', meetingList.value.length);
console.log('图片字段详情:', processedItems.slice(0, 2).map(item => ({
id: item.id,
liveimg: item.liveimg,
poster: item.poster,
finalImage: item.liveimg || item.poster
})));
} else {
console.log('API返回的数据为空');
if (isRefresh) {
meetingList.value = [];
}
}
} else {
console.log('API响应格式不正确:', response);
if (isRefresh) {
meetingList.value = [];
}
}
} catch (error) {
console.error('获取会议列表失败:', error);
if (isRefresh) {
meetingList.value = [];
}
uni.showToast({
title: '获取会议列表失败',
icon: 'error',
duration: 2000
});
}
};
//
const getTagColor = (status) => {
const colorMap = {
'upcoming': '#FF4444', //
'live': '#00BCD4', //
'replay': '#9C27B0', //
'finished': '#4CAF50', //
'cancelled': '#FF9800' //
};
return colorMap[status] || '#FF4444';
};
//
const formatMeetingTime = (timeStr) => {
if (!timeStr) return '2025.08.13';
try {
//
if (typeof timeStr === 'number') {
const date = new Date(timeStr);
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
}
//
const date = new Date(timeStr);
if (!isNaN(date.getTime())) {
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
}
//
return timeStr;
} catch (error) {
console.error('时间格式化失败:', error);
return timeStr;
}
};
//
const showTimePopup = () => {
@ -255,7 +397,8 @@
const selectMonth = (month) => {
selectedMonth.value = month.value;
console.log('选择月份:', month.label);
//
//
getMeetingList(true);
hideTimePopup();
};
@ -273,7 +416,8 @@
const selectProvince = (province) => {
selectedProvince.value = province.code;
console.log('选择省份:', province.name);
//
//
getMeetingList(true);
hideLocationPopup();
};
@ -283,102 +427,64 @@
//
};
//
const onImageError = (e) => {
const itemId = e.currentTarget.dataset.itemId;
console.log('图片加载失败项目ID:', itemId);
//
const itemIndex = meetingList.value.findIndex(item => item.id === itemId);
if (itemIndex !== -1) {
// liveimg使poster
if (meetingList.value[itemIndex].liveimg && meetingList.value[itemIndex].liveimg !== meetingList.value[itemIndex].poster) {
console.log('liveimg加载失败切换到poster');
meetingList.value[itemIndex].liveimg = meetingList.value[itemIndex].poster;
} else {
// poster使
console.log('设置默认图片');
meetingList.value[itemIndex].poster = '/static/meeting-poster-1.jpg';
}
}
};
//
const onImageLoad = (e) => {
const itemId = e.currentTarget.dataset.itemId;
console.log('图片加载成功项目ID:', itemId, '图片地址:', e.currentTarget.src);
};
//
const onRefresh = () => {
isRefreshing.value = true;
currentPage.value = 1;
hasMoreData.value = true;
//
setTimeout(() => {
//
meetingList.value = [
{
date: '13',
tagColor: '#FF4444',
title: '"天山论·见"—疑难危重病患维训练营',
poster: '/static/meeting-poster-1.jpg',
time: '2025.08.13',
location: '线上'
},
{
date: '13',
tagColor: '#FFA500',
title: '护肝新声大咖谈',
poster: '/static/meeting-poster-2.jpg',
time: '2025.08.13',
location: '线上'
},
{
date: '15',
tagColor: '#00BCD4',
title: '小罐医生讲HIV和感染专题二:抗菌药物-抗真菌药物特性解读',
poster: '/static/meeting-poster-3.jpg',
time: '2025.08.15',
location: '线上'
}
];
isRefreshing.value = false;
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
}, 1500);
//
getMeetingList(true).finally(() => {
//
setTimeout(() => {
isRefreshing.value = false;
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
}, 500);
});
};
//
const onLoadMore = () => {
console.log('上拉加载');
console.log('上拉加载更多');
if (isLoadingMore.value || !hasMoreData.value) return;
isLoadingMore.value = true;
currentPage.value++;
//
setTimeout(() => {
//
const newMeetings = [
{
date: '16',
tagColor: '#9C27B0',
title: '肝胆外科微创技术研讨会',
poster: '/static/meeting-poster-4.jpg',
time: '2025.08.16',
location: '北京'
},
{
date: '17',
tagColor: '#FF9800',
title: '胆囊疾病诊疗新进展',
poster: '/static/meeting-poster-5.jpg',
time: '2025.08.17',
location: '上海'
},
{
date: '18',
tagColor: '#4CAF50',
title: '肝移植术后管理专题讲座',
poster: '/static/meeting-poster-6.jpg',
time: '2025.08.18',
location: '广州'
}
];
meetingList.value.push(...newMeetings);
// 3
if (currentPage.value >= 3) {
hasMoreData.value = false;
}
// API
getMeetingList(false).finally(() => {
isLoadingMore.value = false;
// scroll-view
nextTick(() => {
console.log('数据加载完成,列表长度:', meetingList.value.length);
});
}, 1000);
});
};
</script>
@ -701,7 +807,29 @@ $shadow: 0 2px 8px rgba(0,0,0,0.1);
position: absolute;
top: 16rpx;
right: 16rpx;
border: 4rpx solid #fff;
background-color: #FF4444;
color: $white;
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 20rpx;
}
.live-tag {
position: absolute;
top: 16rpx;
right: 16rpx;
background-color: #00BCD4;
color: $white;
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 20rpx;
}
.replay-tag {
position: absolute;
top: 16rpx;
right: 16rpx;
background-color: #9C27B0;
color: $white;
font-size: 24rpx;
padding: 4rpx 16rpx;
@ -730,6 +858,31 @@ $shadow: 0 2px 8px rgba(0,0,0,0.1);
}
}
//
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 30rpx;
.empty-icon {
margin-bottom: 30rpx;
}
.empty-text {
font-size: 32rpx;
color: $gray;
margin-bottom: 16rpx;
}
.empty-subtext {
font-size: 26rpx;
color: $gray-text;
text-align: center;
}
}
//
.load-more {
padding: 30rpx;

View File

@ -0,0 +1,196 @@
<template>
<view class="group-edit-page">
<uni-nav-bar
left-icon="left"
title="编辑分组"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eee"
>
<template #right>
<text class="save-text" @click="saveGroup">保存</text>
</template>
</uni-nav-bar>
<!-- 分组名称 -->
<view class="section-header">分组名称</view>
<view class="name-row">
<input class="name-input" v-model.trim="groupName" placeholder="请输入分组名称" maxlength="20" />
<view class="icon-btn" v-if="groupName" @click="clearName">
<up-image :src="delImg" width="48rpx" height="48rpx" />
</view>
</view>
<!-- 分组成员 -->
<view class="section-header">分组成员</view>
<view class="add-member" @click="addMember">
<view class="add-circle">
<up-icon name="plus" size="34" color="#bfbfbf" />
</view>
<text class="add-text">添加组患者</text>
</view>
<view class="member-list">
<view class="member-item" v-for="(m, idx) in members" :key="m.uuid || idx">
<image class="avatar" :src="docUrl + (m.photo || '')" mode="aspectFill" />
<text class="member-name">{{ m.realName || '未知' }}</text>
<view class="remove-btn" @click="removeMember(idx)">
<view class="remove-circle">
<up-icon name="minus" color="#fff" size="28rpx" bold></up-icon>
</view>
</view>
</view>
</view>
<!-- 底部删除按钮 -->
<view class="bottom-danger">
<button class="danger-btn" @click="deleteGroup">删除分组</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import navTo from '@/utils/navTo.js'
import docUrl from '@/utils/docUrl.js'
import delImg from "@/static/iv_delete.png"
const groupUuid = ref('')
const groupName = ref('')
const members = ref([])
onLoad((query) => {
groupUuid.value = query?.uuid || ''
// TODO: uuid
//
groupName.value = '看看'
members.value = [
{ uuid: 'u1', realName: '测试', photo: '' }
]
})
onShow(() => {
//
try {
const cached = uni.getStorageSync('patientsSelectedPayload')
if (cached && Array.isArray(cached.list) && cached.list.length) {
mergeSelected(cached.list)
uni.removeStorageSync('patientsSelectedPayload')
}
} catch (e) {}
})
const goBack = () => uni.navigateBack()
const clearName = () => { groupName.value = '' }
const saveGroup = () => {
// TODO: : { uuid: groupUuid, name: groupName, members }
uni.showToast({ title: '已保存', icon: 'success' })
}
const addMember = () => {
//
uni.navigateTo({
url: '/pages_app/selectPatient/selectPatient',
events: {
onPatientsSelected: ({ ids, list }) => {
if (Array.isArray(list)) mergeSelected(list)
}
}
})
}
const removeMember = (idx) => {
members.value.splice(idx, 1)
}
const deleteGroup = () => {
uni.showModal({
title: '删除分组',
content: '确定要删除该分组吗?',
success: (res) => {
if (res.confirm) {
// TODO:
uni.showToast({ title: '已删除', icon: 'success' })
setTimeout(() => goBack(), 700)
}
}
})
}
// uuid
const mergeSelected = (selectedList) => {
const existIds = new Set(members.value.map(m => m.uuid))
selectedList.forEach(s => {
if (!existIds.has(s.uuid)) {
existIds.add(s.uuid)
members.value.push({ uuid: s.uuid, realName: s.realName, photo: s.photo || '' })
}
})
}
</script>
<style lang="scss" scoped>
.group-edit-page{
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 160rpx;
}
.save-text{ color:#8B2316; font-size: 30rpx; }
.section-header{
background:#d9d9d9;
color:#333;
padding: 22rpx 30rpx;
font-size: 30rpx;
}
.name-row{
background:#fff;
display:flex;
align-items:center;
justify-content:space-between;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #eee;
.name-input{
flex:1;
font-size: 32rpx;
color:#333;
}
.icon-btn{ padding-left: 20rpx; }
}
.add-member{
background:#fff;
display:flex;
align-items:center;
gap:20rpx;
padding: 26rpx 30rpx;
border-bottom: 1rpx solid #eee;
.add-circle{
width: 96rpx; height: 96rpx; border-radius: 50%;
border: 4rpx solid #e5e5e5;
display:flex; align-items:center; justify-content:center;
background:#fff;
}
.add-text{ font-size: 32rpx; color:#666; }
}
.member-list{
background:#fff;
.member-item{
display:flex; align-items:center; justify-content:space-between;
padding: 26rpx 30rpx; border-bottom: 1rpx solid #eee;
.avatar{ width: 100rpx; height: 100rpx; border-radius: 16rpx; background:#ffe; }
.member-name{ flex:1; margin-left: 20rpx; font-size: 32rpx; color:#333; }
.remove-btn{ padding-left: 20rpx; }
.remove-circle{ width: 48rpx; height:48rpx; background:#8B2316; border-radius:50%; display:flex; align-items:center; justify-content:center; }
}
}
.bottom-danger{
position: fixed; left:30rpx; right:30rpx; bottom: 30rpx;
background:#fff; border-top: 1rpx solid #eee;
.danger-btn{ width:100%; height: 96rpx; background:#8B2316; color:#fff; border:none; border-radius: 12rpx; font-size: 32rpx;display: flex; align-items: center; justify-content: center; }
}
</style>

View File

@ -24,20 +24,20 @@
</view>
<!-- 随访申请 -->
<view class="follow-up-section">
<view class="follow-up-section" v-if="applyList.length > 0">
<view class="section-title">随访申请</view>
<view class="pending-request" v-if="pendingRequest">
<view class="pending-request" v-for="(item, index) in applyList" :key="index">
<view class="request-item">
<view class="avatar">
<view class="avatar-icon"></view>
<up-image :src="docUrl+item.photo" radius="10rpx" width="80rpx" height="80rpx" ></up-image>
</view>
<view class="request-content">
<view class="request-time">2025-08-18 15:55:03</view>
<view class="request-text">我是陈新华,在线上和您沟通过,请您同意我作为您的随访患者</view>
<view class="request-time">{{ item.createDate }}</view>
<view class="request-text">{{ item.content }}</view>
<view class="action-buttons">
<button class="reject-btn" @click="rejectRequest">拒绝</button>
<button class="agree-btn" @click="agreeRequest">同意</button>
<button class="reject-btn" @click="applyListOperate(item.uuid,3)">拒绝</button>
<button class="agree-btn" @click="applyListOperate(item.uuid,2)">同意</button>
</view>
</view>
</view>
@ -45,22 +45,22 @@
</view>
<!-- 申请记录 -->
<view class="history-section">
<view class="history-section" v-if="historyList.length > 0">
<view class="section-title">申请记录(近一月)</view>
<view class="history-list">
<view class="history-item" v-for="(item, index) in historyList" :key="index">
<view class="avatar">
<view class="avatar-icon"></view>
<up-image v-if="docUrl+item.patient_photo" :src="docUrl + item.patient_photo" radius="10rpx" width="80rpx" height="80rpx"></up-image>
</view>
<view class="history-content">
<view class="history-time">{{ item.time }}</view>
<view class="nickname">昵称: {{ item.nickname }}</view>
<view class="history-text">{{ item.message }}</view>
<view class="history-time">{{ formatDate(item.createDate) }}</view>
<view class="nickname">{{ item.nickname || item.patient_name }}</view>
<view class="history-text">{{ item.content}}</view>
<view class="status-info">
<up-image :src="goImg" width="30rpx" height="30rpx" ></up-image>
<text class="status-text">已同意</text>
<up-image :src="goImg" width="30rpx" height="30rpx" v-if="item.status==2"></up-image>
<text class="status-text">{{ getStatusText(item.status) }}</text>
</view>
</view>
</view>
</view>
@ -75,36 +75,19 @@
<script setup>
import { ref } from 'vue';
import { onLoad,onShow} from '@dcloudio/uni-app';
import goImg from "@/static/go_big.png"
import api from '@/api/api.js'
import docUrl from "@/utils/docUrl"
import navTo from "@/utils/navTo.js"
//
const pendingRequest = ref({
name: '陈新华',
message: '我是陈新华,在线上和您沟通过,请您同意我作为您的随访患者',
time: '2025-08-18 15:55:03'
});
const historyList = ref([
{
nickname: '韩夫臣',
message: '我是韩夫臣,在线上和您沟通过,请您...',
time: '2025-08-19 22:41:35'
},
{
nickname: '鲁保山',
message: '我是鲁保山,在线上和您沟通过,请您...',
time: '2025-08-19 22:41:27'
},
{
nickname: '蒋宁宁',
message: '我是蒋宁宁,在线上和您沟通过,请您...',
time: '2025-08-19 11:25:46'
},
{
nickname: '蒋宁',
message: '我是蒋宁,在线上和您沟通过,请您同...',
time: '2025-08-19 11:25:39'
}
]);
const applyList = ref([]);
const historyList = ref([]);
//
const goBack = () => {
@ -142,11 +125,90 @@ const agreeRequest = () => {
}
});
};
const getRelationRecordLately = async () => {
const res = await api.relationRecordLately({
page:1,
pageSize:100
});
console.log('随访记录API响应:', res);
if(res.code === 200){
historyList.value = res.data.list;
}
};
const getApplyList = async () => { //
try {
let userInfo=uni.getStorageSync('userInfo')
const res = await api.applyList();
console.log('申请列表API响应:', res);
if (res && res.code === 200) {
applyList.value = res.data;
} else {
console.log('申请列表API响应异常:', res);
uni.showToast({
title: res.message || '获取申请列表失败',
icon: 'error',
duration: 2000
});
}
} catch (error) {
console.error('获取申请列表失败:', error);
uni.showToast({
title: '网络请求失败',
icon: 'error',
duration: 2000
});
}
};
const applyListOperate = async (uuid,status) => {
let data = {
uuid: uuid,
status: status
}
const res = await api.applyListOperate(data);
if(res.code === 200){
uni.showToast({
title: '操作成功',
icon: 'none',
duration: 1500
});
getApplyList();
getRelationRecordLately();
}
};
onShow(() => {
getApplyList();
getRelationRecordLately();
});
//
//
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
//
const getStatusText = (status) => {
switch (status) {
case 1: return '待审核';
case 2: return '已同意';
case 3: return '已拒绝';
}
};
const addPatient = () => {
uni.showToast({
title: '跳转到添加患者页面',
icon: 'none'
navTo({
url:'/pages_app/myCode/myCode'
});
//
};
@ -378,7 +440,7 @@ const addPatient = () => {
}
.history-list {
padding-bottom: 100rpx;
margin-bottom: 100rpx;
.history-item {
display: flex;
gap: 20rpx;

View File

@ -12,7 +12,7 @@
backgroundColor="#eeeeee"
>
<template #right>
<view class="nav-right" @click="editPatient">
<view class="nav-right" @click.stop="editPatient">
<uni-icons type="compose" size="22" color="#8B2316"></uni-icons>
</view>
</template>

View File

@ -17,14 +17,14 @@
<!-- 筛选排序栏 -->
<view class="filter-sort-bar">
<view class="sort-section" @click="toggleGroupSort">
<text class="sort-label">分组排序</text>
<text class="sort-label">{{ groupSortTitle }}</text>
<view class="imgbox">
<up-image :src="upImg" width="26rpx" height="26rpx" ></up-image>
</view>
</view>
<view class="divider"></view>
<view class="current-sort" @click="toggleInnerSort">
<text class="sort-text">按首字母</text>
<text class="sort-text">{{ innerSortTitle }}</text>
<view class="imgbox">
<up-image :src="upImg" width="26rpx" height="26rpx" ></up-image>
</view>
@ -33,12 +33,12 @@
<!-- 分组排序弹窗 -->
<view v-if="showGroupSort" class="popup-panel">
<view class="popup-item" :class="{ active: selectedGroupSort==='letter' }" @click.stop="chooseGroupSort('letter')">
<view class="popup-item" :class="{ active: group_sort==0 }" @click.stop="chooseGroupSort('letter')">
<text class="item-text">按首字母</text>
<uni-icons v-if="selectedGroupSort==='letter'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
<view class="popup-divider"></view>
<view class="popup-item" :class="{ active: selectedGroupSort==='count' }" @click.stop="chooseGroupSort('count')">
<view class="popup-item" :class="{ active:group_sort==1}" @click.stop="chooseGroupSort('count')">
<text class="item-text">分组人数</text>
<uni-icons v-if="selectedGroupSort==='count'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
@ -47,13 +47,13 @@
<!-- 组内排序弹窗 -->
<view v-if="showInnerSort" class="popup-panel">
<view class="popup-item" :class="{ active: selectedInnerSort==='letter' }" @click.stop="chooseInnerSort('letter')">
<view class="popup-item" :class="{ active:list_sort==0 }" @click.stop="chooseInnerSort('letter')">
<text class="item-text">按首字母</text>
<uni-icons v-if="selectedInnerSort==='letter'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
<view class="popup-divider"></view>
<view class="popup-item" :class="{ active: selectedInnerSort==='count' }" @click.stop="chooseInnerSort('count')">
<text class="item-text">分组人数</text>
<view class="popup-item" :class="{ active:list_sort==1 }" @click.stop="chooseInnerSort('count')">
<text class="item-text">随访时间</text>
<uni-icons v-if="selectedInnerSort==='count'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
</view>
@ -61,22 +61,29 @@
<!-- 患者列表 -->
<scroll-view class="patient-list-section" scroll-y="true" :style="{ height: scrollViewHeight }">
<view class="groupcell">
<view class="section-title">
<view class="imgbox">
<up-image :src="groupRightImg" width="19rpx" height="32rpx" ></up-image>
<!-- 分组循环渲染 -->
<view class="groupcell" v-for="(group, gi) in groups" :key="group.uuid || gi">
<view class="section-title" @click="toggleGroup(gi)">
<view class="left">
<view class="imgbox">
<up-image v-if="!openGroups[gi]" :src="groupRightImg" width="19rpx" height="32rpx" ></up-image>
<up-image v-else :src="groupDownImg" width="32rpx" height="19rpx" ></up-image>
</view>
<view class="title">{{ group.name || '未命名分组' }} | {{ group.patientNum || (group.patientList ? group.patientList.length : 0) }}</view>
</view>
<view class="right-edit" @click.stop="editGroup(group)">
<text class="edit-text">编辑</text>
</view>
<view class="title">待分组患者 | 5</view>
</view>
<view class="patient-list" >
<view class="patient-item" v-for="(patient, index) in patientList" :key="index">
<view class="patient-list" v-if="openGroups[gi]">
<view class="patient-item" v-for="(patient, index) in (group.patientList || [])" :key="patient.uuid || index">
<view class="patient-avatar">
<up-image :src="patient.avatar" width="80rpx" height="80rpx" mode="aspectFill"></up-image>
<up-image :src="docUrl + patient.photo" width="80rpx" height="80rpx" mode="aspectFill"></up-image>
</view>
<view class="patient-info">
<view class="patient-name">{{ patient.name }}</view>
<view class="follow-up-time">随访于{{ patient.lastFollowUp }}</view>
<view class="patient-name">{{ patient.realName || patient.nickname || '-' }}</view>
<view class="follow-up-time">随访于{{ formatYMD(patient.join_date) }}</view>
</view>
</view>
</view>
@ -87,53 +94,59 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import upImg from "@/static/triangle_green_theme.png"
import downImg from "@/static/triangle_normal.png"
import groupRightImg from "@/static/groupright_big.png"
import groupDownImg from "@/static/groupup_big.png"
//
const patientList = ref([
{
name: 'aa',
lastFollowUp: '2020-12-02',
avatar: '/static/avatar1.png' //
},
{
name: '测试',
lastFollowUp: '2023-10-08',
avatar: '/static/avatar2.png' //
},
{
name: '刘三多',
lastFollowUp: '2021-12-16',
avatar: '/static/avatar3.png' // 绿
},
{
name: '路测试',
lastFollowUp: '2019-10-28',
avatar: '/static/avatar4.png' //
},
{
name: '哦哦哦',
lastFollowUp: '2023-10-08',
avatar: '/static/avatar5.png' //
}
]);
import api from '@/api/api.js';
import docUrl from '@/utils/docUrl'
import dayjs from 'dayjs'
const list_sort = ref(0);
const group_sort = ref(0);
//
const scrollViewHeight = computed(() => {
//
// (140rpx) + (80rpx) + (80rpx) = 300rpx
// px
const systemInfo = uni.getSystemInfoSync();
const windowHeight = systemInfo.windowHeight;
const navHeight = 140 / 2; // rpxpx
const filterHeight = 80 / 2;
const titleHeight = 80 / 2;
const availableHeight = windowHeight - navHeight - filterHeight - titleHeight;
return `${availableHeight}px`;
//
const groups = ref([]);
const openGroups = ref({});
const formatYMD = (val) => {
if (!val) return '';
const d = dayjs(val);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
};
//
const groupSortTitle = computed(() => group_sort.value === 0 ? '按首字母' : '分组人数');
const innerSortTitle = computed(() => list_sort.value === 0 ? '按首字母' : '随访时间');
onShow(() => {
fetchGroupList();
});
const fetchGroupList = async () => {
const res = await api.groupList({
list_sort:list_sort.value,
page:1,
pageSize:10,
group_sort:group_sort.value
});
if(res.code === 200){
groups.value = Array.isArray(res.data) ? res.data : [];
//
openGroups.value = {};
groups.value.forEach((_, idx) => openGroups.value[idx] = false);
}
};
const toggleGroup = (idx) => {
openGroups.value[idx] = !openGroups.value[idx];
};
const editGroup = (group) => {
// TODO:
uni.showToast({ title: `编辑:${group.name || '未命名分组'}`, icon: 'none' });
};
//
const showGroupSort = ref(false);
const selectedGroupSort = ref('letter'); // letter | count
@ -142,18 +155,23 @@ const selectedGroupSort = ref('letter'); // letter | count
const showInnerSort = ref(false);
const selectedInnerSort = ref('letter');
// /
const showPending = ref(false);
const toggleGroupSort = () => {
showGroupSort.value = !showGroupSort.value;
if (showGroupSort.value) showInnerSort.value = false;
};
const closeGroupSort = () => {
showGroupSort.value = false;
};
const chooseGroupSort = (type) => {
selectedGroupSort.value = type;
closeGroupSort();
selectedGroupSort.value = type;
// =0=1
group_sort.value = type === 'letter' ? 0 : 1;
closeGroupSort();
fetchGroupList();
};
const toggleInnerSort = () => {
@ -166,8 +184,11 @@ const closeInnerSort = () => {
};
const chooseInnerSort = (type) => {
selectedInnerSort.value = type;
closeInnerSort();
selectedInnerSort.value = type;
// =0访=1
list_sort.value = type === 'letter' ? 0 : 1;
closeInnerSort();
fetchGroupList();
};
//
@ -182,6 +203,7 @@ const createNew = () => {
});
//
};
</script>
<style lang="scss" scoped>
@ -356,6 +378,7 @@ const createNew = () => {
z-index: 9;
border-bottom: 1rpx solid #f0f0f0;
.imgbox{
width: 32rpx;
margin-top: -10rpx;
}
.sort-section {
@ -418,14 +441,28 @@ const createNew = () => {
.section-title {
padding: 30rpx 30rpx 20rpx;
font-size: 32rpx;
width: 100%;
box-sizing: border-box;
font-weight: normal;
display: flex;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
align-items: center;
justify-content: space-between;
.left{
display:flex;
align-items:center;
}
.imgbox{
margin-top: 4rpx;
margin-right: 8rpx;
}
.right-edit{
display:flex;
align-items:center;
gap: 8rpx;
.edit-text{ font-size: 28rpx; color:#8B2316; }
}
}
.patient-list {

View File

@ -9,7 +9,7 @@
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#ffffff"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right">
@ -20,31 +20,43 @@
</uni-nav-bar>
<!-- 消息列表区域 -->
<view class="message-list" v-if="activeTab === 'message'">
<scroll-view
class="message-list"
v-if="activeTab === 'message'"
scroll-y="true"
refresher-enabled="true"
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
>
<!-- 消息项 -->
<view class="message-item" @click="openMessage">
<view class="message-item" v-for="(item, index) in messageList" :key="item.id || index" @click="openMessage(item)">
<view class="message-avatar">
<view class="avatar-placeholder">
<view class="avatar-placeholder" v-if="!item.avatar">
<uni-icons type="person" size="32" color="#ffffff"></uni-icons>
</view>
<image v-else :src="item.avatar" class="patient-avatar" mode="aspectFill"></image>
</view>
<view class="message-content">
<view class="message-header">
<text class="patient-name">测试</text>
<text class="message-time">2025-08-11</text>
<text class="patient-name">{{ item.patientName }}</text>
<text class="message-time">{{ item.time }}</text>
</view>
<view class="message-preview">
<text class="preview-text">[图片]</text>
<text class="preview-text">{{ item.messagePreview }}</text>
</view>
</view>
</view>
<!-- 空状态提示 -->
<view class="empty-state" v-if="messageList.length === 0">
<view class="empty-state" v-if="messageList.length === 0 && !isRefreshing">
<uni-icons type="chat" size="80" color="#cccccc"></uni-icons>
<text class="empty-text">暂无患者消息</text>
<text class="empty-subtext">下拉刷新获取最新申请</text>
<view class="debug-actions">
<button class="debug-btn" @click="getApplyList">测试API调用</button>
</view>
</view>
</view>
</scroll-view>
<!-- 患者列表区域 -->
<view class="patient-list" v-if="activeTab === 'list'">
@ -55,7 +67,7 @@
<uni-icons type="person" size="24" color="#ffffff"></uni-icons>
<uni-icons type="plus" size="16" color="#ffffff" style="position: absolute; right: 8rpx; bottom: 8rpx;"></uni-icons>
</view>
<text class="action-text">新的患者</text>
<text class="action-text">新的患者<text class="new-patient-count" v-if="applyList.length > 0">(待审核{{ applyList.length }})</text></text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
@ -68,7 +80,7 @@
<view class="grid-item"></view>
</view>
</view>
<text class="action-text">患者分组 (随访5人)</text>
<text class="action-text">患者分组 <text class="new-patient-count" v-if="patientList.length > 0">(随访{{ patientList.length }})</text></text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
@ -81,24 +93,24 @@
<up-index-anchor :text="group.letter" />
<view class="group-section">
<view class="patient-item" v-for="item in group.items" :key="item.name" @click="openPatient(item.name)">
<view class="patient-item" v-for="item in group.items" :key="item.uuid || item.id" >
<template v-if="item.placeholder">
<view class="patient-avatar-placeholder">
<uni-icons type="person" size="32" color="#ffffff"></uni-icons>
</view>
</template>
<template v-else>
<image class="patient-avatar" :src="item.avatar" mode="aspectFill"></image>
<image class="patient-avatar" :src="docUrl+item.photo" mode="aspectFill"></image>
</template>
<view class="patient-info">
<text class="patient-name">{{ item.name }}</text>
<view class="patient-info" @click="goPatientDetail(item.uuid)">
<text class="patient-name">{{ item.realName }}</text>
<view class="patient-badge" v-if="item.badge">
<text class="badge-text">{{ item.badge }}</text>
</view>
</view>
<view class="patient-status">
<uni-icons type="compose" size="20" color="#8B2316"></uni-icons>
<text class="follow-date">随访于{{ item.date }}</text>
<uni-icons type="compose" size="20" color="#8B2316" @click.stop="editPatient(item.uuid)"></uni-icons>
<text class="follow-date">随访于{{ formatYMD(item.join_date) }}</text>
</view>
</view>
</view>
@ -108,7 +120,15 @@
</view>
</view>
<view class="plan" v-if="activeTab === 'plan'">
<empty></empty>
<view class="apply-list" v-if="applyList.length > 0">
<view class="apply-item" v-for="item in applyList" :key="item.id">
<view class="apply-content">
<text class="apply-text">{{ item.messagePreview }}</text>
</view>
</view>
</view>
<empty v-else></empty>
<!-- 悬浮添加按钮 -->
<view class="floating-add-btn" @click="showAddMenu">
@ -159,8 +179,30 @@
import { onShow } from "@dcloudio/uni-app";
import dayImg from "@/static/visit_data11.png"
import planImg from "@/static/visitplan.png"
//
import api from '@/api/api.js';
import navTo from '@/utils/navTo.js';
import docUrl from '@/utils/docUrl.js';
const patientList = ref([]);
import pinyin from 'pinyin';
import dayjs from 'dayjs'
const goPatientDetail = (uuid) => {
navTo({
url: `/pages_app/patientDetail/patientDetail?uuid=${uuid}`
})
}
const editPatient = (uuid) => {
console.log(uuid)
navTo({
url: `/pages_app/patientSetting/patientSetting?uuid=${uuid}`
})
}
//
const formatYMD = (input) => {
if (!input) return '';
const d = dayjs(input);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
}
//
const messageList = ref([
{
id: 1,
@ -184,8 +226,9 @@
// selectorQuery
const { proxy } = getCurrentInstance();
// up-index-list
const indexList = ref([]);
// up-index-list 26
// A-Z
const indexList = ref('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''));
// - (rpxpx) - tab(100rpx->px) - (200rpx->px)
const groupsListHeight = ref(0);
const rpxToPx = (rpx) => {
@ -201,59 +244,117 @@
//
const patientGroups = ref([]);
//
// A-Z
const rebuildIndexList = () => {
// A-Z
indexList.value = patientGroups.value.map(g => g.letter);
};
//
const generateMockDate = () => {
const start = new Date(2019, 0, 1).getTime();
const end = new Date().getTime();
const d = new Date(start + Math.random() * (end - start));
const y = d.getFullYear();
const m = `${d.getMonth() + 1}`.padStart(2, '0');
const day = `${d.getDate()}`.padStart(2, '0');
return `${y}-${m}-${day}`;
};
const generateMockGroups = () => {
const lettersPool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const avatarSamples = ['/static/avatar-a.png','/static/avatar-l.png','/static/avatar-l2.png'];
const groups = [];
lettersPool.forEach((L) => {
// 0-3
const count = Math.floor(Math.random() * 3);
if (count === 0) return;
const items = Array.from({length: count}).map((_, i) => {
const usePlaceholder = Math.random() < 0.4;
return {
name: `${L}患者${i+1}`,
avatar: usePlaceholder ? '' : avatarSamples[Math.floor(Math.random()*avatarSamples.length)],
placeholder: usePlaceholder,
badge: Math.random() < 0.2 ? 'GOOD' : '',
date: generateMockDate()
};
});
groups.push({ letter: L, items });
//
const generateMockDate = () => '';
const getFirstLetter = (chineseName) => {
//
const firstChar = chineseName.charAt(0); //
const pinyinArray = pinyin(firstChar, { style: pinyin.STYLE_NORMAL }); //
return pinyinArray[0][0].charAt(0); //
}
// patientList
const buildGroupsFromPatients = () => {
const map = new Map();
patientList.value.forEach((p) => {
const name = p.realName;
const first = getFirstLetter(name).toUpperCase();
const letter = /^[A-Z]$/.test(first) ? first : '#';
if (!map.has(letter)) map.set(letter, []);
map.get(letter).push(p);
});
return groups;
};
//
const loadMockGroups = () => {
uni.showLoading({ title: '加载中' });
setTimeout(() => {
patientGroups.value = generateMockGroups();
rebuildIndexList();
uni.hideLoading();
}, 300);
const letters = Array.from(map.keys()).sort((a,b) => a.localeCompare(b));
patientGroups.value = letters.map(l => ({ letter: l, items: map.get(l) }));
rebuildIndexList();
};
//
const goBack = () => {
uni.navigateBack();
};
//
const applyList = ref([]);
//
const isRefreshing = ref(false);
const getApplyList = async () => { //
try {
let userInfo=uni.getStorageSync('userInfo')
const res = await api.applyList();
if (res && res.code === 200) {
applyList.value = res.data;
} else {
uni.showToast({
title: res.message || '获取申请列表失败',
icon: 'error',
duration: 2000
});
}
} catch (error) {
console.error('获取申请列表失败:', error);
uni.showToast({
title: '网络请求失败',
icon: 'error',
duration: 2000
});
}
};
const patientListByGBK = async () => {
const res = await api.patientListByGBK();
if(res.code == 1){
patientList.value = res.data;
buildGroupsFromPatients()
}
};
const page=ref(1)
const followUpList = async () => {
let userInfo=uni.getStorageSync('userInfo')
const res = await api.followUpList({
page:page.value,
pageSize:10
});
if(res.code === '1'){
followUpList.value = res.data;
}
};
//
const onRefresh = async () => {
isRefreshing.value = true;
try {
await getApplyList();
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
} catch (error) {
console.error('刷新失败:', error);
uni.showToast({
title: '刷新失败',
icon: 'error',
duration: 1500
});
} finally {
isRefreshing.value = false;
}
};
//
const searchPatients = () => {
@ -272,11 +373,15 @@
};
//
const openMessage = () => {
const openMessage = (item) => {
uni.showToast({
title: '打开消息',
title: `打开患者 ${item?.patientName || '未知'} 的消息`,
icon: 'none'
});
//
// uni.navigateTo({
// url: `/pages_app/messageDetail/messageDetail?id=${item?.id}`
// });
};
//
@ -285,7 +390,8 @@
switch(tab) {
case 'message':
//
// -
getApplyList();
break;
case 'list':
//
@ -302,18 +408,16 @@
//
const addNewPatient = () => {
uni.showToast({
title: '添加新患者',
icon: 'none'
});
navTo({
url: '/pages_app/myPatient/myPatient'
})
};
//
const managePatientGroups = () => {
uni.showToast({
title: '管理患者分组',
icon: 'none'
});
navTo({
url: '/pages_app/patientGroup/patientGroup'
})
};
//
@ -354,14 +458,15 @@
//
onShow(() => {
loadMessageList();
loadMockGroups();
computeListHeight();
getApplyList();
patientListByGBK();
followUpList();
});
//
const loadMessageList = () => {
// API
console.log('加载患者消息列表');
};
</script>
@ -446,13 +551,32 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
padding: 100rpx 30rpx;
.empty-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999999;
}
.empty-subtext {
font-size: 26rpx;
color: #999999;
margin-top: 16rpx;
}
.debug-actions {
margin-top: 30rpx;
.debug-btn {
background-color: #8B2316;
color: #ffffff;
border: none;
padding: 16rpx 32rpx;
border-radius: 8rpx;
font-size: 26rpx;
}
}
}
}
@ -510,6 +634,7 @@
//
.patient-list {
height:calc(100vh - 265rpx);
display: flex;
@ -576,6 +701,11 @@
flex: 1;
font-size: 32rpx;
color: #333333;
.new-patient-count {
color: red;
font-size: 32rpx;
margin-left: 10rpx;
}
}
}
}
@ -670,7 +800,8 @@
.patient-status {
display: flex;
align-items: center;
align-items: flex-end;
flex-direction: column;
.follow-date {
font-size: 24rpx;

View File

@ -0,0 +1,112 @@
<template>
<view class="remark-page">
<uni-nav-bar
left-icon="left"
title="设置备注和分组"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eee"
/>
<view class="form-block">
<view class="label">备注</view>
<input class="input" v-model.trim="remark" placeholder="给患者添加备注名" placeholder-class="ph" maxlength="20"/>
</view>
<view class="form-block">
<view class="label">分组</view>
<view class="iptbox">
<input class="input" v-model.trim="remark" placeholder="通过分组给患者分类" placeholder-class="ph" maxlength="20" readonly/>
<uni-icons type="right" size="20" color="#666"></uni-icons>
</view>
</view>
<view class="form-block">
<view class="label">描述</view>
<textarea class="textarea" v-model.trim="note" placeholder="补充患者关键信息,方便随访患者" placeholder-class="ph" auto-height maxlength="140"/>
</view>
<view class="bottom-bar">
<button class="save-btn" @click="saveRemark">保存</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
const remark = ref('')
const note = ref('')
const goBack = () => {
uni.navigateBack()
}
const saveRemark = () => {
if (!remark.value) {
uni.showToast({ title: '请输入备注名', icon: 'none' })
return
}
// TODO:
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => goBack(), 700)
}
</script>
<style lang="scss" scoped>
.remark-page{
min-height: 100vh;
background: #f7f7f7;
padding-bottom: 140rpx;
}
.form-block{
.iptbox{
background: #f8f8f8;
display: flex;
align-items: center;
}
background: #fff;
padding: 24rpx 30rpx 30rpx;
.label{
font-size: 28rpx;
color: #666;
margin-bottom: 16rpx;
}
.input{
flex:1;
background: #f8f8f8;
border-radius: 12rpx;
padding: 24rpx;
font-size: 30rpx;
color: #333;
}
.textarea{
background: #f8f8f8;
border-radius: 12rpx;
padding: 24rpx;
font-size: 28rpx;
color: #333;
min-height: 180rpx;
line-height: 1.6;
}
.ph{
color: #bfbfbf;
}
}
.bottom-bar{
position: fixed;
left: 0; right: 0; bottom: 0;
background: #ffffff;
border-top: 1rpx solid #f0f0f0;
padding: 20rpx 24rpx env(safe-area-inset-bottom);
.save-btn{
width: 100%;
height: 92rpx;
background: #8B2316;
color: #fff;
border: none;
border-radius: 12rpx;
font-size: 32rpx;
}
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<view class="setting-page">
<uni-nav-bar
left-icon="left"
title="常用设置"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eee"
>
</uni-nav-bar>
<view class="list-block">
<view class="cell" @click="goRemark">
<text class="cell-left">设置备注</text>
<view class="cell-right">
<text class="cell-desc">给患者添加备注名</text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="cell" @click="goRemark">
<text class="cell-left">设置分组</text>
<view class="cell-right">
<text class="cell-desc">通过分组给患者分类</text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="cell" @click="goRemark">
<text class="cell-left">患者描述</text>
<view class="cell-right">
<text class="cell-desc">补充患者关键信息方便随访患者</text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="cell" @click="setNextFollow">
<text class="cell-left">下次随访时间</text>
<view class="cell-right">
<text class="cell-desc">添加随访提醒</text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="cell" @click="goFeedback">
<text class="cell-left">投诉反馈</text>
<view class="cell-right">
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
</view>
<view class="danger-block">
<text class="danger-text" @click="unbindPatient">解除随访</text>
</view>
</view>
</template>
<script setup>
import navTo from '@/utils/navTo.js'
import { onLoad } from '@dcloudio/uni-app'
const goBack = () => {
uni.navigateBack();
}
const goRemark = () => {
navTo({ url: '/pages_app/patientRemark/patientRemark' })
}
const setNextFollow = () => {
navTo({ url: '/pages_app/visit/visit' })
}
const goFeedback = () => {
navTo({ url: '/pages_app/feedback/feedback' })
}
const unbindPatient = () => {
uni.showModal({
title: '确认解除',
content: '确定要解除随访关系吗?',
success: (res) => {
if (res.confirm) {
uni.showToast({ title: '已提交解除', icon: 'success' })
// TODO: 访
}
}
})
}
</script>
<style lang="scss" scoped>
.setting-page{
min-height: 100vh;
background:#f7f7f7;
}
.list-block{
margin-top: 20rpx;
background: #fff;
.cell{
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child{ border-bottom: none; }
.cell-left{
font-size: 32rpx;
color: #333;
}
.cell-right{
display: flex;
align-items: center;
gap: 16rpx;
.cell-desc{
font-size: 26rpx;
color: #999;
}
}
}
}
.danger-block{
margin-top: 30rpx;
background: #fff;
padding: 40rpx 0;
display: flex;
justify-content: center;
.align-center{ align-items: center; }
.danger-text{
color: #8B2316;
font-size: 32rpx;
}
}
</style>

View File

@ -9,14 +9,20 @@
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
>
<template v-slot:right>
<view class="nav-right" @click="testLoadMore">
<text style="font-size: 24rpx; color: #8B2316;">测试加载</text>
</view>
</template>
</uni-nav-bar>
<view class="courseware-container">
<!-- 排序和筛选栏 -->
<view class="filter-bar">
<view class="filter-item" @click="toggleSort">
<text class="filter-text">{{sort==2?'最新':'最热'}}</text>
<text class="filter-text">{{sort==0?'最新':'最热'}}</text>
<view class="newbox">
<up-image :src="upImg" width="20rpx" height="26rpx" ></up-image>
</view>
@ -47,17 +53,24 @@
<view class="pdf-icon">📄</view>
</view>
<view class="item-content">
<view class="item-title">{{ item.title }}</view>
<view class="item-author">{{ item.author }}</view>
<view class="item-title">{{item.title}}</view>
<view class="info">
<view >{{ item.providername }}</view>
<view class="item-author">{{ item.hospitalname }}</view>
</view>
<view class="item-stats">
<view class="views">
<uni-icons type="eye" size="30rpx" color="#999"></uni-icons>
<text class="view-count">{{ item.views }}人阅读</text>
<text class="view-count">{{ item.readnum }}人阅读</text>
</view>
<view class="price">
<up-image :src="downLoadImg" width="32rpx" height="32rpx" ></up-image>
<text class="price-value">{{ item.price }}</text>
<view class="priceImg">
<up-image :src="downLoadImg" width="32rpx" height="32rpx" ></up-image>
</view>
<text class="price-value" v-if="item.price>0"><text class="money-unit">¥</text>{{ item.price>item.discount?fromatPrice(item.discount):fromatPrice(item.price) }}</text>
<text class="yuanjia" v-if="item.price>0 && item.price>item.discount">原价<text class="jiaprice">{{fromatPrice(item.price/100)}}</text></text>
<text v-else class="free">免费</text>
</view>
</view>
</view>
@ -75,7 +88,7 @@
:class="{ active: tag.selected }"
@click="toggleTag(index)"
>
{{ tag.name }}
{{ tag.DES}}
</view>
</view>
@ -106,7 +119,7 @@
import downLoadImg from "@/static/wdxz.png"
import api from '@/api/api.js';
const isFilterActive=ref(false)
const sort = ref(2);
const sort = ref(0); //
//
const refreshing = ref(false);
const loading = ref(false);
@ -136,7 +149,10 @@
};
const toggleSort = () => {
sort.value=sort.value==1?2:1
sort.value = sort.value === 0 ? 1 : 0;
console.log('切换排序:', sort.value);
//
loadData(true);
};
@ -154,7 +170,24 @@
icon: 'none'
});
};
const loadGuideTags = async () => {
try {
const res = await api.guideTag({
type:6
});
console.log('指南标签API响应:', res);
if(res && res.code === 200 && res.data) {
// API
filterTags.value = res.data.map(tag => ({
...tag,
selected: false
}));
console.log('指南标签加载成功:', filterTags.value);
}
} catch (e) {
console.error('加载指南标签失败:', e);
}
};
//
const onRefresh = async () => {
@ -184,22 +217,24 @@
//
const onLoadMore = async () => {
console.log('加载更多')
if (loading.value || noMore.value) return;
console.log('=== onLoadMore 触发 ===');
console.log('当前状态:', {
loading: loading.value,
noMore: noMore.value,
page: page.value,
listLength: coursewareList.value.length
});
loading.value = true;
try {
//
await loadData(false);
} catch (error) {
uni.showToast({
title: '加载失败',
icon: 'error'
if (loading.value || noMore.value) {
console.log('条件不满足,跳过加载:', {
loading: loading.value,
noMore: noMore.value
});
} finally {
loading.value = false;
return;
}
console.log('条件满足,开始加载更多数据');
await loadData(false);
};
//
@ -219,7 +254,6 @@
// API
const res = await api.ganDanFileByKeyWords({
page: page.value,
pageSize: pageSize.value,
sort: sort.value,
//
keywords: '',
@ -237,7 +271,7 @@
// API
if (res.data && res.data.list) {
newItems = res.data.list;
totalCount = res.data.total || res.data.totalRow || 0;
totalCount = res.data.totalRow;
console.log('使用 res.data.list 结构');
} else if (res.data && Array.isArray(res.data)) {
newItems = res.data;
@ -265,8 +299,11 @@
noMore.value = true;
console.log('没有更多数据了');
} else {
page.value++;
console.log(`还有更多数据,下一页: ${page.value}`);
//
if (!isRefresh) {
page.value=page.value+1;
console.log(`还有更多数据,下一页: ${page.value}`);
}
}
} else {
console.error('API返回的数据不是数组格式:', newItems);
@ -332,11 +369,40 @@
hideFilterPopup();
//
};
const fromatPrice=(price)=>{
if(price<1){
return price.toFixed(2)
}else{
return price.toFixed(1)
}
}
onShow(() => {
//
console.log('页面显示,开始加载课件数据');
loadData(true);
loadGuideTags()
});
//
const testLoadMore = () => {
console.log('=== 手动测试加载更多 ===');
console.log('当前状态:', {
loading: loading.value,
noMore: noMore.value,
page: page.value,
listLength: coursewareList.value.length
});
if (!loading.value && !noMore.value) {
onLoadMore();
} else {
uni.showToast({
title: `加载中: ${loading.value}, 无更多: ${noMore.value}`,
icon: 'none',
duration: 2000
});
}
};
</script>
<style lang="scss" scoped>
//
@ -438,15 +504,16 @@
top:240rpx;
flex: 1;
bottom:0;
width:100%;
left:30rpx;
right:30rpx;
padding-top: 20rpx;
margin: 0 30rpx;
box-sizing: border-box;
width:auto;
.courseware-item {
background-color: $white;
border-radius: 16rpx;
padding: 30rpx;
padding: 30rpx 0;
margin-bottom: 20rpx;
@include shadow;
display: flex;
@ -464,18 +531,23 @@
display: flex;
flex-direction: column;
gap: 16rpx;
.item-title {
.item-title{
font-size: 32rpx;
color: $text-primary;
font-weight: 500;
line-height: 1.4;
color:#333;
}
.info{
display: flex;
font-size: 26rpx;
color:#666;
}
.item-author {
font-size: 28rpx;
color: $text-secondary;
line-height: 1.3;
margin-left: 10rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx; /* 限制最大宽度 */
}
.item-stats {
@ -518,10 +590,28 @@
font-weight: 500;
}
.free{
font-size: 26rpx;
color:#999;
}
.yuanjia{
font-size: 26rpx;
margin-left: 10rpx;
color:#999;
.jiaprice{
text-decoration: line-through;
}
}
.priceImg{
margin-top: 4rpx;
}
.price-value {
font-size: 32rpx;
font-size: 26rpx;
color: $primary-color;
font-weight: bold;
.money-unit{
font-size: 28rpx;
}
}
}
}

View File

@ -9,7 +9,7 @@
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#ffffff"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right" @click="submitSchedule">

View File

@ -0,0 +1,130 @@
<template>
<view class="select-page">
<uni-nav-bar
left-icon="left"
title="选择患者"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eee"
>
<template #right>
<view class="confirm-btn" :class="{ active: selectedIds.length > 0 }" @click="confirmSelect">
<text class="confirm-text">确定({{ selectedIds.length }})</text>
</view>
</template>
</uni-nav-bar>
<!-- 搜索框 -->
<view class="search-bar">
<view class="input-wrap">
<input class="search-input" v-model.trim="keyword" placeholder="搜索患者的备注名、昵称或手机号" placeholder-class="ph" @confirm="onSearch" />
</view>
<view class="search-btn" @click="onSearch">
<uni-icons type="search" size="50rpx" color="#999" />
</view>
</view>
<!-- 列表 -->
<scroll-view class="list" scroll-y>
<view class="item" @click="toggle(p.uuid)" v-for="p in patientList" :key="p.uuid">
<image class="avatar" :src="docUrl + (p.photo || '')" mode="aspectFill" />
<view class="name">{{ p.realName || '-' }}</view>
<view class="check" >
<view class="circle" :class="{ active: selectedIds.includes(p.uuid) }"></view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import docUrl from '@/utils/docUrl.js'
import { onShow,onLoad} from "@dcloudio/uni-app";
import api from '@/api/api.js'
const keyword = ref('')
const selectedIds = ref([])
const patientList = ref([])
const selectedDetail = ref([])
const patientListByGBK = async () => {
const res = await api.patientListByGBK();
if(res.code == 1){
patientList.value = res.data;
}
};
onLoad(() => {
})
onShow(() => {
patientListByGBK();
});
const toggle = (id) => {
const i = selectedIds.value.indexOf(id)
if (i > -1) {
selectedIds.value.splice(i, 1)
const di = selectedDetail.value.findIndex(it => it.uuid === id)
if (di > -1) selectedDetail.value.splice(di, 1)
} else {
selectedIds.value.push(id)
const p = patientList.value.find(x => x.uuid === id)
selectedDetail.value.push({ uuid: id, realName: p?.realName || '', photo: p?.photo || '' })
}
}
const onSearch = () => {}
const goBack = () => uni.navigateBack()
const confirmSelect = () => {
const payload = { ids: selectedIds.value, list: selectedDetail.value }
//
try {
const pages = getCurrentPages()
const curr = pages[pages.length - 1]
const ec = curr?.getOpenerEventChannel?.()
ec?.emit && ec.emit('onPatientsSelected', payload)
} catch (e) {}
// 使
try { uni.setStorageSync('patientsSelectedPayload', payload) } catch (e) {}
uni.navigateBack()
}
</script>
<style lang="scss" scoped>
.select-page{
min-height: 100vh; background:#fefefe;
}
.confirm-text{ color:#fff; font-size: 28rpx;white-space: nowrap; }
.confirm-btn{ background:#7f7f7f; padding: 10rpx 18rpx; border-radius: 26rpx; }
.confirm-btn.active{ background:#8B2316; }
.search-bar{
border: 2rpx solid #eee;
margin: 20rpx 30rpx; display:flex; align-items:center; gap: 16rpx;
.input-wrap{ flex:1; background:#fff; border-radius: 12rpx; padding: 16rpx 20rpx; }
.search-input{ font-size: 28rpx; color:#333; }
.ph{ color:#bfbfbf; }
.search-btn{
display: flex;
align-items: center;
justify-content: center;
width: 88rpx; height: 72rpx; background:#fff;
}
}
.list{ border-radius: 12rpx; }
.item{background:#fff; display:flex; align-items:center;padding: 24rpx 30rpx; border-bottom: 2rpx solid #eee; }
.avatar{ width: 96rpx; height:96rpx; border-radius: 16rpx; background:#ffe; }
.name{ flex:1; margin-left: 20rpx; font-size: 32rpx; color:#333; }
.check{ padding-left: 12rpx; }
.circle{ width: 40rpx; height: 40rpx; border-radius: 50%; border: 2rpx solid #cfcfcf; }
.circle.active{ background:#8B2316; border-color:#8B2316; position: relative; }
.circle.active::after{ content:''; position:absolute; left: 14rpx; top: 6rpx; width: 10rpx; height: 18rpx; border: 4rpx solid #fff; border-top: 0; border-left: 0; transform: rotate(45deg); }
</style>

View File

@ -9,7 +9,7 @@
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#ffffff"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right" @click="submitPlan">

View File

@ -263,6 +263,7 @@
import delImg from "@/static/delete_paper.png"
import api from '@/api/api.js'
import docUrl from "@/utils/docUrl.js"
import navTo from '../../utils/navTo';
const total=ref(0);
const tab=ref('zhinan')
//
@ -317,7 +318,9 @@
keywords.value=selectedTags[i].DES
}
navTo({
url:'/pages_app/zhinanList/zhinanList?keywords='+encodeURIComponent(keywords.value)
})
}
isFilterActive.value=true;
@ -366,7 +369,9 @@
//
const loadGuideTags = async () => {
try {
const res = await api.guideTag({});
const res = await api.guideTag({
type:3
});
console.log('指南标签API响应:', res);
if(res && res.code === 200 && res.data) {
// API

View File

@ -10,115 +10,149 @@
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
>
<template v-slot:right>
<view class="nav-right" @click="testLoadMore">
<text style="font-size: 24rpx; color: #8B2316;">测试加载</text>
</view>
</template>
</uni-nav-bar>
<!-- 指南列表 -->
<view class="guidelines-list">
<view
class="guideline-item"
v-for="(item, index) in guidelinesList"
:key="item.uuid || index"
>
<!-- 指南信息 -->
<view class="item-content">
<view class="item-title">{{ item.title }}</view>
<view class="item-bottom">
<view class="item-date">{{ formatDate(item.releaseTime) }}</view>
<!-- 操作按钮 -->
<view class="item-action">
<view
v-if="item.can_download"
class="download-btn"
@click="downloadGuideline(item)"
>
<up-icon name="download" color="#8D2316" size="28"></up-icon>
</view>
<view
v-else
class="view-btn"
@click="viewGuideline(item)"
>
查看
<!-- 使用scroll-view实现列表 -->
<scroll-view
class="guidelines-scroll-view"
scroll-y="true"
:refresher-enabled="true"
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onScrollToLower"
:show-scrollbar="false"
>
<!-- 指南列表 -->
<view class="guidelines-list">
<view
class="guideline-item"
v-for="(item, index) in guidelinesList"
:key="item.uuid || index"
>
<!-- 指南信息 -->
<view class="item-content">
<view class="item-title">{{ item.title }}</view>
<view class="item-bottom">
<view class="item-date">{{ formatDate(item.releaseTime) }}</view>
<!-- 操作按钮 -->
<view class="item-action">
<view
v-if="item.can_download"
class="download-btn"
@click="downloadGuideline(item)"
>
<up-icon name="download" color="#8D2316" size="28"></up-icon>
</view>
<view
v-else
class="view-btn"
@click="viewGuideline(item)"
>
查看
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-status">
<view v-if="isLoading && currentPage === 1" class="loading">
<uni-load-more status="loading" :content-text="loadingText"></uni-load-more>
<!-- 加载更多状态 -->
<view class="load-more-status">
<view v-if="loadMoreStatus === 'loading'" class="loading">
<uni-icons type="spinner-cycle" size="20" color="#999"></uni-icons>
<text>加载中...</text>
</view>
<view v-else-if="loadMoreStatus === 'more'" class="more">
<text>上拉加载更多</text>
</view>
<view v-else-if="loadMoreStatus === 'noMore'" class="no-more">
<text>没有更多数据了</text>
</view>
</view>
<view v-else-if="hasMoreData && !isLoading" class="load-more" @click="loadMoreData">
<uni-load-more status="more" :content-text="loadingText"></uni-load-more>
<!-- 无数据提示 -->
<view class="no-data" v-if="guidelinesList.length === 0 && !isLoading">
<uni-icons type="info" size="60" color="#999"></uni-icons>
<text>暂无诊疗指南数据</text>
</view>
<view v-else-if="!hasMoreData && guidelinesList.length > 0" class="no-more">
<uni-load-more status="noMore" :content-text="loadingText"></uni-load-more>
</view>
</view>
<!-- 无数据提示 -->
<view class="no-data" v-if="guidelinesList.length === 0 && !isLoading">
<uni-icons type="info" size="60" color="#999"></uni-icons>
<text>暂无诊疗指南数据</text>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted} from 'vue'
import api from '@/api/api.js'
import { onShow, onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
import { onShow,onLoad } from "@dcloudio/uni-app";
//
const guidelinesList = ref([])
const isLoading = ref(false)
const hasMoreData = ref(true)
const currentPage = ref(1)
const pageSize = ref(20)
const pageSize = ref(10)
const isRefreshing = ref(false)
const keywords=ref('');
const loadMoreStatus = ref('more') // 'loading', 'more', 'noMore'
//
const loadingText = {
contentdown: '上拉显示更多',
contentrefresh: '正在加载...',
contentnomore: '没有更多数据了'
}
//
onMounted(() => {
loadGuidelinesList()
onLoad((options)=>{
console.log(options)
keywords.value=decodeURIComponent(options.keywords);
console.log(keywords.value);
})
onShow(() => {
//
loadGuidelinesList()
})
//
onPullDownRefresh(() => {
refreshData()
})
//
onReachBottom(() => {
if (hasMoreData.value && !isLoading.value) {
loadMoreData()
//
if (guidelinesList.value.length === 0) {
loadGuidelinesList(true)
}
})
// scroll-view
const onRefresh = async () => {
console.log('scroll-view 下拉刷新触发');
isRefreshing.value = true;
await refreshData();
isRefreshing.value = false;
};
// scroll-view
const onScrollToLower = () => {
console.log('=== onScrollToLower 触发 ===');
console.log('当前状态:', {
loadMoreStatus: loadMoreStatus.value,
isRefreshing: isRefreshing.value,
currentPage: currentPage.value,
listLength: guidelinesList.value.length,
hasMoreData: hasMoreData.value,
isLoading: isLoading.value
});
if (loadMoreStatus.value === 'more' && !isRefreshing.value && !isLoading.value) {
console.log('条件满足,开始加载更多数据');
loadMoreData();
} else {
console.log('条件不满足,跳过加载:', {
loadMoreStatus: loadMoreStatus.value,
isRefreshing: isRefreshing.value,
isLoading: isLoading.value
});
}
};
//
const refreshData = async () => {
if (isRefreshing.value) return
try {
isRefreshing.value = true
currentPage.value = 1
hasMoreData.value = true
loadMoreStatus.value = 'more'
await loadGuidelinesList()
await loadGuidelinesList(true)
uni.showToast({
title: '刷新成功',
@ -130,42 +164,59 @@ const refreshData = async () => {
title: '刷新失败',
icon: 'none'
})
} finally {
isRefreshing.value = false
uni.stopPullDownRefresh()
}
}
//
const loadGuidelinesList = async (isLoadMore = false) => {
const loadGuidelinesList = async (isRefresh = false) => {
console.log('=== loadGuidelinesList 开始 ===');
console.log('当前参数:', { isRefresh, page: currentPage.value, keywords: keywords.value });
if (isLoading.value) return
try {
isLoading.value = true
loadMoreStatus.value = 'loading'
const params = {
page: currentPage.value,
pageSize: pageSize.value,
type: 1, //
keywords: ''
sort:2,
keywords:keywords.value
}
const res = await api.searchLibraryU(params)
console.log('指南列表响应:', res)
if (res.code === 200 && res.data) {
const newData = res.data
const newData = res.data.list || res.data
if (isLoadMore) {
guidelinesList.value = [...guidelinesList.value, ...newData]
} else {
if (isRefresh) {
guidelinesList.value = newData
console.log('刷新模式:替换列表数据');
} else {
guidelinesList.value = [...guidelinesList.value, ...newData]
console.log('加载更多:追加数据到列表');
}
//
hasMoreData.value = newData.length === pageSize.value
if (newData.length < pageSize.value) {
loadMoreStatus.value = 'noMore'
hasMoreData.value = false
console.log('没有更多数据了');
} else {
loadMoreStatus.value = 'more'
hasMoreData.value = true
//
if (!isRefresh) {
currentPage.value++
console.log(`还有更多数据,下一页: ${currentPage.value}`);
}
}
} else {
console.error('加载指南列表失败:', res.message)
loadMoreStatus.value = 'noMore'
uni.showToast({
title: res.message || '加载失败',
icon: 'none'
@ -173,20 +224,36 @@ const loadGuidelinesList = async (isLoadMore = false) => {
}
} catch (error) {
console.error('加载指南列表异常:', error)
loadMoreStatus.value = 'noMore'
uni.showToast({
title: '网络异常,请重试',
icon: 'none'
})
} finally {
isLoading.value = false
console.log('=== loadGuidelinesList 结束 ===');
console.log('最终状态:', {
loadMoreStatus: loadMoreStatus.value,
page: currentPage.value,
listLength: guidelinesList.value.length
});
}
}
//
const loadMoreData = () => {
console.log('=== loadMoreData 被调用 ===');
console.log('检查条件:', {
hasMoreData: hasMoreData.value,
isLoading: isLoading.value,
currentPage: currentPage.value
});
if (hasMoreData.value && !isLoading.value) {
currentPage.value++
loadGuidelinesList(true)
console.log('条件满足,开始加载更多数据');
loadGuidelinesList(false);
} else {
console.log('条件不满足,跳过加载更多');
}
}
@ -236,6 +303,28 @@ const goBack = () => {
uni.navigateBack()
}
//
const testLoadMore = () => {
console.log('=== 手动测试加载更多 ===');
console.log('当前状态:', {
loadMoreStatus: loadMoreStatus.value,
hasMoreData: hasMoreData.value,
isLoading: isLoading.value,
currentPage: currentPage.value,
listLength: guidelinesList.value.length
});
if (loadMoreStatus.value === 'more' && !isLoading.value) {
loadMoreData();
} else {
uni.showToast({
title: `状态: ${loadMoreStatus.value}, 加载中: ${isLoading.value}`,
icon: 'none',
duration: 2000
});
}
}
//
const formatDate = (dateString) => {
if (!dateString) return ''
@ -264,7 +353,12 @@ $white: #ffffff;
.zhinan-list-page {
background-color: $bg-color;
min-height: 100vh;
height: 100vh;
}
.guidelines-scroll-view {
height: calc(100vh - 140rpx);
background-color: $bg-color;
}
//
@ -291,6 +385,11 @@ $white: #ffffff;
line-height: 1.5;
margin-bottom: 16rpx;
font-weight: 500;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.item-bottom {
@ -341,18 +440,22 @@ $white: #ffffff;
}
}
//
.loading-status {
//
.load-more-status {
padding: 20rpx 0;
text-align: center;
.loading, .load-more, .no-more {
.loading, .more, .no-more {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 20rpx 0;
font-size: 26rpx;
color: $text-light;
}
}
//
.no-data {
display: flex;

BIN
static/iv_delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB