增加流媒体主页页面框架

This commit is contained in:
2026-09-19 18:13:27 +08:00
parent 8783182e49
commit d0c556c6a5
13 files changed
+507 -6

No files matched your search

+2 -2
View File
@@ -1,10 +1,10 @@
<!DOCTYPE html>
<html lang="">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>Rill</title>
</head>
<body>
<div id="app"></div>
+7 -1
View File
@@ -1,7 +1,13 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import AppHeader from '@/components/layout/AppHeader.vue'
import AppSidebar from '@/components/layout/AppSidebar.vue'
</script>
<template>
<RouterView />
<AppHeader />
<AppSidebar />
<main class="pt-14 pl-24">
<RouterView />
</main>
</template>
+18
View File
@@ -1 +1,19 @@
@import "tailwindcss";
@theme {
--color-primary: #fb7299;
--color-primary-hover: #fc8bab;
--color-accent: #00aeec;
--color-page: #f6f7f8;
--color-line: #e3e5e7;
--color-content-1: #18191c;
--color-content-2: #61666d;
--color-content-3: #9499a0;
}
@layer base {
body {
background-color: var(--color-page);
color: var(--color-content-1);
}
}
@@ -0,0 +1,31 @@
<script setup lang="ts">
withDefaults(
defineProps<{
icon?: boolean
}>(),
{
icon: true,
},
)
</script>
<template>
<div class="flex items-center justify-center overflow-hidden bg-line text-content-3">
<svg
v-if="icon"
class="h-8 w-8 opacity-70"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect x="3" y="4" width="18" height="16" rx="2" />
<circle cx="8.5" cy="9.5" r="1.5" />
<path d="m3 16 5-5 4 4 3-3 6 6" />
</svg>
<slot />
</div>
</template>
@@ -0,0 +1,21 @@
<script setup lang="ts">
import PlaceholderBox from '@/components/common/PlaceholderBox.vue'
import { categories } from '@/mock/home'
</script>
<template>
<section class="grid grid-cols-4 gap-4 rounded-xl bg-white p-5 sm:grid-cols-8">
<a
v-for="item in categories"
:key="item"
href="#"
class="group flex flex-col items-center gap-2"
>
<PlaceholderBox
:icon="false"
class="h-12 w-12 rounded-full transition group-hover:opacity-80"
/>
<span class="text-xs text-content-2 group-hover:text-primary">{{ item }}</span>
</a>
</section>
</template>
@@ -0,0 +1,33 @@
<script setup lang="ts">
import PlaceholderBox from '@/components/common/PlaceholderBox.vue'
import { banners, sideBanners } from '@/mock/home'
</script>
<template>
<section class="grid gap-3 lg:grid-cols-[2fr_1fr]">
<div class="relative">
<PlaceholderBox :icon="false" class="aspect-[16/7] w-full rounded-xl text-sm font-medium">
{{ banners[0]?.title }}轮播大图占位
</PlaceholderBox>
<div class="absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-1.5">
<span
v-for="item in banners"
:key="item.id"
:class="item.id === 1 ? 'w-4 bg-white' : 'w-1.5 bg-white/50'"
class="h-1.5 rounded-full transition-all"
/>
</div>
</div>
<div class="grid grid-cols-3 gap-3 lg:grid-cols-2">
<PlaceholderBox
v-for="item in sideBanners"
:key="item.id"
:icon="false"
class="aspect-video w-full rounded-lg text-xs"
>
{{ item.title }}
</PlaceholderBox>
</div>
</section>
</template>
@@ -0,0 +1,33 @@
<script setup lang="ts">
import PlaceholderBox from '@/components/common/PlaceholderBox.vue'
import { liveRooms } from '@/mock/home'
</script>
<template>
<section>
<header class="mb-3 flex items-center justify-between">
<h2 class="text-lg font-medium">正在直播</h2>
<a href="#" class="text-sm text-content-2 hover:text-primary">更多 &gt;</a>
</header>
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 xl:grid-cols-5">
<a v-for="room in liveRooms" :key="room.id" href="#" class="group">
<div class="relative overflow-hidden rounded-lg">
<PlaceholderBox :icon="false" class="aspect-video w-full" />
<span class="absolute left-2 top-2 flex items-center gap-1 rounded bg-primary px-1.5 py-0.5 text-xs text-white">
<span class="h-1.5 w-1.5 rounded-full bg-white" />
直播中
</span>
<span class="absolute bottom-2 left-2 text-xs text-white drop-shadow">
人气 {{ room.popularity }}
</span>
</div>
<p class="mt-2 line-clamp-1 text-sm group-hover:text-primary">{{ room.title }}</p>
<p class="mt-1 flex items-center gap-1.5 text-xs text-content-2">
<span class="h-4 w-4 shrink-0 rounded-full bg-line" />
<span class="line-clamp-1">{{ room.author }}</span>
</p>
</a>
</div>
</section>
</template>
@@ -0,0 +1,45 @@
<script setup lang="ts">
import PlaceholderBox from '@/components/common/PlaceholderBox.vue'
import type { VideoItem } from '@/mock/home'
defineProps<{
video: VideoItem
}>()
</script>
<template>
<a href="#" class="group block">
<div class="relative overflow-hidden rounded-lg">
<PlaceholderBox :icon="false" class="aspect-video w-full" />
<div
class="pointer-events-none absolute inset-x-0 bottom-0 flex items-center gap-3 bg-gradient-to-t from-black/55 to-transparent px-2 pb-1.5 pt-6 text-xs text-white"
>
<span class="flex items-center gap-1">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M8 5v14l11-7z" />
</svg>
{{ video.views }}
</span>
<span class="flex items-center gap-1">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-6l-4 4v-4H5a2 2 0 0 1-2-2z" />
</svg>
{{ video.danmaku }}
</span>
</div>
<span class="absolute bottom-1.5 right-2 rounded bg-black/60 px-1.5 py-0.5 text-xs text-white">
{{ video.duration }}
</span>
</div>
<h3 class="mt-2 line-clamp-2 text-sm leading-5 group-hover:text-accent">
{{ video.title }}
</h3>
<p class="mt-1.5 flex items-center gap-1.5 text-xs text-content-2">
<span class="h-4 w-4 shrink-0 rounded-full bg-line" />
<span class="line-clamp-1">{{ video.author }}</span>
<span class="ml-auto shrink-0">{{ video.date }}</span>
</p>
</a>
</template>
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { ref } from 'vue'
import VideoCard from '@/components/home/VideoCard.vue'
import type { VideoItem } from '@/mock/home'
const props = withDefaults(
defineProps<{
title: string
tabs?: string[]
videos: VideoItem[]
}>(),
{
tabs: () => [],
},
)
const activeTab = ref(props.tabs[0] ?? '')
</script>
<template>
<section>
<header class="mb-3 flex flex-wrap items-center gap-x-4 gap-y-2">
<h2 class="text-lg font-medium">{{ title }}</h2>
<nav v-if="tabs.length" class="flex flex-wrap items-center gap-1 text-sm">
<button
v-for="tab in tabs"
:key="tab"
type="button"
class="rounded-full px-3 py-1 transition-colors"
:class="tab === activeTab ? 'bg-primary/10 font-medium text-primary' : 'text-content-2 hover:text-primary'"
@click="activeTab = tab"
>
{{ tab }}
</button>
</nav>
<button
type="button"
class="ml-auto flex items-center gap-1 rounded-full border border-line px-3 py-1 text-sm text-content-2 transition-colors hover:border-primary hover:text-primary"
>
<svg
class="h-3.5 w-3.5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
换一换
</button>
</header>
<div class="grid grid-cols-2 gap-x-4 gap-y-5 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
<VideoCard v-for="video in videos" :key="video.id" :video="video" />
</div>
</section>
</template>
@@ -0,0 +1,88 @@
<script setup lang="ts">
import { ref } from 'vue'
const navItems = ['首页', '番剧', '直播', '游戏中心', '会员购', '漫画', '赛事', '下载客户端']
const keyword = ref('')
</script>
<template>
<header class="fixed inset-x-0 top-0 z-50 h-14 border-b border-line bg-white">
<div class="mx-auto flex h-full max-w-[1600px] items-center gap-6 px-4">
<a href="/" class="flex shrink-0 items-center gap-2 text-xl font-bold text-accent">
<span class="grid h-8 w-8 place-items-center rounded-lg bg-accent text-sm font-bold text-white">
R
</span>
<span>Rill</span>
</a>
<nav class="hidden items-center gap-5 text-sm text-content-2 lg:flex">
<a
v-for="(item, index) in navItems"
:key="item"
href="#"
:class="index === 0 ? 'font-medium text-content-1' : 'hover:text-primary'"
>
{{ item }}
</a>
</nav>
<div class="ml-auto flex items-center gap-4">
<div class="hidden h-9 items-center overflow-hidden rounded-full border border-line bg-page sm:flex">
<input
v-model="keyword"
type="text"
placeholder="搜索视频、UP主"
class="h-full w-52 bg-transparent px-4 text-sm outline-none placeholder:text-content-3"
/>
<button
type="button"
aria-label="搜索"
class="grid h-full w-11 place-items-center text-content-2 hover:bg-line"
>
<svg
class="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
>
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.5-3.5" />
</svg>
</button>
</div>
<nav class="hidden items-center gap-4 text-sm text-content-2 xl:flex">
<a href="#" class="hover:text-primary">消息</a>
<a href="#" class="hover:text-primary">动态</a>
<a href="#" class="hover:text-primary">收藏</a>
<a href="#" class="hover:text-primary">历史</a>
</nav>
<button
type="button"
class="flex h-9 items-center gap-1.5 rounded-full bg-primary px-4 text-sm font-medium text-white hover:bg-primary-hover"
>
<svg
class="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 16V4" />
<path d="m7 9 5-5 5 5" />
<path d="M4 17v2a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-2" />
</svg>
投稿
</button>
<div class="h-9 w-9 shrink-0 rounded-full bg-line" aria-label="头像占位" />
</div>
</div>
</header>
</template>
@@ -0,0 +1,44 @@
<script setup lang="ts">
import PlaceholderBox from '@/components/common/PlaceholderBox.vue'
const channels = [
'动态',
'热门',
'频道',
'专栏',
'活动',
'直播',
'音乐',
'番剧',
'影视',
'漫画',
'游戏中心',
'会员购',
'赛事',
'课堂',
'公益',
]
</script>
<template>
<aside class="fixed bottom-0 left-0 top-14 z-40 w-24 overflow-y-auto border-r border-line bg-white">
<nav class="flex flex-col gap-1 p-2">
<a
href="/"
class="flex flex-col items-center gap-1.5 rounded-lg py-2 text-primary"
>
<PlaceholderBox :icon="false" class="h-6 w-6 rounded-md" />
<span class="text-xs font-medium">首页</span>
</a>
<a
v-for="item in channels"
:key="item"
href="#"
class="flex flex-col items-center gap-1.5 rounded-lg py-2 text-content-2 hover:bg-page hover:text-primary"
>
<PlaceholderBox :icon="false" class="h-6 w-6 rounded-md" />
<span class="text-xs">{{ item }}</span>
</a>
</nav>
</aside>
</template>
+100
View File
@@ -0,0 +1,100 @@
export interface BannerItem {
id: number
title: string
}
export interface VideoItem {
id: number
title: string
author: string
views: string
danmaku: string
duration: string
date: string
}
export interface LiveRoom {
id: number
title: string
author: string
popularity: string
}
export const banners: BannerItem[] = Array.from({ length: 7 }, (_, index) => ({
id: index + 1,
title: `轮播图 ${index + 1}`,
}))
export const sideBanners: BannerItem[] = Array.from({ length: 6 }, (_, index) => ({
id: index + 1,
title: `推荐位 ${index + 1}`,
}))
export const categories = [
'动画',
'番剧',
'国创',
'音乐',
'舞蹈',
'游戏',
'知识',
'科技',
'运动',
'汽车',
'生活',
'美食',
'动物',
'鬼畜',
'时尚',
'娱乐',
]
export const liveRooms: LiveRoom[] = [
{ id: 1, title: '直播标题占位:赛季揭幕战全程解说', author: '主播占位一', popularity: '24.7万' },
{ id: 2, title: '直播标题占位:深夜电台点歌时间', author: '主播占位二', popularity: '8.2万' },
{ id: 3, title: '直播标题占位:新游开荒第一天', author: '主播占位三', popularity: '12.6万' },
{ id: 4, title: '直播标题占位:周末手工制作现场', author: '主播占位四', popularity: '3.4万' },
{ id: 5, title: '直播标题占位:音乐会彩排幕后', author: '主播占位五', popularity: '5.9万' },
]
const topicPool = [
'城市夜景延时摄影记录',
'从零搭建家庭影音服务器',
'经典电影镜头语言解析',
'周末骑行 Vlog 全记录',
'一人食料理制作全流程',
'新番开播前瞻与无剧透点评',
'游戏高光时刻集锦',
'独立音乐现场演出片段',
'纪录片解说:深海探索',
'硬件装机与性能实测',
'手绘插画过程分享',
'户外露营装备测评',
'经典老片修复对比',
'编程实战:从零写一个播放器',
'羽毛球业余联赛集锦',
]
function createVideos(prefix: string, count: number, startId: number): VideoItem[] {
return Array.from({ length: count }, (_, index) => {
const id = startId + index
const topic = topicPool[id % topicPool.length] ?? '内容标题占位'
return {
id,
title: `${prefix}${topic}(标题文案占位)`,
author: `UP主占位 ${id}`,
views: `${(id * 2.7).toFixed(1)}`,
danmaku: (id * 143).toLocaleString('zh-CN'),
duration: `${String((index % 12) + 1).padStart(2, '0')}:${String((index * 13) % 60).padStart(2, '0')}`,
date: `${(index % 28) + 1}天前`,
}
})
}
export const recommendVideos = createVideos('推荐', 10, 1)
export const hotVideos = createVideos('热门', 10, 11)
export const recommendTabs = ['综合', '动画', '游戏', '知识', '生活', '影视', '音乐', '科技', '美食', '运动']
export const footerLinks = ['关于我们', '帮助中心', '用户协议', '隐私政策', '社区规范', '联系我们']
+22 -3
View File
@@ -1,5 +1,24 @@
<script setup lang="ts">
import CategoryNav from '@/components/home/CategoryNav.vue'
import HomeCarousel from '@/components/home/HomeCarousel.vue'
import LiveSection from '@/components/home/LiveSection.vue'
import VideoSection from '@/components/home/VideoSection.vue'
import { footerLinks, hotVideos, recommendTabs, recommendVideos } from '@/mock/home'
</script>
<template>
<main class="p-8">
<h1 class="text-2xl font-bold">Rill</h1>
</main>
<div class="mx-auto max-w-[1600px] space-y-6 px-4 py-4">
<HomeCarousel />
<CategoryNav />
<LiveSection />
<VideoSection title="推荐" :tabs="recommendTabs" :videos="recommendVideos" />
<VideoSection title="热门" :videos="hotVideos" />
<footer class="rounded-xl bg-white px-6 py-6 text-center text-xs text-content-3">
<nav class="mb-3 flex flex-wrap items-center justify-center gap-x-4 gap-y-2">
<a v-for="link in footerLinks" :key="link" href="#" class="hover:text-primary">{{ link }}</a>
</nav>
<p>Rill 流媒体服务平台 · 页面框架占位 · 所有图片均为空白占位框</p>
</footer>
</div>
</template>