增加流媒体主页页面框架
This commit is contained in:
13 files changed
+507
-6
No files matched your search
+2
-2
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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">更多 ></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>
|
||||
@@ -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 = ['关于我们', '帮助中心', '用户协议', '隐私政策', '社区规范', '联系我们']
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user