feat: add news

This commit is contained in:
王性驊 2025-09-26 01:20:06 +08:00
parent 5e7534966d
commit 61b559b20b
9 changed files with 1929 additions and 486 deletions

View File

@ -7,7 +7,7 @@ import { useDraggable } from '../composables/useDraggable';
import type { SnapType } from '../composables/useDraggable';
import { useResizable } from '../composables/useResizable';
import { useBreakpoint } from '../composables/useBreakpoint';
import LiveStreamHub from './LiveStreamHub.vue';
import NewsHub from './NewsHub.vue';
const props = defineProps<{
instance: AppInstance;
@ -125,7 +125,7 @@ function onMouseDown() {
const appComponent = computed(() => {
switch (props.instance.appId) {
case 'livestream-hub':
return LiveStreamHub;
return NewsHub;
default:
return null;
}

View File

@ -1,401 +0,0 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
// Mock data for live streams
const featuredStreams = computed(() => [
{
id: 1,
image: "https://picsum.photos/800/400?random=1",
externalUrl: "https://twitch.tv/gamingking"
},
{
id: 2,
image: "https://picsum.photos/800/400?random=2",
externalUrl: "https://youtube.com/watch?v=music123"
},
{
id: 3,
image: "https://picsum.photos/800/400?random=3",
externalUrl: "https://twitch.tv/cookingchef"
},
{
id: 4,
image: "https://picsum.photos/800/400?random=4",
externalUrl: "https://youtube.com/watch?v=coding123"
}
]);
const popularStreamers = computed(() => [
{ id: 1, name: t('livestream.streamers.gamingKing'), avatar: "🎮", viewers: 15420, isLive: true },
{ id: 2, name: t('livestream.streamers.musicMaster'), avatar: "🎵", viewers: 8930, isLive: true },
{ id: 3, name: t('livestream.streamers.cookingChef'), avatar: "🍳", viewers: 5670, isLive: true },
{ id: 4, name: t('livestream.streamers.codeTeacher'), avatar: "💻", viewers: 12300, isLive: true },
{ id: 5, name: t('livestream.streamers.artist'), avatar: "🎨", viewers: 4200, isLive: true },
{ id: 6, name: t('livestream.streamers.trainer'), avatar: "💪", viewers: 6800, isLive: false },
{ id: 7, name: t('livestream.streamers.traveler'), avatar: "✈️", viewers: 3200, isLive: true },
{ id: 8, name: t('livestream.streamers.petLover'), avatar: "🐱", viewers: 5100, isLive: false }
]);
// Carousel state
const currentSlide = ref(0);
const carouselInterval = ref<number | null>(null);
// Auto-play carousel
const startCarousel = () => {
carouselInterval.value = window.setInterval(() => {
currentSlide.value = (currentSlide.value + 1) % featuredStreams.value.length;
}, 4000);
};
const stopCarousel = () => {
if (carouselInterval.value) {
clearInterval(carouselInterval.value);
carouselInterval.value = null;
}
};
// Handle stream click
const handleStreamClick = (stream: any) => {
if (stream.externalUrl) {
window.open(stream.externalUrl, '_blank', 'noopener,noreferrer');
}
};
// Handle streamer click
const handleStreamerClick = (streamer: any) => {
console.log(`查看主播: ${streamer.name}`);
// TODO: Open streamer profile
};
onMounted(() => {
startCarousel();
});
onUnmounted(() => {
stopCarousel();
});
</script>
<template>
<div class="livestream-hub">
<!-- Header -->
<div class="hub-header">
<h2 class="hub-title">📺 {{ t('livestream.title') }}</h2>
<div class="hub-subtitle">{{ t('livestream.subtitle') }}</div>
</div>
<!-- Home View -->
<div class="home-view">
<!-- Featured Streams Carousel -->
<div class="carousel-section">
<h3 class="section-title">🔥 {{ t('livestream.featuredStreams') }}</h3>
<div class="carousel-container" @mouseenter="stopCarousel" @mouseleave="startCarousel">
<div class="carousel-track" :style="{ transform: `translateX(-${currentSlide * 100}%)` }">
<div
v-for="stream in featuredStreams"
:key="stream.id"
class="carousel-slide"
@click="handleStreamClick(stream)"
>
<div
class="stream-image-card"
:style="{ backgroundImage: `url(${stream.image})` }"
>
</div>
</div>
</div>
<!-- Carousel Controls -->
<button
class="carousel-btn prev"
@click="currentSlide = currentSlide > 0 ? currentSlide - 1 : featuredStreams.length - 1"
>
</button>
<button
class="carousel-btn next"
@click="currentSlide = (currentSlide + 1) % featuredStreams.length"
>
</button>
<!-- Carousel Indicators -->
<div class="carousel-indicators">
<button
v-for="(stream, index) in featuredStreams"
:key="index"
class="indicator"
:class="{ active: index === currentSlide }"
@click="currentSlide = index"
></button>
</div>
</div>
</div>
<!-- Popular Streamers -->
<div class="streamers-section">
<h3 class="section-title"> {{ t('livestream.popularStreamers') }}</h3>
<div class="streamers-grid">
<div
v-for="streamer in popularStreamers"
:key="streamer.id"
class="streamer-card"
@click="handleStreamerClick(streamer)"
>
<div class="streamer-avatar">{{ streamer.avatar }}</div>
<div class="streamer-info">
<div class="streamer-name">{{ streamer.name }}</div>
<div class="streamer-viewers">{{ streamer.viewers }} {{ t('livestream.viewers') }}</div>
</div>
<div class="streamer-status" :class="{ 'is-live': streamer.isLive }">
{{ streamer.isLive ? `🔴 ${t('livestream.live')}` : `${t('livestream.offline')}` }}
</div>
</div>
</div>
</div>
</div> <!-- End home-view -->
</div>
</template>
<style scoped>
.livestream-hub {
width: 100%;
height: 100%;
background: var(--window-background);
display: flex;
flex-direction: column;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
}
.hub-header {
text-align: center;
margin-bottom: 24px;
}
.hub-title {
font-size: 24px;
font-weight: bold;
color: var(--content-text-color);
margin: 0 0 8px 0;
}
.hub-subtitle {
font-size: 14px;
color: var(--content-text-color);
opacity: 0.7;
}
.carousel-section {
margin-bottom: 32px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: var(--content-text-color);
margin: 0 0 16px 0;
}
.carousel-container {
position: relative;
width: 100%;
height: 300px;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.carousel-track {
display: flex;
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out;
}
.carousel-slide {
min-width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.stream-image-card {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
cursor: pointer;
transition: transform 0.3s ease;
}
.stream-image-card:hover {
transform: scale(1.02);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.carousel-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
border: none;
color: white;
font-size: 24px;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
z-index: 10;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
}
.carousel-btn:hover {
background: rgba(0, 0, 0, 0.7);
transform: translateY(-50%) scale(1.1);
}
.carousel-btn.prev {
left: 16px;
}
.carousel-btn.next {
right: 16px;
}
.carousel-indicators {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 10;
}
.indicator {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.3);
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
aspect-ratio: 1;
min-width: 12px;
min-height: 12px;
}
.indicator.active {
background: white;
border-color: white;
transform: scale(1.2);
}
.streamers-section {
flex: 1;
}
.streamers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.streamer-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--window-background);
border: 1px solid var(--window-border-color);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
}
.streamer-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--window-border-color-focused);
}
.streamer-avatar {
font-size: 32px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: var(--title-bar-background);
border-radius: 50%;
flex-shrink: 0;
}
.streamer-info {
flex: 1;
}
.streamer-name {
font-weight: bold;
color: var(--content-text-color);
margin-bottom: 4px;
}
.streamer-viewers {
font-size: 12px;
color: var(--content-text-color);
opacity: 0.7;
}
.streamer-status {
font-size: 16px;
flex-shrink: 0;
}
.streamer-status.is-live {
animation: pulse 2s infinite;
}
/* Light theme adjustments */
.theme-light .carousel-btn {
background: rgba(255, 255, 255, 0.8);
color: #333;
}
.theme-light .carousel-btn:hover {
background: rgba(255, 255, 255, 0.9);
}
.theme-light .indicator {
border-color: rgba(0, 0, 0, 0.6);
background: rgba(0, 0, 0, 0.3);
}
.theme-light .indicator.active {
background: #333;
border-color: #333;
}
</style>

875
components/NewsHub.vue Normal file
View File

@ -0,0 +1,875 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import StreamerFlipCard from './StreamerFlipCard.vue';
const { t } = useI18n();
// Mock data for live streams
const featuredStreams = computed(() => [
{
id: 1,
image: "https://imgproxy.goplayone.com/1/auto/768/0/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvYmFubmVyLzNlYTNkMzI0ZjJhMmZhNDQ5MDAyZWQyNDE0ZDNhZDhlLnBuZw==",
externalUrl: "https://twitch.tv/gamingking"
},
{
id: 2,
image: "https://imgproxy.goplayone.com/1/auto/768/0/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvYmFubmVyLzE5OGI4OGZhZmQ3ZDM2NjEyZmE0YTBmZDQ0NzViMmVjLnBuZw==",
externalUrl: "https://youtube.com/watch?v=music123"
},
{
id: 3,
image: "https://imgproxy.goplayone.com/1/auto/768/0/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvYmFubmVyLzVjMmM1NzRhNTY0ZDNhMmU4ODM5OTlhZTQ3NDk1NTQ5LnBuZw==",
externalUrl: "https://twitch.tv/cookingchef"
},
{
id: 4,
image: "https://imgproxy.goplayone.com/1/auto/768/0/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvYmFubmVyL2JjNGI4NmJlNzM5OWJlNGJmNTY2MTk0YjZmOWZkZDYwLnBuZw==",
externalUrl: "https://youtube.com/watch?v=coding123"
}
]);
const popularStreamers = computed(() => [
{
id: 1,
name: "音樂小天使",
photo: "https://playone-assets.goplayone.com/playone/user/play/avatar/ea249b64-a3d5-4ff1-bdc5-2eb0d0f956ea",
description: "音樂達人♫",
rank: "黃金",
fans: 1560,
orders: 112,
badges: ["rank-gold", "pro-music", "feature-verified"],
gender: "female" as const,
birthday: "1998-03-15",
greeting: "你好~我是音樂小天使~音樂達人♫",
status: "專業音樂陪陪!",
availability: "隨時可約 聲音甜美",
personality: "溫柔體貼 歌聲動人",
promise: "用音樂治癒你的心靈~快來聽我唱歌吧💕"
},
{
id: 2,
name: "電競女武神",
photo: "https://imgproxy.goplayone.com/1/auto/244/244/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvdXNlci9wbGF5L2F2YXRhci83NDk5ODA2OS04YTU4LTQ5YWQtOTc1YS1jNTczZmY0YjNjNTY=",
description: "FPS女王",
rank: "大師",
fans: 2800,
orders: 198,
badges: ["rank-master", "pro-gaming", "feature-live"],
gender: "female" as const,
birthday: "1995-07-22",
greeting: "Yo~我是電競女武神~FPS女王🔫",
status: "前職業選手 現役陪陪!",
availability: "晚上8-12點 週末全天",
personality: "冷靜狙擊 一槍一個",
promise: "帶你體驗職業級操作!從菜鳥到高手 包教包會🎯"
},
{
id: 3,
name: "二次元萌妹",
photo: "https://playone-assets.goplayone.com/playone/user/play/avatar/a4b858a7-a1f3-4543-8d18-ae94e8db573b",
description: "動漫專家",
rank: "黃金",
fans: 1200,
orders: 89,
badges: ["rank-gold", "special-anime", "feature-new"],
gender: "female" as const,
birthday: "2000-11-08",
greeting: "こんにちは~我是二次元萌妹~動漫專家🌸",
status: "動漫系大學生 兼職陪陪!",
availability: "平日晚上 週末下午",
personality: "超愛動漫 聲音超萌",
promise: "一起討論最新番劇!陪你刷副本 收集老婆💖"
},
{
id: 4,
name: "策略大師",
photo: "https://imgproxy.goplayone.com/1/auto/244/244/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvdXNlci9wbGF5L2F2YXRhci8zZjMzMTZlYS04YzRkLTQ0Y2UtOTc5Mi0zMTk2ZmUxZmJhOGU=",
description: "戰術專家",
rank: "鑽石",
fans: 4200,
orders: 312,
badges: ["rank-diamond", "achievement-expert", "feature-vip"],
gender: "male" as const,
birthday: "1992-05-18",
greeting: "你好~我是策略大師~戰術專家🧠",
status: "前職業教練 現專職陪陪!",
availability: "週一到週五 下午2-8點",
personality: "理性分析 耐心指導",
promise: "從戰術思維到操作細節 全面提升你的遊戲智商!📊"
},
{
id: 5,
name: "派對女王",
photo: "https://imgproxy.goplayone.com/1/auto/244/244/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvdXNlci9wbGF5L2F2YXRhci80YmUzOWUxZi04YzJhLTRiMmItYjk0NS1lYjAyMWEzNjc5ZGM=",
description: "社交達人",
rank: "鑽石",
fans: 2100,
orders: 167,
badges: ["rank-diamond", "special-party", "feature-popular"],
gender: "female" as const,
birthday: "1996-09-12",
greeting: "Hey~我是派對女王~社交達人🎉",
status: "全職陪陪 專攻社交遊戲!",
availability: "24小時待命 隨時開趴",
personality: "超會帶氣氛 人緣超好",
promise: "讓你的遊戲時光充滿歡笑!組隊開黑 一起嗨翻天🎊"
},
{
id: 6,
name: "生存專家",
photo: "https://imgproxy.goplayone.com/1/auto/420/420/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvdXNlci9wbGF5L2F2YXRhci9hYTFhNzdjMy05YTI2LTRmNTctOTlkMC02NTA2M2IwMDgyYTY=",
description: "荒野求生",
rank: "大師",
fans: 1800,
orders: 134,
badges: ["rank-master", "special-survival", "achievement-mentor"],
gender: "male" as const,
birthday: "1994-12-03",
greeting: "Hello~我是生存專家~荒野求生🏕️",
status: "建築系學生 兼職陪陪!",
availability: "晚上7點後 週末全天",
personality: "創意無限 耐心建造",
promise: "帶你建造夢想家園!從零開始 打造專屬世界🏗️"
}
]);
// Mock data for recommended services
const recommendedServices = computed(() => [
{
id: 1,
name: "1v1 聊天",
nameEn: "1v1 Chat",
image: "https://imgproxy.goplayone.com/1/auto/244/94/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvc2tpbGwvYzQ4YzUxYjE1YmUyZTFjMDcyOTk1ZGJhZGE0MmExY2EucG5n",
description: "一對一聊天服務",
category: "chat"
},
{
id: 2,
name: "唱歌",
nameEn: "Singing",
image: "http://imgproxy.goplayone.com/1/auto/244/94/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvc2tpbGwvYzY1ZTJhMjAzYzRlY2U2NzhkNjkxNGE3YTBhMmQ0ODMucG5n",
description: "音樂歌唱服務",
category: "music"
},
{
id: 3,
name: "STEAM",
nameEn: "STEAM",
image: "https://imgproxy.goplayone.com/1/auto/244/94/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvc2tpbGwvM2RmOGQ2NzdiYzZmMGIzMzIzZmQ1MGNhYWFhZDUyMjUucG5n",
description: "Steam 遊戲平台",
category: "gaming"
},
{
id: 4,
name: "英雄聯盟",
nameEn: "League of Legends",
image: "https://imgproxy.goplayone.com/1/auto/244/94/sm/0/aHR0cHM6Ly9wbGF5b25lLWFzc2V0cy5nb3BsYXlvbmUuY29tL3BsYXlvbmUvc2tpbGwvMTljZmUxYWYwMGYyN2M4YWY1ZTYzOGFkNjM0ZDNkMDYucG5n",
description: "英雄聯盟遊戲",
category: "gaming"
},
{
id: 5,
name: "王者榮耀",
nameEn: "Honor of Kings",
image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDIwMCAxMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9ImtpbmdHcmFkaWVudCIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+CjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiNGRkQ3MDAiLz4KPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjRkZBNTAwIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIxMjAiIGZpbGw9InVybCgja2luZ0dyYWRpZW50KSIvPgo8dGV4dCB4PSIxMDAiIHk9IjYwIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTgiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+546L5a2Q5rW35rKzPC90ZXh0Pgo8c3ZnIHg9IjgwIiB5PSIzMCIgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiBmaWxsPSIjRkZGRkZGIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMTgiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xMCAxMCBMMzAgMzAgTTMwIDEwIEwxMCAzMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cjwvc3ZnPgo=",
description: "王者榮耀手遊",
category: "gaming"
},
{
id: 6,
name: "原神",
nameEn: "Genshin Impact",
image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDIwMCAxMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9ImdlbnNoaW5HcmFkaWVudCIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+CjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM2NkMzRkYiLz4KPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDA2NkZGIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIxMjAiIGZpbGw9InVybCgjZ2Vuc2hpbkdyYWRpZW50KSIvPgo8dGV4dCB4PSIxMDAiIHk9IjYwIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMjQiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+5Y6f5YibPC90ZXh0Pgo8c3ZnIHg9IjgwIiB5PSIzMCIgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiBmaWxsPSIjRkZGRkZGIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMTgiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xMCAxMCBMMzAgMzAgTTMwIDEwIEwxMCAzMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cjwvc3ZnPgo=",
description: "原神開放世界遊戲",
category: "gaming"
},
{
id: 7,
name: "直播",
nameEn: "Live Streaming",
image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDIwMCAxMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9ImxpdmVHcmFkaWVudCIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+CjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiNGRjAwMDAiLz4KPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjQ0MwMDAwIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIxMjAiIGZpbGw9InVybCgjbGl2ZUdyYWRpZW50KSIvPgo8dGV4dCB4PSIxMDAiIHk9IjYwIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMjQiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+55m75b2VPC90ZXh0Pgo8c3ZnIHg9IjgwIiB5PSIzMCIgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiBmaWxsPSIjRkZGRkZGIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMTgiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xMCAxMCBMMzAgMzAgTTMwIDEwIEwxMCAzMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cjwvc3ZnPgo=",
description: "直播服務",
category: "streaming"
},
{
id: 8,
name: "陪玩",
nameEn: "Gaming Companion",
image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDIwMCAxMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9ImNvbXBhbm9uR3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgo8c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjRkY2NkNDIi8+CjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI0ZGMDA5OSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTIwIiBmaWxsPSJ1cmwoI2NvbXBhbm9uR3JhZGllbnQpIi8+Cjx0ZXh0IHg9IjEwMCIgeT0iNjAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj7mh6Dmh6A8L3RleHQ+CjxzdmcgeD0iODAiIHk9IjMwIiB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9IiNGRkZGRkYiPgo8Y2lyY2xlIGN4PSIyMCIgY3k9IjIwIiByPSIxOCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTEwIDEwIEwzMCAzMCBNMzAgMTAgTDEwIDMwIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4KPC9zdmc+Cg==",
description: "遊戲陪玩服務",
category: "companion"
}
]);
// Services drag state
const isDragging = ref(false);
const startX = ref(0);
const scrollLeft = ref(0);
const servicesContainer = ref<HTMLElement | null>(null);
// Carousel state
const currentSlide = ref(0);
const carouselInterval = ref<number | null>(null);
// Auto-play carousel
const startCarousel = () => {
carouselInterval.value = window.setInterval(() => {
currentSlide.value = (currentSlide.value + 1) % featuredStreams.value.length;
}, 4000);
};
const stopCarousel = () => {
if (carouselInterval.value) {
clearInterval(carouselInterval.value);
carouselInterval.value = null;
}
};
// Handle stream click
const handleStreamClick = (stream: any) => {
if (stream.externalUrl) {
window.open(stream.externalUrl, '_blank', 'noopener,noreferrer');
}
};
// Handle streamer click
const handleStreamerClick = (streamer: any) => {
console.log(`查看主播: ${streamer.name}`);
// TODO: Open streamer profile
};
// Handle more button click
const handleMoreClick = (streamer: any) => {
console.log(`查看更多: ${streamer.name}`);
// TODO: Open detailed streamer page
};
// Handle profile button click
const handleProfileClick = (streamer: any) => {
console.log(`前往主播主頁: ${streamer.name}`);
// TODO: Navigate to streamer profile page
};
// Handle service click
const handleServiceClick = (service: any) => {
console.log(`點擊服務: ${service.name}`);
// TODO: Navigate to service page or open service modal
};
// Handle service drag functionality
const handleMouseDown = (e: MouseEvent) => {
if (!servicesContainer.value) return;
isDragging.value = true;
startX.value = e.pageX - servicesContainer.value.offsetLeft;
scrollLeft.value = servicesContainer.value.scrollLeft;
// Prevent text selection while dragging
e.preventDefault();
};
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging.value || !servicesContainer.value) return;
e.preventDefault();
const x = e.pageX - servicesContainer.value.offsetLeft;
const walk = (x - startX.value) * 2; // Multiply for faster scrolling
servicesContainer.value.scrollLeft = scrollLeft.value - walk;
};
const handleMouseUp = () => {
isDragging.value = false;
};
const handleMouseLeave = () => {
isDragging.value = false;
};
// Handle more services button click
const handleMoreServicesClick = () => {
console.log('點擊更多服務');
// TODO: Navigate to services page
};
onMounted(() => {
startCarousel();
});
onUnmounted(() => {
stopCarousel();
});
</script>
<template>
<div class="news-hub">
<!-- Header -->
<div class="hub-header">
<h2 class="hub-title">📰 {{ t('livestream.title') }}</h2>
<div class="hub-subtitle">{{ t('livestream.subtitle') }}</div>
</div>
<!-- Home View -->
<div class="home-view">
<!-- Featured Streams Carousel -->
<div class="carousel-section">
<h3 class="section-title">🔥 {{ t('livestream.featuredStreams') }}</h3>
<div class="carousel-container" @mouseenter="stopCarousel" @mouseleave="startCarousel">
<div class="carousel-track" :style="{ transform: `translateX(-${currentSlide * 100}%)` }">
<div
v-for="stream in featuredStreams"
:key="stream.id"
class="carousel-slide"
@click="handleStreamClick(stream)"
>
<div class="stream-image-card">
<img :src="stream.image" :alt="`Stream ${stream.id}`" />
</div>
</div>
</div>
<!-- Carousel Controls -->
<button
class="carousel-btn prev"
@click="currentSlide = currentSlide > 0 ? currentSlide - 1 : featuredStreams.length - 1"
>
</button>
<button
class="carousel-btn next"
@click="currentSlide = (currentSlide + 1) % featuredStreams.length"
>
</button>
<!-- Carousel Indicators -->
<div class="carousel-indicators">
<button
v-for="(stream, index) in featuredStreams"
:key="index"
class="indicator"
:class="{ active: index === currentSlide }"
@click="currentSlide = index"
></button>
</div>
</div>
</div>
<!-- Recommended Services -->
<div class="services-section">
<div class="services-header">
<h3 class="section-title">🎯 {{ t('livestream.recommendedServices') }}</h3>
<button class="more-services-btn" @click="handleMoreServicesClick">
更多
</button>
</div>
<div
ref="servicesContainer"
class="services-scroll-container"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
:class="{ 'dragging': isDragging }"
>
<div class="services-grid">
<div
v-for="service in recommendedServices"
:key="service.id"
class="service-card"
@click="handleServiceClick(service)"
>
<div class="service-image">
<img :src="service.image" :alt="service.name" />
</div>
<div class="service-label">{{ service.name }}</div>
</div>
</div>
</div>
</div>
<!-- Popular Streamers -->
<div class="streamers-section">
<h3 class="section-title"> {{ t('livestream.popularStreamers') }}</h3>
<div class="streamers-grid">
<StreamerFlipCard
v-for="streamer in popularStreamers"
:key="streamer.id"
:streamer="streamer"
@click="handleStreamerClick"
@more="handleMoreClick"
@profile="handleProfileClick"
/>
</div>
</div>
</div> <!-- End home-view -->
</div>
</template>
<style scoped>
.news-hub {
width: 100%;
height: 100%;
background: var(--window-background);
display: flex;
flex-direction: column;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
}
.hub-header {
text-align: center;
margin-bottom: 24px;
}
.hub-title {
font-size: 24px;
font-weight: bold;
color: var(--content-text-color);
margin: 0 0 8px 0;
}
.hub-subtitle {
font-size: 14px;
color: var(--content-text-color);
opacity: 0.7;
}
.carousel-section {
margin-bottom: 32px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: var(--content-text-color);
margin: 0 0 16px 0;
}
.carousel-container {
position: relative;
width: 100%;
aspect-ratio: 16/9;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.carousel-track {
display: flex;
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out;
}
.carousel-slide {
min-width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.stream-image-card {
width: 100%;
height: 100%;
position: relative;
cursor: pointer;
transition: transform 0.3s ease;
overflow: hidden;
}
.stream-image-card img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
transition: transform 0.3s ease;
}
.stream-image-card:hover img {
transform: scale(1.05);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.carousel-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
border: none;
color: white;
font-size: 24px;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
z-index: 10;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
}
.carousel-btn:hover {
background: rgba(0, 0, 0, 0.7);
transform: translateY(-50%) scale(1.1);
}
.carousel-btn.prev {
left: 16px;
}
.carousel-btn.next {
right: 16px;
}
.carousel-indicators {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 10;
}
.indicator {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.3);
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
aspect-ratio: 1;
min-width: 12px;
min-height: 12px;
}
.indicator.active {
background: white;
border-color: white;
transform: scale(1.2);
}
.services-section {
margin-bottom: 32px;
}
.services-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.more-services-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.more-services-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.services-scroll-container {
overflow-x: auto;
overflow-y: hidden;
cursor: grab;
border-radius: 12px;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.services-scroll-container::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.services-scroll-container.dragging {
cursor: grabbing;
user-select: none;
}
.services-grid {
display: flex;
gap: 24px;
padding: 0 12px;
min-width: max-content;
}
.service-card {
background: white;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
width: 200px;
flex-shrink: 0;
}
.service-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.service-image {
width: 100%;
height: 120px;
overflow: hidden;
position: relative;
}
.service-image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.service-label {
padding: 12px 16px;
text-align: center;
font-size: 14px;
font-weight: 600;
color: #333333;
background: white;
}
.streamers-section {
flex: 1;
}
.streamers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
align-items: start;
}
/* 響應式網格佈局 */
/* 響應式設計 */
@media (max-width: 1200px) {
.services-grid {
gap: 20px;
padding: 0 16px;
}
.service-card {
width: 250px;
}
.streamers-grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.carousel-container {
aspect-ratio: 16/9;
}
.carousel-btn {
width: 40px;
height: 40px;
font-size: 20px;
}
.carousel-btn.prev {
left: 12px;
}
.carousel-btn.next {
right: 12px;
}
}
@media (max-width: 768px) {
.news-hub {
padding: 16px;
}
.hub-title {
font-size: 24px;
}
.hub-subtitle {
font-size: 14px;
}
.services-grid {
gap: 16px;
padding: 0 12px;
}
.service-card {
width: 180px;
}
.service-image {
height: 100px;
}
.service-label {
padding: 10px 12px;
font-size: 13px;
}
.more-services-btn {
padding: 6px 12px;
font-size: 12px;
}
.streamers-grid {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
}
.carousel-container {
aspect-ratio: 16/9;
border-radius: 8px;
}
.carousel-btn {
width: 36px;
height: 36px;
font-size: 18px;
}
.carousel-btn.prev {
left: 8px;
}
.carousel-btn.next {
right: 8px;
}
.carousel-indicators {
bottom: 12px;
gap: 6px;
}
.indicator {
width: 10px;
height: 10px;
}
}
@media (max-width: 480px) {
.news-hub {
padding: 12px;
}
.hub-title {
font-size: 20px;
margin-bottom: 8px;
}
.hub-subtitle {
font-size: 13px;
margin-bottom: 20px;
}
.services-grid {
gap: 12px;
padding: 0 8px;
}
.service-card {
width: 150px;
}
.service-image {
height: 80px;
}
.service-label {
padding: 8px 10px;
font-size: 12px;
}
.more-services-btn {
padding: 4px 8px;
font-size: 11px;
}
.streamers-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.carousel-container {
aspect-ratio: 16/9;
border-radius: 6px;
}
.carousel-btn {
width: 32px;
height: 32px;
font-size: 16px;
}
.carousel-btn.prev {
left: 6px;
}
.carousel-btn.next {
right: 6px;
}
.carousel-indicators {
bottom: 8px;
gap: 4px;
}
.indicator {
width: 8px;
height: 8px;
}
}
/* Light theme adjustments */
.theme-light .carousel-btn {
background: rgba(255, 255, 255, 0.8);
color: #333;
}
.theme-light .carousel-btn:hover {
background: rgba(255, 255, 255, 0.9);
}
.theme-light .indicator {
border-color: rgba(0, 0, 0, 0.6);
background: rgba(0, 0, 0, 0.3);
}
.theme-light .indicator.active {
background: #333;
border-color: #333;
}
</style>

View File

@ -0,0 +1,342 @@
<template>
<div :class="['streamer-badge', badgeClass, sizeClass]" :title="badgeConfig.tooltip">
<span v-if="badgeConfig.icon" class="badge-icon">{{ badgeConfig.icon }}</span>
<span v-if="badgeConfig.text" class="badge-text">{{ badgeConfig.text }}</span>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
code: string;
size?: 'small' | 'medium' | 'large';
}
const props = defineProps<Props>();
const badgeConfigs: { [key: string]: { text: string; icon?: string; class: string; tooltip: string; } } = {
//
'rank-diamond': { text: '鑽石', icon: '💎', class: 'badge-rank-diamond', tooltip: '鑽石級玩家' },
'rank-gold': { text: '黃金', icon: '🏆', class: 'badge-rank-gold', tooltip: '黃金級玩家' },
'rank-silver': { text: '白銀', icon: '🥈', class: 'badge-rank-silver', tooltip: '白銀級玩家' },
'rank-bronze': { text: '青銅', icon: '🥉', class: 'badge-rank-bronze', tooltip: '青銅級玩家' },
'rank-master': { text: '大師', icon: '🎖️', class: 'badge-rank-master', tooltip: '大師級玩家' },
//
'pro-gaming': { text: '電競', icon: '🎮', class: 'badge-pro-gaming', tooltip: '專業電競陪玩' },
'pro-music': { text: '音樂', icon: '🎵', class: 'badge-pro-music', tooltip: '音樂專家' },
'pro-anime': { text: '二次元', icon: '🌸', class: 'badge-pro-anime', tooltip: '二次元專家' },
'pro-party': { text: '派對', icon: '🎉', class: 'badge-pro-party', tooltip: '派對達人' },
'pro-strategy': { text: '策略', icon: '♟️', class: 'badge-pro-strategy', tooltip: '策略大師' },
'pro-survival': { text: '生存', icon: '🏹', class: 'badge-pro-survival', tooltip: '生存專家' },
//
'feature-live': { text: '直播中', icon: '🔴', class: 'badge-feature-live', tooltip: '正在直播' },
'feature-verified': { text: '認證', icon: '✅', class: 'badge-feature-verified', tooltip: '認證主播' },
'feature-new': { text: '新人', icon: '⭐', class: 'badge-feature-new', tooltip: '新進主播' },
'feature-popular': { text: '熱門', icon: '🔥', class: 'badge-feature-popular', tooltip: '熱門主播' },
'feature-vip': { text: 'VIP', icon: '👑', class: 'badge-feature-vip', tooltip: 'VIP主播' },
//
'special-anime': { text: '動漫', icon: '🎌', class: 'badge-special-anime', tooltip: '動漫專家' },
'special-party': { text: '派對', icon: '🎊', class: 'badge-special-party', tooltip: '派對女王' },
'special-survival': { text: '生存', icon: '🛡️', class: 'badge-special-survival', tooltip: '生存專家' },
//
'achievement-expert': { text: '專家', icon: '🎯', class: 'badge-achievement-expert', tooltip: '領域專家' },
'achievement-mentor': { text: '導師', icon: '👨‍🏫', class: 'badge-achievement-mentor', tooltip: '新手導師' }
};
const badgeConfig = computed(() => badgeConfigs[props.code] || { text: props.code, class: 'badge-default', tooltip: props.code });
const badgeClass = computed(() => badgeConfig.value.class);
const sizeClass = computed(() => `badge-size-${props.size || 'medium'}`);
</script>
<style scoped>
.streamer-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 80px;
}
.streamer-badge:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.badge-icon {
font-size: 10px;
line-height: 1;
}
.badge-text {
font-size: 9px;
line-height: 1;
font-weight: 700;
}
/* 尺寸變體 */
.badge-size-small {
padding: 2px 6px;
font-size: 8px;
border-radius: 8px;
max-width: 60px;
}
.badge-size-small .badge-icon {
font-size: 8px;
}
.badge-size-small .badge-text {
font-size: 7px;
}
.badge-size-medium {
padding: 4px 8px;
font-size: 10px;
border-radius: 12px;
max-width: 80px;
}
.badge-size-large {
padding: 6px 12px;
font-size: 12px;
border-radius: 16px;
max-width: 100px;
}
.badge-size-large .badge-icon {
font-size: 12px;
}
.badge-size-large .badge-text {
font-size: 11px;
}
/* 等級徽章 */
.badge-rank-diamond {
background: linear-gradient(135deg, #b9f2ff 0%, #00d4ff 100%);
color: #0066cc;
border-color: rgba(0, 102, 204, 0.3);
}
.badge-rank-gold {
background: linear-gradient(135deg, #ffd700 0%, #ffb347 100%);
color: #b8860b;
border-color: rgba(184, 134, 11, 0.3);
}
.badge-rank-silver {
background: linear-gradient(135deg, #c0c0c0 0%, #a8a8a8 100%);
color: #696969;
border-color: rgba(105, 105, 105, 0.3);
}
.badge-rank-bronze {
background: linear-gradient(135deg, #cd7f32 0%, #b87333 100%);
color: #8b4513;
border-color: rgba(139, 69, 19, 0.3);
}
.badge-rank-master {
background: linear-gradient(135deg, #ffd700 0%, #ff8c00 100%);
color: #8b4513;
border-color: rgba(139, 69, 19, 0.3);
animation: shimmer 3s ease-in-out infinite;
}
/* 專業徽章 */
.badge-pro-gaming {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
color: white;
border-color: rgba(255, 255, 255, 0.3);
}
.badge-pro-music {
background: linear-gradient(135deg, #a8e6cf 0%, #7fcdcd 100%);
color: #2c5530;
border-color: rgba(44, 85, 48, 0.3);
}
.badge-pro-anime {
background: linear-gradient(135deg, #ffb3ba 0%, #ffdfba 100%);
color: #d63384;
border-color: rgba(214, 51, 132, 0.3);
}
.badge-pro-party {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
color: #e91e63;
border-color: rgba(233, 30, 99, 0.3);
}
.badge-pro-strategy {
background: linear-gradient(135deg, #a8c8ec 0%, #5d9cec 100%);
color: #2c3e50;
border-color: rgba(44, 62, 80, 0.3);
}
.badge-pro-survival {
background: linear-gradient(135deg, #d4a574 0%, #8b7355 100%);
color: white;
border-color: rgba(255, 255, 255, 0.3);
}
/* 特殊徽章 */
.badge-special-anime {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
color: #e91e63;
border-color: rgba(233, 30, 99, 0.3);
}
.badge-special-party {
background: linear-gradient(135deg, #ff6b9d 0%, #c44569 100%);
color: white;
border-color: rgba(255, 255, 255, 0.3);
animation: pulse 2s infinite;
}
.badge-special-survival {
background: linear-gradient(135deg, #8b7355 0%, #6b5b73 100%);
color: white;
border-color: rgba(255, 255, 255, 0.3);
}
/* 特色徽章 */
.badge-feature-live {
background: linear-gradient(135deg, #ff4757 0%, #c44569 100%);
color: white;
border-color: rgba(255, 255, 255, 0.3);
animation: pulse 2s infinite;
}
.badge-feature-verified {
background: linear-gradient(135deg, #2ed573 0%, #1e90ff 100%);
color: white;
border-color: rgba(255, 255, 255, 0.3);
}
.badge-feature-new {
background: linear-gradient(135deg, #ffa502 0%, #ff6348 100%);
color: white;
border-color: rgba(255, 255, 255, 0.3);
}
.badge-feature-popular {
background: linear-gradient(135deg, #ff3838 0%, #ff6b35 100%);
color: white;
border-color: rgba(255, 255, 255, 0.3);
animation: glow 2s ease-in-out infinite alternate;
}
.badge-feature-vip {
background: linear-gradient(135deg, #ffd700 0%, #ff8c00 100%);
color: #8b4513;
border-color: rgba(139, 69, 19, 0.3);
animation: shimmer 3s ease-in-out infinite;
}
/* 成就徽章 */
.badge-achievement-expert {
background: linear-gradient(135deg, #2ed573 0%, #1e90ff 100%);
color: white;
border-color: rgba(255, 255, 255, 0.3);
animation: glow 2s ease-in-out infinite alternate;
}
.badge-achievement-mentor {
background: linear-gradient(135deg, #ffa502 0%, #ff6348 100%);
color: white;
border-color: rgba(255, 255, 255, 0.3);
}
/* 預設徽章 */
.badge-default {
background: linear-gradient(135deg, #ddd 0%, #bbb 100%);
color: #666;
border-color: rgba(102, 102, 102, 0.3);
}
/* 動畫效果 */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
@keyframes glow {
from { box-shadow: 0 2px 8px rgba(255, 56, 56, 0.3); }
to { box-shadow: 0 4px 16px rgba(255, 56, 56, 0.6); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.badge-feature-vip {
background-size: 200% 100%;
animation: shimmer 3s ease-in-out infinite;
}
/* 響應式設計 */
@media (max-width: 768px) {
.streamer-badge {
padding: 3px 6px;
font-size: 9px;
border-radius: 10px;
max-width: 70px;
}
.badge-size-small {
padding: 2px 4px;
font-size: 7px;
border-radius: 6px;
max-width: 50px;
}
.badge-size-large {
padding: 4px 8px;
font-size: 10px;
border-radius: 12px;
max-width: 80px;
}
}
@media (max-width: 480px) {
.streamer-badge {
padding: 2px 5px;
font-size: 8px;
border-radius: 8px;
max-width: 60px;
}
.badge-size-small {
padding: 1px 3px;
font-size: 6px;
border-radius: 4px;
max-width: 40px;
}
.badge-size-large {
padding: 3px 6px;
font-size: 9px;
border-radius: 10px;
max-width: 70px;
}
}
</style>

View File

@ -0,0 +1,694 @@
<template>
<div
class="flip-card"
:class="{ 'is-flipped': isFlipped }"
@mouseenter="handleCardHover"
@mouseleave="handleCardLeave"
>
<div class="flip-card-inner">
<!-- 卡片背面 (左邊的樣式) -->
<div class="flip-card-back">
<div class="back-content">
<h3 class="back-title">{{ streamer.name }}</h3>
<p class="greeting">{{ streamer.greeting }}</p>
<p class="status">{{ streamer.status }}</p>
<p class="availability">{{ streamer.availability }}</p>
<p class="personality">{{ streamer.personality }}</p>
<p class="promise">{{ streamer.promise }}</p>
<!-- 主播主頁按鈕 -->
<button
class="profile-button"
@click="handleProfileClick"
>
去這個主播主頁
</button>
</div>
</div>
<!-- 卡片正面 - 簡潔設計 -->
<div class="flip-card-front">
<div class="photo-container">
<img
:src="streamer.photo"
:alt="streamer.name"
class="streamer-photo"
/>
<!-- 簡潔的覆蓋層 -->
<div class="photo-overlay">
<!-- 右上角徽章組 -->
<div class="badges-container">
<StreamerBadge
v-for="(badgeCode, index) in streamer.badges.slice(0, 3)"
:key="`${streamer.id}-badge-${index}`"
:code="badgeCode"
size="small"
class="streamer-badge"
/>
</div>
<!-- 底部資訊條 -->
<div class="info-bar">
<div class="streamer-info">
<h3 class="streamer-name">{{ streamer.name }}</h3>
<p class="streamer-description">{{ streamer.description }}</p>
</div>
<!-- 性別和生日資訊 -->
<div class="personal-info">
<div class="gender-badge" :class="streamer.gender">
<span class="gender-icon">{{ streamer.gender === 'female' ? '♀' : '♂' }}</span>
<span class="gender-text">{{ streamer.gender === 'female' ? '女' : '男' }}</span>
</div>
<div class="birthday-info">
<span class="birthday-icon">🎂</span>
<span class="birthday-text">{{ formatBirthday(streamer.birthday) }}</span>
</div>
</div>
<div class="stats-row">
<div class="stat-item">
<span class="stat-number">{{ streamer.fans }}</span>
<span class="stat-label">粉絲</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ streamer.orders }}</span>
<span class="stat-label">接單</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import StreamerBadge from './StreamerBadge.vue';
interface Streamer {
id: number;
name: string;
photo: string;
description: string;
rank: string;
fans: number;
orders: number;
badges: string[];
greeting: string;
status: string;
availability: string;
personality: string;
promise: string;
gender: 'male' | 'female';
birthday: string;
}
interface Props {
streamer: Streamer;
}
const props = defineProps<Props>();
const emit = defineEmits<{
click: [streamer: Streamer];
more: [streamer: Streamer];
profile: [streamer: Streamer];
}>();
const isFlipped = ref(false);
const handleCardHover = () => {
isFlipped.value = true;
console.log('Card hovered, showing back');
};
const handleCardLeave = () => {
isFlipped.value = false;
console.log('Card left, showing front');
};
const handleMoreClick = (event: Event) => {
event.stopPropagation();
emit('more', props.streamer);
};
const handleStreamerClick = (event: Event) => {
event.stopPropagation();
emit('click', props.streamer);
};
const handleProfileClick = (event: Event) => {
event.stopPropagation();
emit('profile', props.streamer);
};
//
const formatBirthday = (birthday: string): string => {
const date = new Date(birthday);
return `${date.getMonth() + 1}/${date.getDate()}`;
};
</script>
<style scoped>
/* 翻轉卡片容器 */
.flip-card {
width: 100%;
height: 320px;
min-height: 320px;
max-height: 320px;
perspective: 1000px;
cursor: pointer;
position: relative;
z-index: 1;
transition: z-index 0.3s ease;
}
.flip-card:hover {
z-index: 10;
}
.flip-card.is-flipped {
z-index: 5;
}
.flip-card-inner {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.6s;
transform-style: preserve-3d;
min-height: 320px;
max-height: 320px;
box-sizing: border-box;
}
.flip-card-back,
.flip-card-front {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1;
/* 確保正面和背面尺寸完全一致 */
min-height: 320px;
max-height: 320px;
box-sizing: border-box;
}
/* 卡片背面樣式 (左邊) */
.flip-card-back {
background: linear-gradient(135deg, rgba(255, 107, 157, 0.8) 0%, rgba(255, 167, 38, 0.8) 100%);
color: white;
display: none;
flex-direction: column;
justify-content: space-between;
padding: 20px;
position: relative;
overflow: hidden;
/* 移除框框效果 */
border: none;
box-shadow: none;
border-radius: 12px;
}
.flip-card-back::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.05) 0%, transparent 70%);
pointer-events: none;
}
.back-content {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
align-items: center;
text-align: center;
position: relative;
z-index: 1;
padding: 50px 25px;
box-sizing: border-box;
gap: 10px;
}
.back-title {
font-size: 20px;
font-weight: bold;
margin: 0 0 16px 0;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.back-content p {
margin: 0;
font-size: 13px;
line-height: 1.4;
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.greeting {
font-weight: 600;
color: #fff3e0;
font-size: 15px;
}
.status {
color: #ffe0b2;
font-size: 14px;
}
.availability {
color: #ffccbc;
font-size: 13px;
}
.personality {
color: #f8bbd9;
font-size: 13px;
}
.promise {
color: #e1bee7;
font-size: 12px;
margin: 0;
opacity: 0.9;
}
/* 主播主頁按鈕 */
.profile-button {
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.4);
color: white;
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 20px;
border-radius: 25px;
backdrop-filter: blur(8px);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
min-width: 140px;
}
.profile-button:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.6);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.profile-button:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
/* 卡片正面樣式 - 簡潔設計 */
.flip-card-front {
background: white;
display: flex;
flex-direction: column;
position: relative;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.photo-container {
width: 100%;
height: 100%;
position: relative;
border-radius: 12px;
overflow: hidden;
}
.streamer-photo {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.photo-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 16px;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.1) 0%,
transparent 30%,
transparent 70%,
rgba(0, 0, 0, 0.7) 100%
);
}
/* 徽章容器 */
.badges-container {
position: absolute;
top: 16px;
right: 16px;
display: flex;
flex-direction: column;
gap: 6px;
max-width: 120px;
align-items: flex-end;
}
.streamer-badge {
flex-shrink: 0;
}
/* 底部資訊條 */
.info-bar {
background: transparent;
backdrop-filter: none;
border-radius: 12px;
padding: 12px;
margin-top: auto;
}
.streamer-info {
margin-bottom: 8px;
}
.streamer-name {
font-size: 18px;
font-weight: 700;
color: white;
margin: 0 0 4px 0;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.8);
}
.streamer-description {
font-size: 12px;
color: white;
margin: 0;
line-height: 1.3;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
}
/* 個人資訊區域 */
.personal-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
gap: 8px;
}
/* 性別徽章 */
.gender-badge {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
backdrop-filter: blur(4px);
}
.gender-badge.female {
background: rgba(255, 182, 193, 0.8);
color: #d63384;
border: 1px solid rgba(214, 51, 132, 0.3);
}
.gender-badge.male {
background: rgba(173, 216, 230, 0.8);
color: #0d6efd;
border: 1px solid rgba(13, 110, 253, 0.3);
}
.gender-icon {
font-size: 12px;
font-weight: bold;
}
.gender-text {
font-size: 10px;
}
/* 生日資訊 */
.birthday-info {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.15);
border-radius: 12px;
font-size: 10px;
font-weight: 500;
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.birthday-icon {
font-size: 10px;
}
.birthday-text {
font-size: 10px;
opacity: 0.9;
}
/* 統計行 */
.stats-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.stat-number {
font-size: 16px;
font-weight: 700;
color: white;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.8);
line-height: 1;
}
.stat-label {
font-size: 10px;
color: white;
margin-top: 2px;
text-transform: uppercase;
letter-spacing: 0.5px;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
}
/* 翻轉狀態樣式 - 懸停時顯示背面(粉橙漸變) */
.flip-card.is-flipped .flip-card-back {
display: flex;
}
.flip-card.is-flipped .flip-card-front {
display: none;
}
/* 響應式設計 */
@media (max-width: 768px) {
.flip-card {
height: 300px;
min-height: 300px;
max-height: 300px;
}
.flip-card-inner {
min-height: 300px;
max-height: 300px;
}
.flip-card-back,
.flip-card-front {
min-height: 300px;
max-height: 300px;
}
.back-content {
padding: 45px 22px;
gap: 12px;
}
.back-title {
font-size: 20px;
margin-bottom: 14px;
}
.back-content p {
font-size: 13px;
line-height: 1.4;
}
.profile-button {
padding: 8px 16px;
font-size: 13px;
margin-top: 16px;
min-width: 120px;
}
/* 正面卡片響應式 */
.streamer-name {
font-size: 16px;
}
.streamer-description {
font-size: 12px;
}
.personal-info {
margin-bottom: 8px;
gap: 6px;
}
.gender-badge {
padding: 3px 6px;
font-size: 10px;
}
.birthday-info {
padding: 3px 6px;
font-size: 9px;
}
.stat-number {
font-size: 14px;
}
.stat-label {
font-size: 10px;
}
/* 徽章響應式 */
.badges-container {
top: 12px;
right: 12px;
max-width: 100px;
gap: 4px;
}
}
@media (max-width: 480px) {
.flip-card {
height: 280px;
min-height: 280px;
max-height: 280px;
}
.flip-card-inner {
min-height: 280px;
max-height: 280px;
}
.flip-card-back,
.flip-card-front {
min-height: 280px;
max-height: 280px;
}
.back-content {
padding: 35px 18px;
gap: 10px;
}
.back-title {
font-size: 18px;
margin-bottom: 12px;
}
.back-content p {
font-size: 12px;
line-height: 1.3;
}
.profile-button {
padding: 6px 12px;
font-size: 12px;
margin-top: 14px;
min-width: 100px;
}
/* 正面卡片響應式 */
.streamer-name {
font-size: 15px;
}
.streamer-description {
font-size: 11px;
}
.personal-info {
margin-bottom: 6px;
gap: 4px;
}
.gender-badge {
padding: 2px 5px;
font-size: 9px;
}
.birthday-info {
padding: 2px 5px;
font-size: 8px;
}
.stat-number {
font-size: 13px;
}
.stat-label {
font-size: 9px;
}
/* 徽章響應式 */
.badges-container {
top: 10px;
right: 10px;
max-width: 90px;
gap: 3px;
}
}
/* 深色主題適配 */
.theme-dark .flip-card-front {
background: var(--window-background);
color: var(--content-text-color);
}
.theme-dark .streamer-name {
color: var(--content-text-color);
}
.theme-dark .streamer-description {
color: var(--content-text-color);
opacity: 0.8;
}
</style>

View File

@ -18,14 +18,15 @@
"back": "Back"
},
"apps": {
"livestream-hub": "LiveStream Hub",
"livestream-hub": "Latest News",
"calculator": "Calculator"
},
"livestream": {
"title": "LiveStream Hub",
"subtitle": "Discover amazing live content",
"featuredStreams": "Featured Streams",
"popularStreamers": "Popular Streamers",
"title": "Latest News",
"subtitle": "Stay updated with the latest information",
"featuredStreams": "Featured News",
"popularStreamers": "Popular Updates",
"recommendedServices": "Recommended Services",
"viewers": "viewers",
"live": "Live",
"offline": "Offline",

View File

@ -18,14 +18,15 @@
"back": "返回"
},
"apps": {
"livestream-hub": "直播中心",
"livestream-hub": "最新消息",
"calculator": "計算機"
},
"livestream": {
"title": "直播中心",
"subtitle": "發現最精彩的直播內容",
"featuredStreams": "精選直播",
"popularStreamers": "熱門主播",
"title": "最新消息",
"subtitle": "掌握最新資訊動態",
"featuredStreams": "精選消息",
"popularStreamers": "熱門資訊",
"recommendedServices": "推薦服務",
"viewers": "觀看",
"live": "直播中",
"offline": "離線",

73
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@nuxt/scripts": "^0.11.13",
"@nuxt/test-utils": "^3.19.2",
"@nuxt/ui": "^3.3.4",
"@nuxtjs/i18n": "^10.1.0",
"@pinia/nuxt": "^0.11.2",
"@unhead/vue": "^2.0.17",
"better-sqlite3": "^12.3.0",
@ -23,9 +24,7 @@
"vue": "^3.5.21",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@nuxtjs/i18n": "^10.1.0"
}
"devDependencies": {}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
@ -1426,7 +1425,6 @@
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-11.0.1.tgz",
"integrity": "sha512-5l10G5wE2cQRsZMS9y0oSFMOLW5IG/SgbkIUltqnwF1EMRrRbUAHFiPabXdGTHeexCsMTcxj/1w9i0rzjJU9IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "^11.1.10",
@ -1455,14 +1453,12 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@intlify/core": {
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/core/-/core-11.1.12.tgz",
"integrity": "sha512-Uccp4VtalUSk/b4F9nBBs7VGgIh9VnXTSHHQ+Kc0AetsHJLxdi04LfhfSi4dujtsTAWnHMHWZw07UbMm6Umq1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.1.12",
@ -1479,7 +1475,6 @@
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.12.tgz",
"integrity": "sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.1.12",
@ -1496,7 +1491,6 @@
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@intlify/h3/-/h3-0.7.1.tgz",
"integrity": "sha512-D/9+L7IzPrOa7e6R/ztepXayAq+snfzBYIwAk3RbaQsLEXwVNjC5c+WKXjni1boc/plGRegw4/m33SaFwvdEpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/core": "^11.0.0",
@ -1513,7 +1507,6 @@
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.12.tgz",
"integrity": "sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.1.12",
@ -1530,7 +1523,6 @@
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.12.tgz",
"integrity": "sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 16"
@ -1543,7 +1535,6 @@
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-11.0.1.tgz",
"integrity": "sha512-nH5NJdNjy/lO6Ne8LDtZzv4SbpVsMhPE+LbvBDmMeIeJDiino8sOJN2QB3MXzTliYTnqe3aB9Fw5+LJ/XVaXCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
@ -1581,7 +1572,6 @@
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@intlify/utils/-/utils-0.13.0.tgz",
"integrity": "sha512-8i3uRdAxCGzuHwfmHcVjeLQBtysQB2aXl/ojoagDut5/gY5lvWCQ2+cnl2TiqE/fXj/D8EhWG/SLKA7qz4a3QA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 18"
@ -1594,7 +1584,6 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@intlify/vue-i18n-extensions/-/vue-i18n-extensions-8.0.0.tgz",
"integrity": "sha512-w0+70CvTmuqbskWfzeYhn0IXxllr6mU+IeM2MU0M+j9OW64jkrvqY+pYFWrUnIIC9bEdij3NICruicwd5EgUuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.24.6",
@ -1630,7 +1619,6 @@
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.8.tgz",
"integrity": "sha512-FoHslNWSoHjdUBLy35bpm9PV/0LVI/DSv9L6Km6J2ad8r/mm0VaGg06C40FqlE8u2ADcGUM60lyoU7Myo4WNZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "10.0.8",
@ -1647,7 +1635,6 @@
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.8.tgz",
"integrity": "sha512-DV+sYXIkHVd5yVb2mL7br/NEUwzUoLBsMkV3H0InefWgmYa34NLZUvMCGi5oWX+Hqr2Y2qUxnVrnOWF4aBlgWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/shared": "10.0.8",
@ -1664,7 +1651,6 @@
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.8.tgz",
"integrity": "sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 16"
@ -1677,14 +1663,12 @@
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"dev": true,
"license": "MIT"
},
"node_modules/@intlify/vue-i18n-extensions/node_modules/vue-i18n": {
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.8.tgz",
"integrity": "sha512-mIjy4utxMz9lMMo6G9vYePv7gUFt4ztOMhY9/4czDJxZ26xPeJ49MAGa9wBAE3XuXbYCrtVPmPxNjej7JJJkZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/core-base": "10.0.8",
@ -1858,7 +1842,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@miyaneee/rollup-plugin-json5/-/rollup-plugin-json5-1.2.0.tgz",
"integrity": "sha512-JjTIaXZp9WzhUHpElrqPnl1AzBi/rvRs065F71+aTmlqvTMVkdbjZ8vfFl4nRlgJy+TPBw69ZK4pwFdmOAt4aA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
@ -2873,7 +2856,6 @@
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@nuxtjs/i18n/-/i18n-10.1.0.tgz",
"integrity": "sha512-2h/6Y4ke+mYq3RrV71erTBn1HzKKKPGEJrzYW6GA8SAc91zb7jqyfRkElG95Cei+2+6XJrt73Djys5qTc0tCUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/core": "^11.1.11",
@ -2917,7 +2899,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.1.2.tgz",
"integrity": "sha512-P5q41xeEOa6ZQC0PvIP7TSBmOAMxXK4qihDcCbYIJq8RcVsEPbGZVlidmxE6EOw1ucSyodq9nbV31FAKwoL4NQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"c12": "^3.2.0",
@ -2954,7 +2935,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -2971,7 +2951,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -2988,7 +2967,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3005,7 +2983,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3022,7 +2999,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3039,7 +3015,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3056,7 +3031,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3073,7 +3047,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3090,7 +3063,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3107,7 +3079,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3124,7 +3095,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3141,7 +3111,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3158,7 +3127,6 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@ -3175,7 +3143,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3192,7 +3159,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3206,7 +3172,6 @@
"version": "0.81.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.81.0.tgz",
"integrity": "sha512-CnOqkybZK8z6Gx7Wb1qF7AEnSzbol1WwcIzxYOr8e91LytGOjo0wCpgoYWZo8sdbpqX+X+TJayIzo4Pv0R/KjA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
@ -3219,7 +3184,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3236,7 +3200,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3253,7 +3216,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3270,7 +3232,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3287,7 +3248,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3304,7 +3264,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3321,7 +3280,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3338,7 +3296,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3355,7 +3312,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3372,7 +3328,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3389,7 +3344,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3406,7 +3360,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3423,7 +3376,6 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@ -3440,7 +3392,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3457,7 +3408,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3471,7 +3421,6 @@
"version": "3.0.0-beta.15",
"resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.0.0-beta.15.tgz",
"integrity": "sha512-DMgq/rIh1H20WYNWU7krIbEfJRYDDhy7ix64GlT4AVUJZZWCZ5pxiYVJR3A3GmWQPkn7Pg7i3oIiGqu4JGC65w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/compiler-sfc": "^3.5.17",
@ -3499,14 +3448,12 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
"integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==",
"dev": true,
"license": "MIT"
},
"node_modules/@nuxtjs/i18n/node_modules/oxc-parser": {
"version": "0.81.0",
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.81.0.tgz",
"integrity": "sha512-iceu9s70mZyjKs6V2QX7TURkJj1crnKi9csGByWvOWwrR5rwq0U0f49yIlRAzMP4t7K2gRC1MnyMZggMhiwAVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "^0.81.0"
@ -3539,7 +3486,6 @@
"version": "0.81.0",
"resolved": "https://registry.npmjs.org/oxc-transform/-/oxc-transform-0.81.0.tgz",
"integrity": "sha512-Sfb7sBZJoA7GPNlgeVvwqSS+fKFG5Lu2N4CJIlKPdkBgMDwVqUPOTVrEXHYaoYilA2x0VXVwLWqjcW3CwrfzSA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@ -3569,7 +3515,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-0.4.0.tgz",
"integrity": "sha512-x5TJAZQD3kRnRBGZ+8uryMZUwkTYddwzBftkqyJIcmpBOXmoK/fwriRKATjZroR2d+aS7+2w1B0oz189bBTwfw==",
"dev": true,
"license": "MIT",
"dependencies": {
"estree-walker": "^3.0.3",
@ -3583,7 +3528,6 @@
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/unplugin-vue-router/-/unplugin-vue-router-0.14.0.tgz",
"integrity": "sha512-ipjunvS5e2aFHBAUFuLbHl2aHKbXXXBhTxGT9wZx66fNVPdEQzVVitF8nODr1plANhTTa3UZ+DQu9uyLngMzoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue-macros/common": "3.0.0-beta.15",
@ -5021,7 +4965,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-yaml/-/plugin-yaml-4.1.2.tgz",
"integrity": "sha512-RpupciIeZMUqhgFE97ba0s98mOFS7CWzN3EJNhJkqSv9XLlWYtwVdtE6cDw6ASOF/sZVFS7kRJXftaqM2Vakdw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
@ -9103,7 +9046,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
@ -9125,7 +9067,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"engines": {
@ -9614,7 +9555,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
@ -11429,7 +11369,6 @@
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.1.tgz",
"integrity": "sha512-uuPNLJkKN8NXAlZlQ6kmUF9qO+T6Kyd7oV4+/7yy8Jz6+MZNyhPq8EdLpdfnPVzUC8qSf1b4j1azKaGnFsjmsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.5.0",
@ -11448,7 +11387,6 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -11461,7 +11399,6 @@
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.9.0",
@ -13515,7 +13452,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/nuxt-define/-/nuxt-define-1.0.0.tgz",
"integrity": "sha512-CYZ2WjU+KCyCDVzjYUM4eEpMF0rkPmkpiFrybTqqQCRpUbPt2h3snswWIpFPXTi+osRCY6Og0W/XLAQgDL4FfQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/bobbiegoede"
@ -16634,7 +16570,6 @@
"version": "2.0.0-alpha.3",
"resolved": "https://registry.npmjs.org/tosource/-/tosource-2.0.0-alpha.3.tgz",
"integrity": "sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==",
"dev": true,
"engines": {
"node": ">=10"
}
@ -18056,7 +17991,6 @@
"version": "11.1.12",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.12.tgz",
"integrity": "sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.1.12",
@ -18077,7 +18011,6 @@
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"dev": true,
"license": "MIT"
},
"node_modules/vue-router": {
@ -18368,7 +18301,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.3.0.tgz",
"integrity": "sha512-E/+VitOorXSLiAqtTd7Yqax0/pAS3xaYMP+AUUJGOK1OZG3rhcj9fcJOM5HJ2VrP1FrStVCWr1muTfQCdj4tAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.0.0",
@ -18385,7 +18317,6 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"

View File

@ -36,8 +36,8 @@ export const useAppsStore = defineStore('apps', () => {
{
id: 'livestream-hub',
name: 'livestream-hub', // Use translation key instead of hardcoded name
icon: '📺',
component: 'LiveStreamHub',
icon: '📰',
component: 'NewsHub',
description: 'Discover and watch live streams from popular streamers',
category: 'Entertainment'
},