fix(admin): 修复日期选择框尺寸过大与自动排入跨日期逻辑

- **日期选择器尺寸优化**:AdminDatePicker 与 AdminMultiDatePicker 采用 w-9 h-9 的紧凑卡片,缩小字号、边距与圆角,贴合后台窄布局。
- **自动排入逻辑重构**:取消严格的 `expectedPlayDate === selectedDate` 过滤,改为按歌曲期望日期/时段自动分配 `playDate` 与 `playTimeId`;其他歌曲排到当前选中日期。
- **跨日期本地暂存池**:引入 `crossDateLocalPool` 保存尚未保存但已排入其他日期的临时排期,切换 `selectedDate` 或时段时正确回填对应日期的草稿,避免自动排入后切换视图导致丢失。
- **排期列表去重与过滤**:时段过滤逻辑修复 `playTimeId == null` 的边界,并在合并公开/草稿/本地暂存时按优先级去重。
This commit is contained in:
2026-09-13 16:18:09 +08:00
parent c4c30de416
commit 37fd18aaea
3 changed files with 158 additions and 66 deletions
+13 -13
View File
@@ -1,28 +1,28 @@
<template> <template>
<div class="relative bg-zinc-900/50 border border-zinc-800/50 rounded-2xl p-1 overflow-hidden"> <div class="relative bg-zinc-900/50 border border-zinc-800/50 rounded-xl p-1 overflow-hidden">
<!-- 简化版日期选择显示 --> <!-- 简化版日期选择显示 -->
<div class="flex items-center justify-between px-3 py-2" @click="toggleCalendar" style="cursor: pointer"> <div class="flex items-center justify-between px-2.5 py-1.5" @click="toggleCalendar" style="cursor: pointer">
<div class="flex items-center gap-3"> <div class="flex items-center gap-2.5">
<div class="flex flex-col items-center justify-center w-12 h-12 rounded-lg bg-blue-600 text-white shadow-lg shadow-blue-900/20"> <div class="flex flex-col items-center justify-center w-9 h-9 rounded-md bg-blue-600 text-white shadow shadow-blue-900/20">
<span class="text-[10px] font-bold uppercase tracking-wider opacity-80">{{ getWeekday(modelValue) }}</span> <span class="text-[9px] font-bold uppercase tracking-wider opacity-80 leading-tight">{{ getWeekday(modelValue) }}</span>
<span class="text-lg font-black leading-none my-0.5">{{ getDay(modelValue) }}</span> <span class="text-base font-black leading-none my-0">{{ getDay(modelValue) }}</span>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-sm font-bold text-zinc-200">{{ modelValue }}</span> <span class="text-xs font-bold text-zinc-200">{{ modelValue }}</span>
<span class="text-xs text-zinc-500 font-medium">{{ isToday(modelValue) ? '今天' : '' }}</span> <span class="text-[10px] text-zinc-500 font-medium leading-tight">{{ isToday(modelValue) ? '今天' : '' }}</span>
</div> </div>
</div> </div>
<div class="flex items-center border-l border-zinc-800 pl-3"> <div class="flex items-center border-l border-zinc-800 pl-2">
<button class="p-2 text-zinc-500 hover:text-emerald-400 transition-colors" title="跳转到今天" @click.stop="scrollToToday"> <button class="p-1.5 text-zinc-500 hover:text-emerald-400 transition-colors" title="跳转到今天" @click.stop="scrollToToday">
<CircleDot class="w-5 h-5" /> <CircleDot class="w-4 h-4" />
</button> </button>
<button <button
class="p-2 text-zinc-500 hover:text-blue-400 transition-colors" class="p-1.5 text-zinc-500 hover:text-blue-400 transition-colors"
:class="{ 'text-blue-400': showCalendar }" :class="{ 'text-blue-400': showCalendar }"
title="打开月历" title="打开月历"
> >
<CalendarIcon class="w-5 h-5" /> <CalendarIcon class="w-4 h-4" />
</button> </button>
</div> </div>
</div> </div>
+29 -13
View File
@@ -1,28 +1,34 @@
<template> <template>
<div class="relative bg-zinc-900/50 border border-zinc-800/50 rounded-2xl p-1 overflow-hidden"> <div class="relative bg-zinc-900/50 border border-zinc-800/50 rounded-xl p-1 overflow-hidden">
<!-- 简化版多选日期显示 --> <!-- 简化版多选日期显示 -->
<div class="flex items-center justify-between px-3 py-2" @click="toggleCalendar" style="cursor: pointer"> <div class="flex items-center justify-between px-2.5 py-1.5" @click="toggleCalendar" style="cursor: pointer">
<div class="flex items-center gap-3"> <div class="flex items-center gap-2.5 min-w-0 flex-1">
<div class="flex flex-col"> <div class="flex flex-col items-center justify-center w-9 h-9 rounded-md bg-blue-600 text-white shadow shadow-blue-900/20 shrink-0">
<span class="text-sm font-bold text-zinc-200"> <span class="text-[9px] font-bold uppercase tracking-wider opacity-80 leading-tight">
已选择 {{ modelValue.length }} 天 {{ modelValue.length }}
</span> </span>
<span class="text-xs text-zinc-500 font-medium truncate max-w-[200px]"> <span class="text-[10px] font-bold leading-none opacity-80">天</span>
{{ modelValue.join(', ') || '未选择日期' }} </div>
<div class="flex flex-col min-w-0 flex-1">
<span class="text-xs font-bold text-zinc-200 truncate">
{{ modelValue.length ? modelValue.slice(0, 3).join('、') + (modelValue.length > 3 ? ` 等${modelValue.length}天` : '') : '未选择日期' }}
</span>
<span class="text-[10px] text-zinc-500 font-medium leading-tight truncate">
{{ selectedDatesSummary }}
</span> </span>
</div> </div>
</div> </div>
<div class="flex items-center border-l border-zinc-800 pl-3"> <div class="flex items-center border-l border-zinc-800 pl-2 ml-2 shrink-0">
<button class="p-2 text-zinc-500 hover:text-emerald-400 transition-colors" title="跳转到今天" @click.stop="scrollToToday"> <button class="p-1.5 text-zinc-500 hover:text-emerald-400 transition-colors" title="跳转到今天" @click.stop="scrollToToday">
<CircleDot class="w-5 h-5" /> <CircleDot class="w-4 h-4" />
</button> </button>
<button <button
class="p-2 text-zinc-500 hover:text-blue-400 transition-colors" class="p-1.5 text-zinc-500 hover:text-blue-400 transition-colors"
:class="{ 'text-blue-400': showCalendar }" :class="{ 'text-blue-400': showCalendar }"
title="打开月历" title="打开月历"
> >
<CalendarIcon class="w-5 h-5" /> <CalendarIcon class="w-4 h-4" />
</button> </button>
</div> </div>
</div> </div>
@@ -165,6 +171,16 @@ const calendarMonth = ref(today.getMonth())
const weekdayNames = ['日', '一', '二', '三', '四', '五', '六'] const weekdayNames = ['日', '一', '二', '三', '四', '五', '六']
const selectedDatesSummary = computed(() => {
if (!props.modelValue || props.modelValue.length === 0) return '点击右侧按钮选择日期'
const sorted = sortDates(props.modelValue)
const todayStr = formatDateStr(today)
if (sorted.includes(todayStr)) return `含今天 共 ${sorted.length} 天`
const first = sorted[0]
const last = sorted[sorted.length - 1]
return `${first} ~ ${last}`
})
const batchActions = [ const batchActions = [
{ label: '今天', action: 'today' }, { label: '今天', action: 'today' },
{ label: '明天起7天', action: 'next7' }, { label: '明天起7天', action: 'next7' },
+116 -40
View File
@@ -1083,6 +1083,9 @@ const localScheduledSongs = ref([])
const replayRequests = ref([]) const replayRequests = ref([])
const scheduledSongIds = ref(new Set()) const scheduledSongIds = ref(new Set())
// 跨日期本地暂存池:保存未保存但分配到其他日期的排期,避免切换日期时丢失
const crossDateLocalPool = ref([])
// 计算是否有未发布的草稿 // 计算是否有未发布的草稿
const hasUnpublishedDrafts = computed(() => { const hasUnpublishedDrafts = computed(() => {
return localScheduledSongs.value.some((schedule) => schedule.isDraft) return localScheduledSongs.value.some((schedule) => schedule.isDraft)
@@ -1693,9 +1696,23 @@ const onSemesterChange = async () => {
// 更新本地排期数据(包括草稿) // 更新本地排期数据(包括草稿)
const updateLocalScheduledSongs = () => { const updateLocalScheduledSongs = () => {
// 第一步:把当前 localScheduledSongs 中的本地暂存(isLocalOnly)回收到跨日期池
for (const s of localScheduledSongs.value) {
if (s.isLocalOnly) {
const idx = crossDateLocalPool.value.findIndex(p => p.id === s.id)
if (idx === -1) crossDateLocalPool.value.push(s)
else crossDateLocalPool.value[idx] = s
}
}
// 清理池子里已经在公开排期或草稿中的(避免重复)
crossDateLocalPool.value = crossDateLocalPool.value.filter(p => {
return ![...publicSchedules.value, ...drafts.value].some(s => s.id === p.id || (s.song?.id && p.song?.id && s.song.id === p.song.id))
})
console.log('更新本地排期数据 - 当前日期:', selectedDate.value) console.log('更新本地排期数据 - 当前日期:', selectedDate.value)
console.log('公开排期数量:', publicSchedules.value.length) console.log('公开排期数量:', publicSchedules.value.length)
console.log('草稿数量:', drafts.value.length) console.log('草稿数量:', drafts.value.length)
console.log('跨日期本地暂存池数量:', crossDateLocalPool.value.length)
const _fmtLocal = (d) => { const _fmtLocal = (d) => {
const x = new Date(d) const x = new Date(d)
@@ -1715,28 +1732,59 @@ const updateLocalScheduledSongs = () => {
const draftDateStr = _fmtLocal(draft.playDate) const draftDateStr = _fmtLocal(draft.playDate)
return draftDateStr === selectedDate.value return draftDateStr === selectedDate.value
}) })
// 获取本地暂存池中属于当前日期的
const todayLocalOnly = crossDateLocalPool.value.filter((p) => {
return p.playDate === selectedDate.value
})
// 从池中移除今天的,因为接下来要放入 localScheduledSongs
crossDateLocalPool.value = crossDateLocalPool.value.filter(p => p.playDate !== selectedDate.value)
console.log('当天已发布排期:', todaySchedules.length) console.log('当天已发布排期:', todaySchedules.length)
console.log('当天草稿排期:', todayDrafts.length) console.log('当天草稿排期:', todayDrafts.length)
console.log('当天本地暂存排期:', todayLocalOnly.length)
// 合并已发布和草稿排期 // 合并已发布、草稿、本地暂存
let allSchedules = [...todaySchedules, ...todayDrafts] let allSchedules = [...todaySchedules, ...todayDrafts, ...todayLocalOnly]
// 如果选择了特定播出时段,进行过滤 // 如果选择了特定播出时段,进行过滤
if (selectedPlayTime.value) { if (selectedPlayTime.value) {
allSchedules = allSchedules.filter((s) => s.playTimeId === parseInt(selectedPlayTime.value)) const ptId = parseInt(selectedPlayTime.value)
allSchedules = allSchedules.filter((s) => {
if (!selectedPlayTime.value) return true
if (s.playTimeId === ptId) return true
// 如果未选择时段(为空)则也不显示,除非时段本身就是空
if (s.playTimeId == null && ptId == null) return true
return s.playTimeId === ptId
})
} }
// 去重(根据 song.id):保留优先级:已发布 > 草稿 > 本地暂存
const seenIds = new Set()
allSchedules = allSchedules.filter(s => {
const key = s.song?.id || s.songId || s.id
if (!key) return true
if (seenIds.has(key)) return false
seenIds.add(key)
return true
})
// 按 sequence 字段排序 // 按 sequence 字段排序
allSchedules.sort((a, b) => (a.sequence || 0) - (b.sequence || 0)) allSchedules.sort((a, b) => {
const sa = a.sequence || 0
const sb = b.sequence || 0
if (sa !== sb) return sa - sb
return (a.id || 0) - (b.id || 0)
})
localScheduledSongs.value = allSchedules.map((s) => ({ ...s })) localScheduledSongs.value = allSchedules.map((s) => ({ ...s }))
console.log('最终显示排期数量:', localScheduledSongs.value.length) console.log('最终显示排期数量:', localScheduledSongs.value.length)
// 更新已排期歌曲ID集合(包括草稿) // 更新已排期歌曲ID集合(包括草稿、跨日期暂存、公开排期)
const allScheduledSongs = [...publicSchedules.value, ...drafts.value, ...crossDateLocalPool.value, ...localScheduledSongs.value]
scheduledSongIds.value = new Set( scheduledSongIds.value = new Set(
[...publicSchedules.value, ...drafts.value] allScheduledSongs
.filter((s) => s.song && s.song.id) .filter((s) => s.song && s.song.id)
.map((s) => s.song.id) .map((s) => s.song.id)
) )
@@ -2055,21 +2103,18 @@ const saveSequence = async () => {
// 一键自动排入期望播出的歌曲 // 一键自动排入期望播出的歌曲
const autoScheduleExpected = () => { const autoScheduleExpected = () => {
let count = 0 let count = 0
const expectedSongs = [...allUnscheduledSongs.value].filter(song => {
// 严格按期望播出日期排期:如果歌曲有期望播出日期,必须匹配当前选择的日期 // 1. 收集所有待排期歌曲(不做严格的日期过滤,根据期望日期自动分配)
if (song.expectedPlayDate && song.expectedPlayDate !== selectedDate.value) { const pool = [...allUnscheduledSongs.value]
return false
}
return true
})
if (expectedSongs.length === 0) { if (pool.length === 0) {
showToast('当前没有待排期的歌曲', 'info') showToast('当前没有待排期的歌曲', 'info')
return return
} }
// 2. 排序(和之前保持一致:按策略)
if (autoScheduleStrategy.value === 'contributor-count') { if (autoScheduleStrategy.value === 'contributor-count') {
expectedSongs.sort((a, b) => { pool.sort((a, b) => {
const diffA = getSongContributorCount(b) - getSongContributorCount(a) const diffA = getSongContributorCount(b) - getSongContributorCount(a)
if (diffA !== 0) return diffA if (diffA !== 0) return diffA
const seqDiff = (a.expectedPlaySequence || 9999) - (b.expectedPlaySequence || 9999) const seqDiff = (a.expectedPlaySequence || 9999) - (b.expectedPlaySequence || 9999)
@@ -2077,45 +2122,62 @@ const autoScheduleExpected = () => {
return (b.voteCount || 0) - (a.voteCount || 0) return (b.voteCount || 0) - (a.voteCount || 0)
}) })
} else { } else {
expectedSongs.sort((a, b) => (a.expectedPlaySequence || 9999) - (b.expectedPlaySequence || 9999)) pool.sort((a, b) => (a.expectedPlaySequence || 9999) - (b.expectedPlaySequence || 9999))
} }
const baseId = Date.now() const baseId = Date.now()
const filteredPtId = selectedPlayTime.value ? parseInt(selectedPlayTime.value) : null const filteredPtId = selectedPlayTime.value ? parseInt(selectedPlayTime.value) : null
const playTimeSeqCounters = new Map() // 3. 初始化每个日期 × 时段的序列计数器
for (const s of localScheduledSongs.value) { // 结构: dateStr -> Map(playTimeIdStr -> seq)
const key = String(s.playTimeId || '') const dateSeqCounters = new Map()
playTimeSeqCounters.set(key, (playTimeSeqCounters.get(key) || 0) + 1) const initDateCounters = (dateStr) => {
if (!dateSeqCounters.has(dateStr)) {
const ptMap = new Map()
for (const s of localScheduledSongs.value) {
if (s.playDate !== dateStr) continue
const k = String(s.playTimeId || '')
ptMap.set(k, (ptMap.get(k) || 0) + 1)
}
dateSeqCounters.set(dateStr, ptMap)
}
return dateSeqCounters.get(dateStr)
} }
expectedSongs.forEach((song, idx) => { // 4. 处理歌曲,根据期望日期分配 playDate
const existingIndex = localScheduledSongs.value.findIndex((s) => s.song?.id === song.id) pool.forEach((song, idx) => {
if (existingIndex !== -1) return // 4.1 确定播出日期:优先期望日期,否则用当前 selectedDate
let targetPlayDate = (song.expectedPlayDate && String(song.expectedPlayDate).trim()) || selectedDate.value
// 4.2 确定播出时段:优先期望时段,其次是当前选中的时段视图
let targetPlayTimeId = null let targetPlayTimeId = null
if (song.preferredPlayTimeId != null) { if (song.preferredPlayTimeId != null) {
const matched = (playTimes.value || []).find(pt => String(pt.id) === String(song.preferredPlayTimeId)) const matched = (playTimes.value || []).find(pt => String(pt.id) === String(song.preferredPlayTimeId))
if (matched) targetPlayTimeId = matched.id if (matched) targetPlayTimeId = matched.id
} }
// 如果没有明确期望时段,但当前处于特定时段视图下,则排入该时段
if (targetPlayTimeId == null && filteredPtId != null) { if (targetPlayTimeId == null && filteredPtId != null) {
targetPlayTimeId = filteredPtId targetPlayTimeId = filteredPtId
} }
// 如果处于特定时段视图,但该歌曲的期望时段不是当前时段,则跳过不排入 // 4.3 如果处于特定时段视图,且该歌曲的时段不匹配(且非空),则跳过
if (filteredPtId != null && targetPlayTimeId != null && String(targetPlayTimeId) !== String(filteredPtId)) { if (filteredPtId != null && targetPlayTimeId != null && String(targetPlayTimeId) !== String(filteredPtId)) {
return return
} }
// 4.4 检查是否已经存在于已排期列表中
const existingIndex = localScheduledSongs.value.findIndex((s) => s.song?.id === song.id)
if (existingIndex !== -1) return
// 4.5 获取该日期下的计数器
const ptSeq = initDateCounters(targetPlayDate)
const ptKey = String(targetPlayTimeId || '') const ptKey = String(targetPlayTimeId || '')
const nextSeq = (playTimeSeqCounters.get(ptKey) || 0) + 1 const nextSeq = (ptSeq.get(ptKey) || 0) + 1
playTimeSeqCounters.set(ptKey, nextSeq) ptSeq.set(ptKey, nextSeq)
const newSchedule = { const newSchedule = {
id: baseId + idx, id: baseId + idx,
song: song, song: song,
playDate: selectedDate.value, playDate: targetPlayDate,
sequence: nextSeq, sequence: nextSeq,
playTimeId: targetPlayTimeId, playTimeId: targetPlayTimeId,
isNew: true, isNew: true,
@@ -2126,6 +2188,7 @@ const autoScheduleExpected = () => {
count++ count++
}) })
// 5. 按 playDate + playTime 顺序重排 sequence
const ptOrder = (id) => { const ptOrder = (id) => {
if (id == null || id === '') return 1e9 if (id == null || id === '') return 1e9
const pt = (playTimes.value || []).find(p => p.id === id) const pt = (playTimes.value || []).find(p => p.id === id)
@@ -2133,20 +2196,33 @@ const autoScheduleExpected = () => {
const [h, m] = pt.startTime.split(':').map(Number) const [h, m] = pt.startTime.split(':').map(Number)
return h * 60 + (m || 0) return h * 60 + (m || 0)
} }
localScheduledSongs.value.sort((a, b) => { // 5.1 按日期分组后再排序
const po = ptOrder(a.playTimeId) - ptOrder(b.playTimeId) const dateGroups = new Map()
if (po !== 0) return po for (const s of localScheduledSongs.value) {
return (a.sequence || 0) - (b.sequence || 0) const key = String(s.playDate || '')
}) if (!dateGroups.has(key)) dateGroups.set(key, [])
localScheduledSongs.value.forEach((item, idx) => { dateGroups.get(key).push(s)
item.sequence = idx + 1 }
})
const allSorted = []
const sortedDates = [...dateGroups.keys()].sort()
for (const d of sortedDates) {
const group = dateGroups.get(d)
group.sort((a, b) => {
const po = ptOrder(a.playTimeId) - ptOrder(b.playTimeId)
if (po !== 0) return po
return (a.sequence || 0) - (b.sequence || 0)
})
group.forEach((item, idx) => { item.sequence = idx + 1 })
allSorted.push(...group)
}
localScheduledSongs.value = allSorted
hasChanges.value = true hasChanges.value = true
const strategyLabel = const strategyLabel =
autoScheduleStrategy.value === 'contributor-count' ? '(按投稿人数优先)' : '' autoScheduleStrategy.value === 'contributor-count' ? '(按投稿人数优先)' : ''
const byPref = expectedSongs.filter(s => s.preferredPlayTimeId).length const byPref = pool.filter(s => s.preferredPlayTimeId || s.expectedPlayDate).length
const prefInfo = byPref ? `,其中 ${byPref} 首按期望播出时段排入` : '' const prefInfo = byPref ? `,其中 ${byPref} 首按期望日期/时段排入` : ''
showToast(`已自动排入 ${count} 首待排歌曲${strategyLabel}${prefInfo},请注意调整顺序后保存`, 'success') showToast(`已自动排入 ${count} 首待排歌曲${strategyLabel}${prefInfo},请注意调整顺序后保存`, 'success')
} }