forked from miao-moe/QZMusic_PC
fix: 优化&功能
- 播放核心异常提示 - 支持更改缓存位置
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
<title>QZMusic</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -56,6 +56,11 @@
|
||||
<Icon icon="lucide:folder-open" />
|
||||
打开目录
|
||||
</button>
|
||||
<button class="action-btn" @click="changeCacheLocation" :disabled="isChangingCache">
|
||||
<Icon v-if="isChangingCache" icon="lucide:loader-2" class="spin" />
|
||||
<Icon v-else icon="lucide:folder-edit" />
|
||||
{{ isChangingCache ? '迁移中...' : '更改' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,6 +79,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 插件管理 -->
|
||||
<div v-else-if="activeCategory === 'plugins'" class="section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h2 class="section-title" style="border:none;margin:0;padding:0">插件管理</h2>
|
||||
<div class="setting-desc">管理已安装的音乐源插件</div>
|
||||
</div>
|
||||
<button class="action-btn primary" @click="installPluginFromFile">
|
||||
<Icon icon="lucide:plus" />
|
||||
安装插件
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="plugin-grid">
|
||||
<div v-if="plugins.length === 0" class="empty-state">
|
||||
<Icon icon="lucide:package-open" class="empty-icon"/>
|
||||
<p>暂无已安装的插件</p>
|
||||
<button class="text-btn" @click="installPluginFromFile">点击安装</button>
|
||||
</div>
|
||||
<div v-for="plugin in plugins" :key="plugin.id" class="plugin-card">
|
||||
<div class="plugin-info">
|
||||
<div class="plugin-header">
|
||||
<span class="plugin-name">{{ plugin.name || plugin.id }}</span>
|
||||
<span class="plugin-version" v-if="plugin.version">v{{ plugin.version }}</span>
|
||||
</div>
|
||||
<p class="plugin-desc">{{ plugin.description || '暂无描述' }}</p>
|
||||
<div class="plugin-tags" v-if="plugin.quality?.length">
|
||||
<span v-for="q in plugin.quality" :key="q.id" class="tag" :title="q.name">{{ q.ui }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plugin-actions">
|
||||
<button class="action-btn danger small" @click="confirmUninstall(plugin)">
|
||||
<Icon icon="lucide:trash-2" />
|
||||
卸载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uninstall Confirm Modal -->
|
||||
|
||||
|
||||
<!-- 外观设置 -->
|
||||
<div v-else-if="activeCategory === 'appearance'" class="section">
|
||||
<h2 class="section-title">外观设置</h2>
|
||||
@@ -179,6 +227,20 @@
|
||||
<p class="copyright">©2026 QZ <DEVELOPERS></DEVELOPERS></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uninstall Confirm Modal -->
|
||||
<Transition name="fade">
|
||||
<div class="modal-overlay" v-if="showUninstallModal">
|
||||
<div class="modal-content">
|
||||
<h3>卸载插件</h3>
|
||||
<p>确定要卸载插件 "{{ pluginToUninstall?.name }}" 吗?此操作无法撤销。</p>
|
||||
<div class="modal-actions">
|
||||
<button class="action-btn" @click="showUninstallModal = false">取消</button>
|
||||
<button class="action-btn danger" @click="executeUninstall">确认卸载</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,8 +249,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onBeforeMount, nextTick } from 'vue';
|
||||
import { ref, reactive, onBeforeMount, nextTick, watch } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { usePlayerStore } from '../stores/player';
|
||||
|
||||
const playerStore = usePlayerStore();
|
||||
@@ -197,6 +260,7 @@ defineEmits(['close']);
|
||||
|
||||
const categories = [
|
||||
{ id: 'storage', name: '存储', icon: 'lucide:hard-drive' },
|
||||
{ id: 'plugins', name: '插件', icon: 'lucide:blocks' },
|
||||
{ id: 'appearance', name: '外观', icon: 'lucide:palette' },
|
||||
{ id: 'playback', name: '播放', icon: 'lucide:headphones' },
|
||||
{ id: 'shortcuts', name: '快捷键', icon: 'lucide:keyboard' },
|
||||
@@ -217,6 +281,7 @@ const accentColors = [
|
||||
const activeCategory = ref('storage');
|
||||
const isLoaded = ref(false);
|
||||
const enableTransition = ref(false);
|
||||
const plugins = ref<any[]>([]);
|
||||
|
||||
const settings = reactive({
|
||||
persistCache: true,
|
||||
@@ -232,6 +297,70 @@ const cacheInfo = reactive({
|
||||
size: '',
|
||||
});
|
||||
|
||||
const loadPlugins = async () => {
|
||||
try {
|
||||
if (window.electronAPI?.plugin?.getAll) {
|
||||
plugins.value = await window.electronAPI.plugin.getAll();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load plugins', e);
|
||||
}
|
||||
};
|
||||
|
||||
const uninstallPlugin = async (id: string) => {
|
||||
try {
|
||||
if (window.electronAPI?.plugin?.uninstall) {
|
||||
await window.electronAPI.plugin.uninstall(id);
|
||||
await loadPlugins();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to uninstall plugin', e);
|
||||
}
|
||||
}
|
||||
|
||||
const installPluginFromFile = async () => {
|
||||
try {
|
||||
if (window.electronAPI?.plugin?.install) {
|
||||
const result = await window.electronAPI.plugin.install();
|
||||
if (result.success) {
|
||||
MessagePlugin.success(result.message || '安装成功');
|
||||
await loadPlugins();
|
||||
} else {
|
||||
if (result.message !== 'canceled') { // Assuming 'canceled' might be a thing, or just show whatever message comes back
|
||||
MessagePlugin.error(result.message || '安装失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to install plugin', e);
|
||||
MessagePlugin.error('安装过程中发生错误');
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Logic
|
||||
const showUninstallModal = ref(false);
|
||||
const pluginToUninstall = ref<any>(null);
|
||||
|
||||
const confirmUninstall = (plugin: any) => {
|
||||
pluginToUninstall.value = plugin;
|
||||
showUninstallModal.value = true;
|
||||
};
|
||||
|
||||
const executeUninstall = async () => {
|
||||
if (pluginToUninstall.value) {
|
||||
await uninstallPlugin(pluginToUninstall.value.id);
|
||||
showUninstallModal.value = false;
|
||||
pluginToUninstall.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeCategory, (newVal) => {
|
||||
if (newVal === 'plugins') {
|
||||
loadPlugins();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const loadCacheInfo = async () => {
|
||||
if (window.electronAPI) {
|
||||
const info = await window.electronAPI.getCacheInfo();
|
||||
@@ -294,6 +423,31 @@ const clearCache = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const isChangingCache = ref(false);
|
||||
|
||||
const changeCacheLocation = async () => {
|
||||
if (window.electronAPI && !isChangingCache.value) {
|
||||
try {
|
||||
const path = await window.electronAPI.selectDirectory();
|
||||
if (path) {
|
||||
isChangingCache.value = true;
|
||||
const result = await window.electronAPI.changeCacheLocation(path);
|
||||
if (result.success) {
|
||||
MessagePlugin.success('缓存位置已修改');
|
||||
await loadCacheInfo();
|
||||
} else {
|
||||
MessagePlugin.error(result.message || '修改失败');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
MessagePlugin.error('操作失败');
|
||||
console.error(e);
|
||||
} finally {
|
||||
isChangingCache.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load settings BEFORE mount to avoid visual flicker
|
||||
onBeforeMount(async () => {
|
||||
await Promise.all([loadCacheInfo(), loadAppearance()]);
|
||||
@@ -479,6 +633,9 @@ onBeforeMount(async () => {
|
||||
|
||||
.setting-control {
|
||||
margin-left: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
@@ -702,6 +859,19 @@ input:checked + .toggle-slider:before {
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Radio Group */
|
||||
.radio-group {
|
||||
display: flex;
|
||||
@@ -724,4 +894,167 @@ input:checked + .toggle-slider:before {
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Plugin Card Styles */
|
||||
.plugin-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.plugin-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.plugin-card:hover {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.plugin-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.plugin-tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 11px;
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-btn.small {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.plugin-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.text-btn {
|
||||
margin-top: 12px;
|
||||
color: var(--color-accent);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--color-bg-primary);
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
width: 400px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.plugin-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.plugin-name {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.plugin-version {
|
||||
font-size: 12px;
|
||||
background: var(--color-bg-tertiary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.plugin-desc {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.action-btn.small {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -185,12 +185,12 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
if (!song || !song.id) return;
|
||||
try {
|
||||
// Check if plugin API exists
|
||||
if (window.electronAPI?.plugin?.getLyric) {
|
||||
const rawLyric = await window.electronAPI.plugin.getLyric(song.source || 'kw', song.id.toString());
|
||||
console.log(rawLyric)
|
||||
} else {
|
||||
MessagePlugin.warning("当前插件不支持歌词获取").then()
|
||||
}
|
||||
// if (window.electronAPI?.plugin?.getLyric) {
|
||||
// const rawLyric = await window.electronAPI.plugin.getLyric(song.source || 'kw', song.id.toString());
|
||||
// console.log(rawLyric)
|
||||
// } else {
|
||||
// MessagePlugin.warning("当前插件不支持歌词获取").then()
|
||||
// }
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch lyrics:', e);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,36 @@ input {
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 50%;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
input[type='radio']:checked {
|
||||
border-color: var(--color-accent);
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
input[type='radio']:checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for 'Exquisite' look */
|
||||
|
||||
5
src/renderer/src/types/electron.d.ts
vendored
5
src/renderer/src/types/electron.d.ts
vendored
@@ -17,12 +17,17 @@ export interface IElectronAPI {
|
||||
call: (pluginId: string, method: string, args: any[]) => Promise<any>;
|
||||
search: (pluginId: string, query: string, page: number, limit: number) => Promise<any>;
|
||||
getLyric: (pluginId: string, id: string) => Promise<any>;
|
||||
getAll: () => Promise<any[]>;
|
||||
uninstall: (pluginId: string) => Promise<boolean>;
|
||||
install: () => Promise<{ success: boolean; message: string }>;
|
||||
};
|
||||
// Cache Control
|
||||
getCacheInfo: () => Promise<{ path: string; size: string; persistCache: boolean }>;
|
||||
setCachePersist: (persist: boolean) => Promise<void>;
|
||||
openCacheFolder: () => Promise<void>;
|
||||
clearCache: () => Promise<void>;
|
||||
changeCacheLocation: (newPath: string) => Promise<{ success: boolean; message: string; path?: string }>;
|
||||
selectDirectory: () => Promise<string | null>;
|
||||
// Settings
|
||||
settings: {
|
||||
getAll: () => Promise<{ persistCache: boolean; theme: 'dark' | 'light'; accentColor: string }>;
|
||||
|
||||
@@ -13,13 +13,36 @@
|
||||
|
||||
<transition name="slide-fade">
|
||||
<div class="settings-panel" v-if="showSettings">
|
||||
<div class="setting-item">
|
||||
<div class="limit-setting">
|
||||
<span class="setting-label">每页显示: {{ limit }} 首</span>
|
||||
<div class="slider-container">
|
||||
<input type="range" min="10" max="100" step="10" v-model.number="limit" class="setting-slider">
|
||||
<div class="slider-track" :style="{ width: ((limit - 10) / 90) * 100 + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Selector -->
|
||||
<div class="plugin-select-container">
|
||||
<button class="select-trigger" @click.stop="toggleDropdown" :title="'当前源: ' + activePluginName">
|
||||
<span>{{ activePluginName }}</span>
|
||||
<Icon icon="lucide:chevron-down" class="dropdown-icon" :class="{ open: isDropdownOpen }" />
|
||||
</button>
|
||||
|
||||
<transition name="fade">
|
||||
<div class="select-options" v-if="isDropdownOpen">
|
||||
<div
|
||||
v-for="plugin in plugins"
|
||||
:key="plugin.id"
|
||||
class="option"
|
||||
:class="{ active: plugin.id === activePlugin }"
|
||||
@click="selectPlugin(plugin)"
|
||||
>
|
||||
{{ plugin.name }}
|
||||
<Icon icon="lucide:check" v-if="plugin.id === activePlugin" class="check-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
@@ -102,7 +125,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { ref, watch, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { usePlayerStore } from '../stores/player';
|
||||
@@ -110,9 +133,9 @@ import { transformSearchSong } from '../utils/songUtils';
|
||||
import type { Song } from '../types/song';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const playerStore = usePlayerStore();
|
||||
|
||||
// --- State ---
|
||||
const query = computed(() => route.query.q as string || '');
|
||||
const currentPage = ref(1);
|
||||
const showSettings = ref(false);
|
||||
@@ -124,6 +147,39 @@ const loading = ref(false);
|
||||
const error = ref(false);
|
||||
const songs = ref<Song[]>([]);
|
||||
|
||||
// Plugin Selector State
|
||||
const plugins = ref<any[]>([]);
|
||||
const activePlugin = ref<string>(''); // Plugin ID
|
||||
const isDropdownOpen = ref(false);
|
||||
|
||||
const activePluginName = computed(() => {
|
||||
const p = plugins.value.find(p => p.id === activePlugin.value);
|
||||
return p ? p.name : '选择源';
|
||||
});
|
||||
|
||||
const toggleDropdown = () => {
|
||||
isDropdownOpen.value = !isDropdownOpen.value;
|
||||
};
|
||||
|
||||
const selectPlugin = (plugin: any) => {
|
||||
if (activePlugin.value !== plugin.id) {
|
||||
activePlugin.value = plugin.id;
|
||||
sessionStorage.setItem('qz-active-plugin', plugin.id);
|
||||
isDropdownOpen.value = false;
|
||||
} else {
|
||||
isDropdownOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Close dropdown on click outside
|
||||
const closeDropdown = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('.plugin-select-container')) {
|
||||
isDropdownOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Computed ---
|
||||
const totalPages = computed(() => {
|
||||
const t = Number(total.value) || 0;
|
||||
const l = Number(limit.value) || 30;
|
||||
@@ -132,9 +188,8 @@ const totalPages = computed(() => {
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const current = currentPage.value;
|
||||
// Ensure totalPages is strictly valid
|
||||
const total = totalPages.value;
|
||||
const delta = 2; // 2 on each side
|
||||
const delta = 2;
|
||||
|
||||
let start = Math.max(1, current - delta);
|
||||
let end = Math.min(total, current + delta);
|
||||
@@ -153,19 +208,41 @@ const visiblePages = computed(() => {
|
||||
return pages;
|
||||
});
|
||||
|
||||
// --- Methods ---
|
||||
const loadPlugins = async () => {
|
||||
try {
|
||||
if (window.electronAPI?.plugin?.getAll) {
|
||||
const all = await window.electronAPI.plugin.getAll();
|
||||
// Filter valid plugins basically (have id and name)
|
||||
plugins.value = all.filter((p: any) => p.id && p.name);
|
||||
|
||||
// Restore selection or default
|
||||
const saved = sessionStorage.getItem('qz-active-plugin');
|
||||
if (saved && plugins.value.find(p => p.id === saved)) {
|
||||
activePlugin.value = saved;
|
||||
} else if (plugins.value.length > 0) {
|
||||
// Default to 'wy' if present, else first
|
||||
const wy = plugins.value.find(p => p.id === 'wy');
|
||||
activePlugin.value = wy ? 'wy' : plugins.value[0].id;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load plugins", e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!query.value) return;
|
||||
if (!query.value || !activePlugin.value) return;
|
||||
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
songs.value = [];
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.plugin.search('wy', query.value, currentPage.value, limit.value);
|
||||
const result = await window.electronAPI.plugin.search(activePlugin.value, query.value, currentPage.value, limit.value);
|
||||
|
||||
if (result && result.list) {
|
||||
songs.value = result.list.map((item: any) => transformSearchSong(item));
|
||||
// Prioritize songCount if available, otherwise fallback to total, but NEVER use just list length as total
|
||||
total.value = result.songCount || result.total || 0;
|
||||
} else {
|
||||
total.value = 0;
|
||||
@@ -190,27 +267,17 @@ const handlePlaySong = (index: number) => {
|
||||
const getHighlightRegex = (q: string) => {
|
||||
const trimmed = q.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// Escape regex characters
|
||||
const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// If strict or short query, fallback to exact match or simple space split
|
||||
// "超过2个字相似" -> If > 2 chars, try to match substrings
|
||||
if (trimmed.length <= 2) {
|
||||
return new RegExp(escapeRegExp(trimmed), 'gi');
|
||||
}
|
||||
|
||||
// Generate all substrings of length >= 2
|
||||
const substrings = new Set<string>();
|
||||
substrings.add(trimmed); // Always include full query
|
||||
|
||||
substrings.add(trimmed);
|
||||
for (let i = 0; i < trimmed.length; i++) {
|
||||
for (let j = i + 2; j <= trimmed.length; j++) {
|
||||
substrings.add(trimmed.slice(i, j));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by length match (longest first)
|
||||
const sorted = Array.from(substrings).sort((a, b) => b.length - a.length);
|
||||
const pattern = sorted.map(s => escapeRegExp(s)).join('|');
|
||||
return new RegExp(pattern, 'gi');
|
||||
@@ -218,23 +285,43 @@ const getHighlightRegex = (q: string) => {
|
||||
|
||||
const highlight = (text: string) => {
|
||||
if (!query.value || !text) return text;
|
||||
|
||||
const regex = getHighlightRegex(query.value);
|
||||
if (!regex) return text;
|
||||
|
||||
return text.replace(regex, match => `<span class="highlight">${match}</span>`);
|
||||
};
|
||||
|
||||
// --- Watchers & Lifecycle ---
|
||||
watch(query, () => {
|
||||
currentPage.value = 1;
|
||||
fetchData();
|
||||
}, { immediate: true });
|
||||
// ensure plugins loaded before fetching? usually mounted happens first
|
||||
if (activePlugin.value) fetchData();
|
||||
}, { immediate: false }); // Wait for mount init
|
||||
|
||||
watch(limit, (newLimit) => {
|
||||
localStorage.setItem('qz-search-limit', newLimit.toString());
|
||||
currentPage.value = 1;
|
||||
fetchData();
|
||||
});
|
||||
|
||||
watch(activePlugin, (newVal, oldVal) => {
|
||||
if (newVal && newVal !== oldVal && query.value) {
|
||||
currentPage.value = 1;
|
||||
fetchData();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
document.addEventListener('click', closeDropdown);
|
||||
await loadPlugins();
|
||||
// After plugins loaded, if we have a query, fetch data
|
||||
if (query.value && activePlugin.value) {
|
||||
fetchData();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', closeDropdown);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -316,19 +403,115 @@ watch(limit, (newLimit) => {
|
||||
|
||||
/* Settings Panel */
|
||||
.settings-panel {
|
||||
background: var(--color-bg-secondary);
|
||||
background: var(--color-bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative; /* Context */
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
.limit-setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Plugin Selector */
|
||||
.plugin-select-container {
|
||||
position: relative;
|
||||
/* ensure it stays on top when options open? No, options use absolute */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.select-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: var(--color-bg-primary);
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
min-width: 120px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.select-trigger:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 16px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-icon.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.select-options {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
width: 160px;
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.option.active {
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Fade util */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.setting-label {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
Reference in New Issue
Block a user