diff --git a/app/components/Admin/PlaylistCardPage.vue b/app/components/Admin/PlaylistCardPage.vue index 7146463..1acef80 100644 --- a/app/components/Admin/PlaylistCardPage.vue +++ b/app/components/Admin/PlaylistCardPage.vue @@ -137,10 +137,23 @@ import { Layout, Image as ImageIcon, Music2, ListPlus, Trash2, Save, ListMusic } import AdminDatePicker from './AdminDatePicker.vue' import AdminPlaylistCardGenerator from './PlaylistCardGenerator.vue' import { useAuth } from '~/composables/useAuth' -import { useAdmin } from '~/composables/useAdmin' -const auth = useAuth() -const admin = useAdmin() +const auth = import.meta.client ? useAuth() : { getAuthConfig: () => ({}) } +let adminInstance: any = null +const getAdmin = async () => { + if (!import.meta.client) return null + if (adminInstance) return adminInstance + const mod = await import('~/composables/useAdmin') + adminInstance = mod.useAdmin() + return adminInstance +} +const adminShim: any = { + loading: { value: false }, + error: { value: '' }, + addSong: async (p: any) => { const a = await getAdmin(); return a ? a.addSong(p) : { success: false } }, + createSchedule: async (...args: any[]) => { const a = await getAdmin(); return a ? a.createSchedule(...args) : { success: false } } +} +const admin = import.meta.client ? adminShim : adminShim const today = new Date() today.setHours(0, 0, 0, 0) @@ -158,7 +171,7 @@ const playTimes = ref([]) const displaySongs = computed(() => parsedSongs.value.length ? parsedSongs.value : schedules.value) const statusClass = (s) => { - const map = { + const map: Record = { '歌曲已创建': 'bg-emerald-500/15 text-emerald-400', '已匹配现有歌曲': 'bg-blue-500/15 text-blue-400', '匹配失败(未创建)': 'bg-amber-500/15 text-amber-400', @@ -169,8 +182,9 @@ const statusClass = (s) => { } const parsePasteText = () => { + if (!import.meta.client) return const lines = pasteText.value.split(/\r?\n/).map(l => l.trim()).filter(Boolean) - const out = [] + const out: Array<{ title: string, artist: string, status?: string }> = [] for (const line of lines) { let t = line t = t.replace(/^[\d]+[\s.、\-、)]+/, '') @@ -189,7 +203,7 @@ const parsePasteText = () => { else { title = t; artist = '' } if (title) out.push({ title, artist }) } - parsedSongs.value = out + parsedSongs.value = out as any } const clearParsed = () => { @@ -198,9 +212,10 @@ const clearParsed = () => { } const fetchSchedules = async () => { + if (!import.meta.client) return loading.value = true try { - const response = await $fetch('/api/admin/schedule/full', { + const response = await $fetch('/api/admin/schedule/full', { ...auth.getAuthConfig(), query: { date: selectedDate.value, includeDrafts: 'true' } }) @@ -209,30 +224,32 @@ const fetchSchedules = async () => { } else schedules.value = [] } catch (error) { console.error('Failed to fetch schedules:', error) - if (window.$showNotification) window.$showNotification('获取排期失败', 'error') + if ((window as any).$showNotification) (window as any).$showNotification('获取排期失败', 'error') } finally { loading.value = false } } const fetchPlayTimes = async () => { + if (!import.meta.client) return try { - const r = await $fetch('/api/admin/play-times', { ...auth.getAuthConfig() }) + const r: any = await $fetch('/api/admin/play-times', { ...auth.getAuthConfig() }) if (r && r.success && r.data) playTimes.value = (r.data.playTimes || r.data || []).filter(Boolean) } catch (e) { console.warn(e) } } -const findOrCreateSong = async ({ title, artist }) => { +const findOrCreateSong = async ({ title, artist }: { title: string, artist: string }) => { + if (!import.meta.client) return { created: false, song: null, failMsg: '客户端-only' } try { - const search = await $fetch('/api/songs', { + const search: any = await $fetch('/api/songs', { ...auth.getAuthConfig(), query: { q: `${title} ${artist}`, limit: 20 } }) - const list = search?.data?.songs || search?.songs || [] + const list: any[] = search?.data?.songs || search?.songs || [] const hit = list.find(s => s.title && s.title.toLowerCase() === title.toLowerCase() && (!artist || !s.artist || s.artist.toLowerCase() === artist.toLowerCase()) ) if (hit) return { created: false, song: hit } - const addResult = await admin.addSong({ + const addResult: any = await (admin as any).addSong({ title, artist: artist || '未知歌手', language: 'Chinese', sourceType: 'kugou', status: 'approved', contributor: 'admin', musicPlatform: 'kugou', platformSongId: '', lyrics: '', coverUrl: '', duration: 0, sourceUrl: '' @@ -241,17 +258,17 @@ const findOrCreateSong = async ({ title, artist }) => { if (addResult?.success && addResult?.data?.song) return { created: true, song: addResult.data.song } if (addResult?.song) return { created: true, song: addResult.song } return { created: false, song: null, failMsg: addResult?.message || '返回值无法解析' } - } catch (e) { + } catch (e: any) { return { created: false, song: null, failMsg: e?.message || '接口调用异常' } } } const generateSchedulesFromList = async () => { - if (displaySongs.value.length === 0) return + if (!import.meta.client || displaySongs.value.length === 0) return generatingSchedule.value = true try { - const toProcess = parsedSongs.value.length ? parsedSongs.value : schedules.value - const results = [] + const toProcess: any[] = parsedSongs.value.length ? parsedSongs.value : schedules.value + const results: Array<{ title: string, artist: string, status: string }> = [] const playDateStr = selectedDate.value let nextSeq = 1 for (const row of toProcess) { @@ -274,28 +291,32 @@ const generateSchedulesFromList = async () => { scheduleRow.status = '已匹配现有歌曲' } try { - const res = await admin.createSchedule(Number(songObj.id), playDateStr, + const res: any = await (admin as any).createSchedule(Number(songObj.id), playDateStr, (selectedPlayTimeId.value != null) ? Number(selectedPlayTimeId.value) : (songObj.preferredPlayTimeId || null), nextSeq) if (res?.success) { scheduleRow.status = '排期已创建'; nextSeq++ } else { scheduleRow.status = `排期保存失败:${res?.message || '未知原因'}` } - } catch (e) { scheduleRow.status = `排期保存失败:${e?.message || '接口异常'}` } + } catch (e: any) { scheduleRow.status = `排期保存失败:${e?.message || '接口异常'}` } results.push(scheduleRow) } - parsedSongs.value = results + parsedSongs.value = results as any await fetchSchedules() - if (window.$showNotification) window.$showNotification(`批量保存完成:${results.filter(r => r.status === '排期已创建').length}/${results.length} 成功`, 'success') - } catch (e) { + if ((window as any).$showNotification) (window as any).$showNotification(`批量保存完成:${results.filter(r => r.status === '排期已创建').length}/${results.length} 成功`, 'success') + } catch (e: any) { console.error(e) - if (window.$showNotification) window.$showNotification('批量排期失败:' + (e?.message || '未知错误'), 'error') + if ((window as any).$showNotification) (window as any).$showNotification('批量排期失败:' + (e?.message || '未知错误'), 'error') } finally { generatingSchedule.value = false } } const handleDateChange = () => { if (!parsedSongs.value.length) fetchSchedules() } -watch(selectedDate, () => { if (!parsedSongs.value.length) fetchSchedules() }) +if (import.meta.client) { + watch(selectedDate, () => { if (!parsedSongs.value.length) fetchSchedules() }) +} onMounted(() => { + if (!import.meta.client) return fetchSchedules() fetchPlayTimes() }) + diff --git a/nuxt.config.ts b/nuxt.config.ts index a8c5b78..cf01f52 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -285,6 +285,13 @@ export default defineNuxtConfig({ : process.env.NETLIFY ? 'netlify' : process.env.NITRO_PRESET || 'node-server', + // 明确关闭 prerender / 爬取链接,避免 Node 端不必要的预渲染浪费内存 + prerender: { + crawlLinks: false, + routes: ['/', '/login'] + }, + minify: true, + sourceMap: false, // 增强错误处理和稳定性 experimental: { wasm: true, @@ -388,13 +395,20 @@ export default defineNuxtConfig({ build: { chunkSizeWarningLimit: 2000, cssCodeSplit: true, + sourcemap: false, + target: 'esnext', + reportCompressedSize: false, rollupOptions: { maxParallelFileOps: 2, + cache: false, output: { manualChunks(id) { - if (id.includes('node_modules/lucide-vue-next')) { - return 'lucide' - } + if (!id.includes('node_modules')) return + if (id.includes('lucide-vue-next')) return 'icons' + if (id.includes('@pixi')) return 'pixi' + if (id.includes('@applemusic-like-lyrics')) return 'lyric-engine' + if (id.includes('drizzle-orm') || id.includes('postgres')) return 'database' + if (id.includes('xlsx') || id.includes('jspdf') || id.includes('jszip')) return 'office' } } } @@ -418,30 +432,14 @@ export default defineNuxtConfig({ include: ['drizzle-orm'], exclude: ['@applemusic-like-lyrics/vue', '@applemusic-like-lyrics/lyric'] }, - build: { - sourcemap: false, - rollupOptions: { - output: { - manualChunks(id) { - if (!id.includes('node_modules')) return - if (id.includes('lucide-vue-next')) return 'icons' - if (id.includes('@pixi')) return 'pixi' - if (id.includes('@applemusic-like-lyrics')) return 'lyric-engine' - if (id.includes('drizzle-orm') || id.includes('postgres')) return 'database' - if (id.includes('xlsx') || id.includes('jspdf') || id.includes('jszip')) return 'office' - } - } - } - }, - // 添加 WASM 支持配置 - assetsInclude: ['**/*.wasm'], - // SSR配置 + // SSR配置: Vercel/Netlify serverless 需要 noExternal 把 drizzle/postgres inline; + // Node-server 环境直接从 node_modules require 即可, 避免打包 inline 导致 OOM ssr: { noExternal: process.env.NETLIFY ? ['drizzle-orm', 'postgres'] : process.env.VERCEL ? [] - : ['drizzle-orm', 'postgres'] + : [] } } })