This commit is contained in:
ovo 2024-12-06 00:30:56 +08:00
parent 765f0b6a8f
commit 0e322f198a
3 changed files with 513 additions and 0 deletions

View File

@ -0,0 +1,160 @@
<template>
<div class="video-card" @click="$emit('click')">
<div class="thumbnail-wrapper">
<img :src="video.thumbnail" :alt="video.title">
<span class="duration">{{ video.duration }}</span>
<div class="hover-play">
<el-icon :size="32"><VideoPlay /></el-icon>
</div>
</div>
<div class="video-info">
<h3 class="title">{{ video.title }}</h3>
<div class="meta">
<span class="views">{{ formatNumber(video.views) }}次观看</span>
<span class="date">{{ formatDate(video.date) }}</span>
</div>
<div class="actions">
<el-button
:icon="video.isFavorite ? Star : StarFilled"
circle
@click.stop="$emit('favorite', video.id)"
/>
<el-dropdown trigger="click" @click.stop>
<el-button circle :icon="More" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>添加到播放列表</el-dropdown-item>
<el-dropdown-item>分享</el-dropdown-item>
<el-dropdown-item>举报</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { VideoPlay, Star, StarFilled, More } from '@element-plus/icons-vue'
defineProps<{
video: {
id: number
title: string
thumbnail: string
duration: string
views: number
date: string
isFavorite?: boolean
}
}>()
const formatNumber = (num: number) => {
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万'
}
return num.toString()
}
const formatDate = (date: string) => {
const d = new Date(date)
const now = new Date()
const diff = now.getTime() - d.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) return '今天'
if (days === 1) return '昨天'
if (days < 30) return `${days}天前`
if (days < 365) return `${Math.floor(days / 30)}个月前`
return `${Math.floor(days / 365)}年前`
}
</script>
<style scoped>
.video-card {
background: white;
border-radius: 12px;
overflow: hidden;
transition: transform 0.3s;
cursor: pointer;
position: relative;
}
.video-card:hover {
transform: translateY(-4px);
}
.thumbnail-wrapper {
position: relative;
padding-top: 56.25%; /* 16:9 比例 */
}
.thumbnail-wrapper img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.duration {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.hover-play {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
color: white;
}
.video-card:hover .hover-play {
opacity: 1;
}
.video-info {
padding: 12px;
}
.title {
margin: 0 0 8px;
font-size: 16px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.meta {
display: flex;
gap: 12px;
color: #666;
font-size: 13px;
margin-bottom: 8px;
}
.actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="video-grid">
<video-card
v-for="video in videos"
:key="video.id"
:video="video"
@click="$emit('click', video)"
@favorite="$emit('favorite', video.id)"
/>
</div>
</template>
<script setup lang="ts">
import VideoCard from './VideoCard.vue'
defineProps<{
videos: any[]
}>()
</script>
<style scoped>
.video-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}
</style>

326
src/views/VideoPlayView.vue Normal file
View File

@ -0,0 +1,326 @@
<template>
<div class="video-play-container">
<div class="main-content">
<!-- 视频播放器 -->
<div class="player-wrapper">
<video
ref="videoRef"
class="video-player"
controls
:src="currentVideo.videoUrl"
@timeupdate="handleTimeUpdate"
></video>
</div>
<!-- 视频信息 -->
<div class="video-info">
<h1>{{ currentVideo.title }}</h1>
<div class="meta-info">
<span>{{ formatNumber(currentVideo.views) }}次观看</span>
<span>{{ formatDate(currentVideo.date) }}</span>
</div>
<div class="action-buttons">
<el-button :icon="Like" :class="{ active: isLiked }">
{{ formatNumber(currentVideo.likes) }}
</el-button>
<el-button :icon="Star" :class="{ active: isFavorited }">
收藏
</el-button>
<el-button :icon="Share">分享</el-button>
</div>
<div class="description">
{{ currentVideo.description }}
</div>
</div>
<!-- 评论区 -->
<div class="comments-section">
<h3>评论 ({{ comments.length }})</h3>
<div class="comment-input">
<el-input
v-model="newComment"
type="textarea"
:rows="2"
placeholder="添加评论..."
/>
<el-button type="primary" @click="submitComment">发表评论</el-button>
</div>
<div class="comments-list">
<div v-for="comment in comments" :key="comment.id" class="comment">
<el-avatar :src="comment.avatar" />
<div class="comment-content">
<div class="comment-header">
<span class="username">{{ comment.username }}</span>
<span class="time">{{ formatDate(comment.date) }}</span>
</div>
<p class="comment-text">{{ comment.content }}</p>
<div class="comment-actions">
<el-button text :icon="Like">
{{ comment.likes }}
</el-button>
<el-button text :icon="ChatDotRound">回复</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 推荐视频侧边栏 -->
<div class="sidebar">
<h3>推荐视频</h3>
<div class="recommended-videos">
<video-card
v-for="video in recommendedVideos"
:key="video.id"
:video="video"
@click="playVideo(video)"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { Like, Star, Share, ChatDotRound } from '@element-plus/icons-vue'
import VideoCard from '@/components/video/VideoCard.vue'
const route = useRoute()
const videoRef = ref<HTMLVideoElement>()
const newComment = ref('')
//
const currentVideo = ref({
id: 1,
title: '健康生活指南',
description: '详细的视频描述...',
videoUrl: '/videos/sample.mp4',
views: 15000,
likes: 2300,
date: '2024-01-15'
})
const comments = ref([
{
id: 1,
username: '用户1',
avatar: '/avatars/user1.jpg',
content: '非常实用的视频!',
likes: 12,
date: '2024-01-16'
}
// ...
])
const recommendedVideos = ref([
// ...
])
const isLiked = ref(false)
const isFavorited = ref(false)
//
const handleTimeUpdate = () => {
if (!videoRef.value) return
//
localStorage.setItem(`video-progress-${currentVideo.value.id}`,
videoRef.value.currentTime.toString()
)
}
const submitComment = () => {
if (!newComment.value.trim()) return
comments.value.unshift({
id: Date.now(),
username: '当前用户',
avatar: '/avatars/default.jpg',
content: newComment.value,
likes: 0,
date: new Date().toISOString()
})
newComment.value = ''
}
const playVideo = (video: any) => {
currentVideo.value = video
//
if (videoRef.value) {
videoRef.value.currentTime = 0
videoRef.value.play()
}
}
//
const formatNumber = (num: number) => {
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万'
}
return num.toString()
}
const formatDate = (date: string) => {
const d = new Date(date)
const now = new Date()
const diff = now.getTime() - d.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) return '今天'
if (days === 1) return '昨天'
if (days < 30) return `${days}天前`
if (days < 365) return `${Math.floor(days / 30)}个月前`
return `${Math.floor(days / 365)}年前`
}
onMounted(() => {
//
const progress = localStorage.getItem(`video-progress-${currentVideo.value.id}`)
if (progress && videoRef.value) {
videoRef.value.currentTime = parseFloat(progress)
}
})
</script>
<style scoped>
.video-play-container {
display: flex;
gap: 24px;
padding: 20px;
max-width: 1600px;
margin: 0 auto;
}
.main-content {
flex: 1;
min-width: 0;
}
.player-wrapper {
position: relative;
padding-top: 56.25%; /* 16:9 比例 */
background: black;
margin-bottom: 20px;
}
.video-player {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.video-info {
margin-bottom: 24px;
}
.video-info h1 {
font-size: 24px;
margin: 0 0 12px;
}
.meta-info {
display: flex;
gap: 16px;
color: #666;
margin-bottom: 16px;
}
.action-buttons {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.action-buttons .active {
color: #409EFF;
}
.description {
padding: 16px;
background: #f5f7fa;
border-radius: 8px;
margin-bottom: 24px;
}
.comments-section {
margin-bottom: 24px;
}
.comment-input {
margin-bottom: 24px;
}
.comment-input .el-button {
margin-top: 12px;
}
.comment {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.comment-content {
flex: 1;
min-width: 0;
}
.comment-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.username {
font-weight: 500;
}
.time {
color: #666;
font-size: 13px;
}
.comment-text {
margin: 0 0 8px;
}
.comment-actions {
display: flex;
gap: 16px;
}
.sidebar {
width: 360px;
flex-shrink: 0;
}
.recommended-videos {
display: flex;
flex-direction: column;
gap: 16px;
}
/* 响应式布局 */
@media (max-width: 1200px) {
.video-play-container {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.recommended-videos {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
}
</style>