2025-08-14 17:02:24 +08:00

469 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>