feat: 新增业务逻辑 composables
- useFavorites: 收藏管理 - useHistory: 播放历史 - useSettings: 用户设置 - useChannels: 频道数据获取和解析 - useGroups: 分组管理 - useChannelFilter: 频道过滤 - useDates: 日期列表 - usePrograms: 节目单管理 - useEvent: 键盘事件
This commit is contained in:
parent
5f8165b236
commit
bc4434c93d
94
ui/src/composables/useChannelFilter.js
Normal file
94
ui/src/composables/useChannelFilter.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
export function useChannelFilter() {
|
||||||
|
const selectedChannel = ref(null);
|
||||||
|
const allChannels = ref([]);
|
||||||
|
const selectedGroup = ref("");
|
||||||
|
const favorites = ref(new Set());
|
||||||
|
const validityMap = ref(new Map());
|
||||||
|
|
||||||
|
// 过滤后的频道
|
||||||
|
const filteredChannels = computed(() => {
|
||||||
|
if (!selectedGroup.value) return allChannels.value;
|
||||||
|
|
||||||
|
// 置顶分组特殊处理
|
||||||
|
if (selectedGroup.value === "recent") {
|
||||||
|
// TODO: 返回最近播放
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (selectedGroup.value === "favorite") {
|
||||||
|
return allChannels.value.filter((c) => favorites.value.has(c.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return allChannels.value.filter((c) => c.group === selectedGroup.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取频道 LOGO(取前两个字符)
|
||||||
|
function getChannelLogo(name) {
|
||||||
|
return name?.slice(0, 2) || "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否收藏
|
||||||
|
function isFavorite(channelId) {
|
||||||
|
return favorites.value.has(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取有效线路数
|
||||||
|
function getValidCount(channel) {
|
||||||
|
if (!channel?.urls) return 0;
|
||||||
|
return channel.urls.filter((url) => {
|
||||||
|
const validity = validityMap.value.get(url);
|
||||||
|
return validity?.status === "online";
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择频道
|
||||||
|
function selectChannel(channel) {
|
||||||
|
selectedChannel.value = channel;
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置数据源
|
||||||
|
function setChannels(channels) {
|
||||||
|
allChannels.value = channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedGroup(group) {
|
||||||
|
selectedGroup.value = group;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFavorites(favSet) {
|
||||||
|
favorites.value = favSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValidityMap(map) {
|
||||||
|
validityMap.value = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据当前频道初始化
|
||||||
|
function initSelectedChannel(channel) {
|
||||||
|
if (channel) {
|
||||||
|
selectedChannel.value = channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取第一个频道(用于分组切换时自动选中)
|
||||||
|
function getFirstChannel() {
|
||||||
|
return filteredChannels.value[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedChannel,
|
||||||
|
filteredChannels,
|
||||||
|
selectChannel,
|
||||||
|
setChannels,
|
||||||
|
setSelectedGroup,
|
||||||
|
setFavorites,
|
||||||
|
setValidityMap,
|
||||||
|
initSelectedChannel,
|
||||||
|
getFirstChannel,
|
||||||
|
getChannelLogo,
|
||||||
|
isFavorite,
|
||||||
|
getValidCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
151
ui/src/composables/useChannels.js
Normal file
151
ui/src/composables/useChannels.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useStorage } from "./useStorage.js";
|
||||||
|
|
||||||
|
// 全局状态
|
||||||
|
const channels = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
export function useChannels() {
|
||||||
|
const storage = useStorage();
|
||||||
|
|
||||||
|
// 分组列表(基于频道计算)
|
||||||
|
const groups = computed(() => {
|
||||||
|
return [...new Set(channels.value.map((c) => c.group))];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载频道数据
|
||||||
|
const loadChannels = async (force = false) => {
|
||||||
|
if (loading.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查缓存
|
||||||
|
if (!force) {
|
||||||
|
const isValid = await storage.isListCacheValid();
|
||||||
|
if (isValid) {
|
||||||
|
const cached = await storage.getChannels();
|
||||||
|
if (cached && cached.length > 0) {
|
||||||
|
channels.value = cached;
|
||||||
|
loading.value = false;
|
||||||
|
// 后台刷新
|
||||||
|
refreshChannelsInBackground();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从网络加载
|
||||||
|
const text = await fetchChannelData();
|
||||||
|
if (text && !text.startsWith("ERROR")) {
|
||||||
|
const parsed = parseChannelData(text);
|
||||||
|
channels.value = parsed;
|
||||||
|
await storage.setChannels(parsed);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 后台刷新频道
|
||||||
|
const refreshChannelsInBackground = async () => {
|
||||||
|
try {
|
||||||
|
const text = await fetchChannelData();
|
||||||
|
if (text && !text.startsWith("ERROR")) {
|
||||||
|
const parsed = parseChannelData(text);
|
||||||
|
if (parsed.length > 0) {
|
||||||
|
channels.value = parsed;
|
||||||
|
await storage.setChannels(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("后台刷新失败:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取频道数据(网络)
|
||||||
|
const fetchChannelData = async () => {
|
||||||
|
const platform = import.meta.env.VITE_PLATFORM || "web";
|
||||||
|
|
||||||
|
// Android/TV 使用原生接口
|
||||||
|
if ((platform === "android" || platform === "tv") && window.AndroidAsset) {
|
||||||
|
try {
|
||||||
|
return window.AndroidAsset.readChannelData();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("读取本地数据失败:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web/Desktop 从网络加载
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://cdn.jsdelivr.net/gh/Guovin/iptv-api@gd/output/result.txt"
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.text();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("网络加载失败:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析频道数据
|
||||||
|
const parseChannelData = (text) => {
|
||||||
|
const result = [];
|
||||||
|
const lines = text.split("\n");
|
||||||
|
let currentGroup = "";
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
|
||||||
|
if (trimmed.includes("#genre#")) {
|
||||||
|
currentGroup = trimmed.split(",")[0];
|
||||||
|
} else if (trimmed.includes(",")) {
|
||||||
|
const [name, url] = trimmed.split(",").map((s) => s.trim());
|
||||||
|
if (name && url) {
|
||||||
|
const existing = result.find(
|
||||||
|
(c) => c.name === name && c.group === currentGroup
|
||||||
|
);
|
||||||
|
if (existing) {
|
||||||
|
existing.urls.push(url);
|
||||||
|
} else {
|
||||||
|
result.push({
|
||||||
|
id: `${currentGroup}_${name}`,
|
||||||
|
name,
|
||||||
|
group: currentGroup,
|
||||||
|
urls: [url],
|
||||||
|
logo: "",
|
||||||
|
epgId: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据索引获取频道
|
||||||
|
const getChannelByIndex = (index) => {
|
||||||
|
return channels.value[index] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
onMounted(() => {
|
||||||
|
if (!initialized) {
|
||||||
|
loadChannels();
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
channels,
|
||||||
|
groups,
|
||||||
|
loading,
|
||||||
|
loadChannels,
|
||||||
|
getChannelByIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
47
ui/src/composables/useDates.js
Normal file
47
ui/src/composables/useDates.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
// 日期标签
|
||||||
|
const DAY_LABELS = ["今天", "明天", "后天"];
|
||||||
|
|
||||||
|
export function useDates() {
|
||||||
|
const selectedDate = ref("");
|
||||||
|
|
||||||
|
// 生成日期列表(今天、明天、后天...)
|
||||||
|
const dates = computed(() => {
|
||||||
|
const list = [];
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
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
|
||||||
|
? DAY_LABELS[i]
|
||||||
|
: `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||||
|
|
||||||
|
list.push({ value, day, label });
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选择日期
|
||||||
|
function selectDate(dateValue) {
|
||||||
|
selectedDate.value = dateValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化为今天
|
||||||
|
function initToday() {
|
||||||
|
selectedDate.value = new Date().toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedDate,
|
||||||
|
dates,
|
||||||
|
selectDate,
|
||||||
|
initToday,
|
||||||
|
};
|
||||||
|
}
|
||||||
19
ui/src/composables/useEvent.js
Normal file
19
ui/src/composables/useEvent.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { onBeforeUnmount } from "vue";
|
||||||
|
|
||||||
|
export const useEvent = (eventName, callback) => {
|
||||||
|
window.addEventListener(eventName, callback);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener(eventName, callback);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useKeyEvent = (key, callback) => {
|
||||||
|
const handler = (e) => {
|
||||||
|
console.log("🚀 ~ handler ~ e:", e.key);
|
||||||
|
if (e.key === key) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEvent("keyup", handler);
|
||||||
|
};
|
||||||
76
ui/src/composables/useFavorites.js
Normal file
76
ui/src/composables/useFavorites.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useStorage } from "./useStorage.js";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "favorites_data";
|
||||||
|
|
||||||
|
// 全局状态
|
||||||
|
const favorites = ref(new Set());
|
||||||
|
let initialized = false;
|
||||||
|
let loadPromise = null;
|
||||||
|
|
||||||
|
export function useFavorites() {
|
||||||
|
const storage = useStorage();
|
||||||
|
|
||||||
|
// 加载收藏(从 useStorage)
|
||||||
|
const loadFavorites = async () => {
|
||||||
|
if (loadPromise) return loadPromise;
|
||||||
|
|
||||||
|
loadPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const saved = await storage.get(STORAGE_KEY);
|
||||||
|
if (saved) {
|
||||||
|
favorites.value = new Set(saved);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载收藏失败:", e);
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return loadPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存收藏(到 useStorage)
|
||||||
|
const saveFavorites = async () => {
|
||||||
|
try {
|
||||||
|
await storage.set(STORAGE_KEY, [...favorites.value]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("保存收藏失败:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换收藏状态
|
||||||
|
const toggleFavorite = async (channelId) => {
|
||||||
|
if (!channelId) return;
|
||||||
|
if (favorites.value.has(channelId)) {
|
||||||
|
favorites.value.delete(channelId);
|
||||||
|
} else {
|
||||||
|
favorites.value.add(channelId);
|
||||||
|
}
|
||||||
|
await saveFavorites();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查是否已收藏
|
||||||
|
const isFavorite = (channelId) => {
|
||||||
|
return favorites.value.has(channelId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取所有收藏
|
||||||
|
const getFavorites = () => {
|
||||||
|
return [...favorites.value];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
onMounted(() => {
|
||||||
|
if (!initialized) {
|
||||||
|
loadFavorites();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
favorites,
|
||||||
|
toggleFavorite,
|
||||||
|
isFavorite,
|
||||||
|
getFavorites,
|
||||||
|
};
|
||||||
|
}
|
||||||
82
ui/src/composables/useGroups.js
Normal file
82
ui/src/composables/useGroups.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
// 置顶分组配置
|
||||||
|
const PINNED_GROUPS = [
|
||||||
|
{ id: "recent", name: "最近播放", icon: "⏱" },
|
||||||
|
{ id: "favorite", name: "收藏", icon: "❤️" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 分组图标映射
|
||||||
|
const GROUP_ICONS = {
|
||||||
|
央视: "📺",
|
||||||
|
卫视: "📡",
|
||||||
|
体育: "⚽",
|
||||||
|
电影: "🎬",
|
||||||
|
少儿: "👶",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useGroups() {
|
||||||
|
const selectedGroup = ref("");
|
||||||
|
const rawGroups = ref([]);
|
||||||
|
|
||||||
|
// 计算置顶分组(带数量)
|
||||||
|
const pinnedGroups = computed(() => {
|
||||||
|
return PINNED_GROUPS.map((g) => ({
|
||||||
|
...g,
|
||||||
|
count: 0, // TODO: 实际数量从外部传入
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算普通分组(带图标和数量)
|
||||||
|
const normalGroups = computed(() => {
|
||||||
|
return rawGroups.value.map((group) => ({
|
||||||
|
id: group,
|
||||||
|
name: group,
|
||||||
|
icon: getGroupIcon(group),
|
||||||
|
count: 0, // TODO: 实际数量从外部传入
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 所有分组
|
||||||
|
const allGroups = computed(() => [...pinnedGroups.value, ...normalGroups.value]);
|
||||||
|
|
||||||
|
// 获取分组图标
|
||||||
|
function getGroupIcon(groupName) {
|
||||||
|
for (const [key, icon] of Object.entries(GROUP_ICONS)) {
|
||||||
|
if (groupName.includes(key)) return icon;
|
||||||
|
}
|
||||||
|
return "📺";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择分组
|
||||||
|
function selectGroup(groupId) {
|
||||||
|
selectedGroup.value = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置原始分组数据
|
||||||
|
function setGroups(groups) {
|
||||||
|
rawGroups.value = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据当前频道初始化选中分组
|
||||||
|
function initSelectedGroup(channel, defaultGroup = "") {
|
||||||
|
if (channel?.group) {
|
||||||
|
selectedGroup.value = channel.group;
|
||||||
|
} else if (defaultGroup) {
|
||||||
|
selectedGroup.value = defaultGroup;
|
||||||
|
} else if (rawGroups.value.length > 0) {
|
||||||
|
selectedGroup.value = rawGroups.value[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedGroup,
|
||||||
|
pinnedGroups,
|
||||||
|
normalGroups,
|
||||||
|
allGroups,
|
||||||
|
selectGroup,
|
||||||
|
setGroups,
|
||||||
|
initSelectedGroup,
|
||||||
|
getGroupIcon,
|
||||||
|
};
|
||||||
|
}
|
||||||
84
ui/src/composables/useHistory.js
Normal file
84
ui/src/composables/useHistory.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useStorage } from "./useStorage.js";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "play_history";
|
||||||
|
const MAX_HISTORY = 20;
|
||||||
|
|
||||||
|
// 全局状态
|
||||||
|
const history = ref([]);
|
||||||
|
let initialized = false;
|
||||||
|
let loadPromise = null;
|
||||||
|
|
||||||
|
export function useHistory() {
|
||||||
|
const storage = useStorage();
|
||||||
|
|
||||||
|
// 加载历史(从 useStorage)
|
||||||
|
const loadHistory = async () => {
|
||||||
|
if (loadPromise) return loadPromise;
|
||||||
|
|
||||||
|
loadPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const saved = await storage.get(STORAGE_KEY);
|
||||||
|
if (saved) {
|
||||||
|
history.value = saved;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载历史失败:", e);
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return loadPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存历史(到 useStorage)
|
||||||
|
const saveHistory = async () => {
|
||||||
|
try {
|
||||||
|
await storage.set(STORAGE_KEY, [...history.value]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("保存历史失败:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到历史
|
||||||
|
const addToHistory = async (channel) => {
|
||||||
|
if (!channel) return;
|
||||||
|
// 移除重复项
|
||||||
|
history.value = history.value.filter((h) => h.id !== channel.id);
|
||||||
|
// 添加到开头
|
||||||
|
history.value.unshift({
|
||||||
|
...channel,
|
||||||
|
playedAt: Date.now(),
|
||||||
|
});
|
||||||
|
// 限制数量
|
||||||
|
if (history.value.length > MAX_HISTORY) {
|
||||||
|
history.value = history.value.slice(0, MAX_HISTORY);
|
||||||
|
}
|
||||||
|
await saveHistory();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空历史
|
||||||
|
const clearHistory = async () => {
|
||||||
|
history.value = [];
|
||||||
|
await saveHistory();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取历史列表
|
||||||
|
const getHistory = () => {
|
||||||
|
return [...history.value];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
onMounted(() => {
|
||||||
|
if (!initialized) {
|
||||||
|
loadHistory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
history,
|
||||||
|
addToHistory,
|
||||||
|
clearHistory,
|
||||||
|
getHistory,
|
||||||
|
};
|
||||||
|
}
|
||||||
42
ui/src/composables/usePrograms.js
Normal file
42
ui/src/composables/usePrograms.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
// 模拟节目单数据(TODO: 实际从 EPG 获取)
|
||||||
|
const MOCK_PROGRAMS = [
|
||||||
|
{ id: 1, time: "08:00", title: "朝闻天下", isCurrent: false },
|
||||||
|
{ id: 2, time: "09:00", title: "今日说法", isCurrent: false },
|
||||||
|
{ id: 3, time: "10:00", title: "电视剧:繁花", isCurrent: true },
|
||||||
|
{ id: 4, time: "12:00", title: "新闻30分", isCurrent: false },
|
||||||
|
{ id: 5, time: "13:00", title: "电视剧:西游记", isCurrent: false },
|
||||||
|
{ id: 6, time: "15:00", title: "动画剧场", isCurrent: false },
|
||||||
|
{ id: 7, time: "19:00", title: "新闻联播", isCurrent: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function usePrograms() {
|
||||||
|
const selectedProgram = ref(null);
|
||||||
|
const programs = ref([...MOCK_PROGRAMS]);
|
||||||
|
|
||||||
|
// 选择节目
|
||||||
|
function selectProgram(program) {
|
||||||
|
selectedProgram.value = program;
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载指定日期的节目(TODO: 实际从 EPG 获取)
|
||||||
|
function loadProgramsByDate(dateValue) {
|
||||||
|
// TODO: 根据日期加载节目单
|
||||||
|
console.log("加载节目单:", dateValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新节目单
|
||||||
|
function setPrograms(newPrograms) {
|
||||||
|
programs.value = newPrograms;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedProgram,
|
||||||
|
programs,
|
||||||
|
selectProgram,
|
||||||
|
loadProgramsByDate,
|
||||||
|
setPrograms,
|
||||||
|
};
|
||||||
|
}
|
||||||
85
ui/src/composables/useSettings.js
Normal file
85
ui/src/composables/useSettings.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useStorage } from "./useStorage.js";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "user_settings";
|
||||||
|
|
||||||
|
// 默认设置
|
||||||
|
const defaultSettings = {
|
||||||
|
autoPlay: true,
|
||||||
|
defaultVolume: 0.8,
|
||||||
|
showEpg: true,
|
||||||
|
theme: "dark",
|
||||||
|
checkTimeout: 2000, // 检测超时时间(ms)
|
||||||
|
checkConcurrency: 5, // 并发数
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全局状态
|
||||||
|
const settings = ref({ ...defaultSettings });
|
||||||
|
let initialized = false;
|
||||||
|
let loadPromise = null;
|
||||||
|
|
||||||
|
export function useSettings() {
|
||||||
|
const storage = useStorage();
|
||||||
|
|
||||||
|
// 加载设置(从 useStorage)
|
||||||
|
const loadSettings = async () => {
|
||||||
|
if (loadPromise) return loadPromise;
|
||||||
|
|
||||||
|
loadPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const saved = await storage.get(STORAGE_KEY);
|
||||||
|
if (saved) {
|
||||||
|
settings.value = { ...defaultSettings, ...saved };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载设置失败:", e);
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return loadPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存设置(到 useStorage)
|
||||||
|
const saveSettings = async () => {
|
||||||
|
try {
|
||||||
|
await storage.set(STORAGE_KEY, settings.value);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("保存设置失败:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新单个设置
|
||||||
|
const updateSetting = async (key, value) => {
|
||||||
|
if (key in settings.value) {
|
||||||
|
settings.value[key] = value;
|
||||||
|
await saveSettings();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新多个设置
|
||||||
|
const updateSettings = async (newSettings) => {
|
||||||
|
settings.value = { ...settings.value, ...newSettings };
|
||||||
|
await saveSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置设置为默认值
|
||||||
|
const resetSettings = async () => {
|
||||||
|
settings.value = { ...defaultSettings };
|
||||||
|
await saveSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
onMounted(() => {
|
||||||
|
if (!initialized) {
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
updateSetting,
|
||||||
|
updateSettings,
|
||||||
|
resetSettings,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,33 +1,20 @@
|
|||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from "vue";
|
||||||
|
import { debounce } from "../utils/common.js";
|
||||||
// 防抖工具函数
|
|
||||||
function debounce(fn, delay) {
|
|
||||||
let timer = null;
|
|
||||||
return function (...args) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(() => fn.apply(this, args), delay);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当前激活的栏索引(用于TV导航)
|
// 当前激活的栏索引(用于TV导航)
|
||||||
const activeColumnIndex = ref(0);
|
const activeColumnIndex = ref(0);
|
||||||
const leftPanelVisible = ref(false);
|
const leftPanelVisible = ref(false);
|
||||||
const bottomPanelVisible = ref(false);
|
const bottomPanelVisible = ref(false);
|
||||||
|
|
||||||
// 防抖隐藏底部栏
|
|
||||||
const debouncedHideBottomPanel = debounce(() => {
|
|
||||||
bottomPanelVisible.value = false;
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
// 当前选中的分组/频道/日期/节目
|
// 当前选中的分组/频道/日期/节目
|
||||||
const selectedGroup = ref('');
|
const selectedGroup = ref("");
|
||||||
const selectedChannel = ref(null);
|
const selectedChannel = ref(null);
|
||||||
const selectedDate = ref('');
|
const selectedDate = ref("");
|
||||||
const selectedProgram = ref(null);
|
const selectedProgram = ref(null);
|
||||||
|
|
||||||
export function useUI() {
|
export function useUI() {
|
||||||
const platform = import.meta.env.VITE_PLATFORM || 'web';
|
const platform = import.meta.env.VITE_PLATFORM || "web";
|
||||||
const isTV = platform === 'tv';
|
const isTV = platform === "tv";
|
||||||
|
|
||||||
// 显示左侧面板
|
// 显示左侧面板
|
||||||
const showLeftPanel = () => {
|
const showLeftPanel = () => {
|
||||||
@ -54,18 +41,13 @@ export function useUI() {
|
|||||||
// 显示底部栏(启动防抖隐藏)
|
// 显示底部栏(启动防抖隐藏)
|
||||||
const showBottomPanel = () => {
|
const showBottomPanel = () => {
|
||||||
bottomPanelVisible.value = true;
|
bottomPanelVisible.value = true;
|
||||||
debouncedHideBottomPanel();
|
hideBottomPanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 隐藏底部栏
|
// 隐藏底部栏
|
||||||
const hideBottomPanel = () => {
|
const hideBottomPanel = debounce(() => {
|
||||||
bottomPanelVisible.value = false;
|
bottomPanelVisible.value = false;
|
||||||
};
|
}, 3000);
|
||||||
|
|
||||||
// 底部栏交互(重置防抖)
|
|
||||||
const onBottomInteraction = () => {
|
|
||||||
debouncedHideBottomPanel();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 设置选中项
|
// 设置选中项
|
||||||
const setSelectedGroup = (group) => {
|
const setSelectedGroup = (group) => {
|
||||||
@ -89,9 +71,9 @@ export function useUI() {
|
|||||||
if (!leftPanelVisible.value) return;
|
if (!leftPanelVisible.value) return;
|
||||||
|
|
||||||
const maxIndex = 3; // 0-3 四栏
|
const maxIndex = 3; // 0-3 四栏
|
||||||
if (direction === 'right') {
|
if (direction === "right") {
|
||||||
activeColumnIndex.value = Math.min(activeColumnIndex.value + 1, maxIndex);
|
activeColumnIndex.value = Math.min(activeColumnIndex.value + 1, maxIndex);
|
||||||
} else if (direction === 'left') {
|
} else if (direction === "left") {
|
||||||
activeColumnIndex.value = Math.max(activeColumnIndex.value - 1, 0);
|
activeColumnIndex.value = Math.max(activeColumnIndex.value - 1, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -118,7 +100,6 @@ export function useUI() {
|
|||||||
toggleLeftPanel,
|
toggleLeftPanel,
|
||||||
showBottomPanel,
|
showBottomPanel,
|
||||||
hideBottomPanel,
|
hideBottomPanel,
|
||||||
onBottomInteraction,
|
|
||||||
setSelectedGroup,
|
setSelectedGroup,
|
||||||
setSelectedChannel,
|
setSelectedChannel,
|
||||||
setSelectedDate,
|
setSelectedDate,
|
||||||
@ -126,12 +107,3 @@ export function useUI() {
|
|||||||
moveColumn,
|
moveColumn,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建单例
|
|
||||||
let uiInstance = null;
|
|
||||||
export function useUISingleton() {
|
|
||||||
if (!uiInstance) {
|
|
||||||
uiInstance = useUI();
|
|
||||||
}
|
|
||||||
return uiInstance;
|
|
||||||
}
|
|
||||||
|
|||||||
8
ui/src/utils/common.js
Normal file
8
ui/src/utils/common.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// 防抖工具函数
|
||||||
|
export const debounce = (fn, delay) => {
|
||||||
|
let timer = null;
|
||||||
|
return function (...args) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => fn.apply(this, args), delay);
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user