From ac4727551c5dd46579645c5f7a8ad4d9d78e9cb8 Mon Sep 17 00:00:00 2001 From: HenryDu8133 <813367384@qq.com> Date: Sun, 13 Sep 2026 12:22:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=9B=BE=E7=89=87=E6=AD=8C?= =?UTF-8?q?=E5=8D=95=E6=94=B9=E5=AF=BC=E5=85=A5=E9=85=B7=E7=8B=97+?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=8E=92=E5=85=A5=E6=8C=89=E6=9C=9F=E6=9C=9B?= =?UTF-8?q?=E6=97=B6=E6=AE=B5+=E6=97=A5=E6=9C=9F=E6=BB=9A=E5=8A=A8?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 菜单重命名: 图片歌单 -> 导入酷狗 (Sidebar/Dashboard/教程 + 页面标题) 2. PlaylistCardPage(导入酷狗) 新增: - 粘贴酷狗歌单文本解析(支持序号/横杠/括号格式) - 预览区歌名与歌手名完整显示(去除truncate, 支持换行展示) - 选日期+选播出时段, 一键将解析后的歌曲(没找到则自动创建)生成为该日期的排期 - 批量保存后展示状态徽章(匹配/创建/失败) 3. PlaylistCardGenerator 画布: - 去除 substring(0,12)/substring(0,10) 截断, 改用 fitText 按 canvas 宽度逐字符换行切分 - canvas 高度根据实际行数动态计算, 最多支持 200 首 - 中文字体堆栈加入 PingFang SC/Microsoft YaHei 4. ScheduleManager.autoScheduleExpected: - 优先按歌曲自己的 preferredPlayTimeId(期望播出时段) 分组排入 - 未指定才 fallback 到顶部当前选中时段 - 分时段各自维护 sequence, 最后按时段 startTime 升序+分序 合并重排 - 顶部过滤了单时段时, 只排入匹配该时段的歌曲(含未指定fallback) 5. 日期滚动修复: - AdminDatePicker / AdminMultiDatePicker scrollToDateElement 弃用 scrollIntoView - 改用精确的 offsetLeft - (clientWidth-offsetWidth)/2 计算 scrollTo left 值, 避免受父级滚动容器/左箭头影响 --- app/components/Admin/AdminDatePicker.vue | 4 +- app/components/Admin/AdminMultiDatePicker.vue | 5 +- .../Admin/PlaylistCardGenerator.vue | 84 +++-- app/components/Admin/PlaylistCardPage.vue | 299 ++++++++++++++---- app/components/Admin/ScheduleManager.vue | 41 ++- app/components/Admin/Sidebar.vue | 2 +- app/components/Admin/WorkflowTutorial.vue | 2 +- app/pages/dashboard.vue | 2 +- 8 files changed, 355 insertions(+), 84 deletions(-) diff --git a/app/components/Admin/AdminDatePicker.vue b/app/components/Admin/AdminDatePicker.vue index fb513ad..7303b81 100644 --- a/app/components/Admin/AdminDatePicker.vue +++ b/app/components/Admin/AdminDatePicker.vue @@ -244,7 +244,9 @@ const scrollToDateElement = (behavior = 'smooth', targetDate = props.modelValue) const dateKey = targetDate || formatDateStr(today) const el = dateSelector.value.querySelector(`[data-date="${dateKey}"]`) if (!el) return - el.scrollIntoView({ behavior, block: 'nearest', inline: 'center' }) + const cw = dateSelector.value.clientWidth + const target = el.offsetLeft - (cw - el.offsetWidth) / 2 + dateSelector.value.scrollTo({ left: target, behavior }) } const handleWheel = (event) => { diff --git a/app/components/Admin/AdminMultiDatePicker.vue b/app/components/Admin/AdminMultiDatePicker.vue index dfe6b9f..e3dc583 100644 --- a/app/components/Admin/AdminMultiDatePicker.vue +++ b/app/components/Admin/AdminMultiDatePicker.vue @@ -380,7 +380,10 @@ const handleScroll = () => { const scrollToDateElement = (targetDate, behavior = 'smooth') => { if (!dateSelector.value || !targetDate) return const el = dateSelector.value.querySelector(`[data-date="${targetDate}"]`) - if (el) el.scrollIntoView({ behavior, block: 'nearest', inline: 'center' }) + if (!el) return + const cw = dateSelector.value.clientWidth + const target = el.offsetLeft - (cw - el.offsetWidth) / 2 + dateSelector.value.scrollTo({ left: target, behavior }) } const scrollToToday = () => { diff --git a/app/components/Admin/PlaylistCardGenerator.vue b/app/components/Admin/PlaylistCardGenerator.vue index f612fd6..64ddb84 100644 --- a/app/components/Admin/PlaylistCardGenerator.vue +++ b/app/components/Admin/PlaylistCardGenerator.vue @@ -86,13 +86,48 @@ const aiPrompt = ref('anime style school campus sunset beautiful sky highly deta const isGeneratingAi = ref(false) const bgImageObj = ref(null) -const maxDisplaySongs = 35 +const maxDisplaySongs = 200 +const songLeftX = 44 +const songRightX = 360 +const songAreaWidth = songRightX - songLeftX + +const fitText = (ctx, text, fontSize, maxWidth, lineHeight) => { + ctx.font = `${fontSize}px "Inter", "system-ui", "PingFang SC", "Microsoft YaHei", sans-serif` + const chars = Array.from(text) + const lines = [] + let current = '' + for (const ch of chars) { + const tryStr = current + ch + const w = ctx.measureText(tryStr).width + if (w > maxWidth && current) { + lines.push(current) + current = ch + } else { + current = tryStr + } + } + if (current) lines.push(current) + return { lines, lineHeight } +} const canvasHeight = computed(() => { const count = Math.min(props.songs.length, maxDisplaySongs) - if (count <= 10) return 600 - // 每多一首增加 40px 高度 - return 600 + (count - 10) * 40 + if (!count) return 600 + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + let totalH = 0 + const maxWidthTitle = songAreaWidth + const maxWidthArtist = Math.max(100, songAreaWidth - 10) + for (let i = 0; i < count; i++) { + const s = props.songs[i] + const song = s?.song || s || {} + const title = song.title || '未知歌曲' + const artist = song.artist || '未知歌手' + const t1 = fitText(ctx, title, 16, maxWidthTitle, 22) + const t2 = fitText(ctx, `- ${artist}`, 13, maxWidthArtist, 18) + totalH += Math.max(28, (t1.lines.length * t1.lineHeight) + (t2.lines.length * t2.lineHeight) + 10) + } + return 240 + totalH }) @@ -155,33 +190,44 @@ const drawCanvas = () => { // 绘制歌曲列表 ctx.textAlign = 'left' + ctx.textBaseline = 'top' let y = 160 - const displaySongs = props.songs.slice(0, maxDisplaySongs) // 最多显示 35 首 + const displaySongs = props.songs.slice(0, maxDisplaySongs) displaySongs.forEach((schedule, index) => { const song = schedule.song || schedule // 序号 ctx.fillStyle = '#2563eb' - ctx.font = 'bold 16px "Inter", "system-ui", sans-serif' + ctx.font = 'bold 16px "Inter", "system-ui", "PingFang SC", "Microsoft YaHei", sans-serif' + ctx.textBaseline = 'top' ctx.fillText(`${index + 1}.`, 40, y) - // 歌名 - ctx.fillStyle = '#0F172A' // Neutral Dark - ctx.font = 'bold 16px "Inter", "system-ui", sans-serif' - // 限制歌名长度 - let title = song.title || '未知歌曲' - if (title.length > 12) title = title.substring(0, 12) + '...' - ctx.fillText(title, 70, y) + const maxWidthTitle = songAreaWidth + const maxWidthArtist = Math.max(120, songAreaWidth - 10) + const title = song.title || '未知歌曲' + const artist = song.artist || '未知歌手' - // 歌手 + const t1 = fitText(ctx, title, 16, maxWidthTitle, 22) + ctx.fillStyle = '#0F172A' + ctx.font = `bold 16px "Inter", "system-ui", "PingFang SC", "Microsoft YaHei", sans-serif` + let ty = y + for (const line of t1.lines) { + ctx.fillText(line, songLeftX, ty) + ty += t1.lineHeight + } + + const t2 = fitText(ctx, `- ${artist}`, 13, maxWidthArtist, 18) ctx.fillStyle = '#64748B' - ctx.font = '14px "Inter", "system-ui", sans-serif' - let artist = song.artist || '未知歌手' - if (artist.length > 10) artist = artist.substring(0, 10) + '...' - ctx.fillText(`- ${artist}`, 240, y) + ctx.font = `13px "Inter", "system-ui", "PingFang SC", "Microsoft YaHei", sans-serif` + let ay = ty + 4 + for (const line of t2.lines) { + ctx.fillText(line, songLeftX, ay) + ay += t2.lineHeight + } - y += 40 + const songBlockHeight = Math.max(28, (ty - y) + (ay - ty) + 4) + y += songBlockHeight + 8 }) if (props.songs.length > maxDisplaySongs) { diff --git a/app/components/Admin/PlaylistCardPage.vue b/app/components/Admin/PlaylistCardPage.vue index fb82f60..7146463 100644 --- a/app/components/Admin/PlaylistCardPage.vue +++ b/app/components/Admin/PlaylistCardPage.vue @@ -1,67 +1,130 @@