refactor: 移除 Pinia 和相关无用文件
- 删除 stores/useStore.js (Pinia) - 删除 components/ConfigPanel.vue - 删除 Layout/ 子组件 (GroupList, ChannelList, DateList, ProgramList)
This commit is contained in:
parent
d85823cc8d
commit
5f8165b236
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "esbenp.prettier-vscode"]
|
||||
}
|
||||
22
.vscode/settings.json
vendored
22
.vscode/settings.json
vendored
@ -1,10 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"IPTV",
|
||||
"iptv",
|
||||
"liyanyan",
|
||||
"tauri"
|
||||
],
|
||||
"cSpell.words": ["IPTV", "iptv", "liyanyan", "tauri"],
|
||||
"cSpell.ignorePaths": [
|
||||
"package-lock.json",
|
||||
"public/api",
|
||||
@ -14,5 +9,16 @@
|
||||
".git/{index,*refs,*HEAD}",
|
||||
".vscode",
|
||||
".vscode-insiders"
|
||||
]
|
||||
}
|
||||
],
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"tsconfig.json": "tsconfig.*.json, env.d.ts",
|
||||
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
|
||||
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .oxfmt*, .prettier*, prettier*, .editorconfig"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
|
||||
@ -1,483 +0,0 @@
|
||||
<template>
|
||||
<div class="config-panel" v-if="show">
|
||||
<div class="panel-overlay" @click="$emit('close')"></div>
|
||||
<div class="panel-content">
|
||||
<div class="panel-header">
|
||||
<h2>应用设置</h2>
|
||||
<button class="close-btn" @click="$emit('close')">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="tab-btn"
|
||||
:class="{ active: currentTab === tab.id }"
|
||||
@click="currentTab = tab.id"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<!-- 数据源 -->
|
||||
<div v-if="currentTab === 'source'" class="tab-content">
|
||||
<div class="setting-group">
|
||||
<label>数据源类型</label>
|
||||
<select v-model="config.apiType">
|
||||
<option value="local">📁 本地文件</option>
|
||||
<option value="guovin">🌐 在线接口</option>
|
||||
<option value="custom">✏️ 自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="config.apiType === 'local'" class="setting-group">
|
||||
<label>选择文件</label>
|
||||
<select v-model="config.localFile">
|
||||
<option value="/api/result.txt">result.txt(完整)</option>
|
||||
<option value="/api/ipv4/result.txt">IPv4 专线</option>
|
||||
<option value="/api/ipv6/result.txt">IPv6 专线</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="config.apiType === 'custom'" class="setting-group">
|
||||
<label>接口地址</label>
|
||||
<input v-model="config.apiUrl" placeholder="http://..." />
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label>EPG 数据源</label>
|
||||
<select v-model="config.epgType">
|
||||
<option value="local">📁 本地文件</option>
|
||||
<option value="custom">✏️ 自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 播放设置 -->
|
||||
<div v-if="currentTab === 'playback'" class="tab-content">
|
||||
<div class="setting-group">
|
||||
<label>自动播放</label>
|
||||
<div class="toggle-switch">
|
||||
<input type="checkbox" v-model="config.autoPlay" id="autoplay">
|
||||
<label for="autoplay"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label>默认音量 {{ Math.round(config.defaultVolume * 100) }}%</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
v-model="config.defaultVolume"
|
||||
class="slider"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label>检测超时 {{ config.checkTimeout / 1000 }}s</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1000"
|
||||
max="10000"
|
||||
step="500"
|
||||
v-model.number="config.checkTimeout"
|
||||
class="slider"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label>缓存过期 {{ Math.round(config.cacheExpire / 1000 / 60 / 60) }}h</label>
|
||||
<input
|
||||
type="range"
|
||||
min="3600000"
|
||||
max="168000000"
|
||||
step="3600000"
|
||||
v-model.number="config.cacheExpire"
|
||||
class="slider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 外观 -->
|
||||
<div v-if="currentTab === 'appearance'" class="tab-content">
|
||||
<div class="setting-group">
|
||||
<label>主题</label>
|
||||
<div class="theme-options">
|
||||
<div
|
||||
class="theme-card"
|
||||
:class="{ active: config.theme === 'dark' }"
|
||||
@click="config.theme = 'dark'"
|
||||
>
|
||||
<div class="theme-icon">🌙</div>
|
||||
<span>深色</span>
|
||||
</div>
|
||||
<div
|
||||
class="theme-card"
|
||||
:class="{ active: config.theme === 'light' }"
|
||||
@click="config.theme = 'light'"
|
||||
>
|
||||
<div class="theme-icon">☀</div>
|
||||
<span>浅色</span>
|
||||
</div>
|
||||
<div
|
||||
class="theme-card"
|
||||
:class="{ active: config.theme === 'system' }"
|
||||
@click="config.theme = 'system'"
|
||||
>
|
||||
<div class="theme-icon">💻</div>
|
||||
<span>跟随系统</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts">
|
||||
<h4>快捷键</h4>
|
||||
<p>S 打开设置 | M 菜单 | I 信息 | F 收藏</p>
|
||||
<p>←→ 切换线路 | ↑↓ 切换频道</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-footer">
|
||||
<button class="btn-primary" @click="saveAndClose">
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useStore } from '../stores/useStore.js'
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'reload'])
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const tabs = [
|
||||
{ id: 'source', name: '数据源' },
|
||||
{ id: 'playback', name: '播放设置' },
|
||||
{ id: 'appearance', name: '外观' }
|
||||
]
|
||||
|
||||
const currentTab = ref('source')
|
||||
|
||||
const config = ref({
|
||||
apiType: localStorage.getItem('iptv_api_type') || 'local',
|
||||
apiUrl: localStorage.getItem('iptv_api_url') || '',
|
||||
localFile: localStorage.getItem('iptv_local_file') || '/api/result.txt',
|
||||
epgType: localStorage.getItem('iptv_epg_type') || 'local',
|
||||
autoPlay: store.settings.autoPlay,
|
||||
defaultVolume: store.settings.defaultVolume,
|
||||
checkTimeout: store.settings.checkTimeout || 2000,
|
||||
cacheExpire: parseInt(localStorage.getItem('iptv_cache_expire')) || (24 * 60 * 60 * 1000),
|
||||
theme: localStorage.getItem('iptv_theme') || 'dark'
|
||||
})
|
||||
|
||||
watch(config, (newVal) => {
|
||||
localStorage.setItem('iptv_api_type', newVal.apiType)
|
||||
localStorage.setItem('iptv_api_url', newVal.apiUrl)
|
||||
localStorage.setItem('iptv_local_file', newVal.localFile)
|
||||
localStorage.setItem('iptv_epg_type', newVal.epgType)
|
||||
localStorage.setItem('iptv_cache_expire', newVal.cacheExpire)
|
||||
localStorage.setItem('iptv_theme', newVal.theme)
|
||||
|
||||
store.updateSetting('autoPlay', newVal.autoPlay)
|
||||
store.updateSetting('defaultVolume', newVal.defaultVolume)
|
||||
store.updateSetting('checkTimeout', newVal.checkTimeout)
|
||||
}, { deep: true })
|
||||
|
||||
function saveAndClose() {
|
||||
emit('reload')
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.panel-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
position: relative;
|
||||
background: #1a1a1a;
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 480px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 标签页 */
|
||||
.tabs {
|
||||
display: flex;
|
||||
padding: 0 20px;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px 8px 0 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #fff;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #fff;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
/* 内容区 */
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.setting-group label {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
select, input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 10px;
|
||||
background: #252525;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
select:focus, input:focus {
|
||||
outline: none;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
/* 开关 */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-switch label {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #333;
|
||||
border-radius: 28px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch label:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + label {
|
||||
background: #00d4ff;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + label:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* 滑块 */
|
||||
.slider {
|
||||
-webkit-appearance: none;
|
||||
height: 6px;
|
||||
background: #333;
|
||||
border-radius: 3px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 主题选项 */
|
||||
.theme-options {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background: #252525;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.theme-card:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.theme-card.active {
|
||||
border-color: #fff;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.theme-card span {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.theme-card.active span {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 快捷键 */
|
||||
.shortcuts {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.shortcuts h4 {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-bottom: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.shortcuts p {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.panel-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
@ -1,160 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="channel-list"
|
||||
:class="{ 'is-active': isActive }"
|
||||
>
|
||||
<div
|
||||
v-for="channel in channels"
|
||||
:key="channel.id"
|
||||
class="channel-item"
|
||||
:class="{ active: modelValue?.id === channel.id }"
|
||||
@click="selectChannel(channel)"
|
||||
>
|
||||
<div class="channel-logo">
|
||||
{{ getChannelLogo(channel.name) }}
|
||||
</div>
|
||||
<div class="channel-info">
|
||||
<div class="channel-name">{{ channel.name }}</div>
|
||||
<div class="channel-meta">
|
||||
<span class="source-count">
|
||||
{{ getValidCount(channel) }}/{{ channel.urls?.length || 0 }}线路
|
||||
</span>
|
||||
<span v-if="isFavorite(channel.id)" class="favorite-icon">★</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
channels: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
favorites: {
|
||||
type: Set,
|
||||
default: () => new Set()
|
||||
},
|
||||
validityMap: {
|
||||
type: Map,
|
||||
default: () => new Map()
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'select']);
|
||||
|
||||
const platform = import.meta.env.VITE_PLATFORM || 'web';
|
||||
const isTV = platform === 'tv';
|
||||
|
||||
// 获取频道 LOGO(取前两个字符)
|
||||
function getChannelLogo(name) {
|
||||
return name.slice(0, 2);
|
||||
}
|
||||
|
||||
// 检查是否收藏
|
||||
function isFavorite(channelId) {
|
||||
return props.favorites.has(channelId);
|
||||
}
|
||||
|
||||
// 获取有效线路数
|
||||
function getValidCount(channel) {
|
||||
if (!channel.urls) return 0;
|
||||
return channel.urls.filter(url => {
|
||||
const validity = props.validityMap.get(url);
|
||||
return validity && validity.status === 'online';
|
||||
}).length;
|
||||
}
|
||||
|
||||
// 选择频道
|
||||
function selectChannel(channel) {
|
||||
emit('update:modelValue', channel);
|
||||
emit('select', channel);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.channel-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.channel-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.channel-item:hover,
|
||||
.channel-item.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.channel-item.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.channel-logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.channel-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.channel-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.favorite-icon {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
/* TV 模式焦点样式 */
|
||||
.is-active .channel-item:focus {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="date-list"
|
||||
:class="{ 'is-active': isActive }"
|
||||
>
|
||||
<div
|
||||
v-for="date in dates"
|
||||
:key="date.value"
|
||||
class="date-item"
|
||||
:class="{ active: modelValue === date.value }"
|
||||
@click="selectDate(date.value)"
|
||||
>
|
||||
<div class="date-day">{{ date.day }}</div>
|
||||
<div class="date-label">{{ date.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'select']);
|
||||
|
||||
// 生成日期列表(今天、明天、后天...)
|
||||
const dates = computed(() => {
|
||||
const list = [];
|
||||
const today = new Date();
|
||||
const labels = ['今天', '明天', '后天'];
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() + i);
|
||||
|
||||
const value = date.toISOString().split('T')[0];
|
||||
const day = `${date.getMonth() + 1}/${date.getDate()}`;
|
||||
const label = i < 3 ? labels[i] : `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
|
||||
list.push({ value, day, label });
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
// 选择日期
|
||||
function selectDate(dateValue) {
|
||||
emit('update:modelValue', dateValue);
|
||||
emit('select', dateValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.date-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.date-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 16px 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.date-item:hover,
|
||||
.date-item.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.date-item.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.date-day {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* TV 模式焦点样式 */
|
||||
.is-active .date-item:focus {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
@ -1,169 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="group-list"
|
||||
:class="{ 'is-active': isActive }"
|
||||
>
|
||||
<!-- 置顶分组 -->
|
||||
<div class="group-section pinned">
|
||||
<div
|
||||
v-for="group in pinnedGroups"
|
||||
:key="group.id"
|
||||
class="group-item"
|
||||
:class="{ active: modelValue === group.id }"
|
||||
@click="selectGroup(group.id)"
|
||||
>
|
||||
<span class="group-icon">{{ group.icon }}</span>
|
||||
<div class="group-info">
|
||||
<span class="group-name">{{ group.name }}</span>
|
||||
<span class="group-count">{{ group.count }}个</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 普通分组 -->
|
||||
<div class="group-section normal">
|
||||
<div
|
||||
v-for="group in normalGroups"
|
||||
:key="group.id"
|
||||
class="group-item"
|
||||
:class="{ active: modelValue === group.id }"
|
||||
@click="selectGroup(group.id)"
|
||||
>
|
||||
<span class="group-icon">{{ group.icon }}</span>
|
||||
<div class="group-info">
|
||||
<span class="group-name">{{ group.name }}</span>
|
||||
<span class="group-count">{{ group.count }}个</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'select']);
|
||||
|
||||
// 置顶分组
|
||||
const pinnedGroups = computed(() => {
|
||||
return [
|
||||
{ id: 'recent', name: '最近播放', icon: '⏱', count: 0 }, // TODO: 实际数量
|
||||
{ id: 'favorite', name: '收藏', icon: '❤️', count: 0 }, // TODO: 实际数量
|
||||
];
|
||||
});
|
||||
|
||||
// 普通分组
|
||||
const normalGroups = computed(() => {
|
||||
return props.groups.map(group => ({
|
||||
id: group,
|
||||
name: group,
|
||||
icon: getGroupIcon(group),
|
||||
count: 0 // TODO: 实际数量
|
||||
}));
|
||||
});
|
||||
|
||||
// 获取分组图标
|
||||
function getGroupIcon(groupName) {
|
||||
if (groupName.includes('央视')) return '📺';
|
||||
if (groupName.includes('卫视')) return '📡';
|
||||
if (groupName.includes('体育')) return '⚽';
|
||||
if (groupName.includes('电影')) return '🎬';
|
||||
if (groupName.includes('少儿')) return '👶';
|
||||
return '📺';
|
||||
}
|
||||
|
||||
// 选择分组
|
||||
function selectGroup(groupId) {
|
||||
emit('update:modelValue', groupId);
|
||||
emit('select', groupId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.group-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.group-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.group-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.group-item:hover,
|
||||
.group-item.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.group-item.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.group-icon {
|
||||
font-size: 20px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.group-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.group-count {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* TV 模式焦点样式 */
|
||||
.is-active .group-item:focus {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="program-list"
|
||||
:class="{ 'is-active': isActive }"
|
||||
>
|
||||
<div
|
||||
v-for="program in programs"
|
||||
:key="program.id"
|
||||
class="program-item"
|
||||
:class="{
|
||||
active: modelValue?.id === program.id,
|
||||
current: program.isCurrent
|
||||
}"
|
||||
@click="selectProgram(program)"
|
||||
>
|
||||
<div class="program-time">{{ program.time }}</div>
|
||||
<div class="program-title">
|
||||
{{ program.title }}
|
||||
<span v-if="program.isCurrent" class="current-badge">当前</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
programs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'select']);
|
||||
|
||||
// 选择节目
|
||||
function selectProgram(program) {
|
||||
emit('update:modelValue', program);
|
||||
emit('select', program);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.program-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.program-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 4px;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.program-item:hover,
|
||||
.program-item.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.program-item.active {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-left-color: #fff;
|
||||
}
|
||||
|
||||
.program-item.current {
|
||||
border-left-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.program-time {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.program-title {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.current-badge {
|
||||
font-size: 10px;
|
||||
background: #ff6b6b;
|
||||
color: #fff;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* TV 模式焦点样式 */
|
||||
.is-active .program-item:focus {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.mount('#app')
|
||||
|
||||
@ -1,118 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 收藏 & 历史记录 状态管理
|
||||
export const useStore = defineStore('iptv', () => {
|
||||
// ============ 收藏 ============
|
||||
const favorites = ref(new Set())
|
||||
|
||||
const loadFavorites = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem('iptv_favorites')
|
||||
if (saved) {
|
||||
favorites.value = new Set(JSON.parse(saved))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载收藏失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const saveFavorites = () => {
|
||||
localStorage.setItem('iptv_favorites', JSON.stringify([...favorites.value]))
|
||||
}
|
||||
|
||||
const toggleFavorite = (channelId) => {
|
||||
if (favorites.value.has(channelId)) {
|
||||
favorites.value.delete(channelId)
|
||||
} else {
|
||||
favorites.value.add(channelId)
|
||||
}
|
||||
saveFavorites()
|
||||
}
|
||||
|
||||
const isFavorite = (channelId) => favorites.value.has(channelId)
|
||||
|
||||
// ============ 播放历史 ============
|
||||
const history = ref([])
|
||||
const MAX_HISTORY = 10
|
||||
|
||||
const loadHistory = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem('iptv_history')
|
||||
if (saved) {
|
||||
history.value = JSON.parse(saved)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载历史失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const saveHistory = () => {
|
||||
localStorage.setItem('iptv_history', JSON.stringify(history.value.slice(0, MAX_HISTORY)))
|
||||
}
|
||||
|
||||
const addToHistory = (channel) => {
|
||||
// 移除重复项
|
||||
history.value = history.value.filter(h => h.id !== channel.id)
|
||||
// 添加到开头
|
||||
history.value.unshift({
|
||||
...channel,
|
||||
playedAt: Date.now()
|
||||
})
|
||||
saveHistory()
|
||||
}
|
||||
|
||||
const clearHistory = () => {
|
||||
history.value = []
|
||||
saveHistory()
|
||||
}
|
||||
|
||||
// ============ 设置 ============
|
||||
const settings = ref({
|
||||
autoPlay: true,
|
||||
defaultVolume: 0.8,
|
||||
showEpg: true,
|
||||
theme: 'dark',
|
||||
checkTimeout: 2000, // 检测超时时间(ms)
|
||||
checkConcurrency: 5 // 并发数
|
||||
})
|
||||
|
||||
const loadSettings = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem('iptv_settings')
|
||||
if (saved) {
|
||||
settings.value = { ...settings.value, ...JSON.parse(saved) }
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载设置失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const saveSettings = () => {
|
||||
localStorage.setItem('iptv_settings', JSON.stringify(settings.value))
|
||||
}
|
||||
|
||||
const updateSetting = (key, value) => {
|
||||
settings.value[key] = value
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
loadFavorites()
|
||||
loadHistory()
|
||||
loadSettings()
|
||||
|
||||
return {
|
||||
// 收藏
|
||||
favorites,
|
||||
toggleFavorite,
|
||||
isFavorite,
|
||||
// 历史
|
||||
history,
|
||||
addToHistory,
|
||||
clearHistory,
|
||||
// 设置
|
||||
settings,
|
||||
updateSetting
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user