This commit is contained in:
zoujiandong 2025-09-01 16:25:46 +08:00
parent 7270482bd9
commit 82e0a5c10d
14 changed files with 3089 additions and 464 deletions

View File

@ -81,13 +81,6 @@ const api = {
return request('/expertAPI/my', data, 'post', false);
},
getVideoList(data) {
return request('/video/list', data, 'get', true);
},
getVideoDetail(data) {
return request('/video/detail', data, 'get', true);
},
// 新闻详情
getNewsDetail(data) {
return request('/expertAPI/newsDetail', data, 'post', false);
@ -160,6 +153,63 @@ const api = {
return request('/expertAPI/addBonusPointsN', data, 'post', false);
},
//视频顶部轮播
videoRoll(data){
return request('/expertAPI/videoRoll', data, 'post', false);
},
//视频标签
videoTagList(data){
return request('/expertAPI/videoTagList', data, 'post', false);
},
//视频首页
videoIndexN(data){
return request('/expertAPI/videoIndexN', data, 'post', false);
},
//视频类型
expertVideoTypeList(data){
return request('/expertAPI/expertVideoTypeList', data, 'post', false);
},
//视频搜索
videoByKeyWordsNew(data){
return request('/expertAPI/videoByKeyWordsNew', data, 'post', false);
},
// 指南杂志 - 治疗指南列表
searchLibraryU(data){
return request('/expertAPI/searchLibraryU', data, 'post', false);
},
// 指南杂志 - 治疗指南最火TOP10
top10ByType(data){
return request('/expertAPI/top10ByType', data, 'post', false);
},
// 指南杂志 - 治疗指南分类
guideType(data){
return request('/expertAPI/guideType', data, 'post', false);
},
//指南标签
guideTag(data){
return request('/expertApp/tagList', data, 'post', false);
},
// 肝胆新闻相关API
// 顶部轮播
newsRollNew(data){
return request('/expertAPI/newsRollNew', data, 'post', false);
},
// 新闻列表
defaultNewsListNew(data){
return request('/expertAPI/defaultNewsListNew', data, 'post', false);
},
// 根据标签查询新闻列表
newsListNew(data){
return request('/expertAPI/newsListNew', data, 'post', false);
},
// 新闻标签列表
newsTagList(data){
return request('/expertAPI/newsTagList', data, 'post', false);
},
}
export default api

View File

@ -2,15 +2,14 @@
"name": "uniapp",
"version": "1.0.0",
"main": "main.js",
"scripts": {
},
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"crypto-js": "^4.2.0",
"dayjs": "^1.11.18",
"js-base64": "^3.7.8",
"js-md5": "^0.8.3",
"uview-plus": "^3.4.73"

View File

@ -511,6 +511,46 @@
}
}
},
{
"path": "zhinanList/zhinanList",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "newsList/newsList",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "personInfo/personInfo",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "pptDetail/pptDetail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "patientMsg/patientMsg",
"style": {

View File

@ -14,7 +14,7 @@
<!-- Fixed Banner Swiper -->
<view class="banner-container">
<view class="swipemask">
<view class="banner-subtitle">111111111</view>
<view class="banner-subtitle">{{bannerList[currentBannerIndex].summary}}</view>
<view class="dotbox">
<view class="circle" :class="{active:currentBannerIndex==index} "v-for="(banner, index) in bannerList"
:key="banner.id"></view>
@ -39,19 +39,12 @@
>
<view class="banner-item">
<image
style="width:100%;"
class="banner-image"
:src="banner.thumbnail || '/static/big_background_my.png'"
mode="aspectFill"
:src="docUrl +banner.headImg"
mode="widthFix"
></image>
<view class="banner-content">
<view class="doctor-avatar" v-if="banner.doctorAvatar">
<image :src="banner.doctorAvatar" mode="aspectFill"></image>
</view>
<view class="banner-info">
<view class="banner-title">{{banner.title}}</view>
<view class="banner-subtitle">{{banner.subtitle || '专业医学视频教育平台'}}</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
@ -59,7 +52,7 @@
<scroll-view class="news-list" scroll-y="true">
<!-- 肝胆新闻 -->
<view class="news-item" @click="goToNews('hepatoBiliaryNews')">
<view class="news-item" @click="goToNews('news')">
<view class="news-icon blue">
<uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
</view>
@ -73,7 +66,7 @@
</view>
<!-- 互动圈 -->
<view class="news-item" @click="goToNews('interactionCircle')">
<!-- <view class="news-item" @click="goToNews('interactionCircle')">
<view class="news-icon purple">
<uni-icons type="redo" size="24" color="#fff"></uni-icons>
</view>
@ -84,10 +77,10 @@
<view class="news-arrow">
<uni-icons type="forward" size="16" color="#ccc"></uni-icons>
</view>
</view>
</view> -->
<!-- 科研项目 -->
<view class="news-item" @click="goToNews('researchProjects')">
<!-- <view class="news-item" @click="goToNews('researchProjects')">
<view class="news-icon orange">
<uni-icons type="star" size="24" color="#fff"></uni-icons>
</view>
@ -99,9 +92,9 @@
<uni-icons type="forward" size="16" color="#ccc"></uni-icons>
</view>
</view>
-->
<!-- 临床招募 -->
<view class="news-item" @click="goToNews('clinicalRecruitment')">
<!-- <view class="news-item" @click="goToNews('clinicalRecruitment')">
<view class="news-icon green">
<uni-icons type="personadd" size="24" color="#fff"></uni-icons>
</view>
@ -112,7 +105,7 @@
<view class="news-arrow">
<uni-icons type="forward" size="16" color="#ccc"></uni-icons>
</view>
</view>
</view> -->
<!-- 肝胆会议 -->
<view class="news-item" @click="goToCourse('hepatoBiliaryConference')">
@ -129,7 +122,7 @@
</view>
<!-- 肝胆活动 -->
<view class="news-item" @click="goToNews('hepatoBiliaryActivities')">
<!-- <view class="news-item" @click="goToNews('hepatoBiliaryActivities')">
<view class="news-icon pink">
<uni-icons type="gift" size="24" color="#fff"></uni-icons>
</view>
@ -140,18 +133,48 @@
<view class="news-arrow">
<uni-icons type="forward" size="16" color="#ccc"></uni-icons>
</view>
</view>
</view> -->
</scroll-view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import api from '@/api/api.js';
import docUrl from "@/utils/docUrl.js";
import navTo from '../../utils/navTo';
onShow(()=>{
bannerList.value = getMockBannerData();
})
onMounted(() => {
loadNewsBanner();
});
onShow(() => {
loadNewsBanner();
});
//
const loadNewsBanner = async () => {
try {
console.log(api)
const res = await api.newsRollNew({});
console.log('新闻轮播响应:', res);
if(res && res.code === 200) {
bannerList.value = res.data || [];
console.log('获取到轮播数据:', bannerList.value);
} else {
console.error('加载新闻轮播失败:', res.message);
// 使
bannerList.value = getMockBannerData();
}
} catch (e) {
console.error('加载新闻轮播异常:', e);
// 使
bannerList.value = getMockBannerData();
}
};
// Banner
const bannerList = ref([]);
const currentBannerIndex = ref(0);
@ -204,21 +227,17 @@
//
const goToNews = (type) => {
console.log('跳转到新闻类型:', type);
//
uni.showToast({
title: '功能开发中',
icon: 'none'
});
if(type=="news"){
navTo({
url:'/pages_app/newsList/newsList'
})
}
};
const goToCourse = (type) => {
console.log('跳转到课程类型:', type);
//
uni.showToast({
title: '功能开发中',
icon: 'none'
});
navTo({
url:'/pages/live/live'
})
};
</script>
@ -271,6 +290,10 @@
.banner-subtitle {
font-size: 24rpx;
color: rgba(255,255,255,0.8);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 60%;
}
.dotbox{

View File

@ -0,0 +1,718 @@
<template>
<view class="news-list-page">
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
title="肝胆新闻"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
<!-- 导航标签 -->
<view class="nav-tabs">
<scroll-view class="tabs-scroll" scroll-x="true" :show-scrollbar="false">
<view class="tabs-container">
<view
class="tab-item"
:class="{ active: activeTab === index }"
v-for="(tab, index) in tabs"
:key="index"
@click="changeTab(index)"
>
<text>{{ tab.NAME }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 新闻列表 -->
<scroll-view
class="news-scroll-view"
scroll-y="true"
:refresher-enabled="true"
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onScrollToLower"
:show-scrollbar="false"
>
<!-- 顶部轮播 -->
<view class="banner-container">
<view class="swipemask">
<view class="banner-subtitle" v-if="bannerList.length>0">{{bannerList[currentBannerIndex].summary}}</view>
<view class="dotbox">
<view class="circle" :class="{active:currentBannerIndex==index} "v-for="(banner, index) in bannerList"
:key="banner.id"></view>
</view>
</view>
<swiper
class="banner-swiper"
:indicator-dots="false"
:autoplay="true"
:interval="5000"
:duration="300"
indicator-color="rgba(255,255,255,0.4)"
indicator-active-color="#fff"
circular
@change="onSwiperChange"
>
<swiper-item
v-for="(banner, index) in bannerList"
:key="banner.id"
@click="playBannerVideo(banner)"
>
<view class="banner-item">
<image
style="width:100%;"
class="banner-image"
:src="docUrl +banner.headImg"
mode="widthFix"
></image>
</view>
</swiper-item>
</swiper>
</view>
<view class="news-list">
<view
class="news-item"
v-for="(news, index) in newsList"
:key="news.uuid || index"
@click="goToNewsDetail(news)"
>
<view class="item-image-container">
<image
class="item-image"
:src="docUrl + news.headImg"
mode="aspectFill"
></image>
<view class="important-tag" v-if="news.isImportant">重要通知</view>
</view>
<view class="item-content">
<view class="item-title">{{ news.title }}</view>
<view class="item-meta">
<text class="item-date">{{ formatDate(news.createDate) }}</text>
<view class="item-stats">
<uni-icons type="eye" size="14" color="#999"></uni-icons>
<text>{{ news.readnum || 0 }}</text>
<uni-icons type="hand-up" size="14" color="#999" style="margin-left: 20rpx;"></uni-icons>
<text>{{ news.likenum || 0 }}</text>
</view>
</view>
</view>
</view>
<!-- 加载更多/无数据提示 -->
<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>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted, reactive } from 'vue';
import { onShow, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app';
import api from '@/api/api.js';
import docUrl from '@/utils/docUrl.js';
import dayjs from 'dayjs';
// Banner
const bannerList = ref([]);
const currentBannerIndex = ref(0);
const newstagid=ref('');
//
const activeTab = ref(-1); // '' is the second tab (index 1)
const tabs = ref(['精选', '肝胆新闻', '技术快讯', '基地动态', '专家动态']);
const newsList = ref([]);
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const loadMoreStatus = ref('more'); // more, loading, noMore
const isRefreshing = ref(false); //
const loadMoreText = reactive({
contentdown: '上拉显示更多',
contentrefresh: '正在加载...',
contentnomore: '没有更多数据了'
});
const newsTags = ref([]); //
onShow(() => {
loadNewsBanner();
loadNewsTags(); //
if(activeTab.value<0){
//
loadNewsListIndex();
}else{
loadNewsList();
}
});
const onSwiperChange = (e) => {
currentBannerIndex.value = e.detail.current;
};
// 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: page.value,
listLength: newsList.value.length
});
if (loadMoreStatus.value === 'more' && !isRefreshing.value) {
console.log('条件满足,开始加载更多数据');
if(activeTab.value<0){
//
loadNewsListIndex();
}else{
loadNewsList();
}
} else {
console.log('条件不满足,跳过加载:', {
loadMoreStatus: loadMoreStatus.value,
isRefreshing: isRefreshing.value
});
}
};
//
const goBack = () => {
uni.navigateBack();
};
const goToSearch = () => {
// Implement navigation to search page
console.log('Go to search page');
};
const changeTab = (index) => {
activeTab.value = index;
// tabs
if (tabs.value && tabs.value.length > 0 && tabs.value[index]) {
newstagid.value = tabs.value[index].ID ;
} else {
newstagid.value = '';
console.warn('标签数据不完整无法设置newstagid');
}
page.value = 1; // Reset page when changing tab
newsList.value = []; // Clear existing list
loadNewsList(true); // Load new data for the selected tab
};
//
const loadNewsBanner = async () => {
try {
const res = await api.newsRollNew({});
console.log('新闻轮播响应:', res);
if (res && res.code === 200) {
bannerList.value = res.data || [];
console.log('获取到轮播数据:', bannerList.value);
} else {
console.error('加载新闻轮播失败:', res.message);
// Fallback to mock data if API fails
}
} catch (e) {
console.error('加载新闻轮播异常:', e);
// Fallback to mock data if API fails
}
};
//
const loadNewsTags = async () => {
try {
const res = await api.newsTagList({});
console.log('新闻标签响应:', res);
if (res && res.code === 200) {
console.log('获取到标签数据:', res.data);
tabs.value = res.data;
//
if (res.data && Array.isArray(res.data) && res.data.length > 0) {
newstagid.value = res.data[0].ID;
//loadNewsList(true); // Initial load
} else {
console.warn('标签数据为空或格式不正确');
newstagid.value = '';
}
} else {
console.error('加载新闻标签失败:', res.message);
}
} catch (e) {
console.error('加载新闻标签异常:', e);
}
};
//
const loadNewsList = async (isRefresh = false) => {
console.log('=== loadNewsList 开始 ===');
console.log('当前参数:', { isRefresh, page: page.value, newstagid: newstagid.value });
// newstagid
if (isRefresh) {
page.value = 1;
newsList.value = [];
loadMoreStatus.value = 'more';
console.log('刷新模式:重置分页状态');
}
//
if (loadMoreStatus.value === 'noMore' || loadMoreStatus.value === 'loading') {
console.log('当前状态不允许加载:', loadMoreStatus.value);
return;
}
//
loadMoreStatus.value = 'loading';
console.log(`开始加载第${page.value}页数据newstagid: ${newstagid.value}`);
try {
const res = await api.newsListNew({
page: page.value,
pageSize: pageSize.value,
newstagid: newstagid.value
});
console.log('新闻列表API响应:', res);
if (res && res.code === 200) {
//
let newItems = [];
let totalCount = 0;
if (res.data && res.data.list) {
newItems = res.data.list;
totalCount = res.data.totalRow;
console.log('使用 res.data.list 结构');
}
console.log('解析后的数据:', { newItems, totalCount, newItemsLength: newItems.length });
// newItems
if (Array.isArray(newItems)) {
if (isRefresh) {
newsList.value = newItems;
console.log('刷新模式:替换列表数据');
} else {
newsList.value = [...newsList.value, ...newItems];
console.log('加载更多:追加数据到列表');
}
total.value = totalCount;
console.log(`当前页数据: ${newItems.length}, 总数: ${total.value}, 当前列表长度: ${newsList.value.length}`);
//
if (newItems.length < pageSize.value || newsList.value.length >= total.value) {
loadMoreStatus.value = 'noMore';
console.log('没有更多数据了');
} else {
loadMoreStatus.value = 'more';
page.value++;
console.log(`还有更多数据,下一页: ${page.value}`);
}
} else {
console.error('API返回的数据不是数组格式:', newItems);
loadMoreStatus.value = 'noMore';
}
} else {
console.error('加载新闻列表失败:', res.message);
loadMoreStatus.value = 'noMore';
}
} catch (e) {
console.error('加载新闻列表异常:', e);
loadMoreStatus.value = 'noMore';
}
console.log('=== loadNewsList 结束 ===');
console.log('最终状态:', {
loadMoreStatus: loadMoreStatus.value,
page: page.value,
listLength: newsList.value.length
});
};
const loadNewsListIndex = async (isRefresh = false) => {
console.log('=== loadNewsList 开始 ===');
console.log('当前参数:', { isRefresh, page: page.value, newstagid: newstagid.value });
// newstagid
if (isRefresh) {
page.value = 1;
newsList.value = [];
loadMoreStatus.value = 'more';
console.log('刷新模式:重置分页状态');
}
//
if (loadMoreStatus.value === 'noMore' || loadMoreStatus.value === 'loading') {
console.log('当前状态不允许加载:', loadMoreStatus.value);
return;
}
//
loadMoreStatus.value = 'loading';
console.log(`开始加载第${page.value}页数据newstagid: ${newstagid.value}`);
try {
const res = await api.defaultNewsListNew({
page: page.value,
pageSize: pageSize.value,
});
console.log('新闻列表API响应:', res);
if (res && res.code === 200) {
//
let newItems = [];
let totalCount = 0;
if (res.data && res.data.list) {
newItems = res.data.list;
totalCount = res.data.totalRow || 0;
console.log('使用 res.data.list 结构');
}
console.log('解析后的数据:', { newItems, totalCount, newItemsLength: newItems.length });
// newItems
if (Array.isArray(newItems)) {
if (isRefresh) {
newsList.value = newItems;
console.log('刷新模式:替换列表数据');
} else {
newsList.value = [...newsList.value, ...newItems];
console.log('加载更多:追加数据到列表');
}
total.value = totalCount;
console.log(`当前页数据: ${newItems.length}, 总数: ${total.value}, 当前列表长度: ${newsList.value.length}`);
//
if (newItems.length < pageSize.value || newsList.value.length >= total.value) {
loadMoreStatus.value = 'noMore';
console.log('没有更多数据了');
} else {
loadMoreStatus.value = 'more';
page.value++;
console.log(`还有更多数据,下一页: ${page.value}`);
}
} else {
console.error('API返回的数据不是数组格式:', newItems);
loadMoreStatus.value = 'noMore';
}
} else {
console.error('加载新闻列表失败:', res.message);
loadMoreStatus.value = 'noMore';
}
} catch (e) {
console.error('加载新闻列表异常:', e);
loadMoreStatus.value = 'noMore';
}
console.log('=== loadNewsList 结束 ===');
console.log('最终状态:', {
loadMoreStatus: loadMoreStatus.value,
page: page.value,
listLength: newsList.value.length
});
};
const refreshData = async () => {
console.log('Refreshing data...');
//
page.value = 1;
newsList.value = [];
loadMoreStatus.value = 'more';
if(activeTab.value<0){
//
await loadNewsListIndex(true);
}else{
await loadNewsList(true);
}
uni.showToast({
title: '刷新成功',
icon: 'success'
});
};
const goToNewsDetail = (item) => {
// Implement navigation to news detail page
console.log('Go to news detail:', item);
uni.navigateTo({
url: `/pages_app/newsDetail/newsDetail?id=${item.uuid || item.id}`
});
};
const formatDate = (dateString) => {
if (!dateString) return '';
try {
// 使 dayjs
return dayjs(dateString).format('YYYY-MM-DD');
} catch (error) {
console.error('日期格式化失败:', error, dateString);
return '';
}
};
</script>
<style lang="scss" scoped>
.news-list-page {
background-color: #f8f8f8;
height: 100vh;
overflow-y: hidden;
}
.uni-navbar {
/* Custom styles for uni-nav-bar if needed */
.uni-navbar__content {
height: 44px; // Standard navbar height
}
}
.nav-tabs {
background-color: #ffffff;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
position: sticky;
top: calc(var(--status-bar-height) + 44px); // Below navbar
z-index: 99;
.tabs-scroll {
width: 100%;
white-space: nowrap;
}
.tabs-container {
display: flex;
padding: 0 20rpx;
min-width: 100%;
}
.tab-item {
flex-shrink: 0;
padding: 10rpx 30rpx;
font-size: 28rpx;
color: #666;
position: relative;
margin-right: 20rpx;
&:last-child {
margin-right: 0;
}
&.active {
color: #ff4757;
font-weight: bold;
&::after {
content: '';
position: absolute;
bottom: -10rpx;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 4rpx;
background-color: #ff4757;
border-radius: 2rpx;
}
}
}
}
/* Fixed Banner Container */
.banner-container {
position: relative;
background-color: #fff;
.swipemask{
width:100%;
z-index:3;
box-sizing:border-box;
position: absolute;
bottom:0;
padding:0 20rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: space-between;
background-color: rgba(0,0,0,0.7);
.banner-subtitle {
font-size: 24rpx;
color: rgba(255,255,255,0.8);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 60%;
}
.dotbox{
display:flex;
align-items: center;
.circle{
margin:0 4rpx;
width:16rpx;
height:16rpx;
background-color: #eee;
border-radius: 50%;
}
.circle.active{
background-color: #8B2316;
}
}
}
}
.news-scroll-view {
position: fixed;
bottom:0;
width:100%;
top:200rpx;
height: calc(100vh - 200rpx); // banner
background-color: #f8f8f8;
}
.news-list {
padding: 20rpx;
background-color: #f8f8f8;
.news-item {
display: flex;
background-color: #ffffff;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
overflow: hidden; // Ensure content respects border-radius
.item-image-container {
flex-shrink: 0;
width: 200rpx;
height: 160rpx;
border-radius: 8rpx;
overflow: hidden;
margin-right: 20rpx;
position: relative;
background-color: #f0f0f0; // Placeholder background
.item-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.important-tag {
position: absolute;
top: 0;
left: 0;
background-color: #ff4757;
color: #fff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-bottom-right-radius: 8rpx;
z-index: 1;
}
}
.item-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-title {
font-size: 32rpx;
font-weight: normal; // Removed bold as per previous request
color: #333;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; // Fixed two-line height
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 10rpx;
}
.item-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 24rpx;
color: #999;
.item-date {
flex-shrink: 0;
}
.item-stats {
display: flex;
align-items: center;
text {
margin-left: 8rpx;
}
}
}
}
}
.load-more-status {
text-align: center;
padding: 40rpx 0;
color: #999;
font-size: 28rpx;
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
}
.more {
color: #666;
}
.no-more {
color: #ccc;
}
}
</style>

View File

@ -0,0 +1,280 @@
<template>
<view class="person-page">
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
title="个人资料"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
<scroll-view class="content" scroll-y>
<!-- 基本资料分组 -->
<view class="section-header">基本资料</view>
<!-- 头像 -->
<view class="row" @click="onChooseAvatar">
<view class="label"><text>头像</text><text class="req">*</text></view>
<view class="value value-avatar">
<image :src="form.avatar" mode="aspectFill" class="avatar" />
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 姓名 -->
<view class="row">
<view class="label"><text>姓名</text><text class="req">*</text></view>
<view class="value">{{ form.name }}</view>
</view>
<!-- 性别 -->
<view class="row" @click="onPickGender">
<view class="label"><text>性别</text><text class="req">*</text></view>
<view class="value with-arrow">
<text>{{ form.gender }}</text>
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 出生日期 -->
<view class="row" @click="onPickBirthday">
<view class="label"><text>出生日期</text></view>
<view class="value with-arrow">
<text>{{ form.birthday || '' }}</text>
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 手机号码 -->
<view class="row" @click="onEditPhone">
<view class="label"><text>手机号</text><text class="req">*</text></view>
<view class="value with-arrow">
<text>{{ form.mobile }}</text>
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 邮箱 -->
<view class="row" @click="onEditEmail">
<view class="label"><text>邮箱</text></view>
<view class="value with-arrow">
<text>{{ form.email || '' }}</text>
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 专业资料分组 -->
<view class="section-header">专业资料</view>
<!-- 医院 -->
<view class="row" @click="onPickHospital">
<view class="label"><text>医院</text><text class="req">*</text></view>
<view class="value with-arrow">
<text>{{ form.hospital }}</text>
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 科室 -->
<view class="row" @click="onPickDept">
<view class="label"><text>科室</text><text class="req">*</text></view>
<view class="value with-arrow">
<text>{{ form.department }}</text>
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 科室电话 -->
<view class="row" @click="onEditDeptPhone">
<view class="label"><text>科室电话</text><text class="req">*</text></view>
<view class="value with-arrow">
<text>{{ form.departmentPhone }}</text>
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 职称 -->
<view class="row" @click="onPickTitle">
<view class="label"><text>职称</text></view>
<view class="value with-arrow">
<text>{{ form.title }}</text>
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 执业医师证编号 -->
<view class="row" @click="onEditLicenseNo">
<view class="label"><text>执业医师证编号</text><text class="req">*</text></view>
<view class="value with-arrow">
<text>{{ form.licenseNo }}</text>
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 执业医师证照片/胸牌 -->
<view class="row" @click="onChooseLicenseImg">
<view class="label"><text>执业医师证图片或胸牌</text><text class="req">*</text></view>
<view class="value value-license">
<image :src="form.licenseImg" mode="aspectFill" class="license-thumb" />
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 专长 -->
<view class="row" @click="onEditSpecialty">
<view class="label"><text>专长</text></view>
<view class="value with-arrow">
<text>{{ form.specialty }}</text>
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
<!-- 个人简介 -->
<view class="row" @click="onEditIntro">
<view class="label"><text>个人简介</text></view>
<view class="value with-arrow">
<uni-icons type="right" size="18" color="#bbb" />
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from "@dcloudio/uni-app";
const form = ref({
avatar: '/static/big_background_my.png',
name: '邱建东',
gender: '男',
birthday: '',
mobile: '17600668628',
email: '',
hospital: '北京博爱医院',
department: '肝病科',
departmentPhone: '1234567890',
title: '主任中医师',
licenseNo: '12345678',
licenseImg: '/static/big_background_my.png',
specialty: '肝炎、肝硬化'
});
const goBack = () => uni.navigateBack();
//
const openCropper = (callback, opts = {}) => {
const { destWidth = 600, rectWidth = 300, fileType = 'jpg' } = opts;
const url = `/uni_modules/uview-plus/components/u-avatar-cropper/u-avatar-cropper?destWidth=${destWidth}&rectWidth=${rectWidth}&fileType=${fileType}`;
uni.navigateTo({
url,
success: (res) => {
res.eventChannel.on('uAvatarCropper', (path) => {
if (typeof callback === 'function') callback(path);
});
}
});
};
const onChooseAvatar = () => {
openCropper((path) => {
form.value.avatar = path;
});
};
const onChooseLicenseImg = () => {
openCropper((path) => {
form.value.licenseImg = path;
}, { destWidth: 1000, rectWidth: 500 });
};
const onPickGender = () => {};
const onPickBirthday = () => {};
const onEditPhone = () => {};
const onEditEmail = () => {};
const onPickHospital = () => {};
const onPickDept = () => {};
const onEditDeptPhone = () => {};
const onPickTitle = () => {};
const onEditLicenseNo = () => {};
const onEditSpecialty = () => {};
const onEditIntro = () => {};
onShow(() => {
//
});
</script>
<style lang="scss" scoped>
.person-page {
background-color: #ffffff;
height: 100vh;
}
.content {
position: fixed;
top: calc(var(--status-bar-height) + 44px);
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
}
.section-header {
background-color: #d9d9d9;
color: #8B2316;
font-size: 28rpx;
padding: 18rpx 24rpx;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 24rpx;
border-bottom: 1px solid #f2f2f2;
}
.label {
display: flex;
align-items: center;
font-size: 30rpx;
color: #222;
}
.req {
color: #e44d3a;
margin-left: 8rpx;
}
.value {
flex-shrink: 0;
display: flex;
align-items: center;
color: #666;
font-size: 30rpx;
}
.with-arrow text {
margin-right: 14rpx;
}
.value-avatar {
gap: 14rpx;
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 12rpx;
background-color: #f5f5f5;
}
.value-license {
gap: 14rpx;
}
.license-thumb {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
background-color: #f5f5f5;
}
</style>

View File

@ -16,7 +16,7 @@
<!-- 排序和筛选栏 -->
<view class="filter-bar">
<view class="filter-item" @click="toggleSort">
<text class="filter-text">最新</text>
<text class="filter-text">{{sort==2?'最新':'最热'}}</text>
<view class="newbox">
<up-image :src="upImg" width="20rpx" height="26rpx" ></up-image>
</view>
@ -104,7 +104,9 @@
import downImg from "@/static/cb_up.png"
import tougaoImg from "@/static/kejiantougao.png"
import downLoadImg from "@/static/wdxz.png"
import api from '@/api/api.js';
const isFilterActive=ref(false)
const sort = ref(2);
//
const refreshing = ref(false);
const loading = ref(false);
@ -113,44 +115,7 @@
const pageSize = ref(10);
//
const coursewareList = ref([
{
title: "颜学兵: 广东省基孔肯雅热诊疗指引",
author: "颜学兵 徐州医科大学附属医院",
views: 179,
price: "10.0"
},
{
title: "颜学兵: 佛山市基孔肯雅热中医诊治推荐用方",
author: "颜学兵 徐州医科大学附属医院",
views: 122,
price: "10.0"
},
{
title: "抗菌药物的临床应用",
author: "肝胆相照官方账号 北京肝胆相照公益基金会",
views: 656,
price: "15.0"
},
{
title: "中国肝癌多学科综合治疗专家共识 (2025)",
author: "肝胆相照官方账号 北京肝胆相照公益基金会",
views: 208,
price: "15.0"
},
{
title: "耐药结核病全口服短程治疗专家共识 (2025)",
author: "肝胆相照官方账号 北京肝胆相照公益基金会",
views: 256,
price: "15.0"
},
{
title: "结核分枝杆菌合并乙型肝炎病毒治专家共识",
author: "肝胆相照官方账号 北京肝胆相照公益基金会",
views: 358,
price: "15.0"
}
]);
const coursewareList = ref([]);
//
const goBack = () => {
@ -171,10 +136,7 @@
};
const toggleSort = () => {
uni.showToast({
title: '排序功能',
icon: 'none'
});
sort.value=sort.value==1?2:1
};
@ -192,110 +154,141 @@
icon: 'none'
});
};
// API
const mockApiData = [
{
id: 7,
title: "肝胆疾病影像学诊断指南",
author: "影像科专家团队 北京协和医院",
views: 445,
price: "20.0"
},
{
id: 8,
title: "肝硬化并发症治疗新进展",
author: "消化内科专家 上海交通大学医学院",
views: 332,
price: "18.0"
},
{
id: 9,
title: "肝胆外科手术技巧精要",
author: "外科专家 中山大学附属第一医院",
views: 567,
price: "25.0"
},
{
id: 10,
title: "肝胆疾病护理规范",
author: "护理专家团队 中国护理学会",
views: 289,
price: "12.0"
}
];
//
const onRefresh = async () => {
refreshing.value = true;
const onRefresh = async () => {
refreshing.value = true;
try {
//
page.value = 1;
noMore.value = false;
try {
// API
await new Promise(resolve => setTimeout(resolve, 1000));
//
page.value = 1;
noMore.value = false;
//
await loadData(true);
uni.showToast({
title: '刷新成功',
icon: 'success'
});
} catch (error) {
uni.showToast({
title: '刷新失败',
icon: 'error'
});
} finally {
refreshing.value = false;
}
};
//
await loadData(true);
uni.showToast({
title: '刷新成功',
icon: 'success'
});
} catch (error) {
uni.showToast({
title: '刷新失败',
icon: 'error'
});
} finally {
refreshing.value = false;
}
};
//
const onLoadMore = async () => {
console.log('加载更多')
if (loading.value || noMore.value) return;
loading.value = true;
try {
// API
await new Promise(resolve => setTimeout(resolve, 800));
//
await loadData(false);
} catch (error) {
uni.showToast({
title: '加载失败',
icon: 'error'
});
} finally {
loading.value = false;
}
};
//
const onLoadMore = async () => {
console.log('加载更多')
if (loading.value || noMore.value) return;
loading.value = true;
try {
//
await loadData(false);
} catch (error) {
uni.showToast({
title: '加载失败',
icon: 'error'
});
} finally {
loading.value = false;
}
};
//
const loadData = async (isRefresh = false) => {
// API
const startIndex = (page.value - 1) * pageSize.value;
const endIndex = startIndex + pageSize.value;
const newData = mockApiData.slice(startIndex, endIndex);
//
const loadData = async (isRefresh = false) => {
if (isRefresh) {
page.value = 1;
coursewareList.value = [];
noMore.value = false;
}
if (loading.value || noMore.value) return;
loading.value = true;
try {
console.log(`开始加载第${page.value}页课件数据`);
if (isRefresh) {
//
coursewareList.value = coursewareList.value.slice(0, 6); // 6
} else {
//
if (newData.length > 0) {
coursewareList.value.push(...newData);
page.value++;
// API
const res = await api.ganDanFileByKeyWords({
page: page.value,
pageSize: pageSize.value,
sort: sort.value,
//
keywords: '',
title: '',
// fileType: selectedFileType.value,
// sortBy: sortType.value
});
console.log('课件列表API响应:', res);
if (res && res.code === 200) {
let newItems = [];
let totalCount = 0;
// API
if (res.data && res.data.list) {
newItems = res.data.list;
totalCount = res.data.total || res.data.totalRow || 0;
console.log('使用 res.data.list 结构');
} else if (res.data && Array.isArray(res.data)) {
newItems = res.data;
totalCount = res.total || 0;
console.log('使用 res.data 数组结构');
} else {
console.error('无法识别的数据结构:', res.data);
noMore.value = true;
return;
}
console.log('解析后的数据:', { newItems, totalCount, newItemsLength: newItems.length });
if (Array.isArray(newItems)) {
if (isRefresh) {
coursewareList.value = newItems;
console.log('刷新模式:替换列表数据');
} else {
coursewareList.value.push(...newItems);
console.log('加载更多:追加数据到列表');
}
//
if (newItems.length < pageSize.value || coursewareList.value.length >= totalCount) {
noMore.value = true;
console.log('没有更多数据了');
} else {
page.value++;
console.log(`还有更多数据,下一页: ${page.value}`);
}
} else {
console.error('API返回的数据不是数组格式:', newItems);
noMore.value = true;
}
} else {
console.error('加载课件列表失败:', res.message);
uni.showToast({
title: res.message || '加载失败',
icon: 'error'
});
}
};
} catch (error) {
console.error('加载课件列表异常:', error);
uni.showToast({
title: '网络异常,请重试',
icon: 'error'
});
} finally {
loading.value = false;
}
};
const showFilter = ref(false)
//
@ -340,7 +333,9 @@
//
};
onShow(() => {
//
//
console.log('页面显示,开始加载课件数据');
loadData(true);
});
</script>
<style lang="scss" scoped>
@ -634,4 +629,39 @@
}
}
}
//
.loading-state,
.empty-state,
.load-more-state,
.no-more-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 20rpx;
color: $text-secondary;
font-size: 28rpx;
text {
margin-top: 20rpx;
}
}
.loading-state {
color: $primary-color;
}
.empty-state {
color: $text-secondary;
}
.load-more-state {
padding: 40rpx 20rpx;
color: $text-secondary;
}
.no-more-state {
padding: 40rpx 20rpx;
color: #999;
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<view class="ppt-detail-page">
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
title="课件详情"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
>
<template v-slot:right>
<view class="nav-actions">
<uni-icons type="paperplane" size="22" color="#8B2316"></uni-icons>
<uni-icons type="heart" size="22" color="#8B2316" style="margin-left: 20rpx;"></uni-icons>
</view>
</template>
</uni-nav-bar>
<!-- 下载提示条 -->
<view class="download-bar">
<view class="download-inner">
<u-icon name="download" color="#fff" size="28"></u-icon>
<text class="download-text">本课件下载</text>
<text class="download-price">{{ price }}</text>
<text class="download-unit"></text>
</view>
</view>
<!-- 图片浏览 -->
<view class="viewer">
<swiper
class="ppt-swiper"
:indicator-dots="false"
:circular="true"
:autoplay="false"
@change="onSwiperChange"
>
<swiper-item v-for="(img, idx) in images" :key="idx">
<view class="slide">
<image :src="img" mode="widthFix" class="slide-image" />
</view>
</swiper-item>
</swiper>
<!-- 页码指示 -->
<view class="page-indicator">{{ currentIndex + 1 }}/{{ images.length }}</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from "@dcloudio/uni-app";
const images = ref([
'/static/big_background_my.png',
'/static/big_background_my.png',
'/static/big_background_my.png'
]);
const currentIndex = ref(1); // 2/23
const price = ref('1.0');
const onSwiperChange = (e) => {
currentIndex.value = e.detail.current;
};
const goBack = () => {
uni.navigateBack();
};
onShow(() => {
//
});
</script>
<style lang="scss" scoped>
.ppt-detail-page {
background-color: #ffffff;
min-height: 100vh;
}
.nav-actions {
display: flex;
align-items: center;
}
.download-bar {
position: sticky;
top: calc(var(--status-bar-height) + 44px);
z-index: 9;
width: 100%;
background-color: #6F6F6F; //
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}
.download-inner {
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.download-text {
margin-left: 12rpx;
font-size: 28rpx;
}
.download-price {
margin-left: 8rpx;
color: #ff3b30; //
font-size: 30rpx;
}
.download-unit {
margin-left: 4rpx;
font-size: 26rpx;
}
.viewer {
padding: 24rpx 0;
}
.ppt-swiper {
width: 100%;
min-height: 400rpx;
background-color: #fff;
}
.slide {
width: 100%;
display: flex;
justify-content: center;
}
.slide-image {
width: 100%;
}
.page-indicator {
margin-top: 20rpx;
text-align: center;
font-size: 32rpx;
color: #333;
}
</style>

View File

@ -6,8 +6,9 @@
<!-- Fixed Banner Swiper -->
<view class="banner-container">
<view class="swipemask">
<view class="banner-subtitle">111111111</view>
<view class="swipemask" >
<view class="banner-subtitle" v-if="bannerList.length>0">{{bannerList[currentBannerIndex].name}}</view>
<view class="dotbox">
<view class="circle" :class="{active:currentBannerIndex==index} "v-for="(banner, index) in bannerList"
:key="banner.id"></view>
@ -33,18 +34,10 @@
<view class="banner-item">
<image
class="banner-image"
:src="banner.thumbnail || '/static/big_background_my.png'"
:src="docUrl+banner.imgpath"
mode="aspectFill"
></image>
<view class="banner-content">
<view class="doctor-avatar" v-if="banner.doctorAvatar">
<image :src="banner.doctorAvatar" mode="aspectFill"></image>
</view>
<view class="banner-info">
<view class="banner-title">{{banner.title}}</view>
<view class="banner-subtitle">{{banner.subtitle || '专业医学视频教育平台'}}</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
@ -57,25 +50,25 @@
class="tab-item"
@click="showAllVideoPopup=!showAllVideoPopup"
>
<up-image :src="allImg" width="30rpx" height="30rpx" ></up-image>
<text class="tab-text">全部视频</text>
<up-image :src="selectImg" width="30rpx" height="30rpx" ></up-image>
<up-image :src="isAllActive?allOnImg:allImg" width="30rpx" height="30rpx" ></up-image>
<text class="tab-text" :class="{active:isAllActive}">{{typeName}}</text>
<up-image :src="isAllActive?selectOnImg:selectImg" width="30rpx" height="30rpx" ></up-image>
</view>
<view class="right">
<view
class="tab-item"
@click="toggleSort"
>
<text class="tab-text">最新</text>
<text class="tab-text active">{{sort==2?'最新':'最热'}}</text>
<view class="newbox">
<up-image :src="upImg" width="20rpx" height="26rpx" ></up-image>
<up-image :src="sort==2?upImg:downImg" width="20rpx" height="26rpx" ></up-image>
</view>
</view>
<view
class="tab-item"
@click="showFilterPopup"
>
<text class="tab-text">筛选</text>
<text class="tab-text " :class="{active:isFilterActive}">筛选</text>
<view class="filterbox">
<up-image :src="isFilterActive ? filterOn : filter" width="30rpx" height="30rpx" ></up-image>
</view>
@ -100,22 +93,22 @@
<view
class="video-item"
v-for="(video, index) in videoList"
:key="video.id"
:key="video.id || video.uuid"
@click="playVideo(video)"
>
<view class="video-thumbnail">
<image :src="video.thumbnail" mode="aspectFill"></image>
<image :src="docUrl + video.imgpath" mode="aspectFill"></image>
</view>
<view class="video-info">
<view class="video-title">{{video.title}}</view>
<view class="video-title">{{video.title || video.name}}</view>
<view class="video-meta">
<text class="author">{{video.author}}</text>
<text class="author">{{video.public_name}}</text>
<view class="stats">
<uni-icons type="eye" size="14" color="#999"></uni-icons>
<text class="view-count">{{video.viewCount}}</text>
<text class="view-count">{{video.readnum}}</text>
</view>
</view>
</view>
</view>
</view>
@ -137,7 +130,7 @@
<text>暂无视频内容</text>
</view>
</scroll-view>
<view class="btnbox">
<view class="btnbox" @click="goPatientVideo">
<up-image :src="videoImg" width="44rpx" height="44rpx" ></up-image>
患教视频
@ -154,7 +147,7 @@
:class="{ active: tag.selected }"
@click="toggleTag(index)"
>
{{ tag.name }}
{{ tag.DES }}
</view>
</view>
@ -180,18 +173,28 @@
:key="category.value"
@click="selectCategory(category.value)"
>
{{ category.label }}
<view class="category-label">{{ category.label }}</view>
<view class="category-date" v-if="category.value !== '全部' && category.originalDate">
{{ formatDate(category.originalDate) }}
</view>
</view>
</view>
<view class="content-area">
<view class="content-list">
<view
class="content-item"
v-for="item in filteredContent"
:key="item.id"
:key="item.uuid"
:class="{active:selectYearContent.uuid==item.uuid}"
@click="selectContent(item)"
>
{{ item.title }}
{{item.name }}
</view>
<view v-if="filteredContent.length === 0" class="empty-content">
<text>该分类暂无内容</text>
</view>
</view>
</view>
@ -202,7 +205,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { ref, onMounted, computed,reactive } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import api from '@/api/api.js';
import allImg from "@/static/video_all.png"
@ -212,8 +215,11 @@
import filter from "@/static/cb_screen_no.png"
import filterOn from "@/static/cb_screen_yes.png"
import upImg from "@/static/cb_up.png"
import downImg from "@/static/cb_up.png"
import downImg from "@/static/cb_down.png"
import videoImg from "@/static/patient_video.png"
import docUrl from '@/utils/docUrl';
import navTo from '@/utils/navTo';
const isAllActive=ref(false)
//
const videoList = ref([]);
const bannerVideo = ref(null);
@ -224,13 +230,18 @@
const currentPage = ref(1);
const pageSize = ref(10);
const hasMoreData = ref(true);
const filteredContent=ref([])
const sort =ref(2);
const keywords=ref('');
const typeUuid=ref('');
const typeName=ref('全部视频')
// Banner
const bannerList = ref([]);
const currentBannerIndex = ref(0);
const isFilterActive=ref(false)
const showFilter = ref(false);
const yearList=ref([]);
const selectYearContent=reactive({})
//
const filterTags = ref([
{ name: '指南解读', selected: false },
@ -249,65 +260,171 @@
//
const showAllVideoPopup = ref(false);
const selectedCategory = ref('all');
const selectedCategory = ref('全部');
const categoryList = ref([
{ value: 'all', label: '全部' },
{ value: 'guide', label: '指南解读' },
{ value: 'case', label: '病例分析' },
{ value: 'lecture', label: '学术讲座' },
{ value: 'surgery', label: '手术视频' },
{ value: 'research', label: '研究进展' }
{ value: 'all', label: '全部' }
]);
//
// - API
const contentByCategory = ref({
all: [
{ id: 1, title: '《2025年版慢加急性肝衰竭指南》解读' },
{ id: 2, title: '自身免疫性肝病专栏|免疫治疗的双刃剑' },
{ id: 3, title: '徐医感染:硬化出血发热路,关关难过关关过' },
{ id: 4, title: '徐医感染:一场呼吸的迷局' },
{ id: 5, title: '南京市第二医院疑难肝病病理读片会' }
],
guide: [
{ id: 6, title: '《2025年版慢加急性肝衰竭指南》解读' },
{ id: 7, title: '《原发性肝癌诊疗指南》更新要点' },
{ id: 8, title: '《慢性乙型肝炎防治指南》解读' }
],
case: [
{ id: 9, title: '自身免疫性肝病专栏|免疫治疗的双刃剑' },
{ id: 10, title: '徐医感染:硬化出血发热路,关关难过关关过' },
{ id: 11, title: '徐医感染:一场呼吸的迷局' }
],
lecture: [
{ id: 12, title: '南京市第二医院疑难肝病病理读片会' },
{ id: 13, title: '肝胆胰外科手术技巧分享' },
{ id: 14, title: '肝移植围手术期管理' }
],
surgery: [
{ id: 15, title: '腹腔镜肝切除术操作要点' },
{ id: 16, title: '机器人辅助肝切除术' },
{ id: 17, title: '肝移植手术技术进展' }
],
research: [
{ id: 18, title: '肝癌免疫治疗最新研究进展' },
{ id: 19, title: '肝纤维化分子机制研究' },
{ id: 20, title: '肝细胞再生与修复研究' }
]
all: []
});
// API
const videoTypesData = ref([]);
//
onMounted(() => {
});
onShow(() => {
// 使
// API
currentPage.value = 1;
hasMoreData.value = true;
loadMockVideoData(true);
// banner
bannerList.value = getMockBannerData();
loadVideoData(true);
loadBannerData();
loadVideoTags();
loadVideoTypes();
// ""
setTimeout(() => {
loadCategoryVideos('all');
}, 500);
});
//
const loadBannerData = async () => {
try {
const response = await api.videoRoll({});
console.log('轮播API原始响应:', response);
bannerList.value=response.data;
} catch (error) {
console.error('加载轮播数据失败:', error);
// 使
bannerList.value = getMockBannerData();
}
};
//
const loadVideoTags = async () => {
try {
const response = await api.videoTagList({
type:2
});
console.log('标签API原始响应:', response);
if(response.code==200){
filterTags.value=response.data;
console.log(22222)
console.log(filterTags.value)
}
} catch (error) {
console.error('加载视频标签失败:', error);
}
};
//
const loadVideoTypes = async () => {
try {
const response = await api.expertVideoTypeList({
});
console.log('类型API原始响应:', response);
if(response.code==200){
yearList.value=response.data;
filteredContent.value=(response.data[0]).list;
console.log(33)
let arr=response.data.map(item=>{
return {
value:item.name,
label:item.name
}
})
categoryList.value=arr
}
console.log(2222)
console.log(categoryList.value)
} catch (error) {
console.error('加载视频类型失败:', error);
}
};
//
const loadCategoryVideos = async (categoryValue) => {
try {
console.log('加载分类视频:', categoryValue);
let response;
if (categoryValue === '全部') {
//
response = await api.videoByKeyWordsNew({
page: 1,
pageSize: 20,
keywords: '',
sort: 0,
typeUuid: '',
tags: ''
});
} else {
//
//
const typesForDate = videoTypesData.value.filter(type => type.create_date === categoryValue);
console.log(`日期 ${categoryValue} 对应的视频类型:`, typesForDate);
if (typesForDate.length > 0) {
// UUID
const typeUuids = typesForDate.map(type => type.id || type.uuid || type.value).join(',');
console.log('类型UUID列表:', typeUuids);
//
response = await api.videoByKeyWordsNew({
page: 1,
pageSize: 20,
keywords: '',
sort: 0,
typeUuid: typeUuids,
tags: ''
});
} else {
console.warn(`未找到日期 ${categoryValue} 对应的视频类型`);
contentByCategory.value[categoryValue] = [];
return;
}
}
console.log('分类视频API响应:', response);
//
let videoData = {};
if (response && response.data) {
if (response.data.code === 200) {
videoData = response.data.data || {};
} else if (response.code === 200) {
videoData = response.data || {};
}
} else if (response && response.code === 200) {
videoData = response.data || {};
}
console.log('解析后的分类视频数据:', videoData);
if (videoData && videoData.list) {
const { list } = videoData;
//
contentByCategory.value[categoryValue] = list.map(video => ({
id: video.id || video.uuid,
title: video.title || video.name,
name: video.name || video.title
}));
console.log(`分类 ${categoryValue} 的内容:`, contentByCategory.value[categoryValue]);
}
} catch (error) {
console.error('加载分类视频失败:', error);
}
};
//
const loadVideoData = async (isRefresh = false) => {
if (loading.value && !isRefresh) return;
@ -316,58 +433,60 @@
try {
// API
const response = await api.getVideoList({
const selectedTags = filterTags.value.filter(tag => tag.selected).map(tag => tag.id).join(',');
console.log('请求参数:', {
page: currentPage.value,
pageSize: pageSize.value,
category: activeTab.value //
keywords: keywords.value,
sort: sort.value,
typeUuid:typeUuid.value,
tags: selectedTags
});
if (response.data.code === 200) {
const { list, hasMore } = response.data.data;
const response = await api.videoByKeyWordsNew({
page: currentPage.value,
pageSize: pageSize.value,
keywords: keywords.value,
sort: sort.value,
typeUuid:typeUuid.value,
});
console.log('视频列表API原始响应:', response);
//
let videoData = {};
if (response && response.data) {
if (response.data.code === 200) {
videoData = response.data.data || {};
} else if (response.code === 200) {
videoData = response.data || {};
}
} else if (response && response.code === 200) {
videoData = response.data || {};
}
console.log('解析后的视频数据:', videoData);
if (videoData && videoData.list) {
const { list, hasMore, total } = videoData;
console.log('视频列表数据:', list);
if (isRefresh) {
videoList.value = list;
videoList.value = list || [];
currentPage.value = 1;
} else {
videoList.value = [...videoList.value, ...list];
videoList.value = [...videoList.value, ...(list || [])];
}
// banner
if (isRefresh || currentPage.value === 1) {
bannerVideo.value = list[0];
}
hasMoreData.value = hasMore;
hasMoreData.value = hasMore !== false;
loadMoreStatus.value = hasMoreData.value ? 'more' : 'noMore';
} else {
throw new Error(response.data.message || '获取数据失败');
throw new Error(response?.message || '获取数据失败');
}
} catch (error) {
console.error('加载视频失败:', error);
// API使
const mockData = await getMockVideoData(currentPage.value, pageSize.value);
if (isRefresh) {
videoList.value = mockData.list;
currentPage.value = 1;
} else {
videoList.value = [...videoList.value, ...mockData.list];
}
// banner
if (isRefresh || currentPage.value === 1) {
bannerVideo.value = mockData.list[0];
}
hasMoreData.value = mockData.hasMore;
loadMoreStatus.value = hasMoreData.value ? 'more' : 'noMore';
uni.showToast({
title: '网络连接异常,显示离线数据',
icon: 'none'
});
} finally {
loading.value = false;
refreshing.value = false;
@ -399,7 +518,7 @@
refreshing.value = true;
currentPage.value = 1;
hasMoreData.value = true;
loadMockVideoData(true);
loadVideoData(true);
};
//
@ -408,7 +527,7 @@
loadMoreStatus.value = 'loading';
currentPage.value++;
loadMockVideoData();
loadVideoData();
};
//
@ -417,13 +536,15 @@
//
currentPage.value = 1;
hasMoreData.value = true;
loadMockVideoData(true);
loadVideoData(true);
};
//
const playVideo = (video) => {
uni.navigateTo({
url: `/pages/videoDetail/videoDetail?id=${video.id}`
const videoId = video.id || video.uuid;
navTo({
url: `/pages_app/videoDetail/videoDetail?id=${videoId}`
});
};
@ -436,6 +557,61 @@
});
};
//
const goPatientVideo = () => {
navTo({
url: '/pages_app/patientVideo/patientVideo'
});
};
//
const searchVideos = async (keywords) => {
if (!keywords || keywords.trim() === '') {
loadVideoData(true);
return;
}
try {
console.log('搜索关键词:', keywords.trim());
const response = await api.videoByKeyWordsNew({
keywords: keywords.trim(),
page: 1,
pageSize: pageSize.value
});
console.log('搜索API原始响应:', response);
//
let searchData = {};
if (response && response.data) {
if (response.data.code === 200) {
searchData = response.data.data || {};
} else if (response.code === 200) {
searchData = response.data || {};
}
} else if (response && response.code === 200) {
searchData = response.data || {};
}
console.log('解析后的搜索数据:', searchData);
if (searchData && searchData.list) {
const { list, hasMore } = searchData;
console.log('搜索结果:', list);
videoList.value = list || [];
hasMoreData.value = hasMore !== false;
loadMoreStatus.value = hasMoreData.value ? 'more' : 'noMore';
currentPage.value = 1;
}
} catch (error) {
console.error('搜索视频失败:', error);
uni.showToast({
title: '搜索失败,请重试',
icon: 'none'
});
}
};
// Banner
const getMockBannerData = () => {
return [
@ -468,8 +644,10 @@
};
const playBannerVideo = (banner) => {
uni.navigateTo({
url: `/pages/videoDetail/videoDetail?id=${banner.id}`
const videoId = banner.id || banner.uuid;
navTo({
url: `/pages_app/videoDetail/videoDetail?id=${videoId}`
});
};
@ -481,23 +659,35 @@
const closeAllVideoPopup = () => {
showAllVideoPopup.value = false;
};
const selectCategory = (categoryValue) => {
const selectCategory = async (categoryValue) => {
selectedCategory.value = categoryValue;
console.log(yearList.value)
console.log('选择分类:', categoryValue);
for (var i = 0; i < yearList.value.length; i++) {
if(categoryValue==yearList.value[i].name){
console.log(yearList.value[i]);
filteredContent.value=yearList.value[i].list;
console.log(filteredContent.value)
break;
}
}
};
const selectContent = (content) => {
//
console.log('选中内容:', content);
closeAllVideoPopup();
//
Object.assign(selectYearContent,content);
typeUuid.value=content.uuid;
typeName.value=content.name;
isAllActive.value=true;
};
//
const filteredContent = computed(() => {
return contentByCategory.value[selectedCategory.value] || [];
});
//
const showFilterPopup = () => {
showFilter.value = true;
@ -513,15 +703,37 @@
};
const resetFilter = () => {
//
filterTags.value.forEach(tag => tag.selected = false);
//
isFilterActive.value = false;
//
keywords.value = '';
//
currentPage.value = 1;
hasMoreData.value = true;
loadVideoData(true);
};
const confirmFilter = () => {
const selectedTags = filterTags.value.filter(tag => tag.selected);
console.log('选中的筛选标签:', selectedTags);
for (var i = 0; i < selectedTags.length; i++) {
if(keywords.value){
keywords.value+=","+selectedTags[i].DES
}else{
keywords.value=selectedTags[i].DES
}
}
isFilterActive.value=true;
hideFilterPopup();
//
//
currentPage.value = 1;
hasMoreData.value = true;
loadVideoData(true);
};
// - API
@ -565,6 +777,13 @@
}
});
}
const toggleSort = () => {
sort.value = sort.value === 1 ? 2 : 1; // 1=, 2=
currentPage.value = 1;
hasMoreData.value = true;
loadVideoData(true);
};
</script>
<style scoped lang="scss">
@ -697,11 +916,16 @@
color: #666;
}
.tab-item:first-child .tab-text{
max-width:600rpx;
max-width:295rpx;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tab-item .tab-text.active {
color: #8B2316;
}
/* Fixed Banner Container */
.banner-container {
position: fixed;
@ -805,6 +1029,9 @@
.banner-subtitle {
font-size: 24rpx;
color: rgba(255,255,255,0.8);
white-space: nowrap; /* 单行显示 */
overflow: hidden; /* 隐藏溢出 */
text-overflow: ellipsis; /* 超出显示省略号 */
}
/* Video List */
@ -837,18 +1064,15 @@
height: 100%;
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0,0,0,0.6);
border-radius: 50%;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.video-duration {
margin-top: 8rpx;
}
.video-duration text {
font-size: 22rpx;
color: #999;
}
.video-info {
@ -857,7 +1081,7 @@
.video-title {
font-size: 28rpx;
font-weight: 600;
font-weight: 400;
color: #333;
line-height: 1.4;
margin-bottom: 16rpx;
@ -866,6 +1090,7 @@
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
height: 2.8em; /* 固定2行高度1.4 * 2 = 2.8em */
}
.video-meta {
@ -988,6 +1213,17 @@
}
.category-label {
font-size: 28rpx;
font-weight: 500;
}
.category-date {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
.content-area {
flex: 1;
padding: 20rpx;
@ -1011,13 +1247,20 @@
border: 2rpx solid transparent;
}
.content-item:hover {
background-color: #e8f4fd;
border-color: #4A90E2;
.content-item.active {
color: #8B2316;
}
.content-item:active {
transform: scale(0.98);
.empty-content {
display: flex;
justify-content: center;
align-items: center;
height: 200rpx;
color: #999;
font-size: 28rpx;
}
//
.filter-popup {
@ -1033,8 +1276,8 @@
z-index: 9999;
.filter-content {
margin-top:329rpx;
margin-top:660rpx;
overflow-y:scroll;
background-color: $white;
// border-radius: 20rpx 20rpx 0 0;
padding: 20rpx 30rpx 60rpx;
@ -1052,11 +1295,13 @@
.tag-item {
background-color: #f8f8f8;
color: $gray-dark;
padding: 8rpx 24rpx;
padding: 8rpx 0; /* 去掉左右padding仅保留上下 */
border-radius: 30rpx;
font-size: 26rpx;
border: 2rpx solid #efefef;
transition: all 0.3s ease;
width: 152rpx; /* 固定宽度 */
text-align: center; /* 文本居中 */
&.active {
background-color: #fff;
@ -1073,6 +1318,7 @@
bottom: 30rpx;
left: 30rpx;
right: 30rpx;
background-color: #fff; /* 背景色为白色 */
.btn-reset,
.btn-confirm {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,370 @@
<template>
<view class="zhinan-list-page">
<!-- 头部导航栏 -->
<uni-nav-bar
left-icon="left"
title="诊疗指南"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
></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)"
>
查看
</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>
<view v-else-if="hasMoreData && !isLoading" class="load-more" @click="loadMoreData">
<uni-load-more status="more" :content-text="loadingText"></uni-load-more>
</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>
</view>
</template>
<script setup>
import { ref, onMounted} from 'vue'
import api from '@/api/api.js'
import { onShow, onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
//
const guidelinesList = ref([])
const isLoading = ref(false)
const hasMoreData = ref(true)
const currentPage = ref(1)
const pageSize = ref(20)
const isRefreshing = ref(false)
//
const loadingText = {
contentdown: '上拉显示更多',
contentrefresh: '正在加载...',
contentnomore: '没有更多数据了'
}
//
onMounted(() => {
loadGuidelinesList()
})
onShow(() => {
//
loadGuidelinesList()
})
//
onPullDownRefresh(() => {
refreshData()
})
//
onReachBottom(() => {
if (hasMoreData.value && !isLoading.value) {
loadMoreData()
}
})
//
const refreshData = async () => {
if (isRefreshing.value) return
try {
isRefreshing.value = true
currentPage.value = 1
hasMoreData.value = true
await loadGuidelinesList()
uni.showToast({
title: '刷新成功',
icon: 'success'
})
} catch (error) {
console.error('刷新失败:', error)
uni.showToast({
title: '刷新失败',
icon: 'none'
})
} finally {
isRefreshing.value = false
uni.stopPullDownRefresh()
}
}
//
const loadGuidelinesList = async (isLoadMore = false) => {
if (isLoading.value) return
try {
isLoading.value = true
const params = {
page: currentPage.value,
pageSize: pageSize.value,
type: 1, //
keywords: ''
}
const res = await api.searchLibraryU(params)
console.log('指南列表响应:', res)
if (res.code === 200 && res.data) {
const newData = res.data
if (isLoadMore) {
guidelinesList.value = [...guidelinesList.value, ...newData]
} else {
guidelinesList.value = newData
}
//
hasMoreData.value = newData.length === pageSize.value
} else {
console.error('加载指南列表失败:', res.message)
uni.showToast({
title: res.message || '加载失败',
icon: 'none'
})
}
} catch (error) {
console.error('加载指南列表异常:', error)
uni.showToast({
title: '网络异常,请重试',
icon: 'none'
})
} finally {
isLoading.value = false
}
}
//
const loadMoreData = () => {
if (hasMoreData.value && !isLoading.value) {
currentPage.value++
loadGuidelinesList(true)
}
}
//
const downloadGuideline = (item) => {
console.log('下载指南:', item)
uni.showToast({
title: '开始下载...',
icon: 'none'
})
// API
// PDF
if (item.file_path) {
uni.downloadFile({
url: item.file_path,
success: (res) => {
if (res.statusCode === 200) {
uni.showToast({
title: '下载成功',
icon: 'success'
})
}
},
fail: (err) => {
console.error('下载失败:', err)
uni.showToast({
title: '下载失败',
icon: 'none'
})
}
})
}
}
//
const viewGuideline = (item) => {
console.log('查看指南:', item)
//
uni.navigateTo({
url: `/pages_app/zhinanDetail/zhinanDetail?uuid=${item.uuid}&title=${encodeURIComponent(item.title)}`
})
}
//
const goBack = () => {
uni.navigateBack()
}
//
const formatDate = (dateString) => {
if (!dateString) return ''
try {
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')
return `${year}-${month}-${day}`
} catch (error) {
return dateString
}
}
</script>
<style lang="scss" scoped>
//
$primary-color: #ff4757;
$text-primary: #333;
$text-secondary: #666;
$text-light: #999;
$border-color: #e0e0e0;
$bg-color: #f6f6f6;
$white: #ffffff;
.zhinan-list-page {
background-color: $bg-color;
min-height: 100vh;
}
//
.guidelines-list {
padding: 20rpx 30rpx;
.guideline-item {
background-color: $white;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
.item-content {
flex: 1;
margin-right: 30rpx;
.item-title {
font-size: 32rpx;
color: $text-primary;
line-height: 1.5;
margin-bottom: 16rpx;
font-weight: 500;
}
.item-bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10rpx;
.item-date {
font-size: 26rpx;
color: $text-secondary;
}
.item-action {
display: flex;
align-items: center;
.download-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(255, 71, 87, 0.1);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
background-color: rgba(255, 71, 87, 0.2);
}
}
.view-btn {
color:#8B2316;
font-size: 28rpx;
font-weight: 500;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
background-color: darken($primary-color, 10%);
}
}
}
}
}
}
}
//
.loading-status {
padding: 20rpx 0;
text-align: center;
.loading, .load-more, .no-more {
padding: 20rpx 0;
}
}
//
.no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
text {
margin-top: 30rpx;
font-size: 28rpx;
color: $text-light;
}
}
</style>

BIN
static/xingxing1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/xingxing2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -54,6 +54,7 @@ export const request = (url, data = {}, method = 'post', loading = false, conten
version: '4.0.0',
user_uuid:userInfo.uuid,
client_type: 'A', //client_type,
timestamp:new Date().getTime()
}
postData={
...data,
@ -65,16 +66,13 @@ export const request = (url, data = {}, method = 'post', loading = false, conten
if(token){
header['Authorization']='Bearer ' +token;
let userInfo= uni.getStorageSync('userInfo')
defaultData = {
defaultData = {
version: '4.0.0',
user_uuid:userInfo.uuid,
client_type: 'A', //client_type,
timestamp:new Date().getTime()
}
defaultData = {
version: '4.0.0',
user_uuid:userInfo.uuid,
client_type: 'A', //client_type,
}
postData={
...data,
...defaultData
@ -87,7 +85,7 @@ export const request = (url, data = {}, method = 'post', loading = false, conten
return new Promise(function(e, n) {
let timestamp = Date.now();
uni.request({
data: {...data,...defaultData},
data: postData,
url: url.indexOf('http') != -1 ? url : encodeURI(BASE_URL + url),
method: method,
sslVerify: false,