From c36b98fb902ff91eb7a2114bb7d97b0d88a48849 Mon Sep 17 00:00:00 2001 From: QZMusic Date: Thu, 4 Jun 2026 14:46:35 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E9=9F=B3=E6=BA=90=E6=8F=92=E4=BB=B6=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E5=9C=A8=E7=BA=BF=E5=AF=BC?= =?UTF-8?q?=E5=85=A5/=E6=9C=AC=E5=9C=B0=E6=96=87=E4=BB=B6/=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=AF=BC=E5=85=A5=E4=B8=89=E7=A7=8D=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Settings.vue | 517 ++++++++++++++++++++++++++++++ src/plugins/impl/defaultPlugin.ts | 1 + src/plugins/pluginManager.ts | 71 ++++ src/types/plugin.ts | 1 + 4 files changed, 590 insertions(+) diff --git a/src/components/Settings.vue b/src/components/Settings.vue index 7e79ff3..1f9324a 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -113,6 +113,123 @@ + +
+

音源管理

+ + +
+

已安装音源

+
+
+
+ +
+
+
{{ plugin.name }}
+
{{ plugin.description || '暂无描述' }}
+
+ v{{ plugin.version }} + by {{ plugin.author }} +
+
+
+ + +
+
+
+
+ + +
+

添加音源

+ + +
+
+ + 在线导入 +
+
输入音源插件的URL地址进行导入
+
+ + +
+
+ + +
+
+ + 本地文件 +
+
上传音源插件的JSON配置文件
+
+ + + {{ selectedFile.name }} +
+
+ + +
+
+ + 代码导入 +
+
直接粘贴音源插件的JSON代码
+ + +
+
+
+

关于

@@ -134,6 +251,8 @@ import { ref, reactive, onBeforeMount, nextTick } from 'vue'; import { Icon } from '@iconify/vue'; import { usePlayerStore } from '../stores/player'; +import { pluginManager } from '../plugins/index'; +import type { PluginInfo } from '../types'; const playerStore = usePlayerStore(); @@ -142,6 +261,7 @@ defineEmits(['close']); const categories = [ { id: 'appearance', name: '外观', icon: 'lucide:palette' }, { id: 'playback', name: '播放', icon: 'lucide:headphones' }, + { id: 'plugins', name: '音源', icon: 'lucide:plug' }, { id: 'about', name: '关于', icon: 'lucide:info' }, ]; @@ -165,6 +285,129 @@ const appearance = reactive({ accentColor: '#ec4141', }); +// 插件管理相关 +const installedPlugins = ref([]); +const activePluginId = ref(''); +const pluginUrl = ref(''); +const pluginCode = ref(''); +const fileInput = ref(null); +const selectedFile = ref(null); + +const refreshPlugins = () => { + installedPlugins.value = pluginManager.getAll(); + activePluginId.value = pluginManager.getActivePluginId(); +}; + +const activatePlugin = (id: string) => { + pluginManager.setActivePlugin(id); + refreshPlugins(); +}; + +const removePlugin = (id: string) => { + if (confirm('确定要删除这个音源吗?')) { + pluginManager.unregister(id); + refreshPlugins(); + } +}; + +const importFromUrl = async () => { + if (!pluginUrl.value) return; + try { + const response = await fetch(pluginUrl.value); + const data = await response.json(); + await importPluginData(data); + pluginUrl.value = ''; + } catch (e) { + alert('导入失败:' + (e as Error).message); + } +}; + +const triggerFileUpload = () => { + fileInput.value?.click(); +}; + +const handleFileUpload = async (event: Event) => { + const target = event.target as HTMLInputElement; + const file = target.files?.[0]; + if (!file) return; + + selectedFile.value = file; + + try { + const text = await file.text(); + const data = JSON.parse(text); + await importPluginData(data); + selectedFile.value = null; + target.value = ''; + } catch (e) { + alert('文件解析失败:' + (e as Error).message); + } +}; + +const importFromCode = async () => { + if (!pluginCode.value) return; + try { + const data = JSON.parse(pluginCode.value); + await importPluginData(data); + pluginCode.value = ''; + } catch (e) { + alert('代码解析失败:' + (e as Error).message); + } +}; + +const importPluginData = async (data: any) => { + // 验证必要字段 + if (!data.id || !data.name) { + alert('插件数据缺少必要字段:id 和 name'); + return; + } + + // 标记为用户插件 + data.source = 'user'; + + // 创建插件模块 + const module = { + info: { + id: data.id, + name: data.name, + description: data.description || '', + version: data.version || '1.0.0', + author: data.author || '未知', + source: 'user' as const, + }, + search: async (query: string, page: number, limit: number) => { + if (data.searchFunction) { + try { + const fn = new Function('query', 'page', 'limit', data.searchFunction); + return await fn(query, page, limit); + } catch (e) { + console.error('Search function error:', e); + return { list: [], total: 0 }; + } + } + return { list: [], total: 0 }; + }, + getSongUrl: data.getSongUrlFunction ? async (songId: string) => { + const fn = new Function('songId', data.getSongUrlFunction); + return await fn(songId); + } : undefined, + getLyric: data.getLyricFunction ? async (songId: string) => { + const fn = new Function('songId', data.getLyricFunction); + return await fn(songId); + } : undefined, + }; + + pluginManager.registerModule(module); + + // 保存到localStorage + const saved = JSON.parse(localStorage.getItem('qz-user-plugins') || '[]'); + saved.push(data); + localStorage.setItem('qz-user-plugins', JSON.stringify(saved)); + + refreshPlugins(); + alert(`音源 "${data.name}" 导入成功!`); +}; + const applyTheme = (theme: 'dark' | 'light') => { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('qz-theme', theme); @@ -203,6 +446,7 @@ const loadAppearance = () => { // Load settings BEFORE mount to avoid visual flicker onBeforeMount(async () => { loadAppearance(); + refreshPlugins(); isLoaded.value = true; // Enable transition after initial render nextTick(() => { @@ -540,4 +784,277 @@ onBeforeMount(async () => { height: 16px; cursor: pointer; } + +/* ===== 插件管理样式 ===== */ +.subsection-title { + font-size: var(--font-size-base); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 16px; + margin-top: 24px; +} + +.plugin-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.plugin-card { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + background: var(--color-bg-secondary); + border-radius: var(--radius-lg); + border: 2px solid transparent; + transition: all 0.2s ease; +} + +.plugin-card:hover { + background: var(--color-bg-tertiary); +} + +.plugin-card.active { + border-color: var(--color-accent); + background: var(--color-accent-soft); +} + +.plugin-icon { + width: 48px; + height: 48px; + border-radius: var(--radius-md); + background: var(--color-accent); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 24px; + flex-shrink: 0; +} + +.plugin-info { + flex: 1; + min-width: 0; +} + +.plugin-name { + font-size: var(--font-size-base); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 4px; +} + +.plugin-desc { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.plugin-meta { + display: flex; + gap: 8px; + font-size: 12px; + color: var(--color-text-muted); +} + +.version { + background: var(--color-bg-tertiary); + padding: 2px 8px; + border-radius: var(--radius-sm); +} + +.author { + color: var(--color-text-secondary); +} + +.plugin-actions { + display: flex; + gap: 8px; + flex-shrink: 0; +} + +.action-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: var(--radius-md); + border: none; + cursor: pointer; + font-size: var(--font-size-sm); + transition: all 0.2s ease; +} + +.action-btn.activate { + background: var(--color-bg-tertiary); + color: var(--color-text-secondary); +} + +.action-btn.activate:hover { + background: var(--color-accent-soft); + color: var(--color-accent); +} + +.action-btn.activate.active { + background: var(--color-accent); + color: white; +} + +.action-btn.delete { + background: transparent; + color: var(--color-text-muted); + padding: 8px; +} + +.action-btn.delete:hover { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; +} + +/* 添加插件区域 */ +.add-plugin-section { + margin-top: 32px; +} + +.add-method { + background: var(--color-bg-secondary); + border-radius: var(--radius-lg); + padding: 20px; + margin-bottom: 16px; +} + +.method-title { + display: flex; + align-items: center; + gap: 8px; + font-size: var(--font-size-base); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 4px; +} + +.method-desc { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + margin-bottom: 12px; +} + +.input-group { + display: flex; + gap: 8px; +} + +.plugin-input { + flex: 1; + padding: 10px 14px; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background: var(--color-bg-primary); + color: var(--color-text-primary); + font-size: var(--font-size-sm); + outline: none; + transition: border-color 0.2s ease; +} + +.plugin-input:focus { + border-color: var(--color-accent); +} + +.plugin-input::placeholder { + color: var(--color-text-muted); +} + +.add-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 20px; + background: var(--color-accent); + color: white; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + font-size: var(--font-size-sm); + font-weight: 500; + transition: all 0.2s ease; + white-space: nowrap; +} + +.add-btn:hover:not(:disabled) { + filter: brightness(1.1); +} + +.add-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.add-btn.full-width { + width: 100%; + justify-content: center; + margin-top: 12px; +} + +/* 文件上传 */ +.file-upload { + display: flex; + align-items: center; + gap: 12px; +} + +.file-input { + display: none; +} + +.upload-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 20px; + background: var(--color-bg-tertiary); + color: var(--color-text-primary); + border: 1px dashed var(--color-border); + border-radius: var(--radius-md); + cursor: pointer; + font-size: var(--font-size-sm); + transition: all 0.2s ease; +} + +.upload-btn:hover { + border-color: var(--color-accent); + color: var(--color-accent); +} + +.file-name { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +/* 代码文本框 */ +.code-textarea { + width: 100%; + padding: 12px; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background: var(--color-bg-primary); + color: var(--color-text-primary); + font-size: 13px; + font-family: 'Courier New', monospace; + resize: vertical; + outline: none; + transition: border-color 0.2s ease; + box-sizing: border-box; +} + +.code-textarea:focus { + border-color: var(--color-accent); +} + +.code-textarea::placeholder { + color: var(--color-text-muted); +} diff --git a/src/plugins/impl/defaultPlugin.ts b/src/plugins/impl/defaultPlugin.ts index ab06568..67d6e1d 100644 --- a/src/plugins/impl/defaultPlugin.ts +++ b/src/plugins/impl/defaultPlugin.ts @@ -29,6 +29,7 @@ export const defaultPlugin: PluginModule = { description: '内置音乐搜索插件', version: '1.0.0', author: 'QZMusic', + source: 'built-in', }, async search(query: string, page: number, limit: number): Promise { const results = searchSongs(query); diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index 948c3c5..59bdad8 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -16,6 +16,29 @@ class PluginManager { } } + // 直接注册插件模块 + registerModule(module: PluginModule): boolean { + if (!module.info || !module.info.id) { + console.error('Plugin module must have info.id'); + return false; + } + this.plugins.set(module.info.id, module); + if (!this.activePluginId) { + this.activePluginId = module.info.id; + } + return true; + } + + // 卸载插件 + unregister(id: string): boolean { + if (this.activePluginId === id) { + // 如果卸载的是当前激活的插件,切换到第一个可用的 + const remaining = this.getAll(); + this.activePluginId = remaining.length > 0 && remaining[0].id !== id ? remaining[0].id : ''; + } + return this.plugins.delete(id); + } + getAll(): PluginInfo[] { return Array.from(this.plugins.values()).map(p => p.info); } @@ -68,6 +91,54 @@ class PluginManager { hasPlugins(): boolean { return this.plugins.size > 0; } + + // 从localStorage加载用户添加的插件 + loadUserPlugins(): void { + try { + const saved = localStorage.getItem('qz-user-plugins'); + if (saved) { + const pluginList = JSON.parse(saved); + pluginList.forEach((pluginData: any) => { + this.loadPluginFromData(pluginData); + }); + } + } catch (e) { + console.error('Failed to load user plugins:', e); + } + } + + // 保存用户插件到localStorage + saveUserPlugins(): void { + const userPlugins = this.getAll().filter(p => p.source === 'user'); + localStorage.setItem('qz-user-plugins', JSON.stringify(userPlugins)); + } + + // 从数据加载插件(用于用户上传的插件) + private loadPluginFromData(data: any): void { + try { + // 创建一个简单的插件模块 + const module: PluginModule = { + info: { + id: data.id, + name: data.name, + description: data.description, + version: data.version, + author: data.author, + }, + search: async (query: string, page: number, limit: number) => { + // 如果插件有自定义搜索逻辑,使用eval执行 + if (data.searchFunction) { + const fn = new Function('query', 'page', 'limit', data.searchFunction); + return await fn(query, page, limit); + } + return { list: [], total: 0 }; + }, + }; + this.registerModule(module); + } catch (e) { + console.error('Failed to load plugin from data:', e); + } + } } export const pluginManager = new PluginManager(); diff --git a/src/types/plugin.ts b/src/types/plugin.ts index ece025c..d1cf742 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -5,6 +5,7 @@ export interface PluginInfo { version?: string; author?: string; icon?: string; + source?: 'built-in' | 'user'; } export interface PluginSearchResult {