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": [
|
"cSpell.words": ["IPTV", "iptv", "liyanyan", "tauri"],
|
||||||
"IPTV",
|
|
||||||
"iptv",
|
|
||||||
"liyanyan",
|
|
||||||
"tauri"
|
|
||||||
],
|
|
||||||
"cSpell.ignorePaths": [
|
"cSpell.ignorePaths": [
|
||||||
"package-lock.json",
|
"package-lock.json",
|
||||||
"public/api",
|
"public/api",
|
||||||
@ -14,5 +9,16 @@
|
|||||||
".git/{index,*refs,*HEAD}",
|
".git/{index,*refs,*HEAD}",
|
||||||
".vscode",
|
".vscode",
|
||||||
".vscode-insiders"
|
".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 { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(createPinia())
|
|
||||||
app.mount('#app')
|
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