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';
|
||||
|
||||
// 防抖工具函数
|
||||
function debounce(fn, delay) {
|
||||
let timer = null;
|
||||
return function (...args) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn.apply(this, args), delay);
|
||||
};
|
||||
}
|
||||
import { ref, computed } from "vue";
|
||||
import { debounce } from "../utils/common.js";
|
||||
|
||||
// 当前激活的栏索引(用于TV导航)
|
||||
const activeColumnIndex = ref(0);
|
||||
const leftPanelVisible = ref(false);
|
||||
const bottomPanelVisible = ref(false);
|
||||
|
||||
// 防抖隐藏底部栏
|
||||
const debouncedHideBottomPanel = debounce(() => {
|
||||
bottomPanelVisible.value = false;
|
||||
}, 3000);
|
||||
|
||||
// 当前选中的分组/频道/日期/节目
|
||||
const selectedGroup = ref('');
|
||||
const selectedGroup = ref("");
|
||||
const selectedChannel = ref(null);
|
||||
const selectedDate = ref('');
|
||||
const selectedDate = ref("");
|
||||
const selectedProgram = ref(null);
|
||||
|
||||
export function useUI() {
|
||||
const platform = import.meta.env.VITE_PLATFORM || 'web';
|
||||
const isTV = platform === 'tv';
|
||||
const platform = import.meta.env.VITE_PLATFORM || "web";
|
||||
const isTV = platform === "tv";
|
||||
|
||||
// 显示左侧面板
|
||||
const showLeftPanel = () => {
|
||||
@ -54,18 +41,13 @@ export function useUI() {
|
||||
// 显示底部栏(启动防抖隐藏)
|
||||
const showBottomPanel = () => {
|
||||
bottomPanelVisible.value = true;
|
||||
debouncedHideBottomPanel();
|
||||
hideBottomPanel();
|
||||
};
|
||||
|
||||
// 隐藏底部栏
|
||||
const hideBottomPanel = () => {
|
||||
const hideBottomPanel = debounce(() => {
|
||||
bottomPanelVisible.value = false;
|
||||
};
|
||||
|
||||
// 底部栏交互(重置防抖)
|
||||
const onBottomInteraction = () => {
|
||||
debouncedHideBottomPanel();
|
||||
};
|
||||
}, 3000);
|
||||
|
||||
// 设置选中项
|
||||
const setSelectedGroup = (group) => {
|
||||
@ -87,11 +69,11 @@ export function useUI() {
|
||||
// TV 导航:切换栏
|
||||
const moveColumn = (direction) => {
|
||||
if (!leftPanelVisible.value) return;
|
||||
|
||||
|
||||
const maxIndex = 3; // 0-3 四栏
|
||||
if (direction === 'right') {
|
||||
if (direction === "right") {
|
||||
activeColumnIndex.value = Math.min(activeColumnIndex.value + 1, maxIndex);
|
||||
} else if (direction === 'left') {
|
||||
} else if (direction === "left") {
|
||||
activeColumnIndex.value = Math.max(activeColumnIndex.value - 1, 0);
|
||||
}
|
||||
};
|
||||
@ -107,18 +89,17 @@ export function useUI() {
|
||||
selectedChannel,
|
||||
selectedDate,
|
||||
selectedProgram,
|
||||
|
||||
|
||||
// 计算属性
|
||||
isTV,
|
||||
currentActiveColumn,
|
||||
|
||||
|
||||
// 方法
|
||||
showLeftPanel,
|
||||
hideLeftPanel,
|
||||
toggleLeftPanel,
|
||||
showBottomPanel,
|
||||
hideBottomPanel,
|
||||
onBottomInteraction,
|
||||
setSelectedGroup,
|
||||
setSelectedChannel,
|
||||
setSelectedDate,
|
||||
@ -126,12 +107,3 @@ export function useUI() {
|
||||
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