feat(admin): 图片歌单改导入酷狗+自动排入按期望时段+日期滚动修复
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 值, 避免受父级滚动容器/左箭头影响
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user