469 lines
10 KiB
Vue
469 lines
10 KiB
Vue
<template>
|
||
<view class="video-page">
|
||
<!-- Header -->
|
||
<view class="header">
|
||
<view class="header-left" @click="goBack">
|
||
<uni-icons type="left" size="20" color="#8B4513"></uni-icons>
|
||
</view>
|
||
<view class="header-title">肝胆视频</view>
|
||
<view class="header-right">
|
||
<uni-icons type="search" size="20" color="#8B4513" style="margin-right: 15rpx;" @click="goSearch"></uni-icons>
|
||
<uni-icons type="list" size="20" color="#8B4513"></uni-icons>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Main Content -->
|
||
<scroll-view
|
||
class="scroll-view"
|
||
scroll-y="true"
|
||
refresher-enabled="true"
|
||
:refresher-triggered="refreshing"
|
||
@refresherrefresh="onRefresh"
|
||
@scrolltolower="onLoadMore"
|
||
lower-threshold="100"
|
||
>
|
||
<!-- Featured Banner -->
|
||
<view class="banner-section" v-if="bannerVideo">
|
||
<image class="banner-image" :src="bannerVideo.thumbnail" mode="aspectFill"></image>
|
||
<view class="banner-overlay">
|
||
<view class="banner-title">{{bannerVideo.title}}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Filter Tabs -->
|
||
<view class="filter-tabs">
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 0 }"
|
||
@click="switchTab(0)"
|
||
>
|
||
<uni-icons type="list" size="16" color="#666"></uni-icons>
|
||
<text class="tab-text">全部视频</text>
|
||
<uni-icons type="bottom" size="12" color="#666"></uni-icons>
|
||
</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 1 }"
|
||
@click="switchTab(1)"
|
||
>
|
||
<text class="tab-text">最新</text>
|
||
<uni-icons type="top" size="12" color="#E74C3C"></uni-icons>
|
||
</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 2 }"
|
||
@click="switchTab(2)"
|
||
>
|
||
<text class="tab-text">筛选</text>
|
||
<uni-icons type="paperplane" size="12" color="#666"></uni-icons>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Video List -->
|
||
<view class="video-list">
|
||
<view
|
||
class="video-item"
|
||
v-for="(video, index) in videoList"
|
||
:key="video.id"
|
||
@click="playVideo(video)"
|
||
>
|
||
<view class="video-thumbnail">
|
||
<image :src="video.thumbnail" mode="aspectFill"></image>
|
||
<view class="play-icon">
|
||
<uni-icons type="play-filled" size="24" color="#fff"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<view class="video-info">
|
||
<view class="video-title">{{video.title}}</view>
|
||
<view class="video-meta">
|
||
<text class="author">{{video.author}}</text>
|
||
<view class="stats">
|
||
<uni-icons type="eye" size="14" color="#999"></uni-icons>
|
||
<text class="view-count">{{video.viewCount}}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Loading More -->
|
||
<uni-load-more
|
||
v-if="videoList.length > 0"
|
||
:status="loadMoreStatus"
|
||
:content-text="{
|
||
contentdown: '上拉加载更多',
|
||
contentrefresh: '正在加载...',
|
||
contentnomore: '没有更多数据了'
|
||
}"
|
||
></uni-load-more>
|
||
|
||
<!-- Empty State -->
|
||
<view class="empty-state" v-if="videoList.length === 0 && !loading">
|
||
<image src="/static/empty-video.png" mode="aspectFit"></image>
|
||
<text>暂无视频内容</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue';
|
||
import { onShow } from "@dcloudio/uni-app";
|
||
import api from '@/api/api.js';
|
||
|
||
// 响应式数据
|
||
const videoList = ref([]);
|
||
const bannerVideo = ref(null);
|
||
const activeTab = ref(1); // 默认选中"最新"
|
||
const loading = ref(false);
|
||
const refreshing = ref(false);
|
||
const loadMoreStatus = ref('more'); // more, loading, noMore
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(10);
|
||
const hasMoreData = ref(true);
|
||
|
||
// 页面加载
|
||
onMounted(() => {
|
||
|
||
});
|
||
|
||
onShow(() => {
|
||
//loadVideoData();
|
||
getMockVideoData()
|
||
// 可以在这里刷新数据
|
||
});
|
||
|
||
// 加载视频数据
|
||
const loadVideoData = async (isRefresh = false) => {
|
||
if (loading.value && !isRefresh) return;
|
||
|
||
loading.value = true;
|
||
|
||
try {
|
||
// 调用真实API
|
||
const response = await api.getVideoList({
|
||
page: currentPage.value,
|
||
pageSize: pageSize.value,
|
||
category: activeTab.value // 根据选中的标签获取不同类别的视频
|
||
});
|
||
|
||
if (response.data.code === 200) {
|
||
const { list, hasMore } = response.data.data;
|
||
|
||
if (isRefresh) {
|
||
videoList.value = list;
|
||
currentPage.value = 1;
|
||
} else {
|
||
videoList.value = [...videoList.value, ...list];
|
||
}
|
||
|
||
// 设置banner视频(第一个视频)
|
||
if (isRefresh || currentPage.value === 1) {
|
||
bannerVideo.value = list[0];
|
||
}
|
||
|
||
hasMoreData.value = hasMore;
|
||
loadMoreStatus.value = hasMoreData.value ? 'more' : 'noMore';
|
||
} else {
|
||
throw new Error(response.data.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;
|
||
}
|
||
};
|
||
|
||
// 下拉刷新
|
||
const onRefresh = () => {
|
||
refreshing.value = true;
|
||
currentPage.value = 1;
|
||
hasMoreData.value = true;
|
||
loadVideoData(true);
|
||
};
|
||
|
||
// 上拉加载更多
|
||
const onLoadMore = () => {
|
||
if (!hasMoreData.value || loading.value) return;
|
||
|
||
loadMoreStatus.value = 'loading';
|
||
currentPage.value++;
|
||
loadVideoData();
|
||
};
|
||
|
||
// 切换标签
|
||
const switchTab = (tabIndex) => {
|
||
activeTab.value = tabIndex;
|
||
// 可以根据标签加载不同的数据
|
||
currentPage.value = 1;
|
||
hasMoreData.value = true;
|
||
loadVideoData(true);
|
||
};
|
||
|
||
// 播放视频
|
||
const playVideo = (video) => {
|
||
uni.navigateTo({
|
||
url: `/pages/videoDetail/videoDetail?id=${video.id}`
|
||
});
|
||
};
|
||
|
||
// 返回
|
||
const goBack = () => {
|
||
uni.navigateBack();
|
||
};
|
||
|
||
// 搜索
|
||
const goSearch = () => {
|
||
uni.navigateTo({
|
||
url: '/pages_app/search/search'
|
||
});
|
||
};
|
||
|
||
// 模拟数据 - 实际开发时替换为真实API
|
||
const getMockVideoData = (page, size) => {
|
||
return new Promise((resolve) => {
|
||
setTimeout(() => {
|
||
const mockList = [];
|
||
const startIndex = (page - 1) * size;
|
||
|
||
for (let i = 0; i < size; i++) {
|
||
const index = startIndex + i;
|
||
mockList.push({
|
||
id: `video_${index}`,
|
||
title: index === 0 ? '《2025年版慢加急性肝衰竭指南》解读' :
|
||
index % 4 === 1 ? '自身免疫性肝病专栏|免疫治疗的双刃剑——1例自…' :
|
||
index % 4 === 2 ? '徐医感染:硬化出血发热路,关关难过关关过' :
|
||
index % 4 === 3 ? '徐医感染:一场呼吸的迷局' :
|
||
'南京市第二医院疑难肝病病理读片会暨疑难肝病MDT',
|
||
thumbnail: '/static/video-thumb-' + (index % 4 + 1) + '.png',
|
||
author: index % 3 === 0 ? '首都医…' :
|
||
index % 3 === 1 ? '徐州医…' : '南京市…',
|
||
viewCount: Math.floor(Math.random() * 2000) + 100,
|
||
duration: '15:30'
|
||
});
|
||
}
|
||
|
||
resolve({
|
||
list: mockList,
|
||
hasMore: page < 5 // 模拟5页数据
|
||
});
|
||
}, 1000);
|
||
});
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.video-page {
|
||
background-color: #f5f5f5;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* Header Styles */
|
||
.header {
|
||
background-color: #fff;
|
||
height: 88rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 30rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||
position: relative;
|
||
z-index: 100;
|
||
}
|
||
|
||
.header-left, .header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #8B4513;
|
||
}
|
||
|
||
/* Scroll View */
|
||
.scroll-view {
|
||
flex: 1;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
/* Banner Styles */
|
||
.banner-section {
|
||
position: relative;
|
||
height: 400rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.banner-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.banner-overlay {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: linear-gradient(transparent, rgba(0,0,0,0.7));
|
||
padding: 60rpx 30rpx 30rpx;
|
||
}
|
||
|
||
.banner-title {
|
||
color: #fff;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* Filter Tabs */
|
||
.filter-tabs {
|
||
background-color: #fff;
|
||
display: flex;
|
||
padding: 20rpx 30rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.tab-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-right: 60rpx;
|
||
padding: 10rpx 0;
|
||
}
|
||
|
||
.tab-item.active .tab-text {
|
||
color: #E74C3C;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.tab-text {
|
||
margin: 0 8rpx;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
|
||
/* Video List */
|
||
.video-list {
|
||
padding: 0 20rpx;
|
||
}
|
||
|
||
.video-item {
|
||
background-color: #fff;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.video-thumbnail {
|
||
position: relative;
|
||
height: 200rpx;
|
||
}
|
||
|
||
.video-thumbnail image {
|
||
width: 100%;
|
||
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-info {
|
||
padding: 24rpx;
|
||
}
|
||
|
||
.video-title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
line-height: 1.4;
|
||
margin-bottom: 16rpx;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
line-clamp: 2;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.video-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.author {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.stats {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.view-count {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-left: 8rpx;
|
||
}
|
||
|
||
/* Empty State */
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 100rpx 0;
|
||
}
|
||
|
||
.empty-state image {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
margin-bottom: 30rpx;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.empty-state text {
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
</style>
|