Files
BYW-VoiceHub/app/components/Admin/AdminMultiDatePicker.vue
T
Henry_Du 37fd18aaea fix(admin): 修复日期选择框尺寸过大与自动排入跨日期逻辑
- **日期选择器尺寸优化**:AdminDatePicker 与 AdminMultiDatePicker 采用 w-9 h-9 的紧凑卡片,缩小字号、边距与圆角,贴合后台窄布局。
- **自动排入逻辑重构**:取消严格的 `expectedPlayDate === selectedDate` 过滤,改为按歌曲期望日期/时段自动分配 `playDate` 与 `playTimeId`;其他歌曲排到当前选中日期。
- **跨日期本地暂存池**:引入 `crossDateLocalPool` 保存尚未保存但已排入其他日期的临时排期,切换 `selectedDate` 或时段时正确回填对应日期的草稿,避免自动排入后切换视图导致丢失。
- **排期列表去重与过滤**:时段过滤逻辑修复 `playTimeId == null` 的边界,并在合并公开/草稿/本地暂存时按优先级去重。
2026-09-13 16:18:09 +08:00

343 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<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-2.5 py-1.5" @click="toggleCalendar" style="cursor: pointer">
<div class="flex items-center gap-2.5 min-w-0 flex-1">
<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-[9px] font-bold uppercase tracking-wider opacity-80 leading-tight">
{{ modelValue.length }}
</span>
<span class="text-[10px] font-bold leading-none opacity-80">天</span>
</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>
</div>
</div>
<div class="flex items-center border-l border-zinc-800 pl-2 ml-2 shrink-0">
<button class="p-1.5 text-zinc-500 hover:text-emerald-400 transition-colors" title="跳转到今天" @click.stop="scrollToToday">
<CircleDot class="w-4 h-4" />
</button>
<button
class="p-1.5 text-zinc-500 hover:text-blue-400 transition-colors"
:class="{ 'text-blue-400': showCalendar }"
title="打开月历"
>
<CalendarIcon class="w-4 h-4" />
</button>
</div>
</div>
<!-- 扩展面板:快捷批量 + 月历 -->
<Transition name="expand">
<div v-if="showCalendar" class="border-t border-zinc-800/60 mt-1 pt-2 px-2 pb-2 space-y-3">
<!-- 批量操作按钮 -->
<div class="flex flex-wrap items-center gap-2">
<span class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1">批量选择</span>
<button
v-for="b in batchActions"
:key="b.label"
class="px-2.5 py-1.5 rounded-lg text-[11px] font-bold bg-zinc-900 border border-zinc-800 text-zinc-400 hover:text-zinc-200 hover:border-zinc-700 transition-all"
@click="applyBatchAction(b.action)"
>
{{ b.label }}
</button>
<div class="w-px h-4 bg-zinc-800 mx-1" />
<button
class="px-2.5 py-1.5 rounded-lg text-[11px] font-bold bg-red-500/10 border border-red-500/20 text-red-400 hover:text-red-300 hover:bg-red-500/15 transition-all"
@click="clearAll"
>
清空所有
</button>
<div class="ml-auto flex items-center gap-2 text-[11px] font-bold text-zinc-500">
<Info :size="12" />
<span>已选 {{ modelValue.length }} 天</span>
<span class="text-zinc-700">·</span>
<span class="text-zinc-600">按住 Shift 点选可范围选择</span>
</div>
</div>
<!-- 月历网格 -->
<div class="bg-zinc-950/60 border border-zinc-800 rounded-xl p-3">
<div class="flex items-center justify-between mb-3 px-1">
<button
class="p-1.5 text-zinc-500 hover:text-zinc-200 rounded-lg hover:bg-zinc-800 transition-colors"
@click="calendarMonth--"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="15,18 9,12 15,6" /></svg>
</button>
<div class="flex items-center gap-2">
<span class="text-sm font-black text-zinc-200">{{ calendarYear }}年</span>
<select
v-model="calendarMonth"
class="bg-zinc-900 border border-zinc-800 rounded-lg px-2 py-1 text-sm font-black text-zinc-200 focus:outline-none focus:border-blue-500/50"
>
<option v-for="m in 12" :key="m" :value="m - 1">{{ m }}月</option>
</select>
</div>
<div class="flex items-center gap-1">
<button
class="p-1.5 text-zinc-500 hover:text-zinc-200 rounded-lg hover:bg-zinc-800 transition-colors"
@click="goToday"
title="回到本月"
>
<CircleDot class="w-3.5 h-3.5" />
</button>
<button
class="p-1.5 text-zinc-500 hover:text-zinc-200 rounded-lg hover:bg-zinc-800 transition-colors"
@click="calendarMonth++"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="9,18 15,12 9,6" /></svg>
</button>
</div>
</div>
<div class="grid grid-cols-7 mb-1">
<div
v-for="w in weekdayNames"
:key="w"
class="text-center text-[10px] font-black text-zinc-600 uppercase tracking-widest py-1.5"
>
{{ w }}
</div>
</div>
<div class="grid grid-cols-7 gap-1">
<button
v-for="(cell, idx) in calendarCells"
:key="idx"
:disabled="!cell.inMonth"
:class="[
'relative aspect-square rounded-lg flex items-center justify-center text-sm font-bold transition-all',
!cell.inMonth ? 'text-zinc-800 cursor-default' : '',
cell.inMonth && !isSelected(cell.dateStr) && !cell.isToday
? 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200' : '',
cell.inMonth && !isSelected(cell.dateStr) && cell.isToday
? 'text-emerald-400 ring-1 ring-emerald-500/40 hover:bg-zinc-800' : '',
isSelected(cell.dateStr)
? 'bg-blue-600 text-white shadow-lg shadow-blue-900/20' : '',
cell.isRangeMiddle && !isSelected(cell.dateStr)
? 'bg-blue-500/10 text-blue-300' : ''
]"
@click="cell.inMonth && toggleDate(cell.dateStr, $event)"
>
{{ cell.day }}
<svg
v-if="isSelected(cell.dateStr)"
class="absolute top-0.5 right-0.5 w-2.5 h-2.5 text-white"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" />
</svg>
</button>
</div>
</div>
</div>
</Transition>
</div>
</template>
<script setup>
import { ref, computed, onMounted, nextTick } from 'vue'
import { CircleDot, Calendar as CalendarIcon, Info } from '@lucide/vue'
const props = defineProps({
modelValue: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const showCalendar = ref(false)
const lastClickedDate = ref('')
const pad = (n) => n.toString().padStart(2, '0')
const formatDateStr = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
const addDays = (d, n) => { const x = new Date(d); x.setHours(0, 0, 0, 0); x.setDate(x.getDate() + n); return x }
const sameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
const sortDates = (arr) => [...new Set(arr)].sort()
const today = new Date(); today.setHours(0, 0, 0, 0)
const calendarYear = ref(today.getFullYear())
const calendarMonth = ref(today.getMonth())
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 = [
{ label: '今天', action: 'today' },
{ label: '明天起7天', action: 'next7' },
{ label: '本周工作日', action: 'thisWeekWork' },
{ label: '本周全周', action: 'thisWeekAll' },
{ label: '下周工作日', action: 'nextWeekWork' },
{ label: '下周全周', action: 'nextWeekAll' },
{ label: '本月工作日', action: 'thisMonthWork' }
]
const calendarCells = computed(() => {
const cells = []
const firstDay = new Date(calendarYear.value, calendarMonth.value, 1)
const startWeekday = firstDay.getDay()
const startDate = addDays(firstDay, -startWeekday)
for (let i = 0; i < 42; i++) {
const d = addDays(startDate, i)
cells.push({
day: d.getDate(),
dateStr: formatDateStr(d),
inMonth: d.getMonth() === calendarMonth.value,
isToday: sameDay(d, today)
})
}
return cells
})
const isSelected = (d) => props.modelValue.includes(d)
const commitChange = (newArr) => {
const sorted = sortDates(newArr)
emit('update:modelValue', sorted)
emit('change', sorted)
}
const toggleDate = (dateValue, event) => {
const newValue = [...props.modelValue]
if (event && event.shiftKey && lastClickedDate.value && !newValue.includes(dateValue)) {
const a = new Date(lastClickedDate.value)
const b = new Date(dateValue)
const [s, e] = a < b ? [a, b] : [b, a]
for (let d = new Date(s); d <= e; d.setDate(d.getDate() + 1)) {
const ds = formatDateStr(d)
if (!newValue.includes(ds)) newValue.push(ds)
}
} else {
const index = newValue.indexOf(dateValue)
if (index > -1) {
newValue.splice(index, 1)
} else {
newValue.push(dateValue)
}
lastClickedDate.value = dateValue
}
commitChange(newValue)
}
const clearAll = () => commitChange([])
const applyBatchAction = (action) => {
const t = new Date(); t.setHours(0, 0, 0, 0)
const toAdd = []
const pushRange = (start, end) => {
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
toAdd.push(formatDateStr(d))
}
}
const getWeekStart = (base) => {
const r = new Date(base); r.setHours(0, 0, 0, 0)
r.setDate(r.getDate() - r.getDay())
return r
}
switch (action) {
case 'today':
toAdd.push(formatDateStr(t))
break
case 'next7':
pushRange(addDays(t, 1), addDays(t, 7))
break
case 'thisWeekWork': {
const wkStart = getWeekStart(t)
for (let i = 1; i <= 5; i++) toAdd.push(formatDateStr(addDays(wkStart, i)))
break
}
case 'thisWeekAll': {
const wkStart = getWeekStart(t)
pushRange(wkStart, addDays(wkStart, 6))
break
}
case 'nextWeekWork': {
const wkStart = addDays(getWeekStart(t), 7)
for (let i = 1; i <= 5; i++) toAdd.push(formatDateStr(addDays(wkStart, i)))
break
}
case 'nextWeekAll': {
const wkStart = addDays(getWeekStart(t), 7)
pushRange(wkStart, addDays(wkStart, 6))
break
}
case 'thisMonthWork': {
const ym = new Date(t.getFullYear(), t.getMonth(), 1)
const end = new Date(t.getFullYear(), t.getMonth() + 1, 0)
for (let d = new Date(ym); d <= end; d.setDate(d.getDate() + 1)) {
if (d.getDay() >= 1 && d.getDay() <= 5) toAdd.push(formatDateStr(d))
}
break
}
}
const merged = [...new Set([...props.modelValue, ...toAdd])]
commitChange(merged)
}
const scrollToToday = () => {
const todayStr = formatDateStr(today)
if (!props.modelValue.includes(todayStr)) {
toggleDate(todayStr, { shiftKey: false })
}
}
const toggleCalendar = () => {
showCalendar.value = !showCalendar.value
if (showCalendar.value && props.modelValue.length > 0) {
const d = new Date(props.modelValue[props.modelValue.length - 1])
calendarYear.value = d.getFullYear()
calendarMonth.value = d.getMonth()
}
}
const goToday = () => {
calendarYear.value = today.getFullYear()
calendarMonth.value = today.getMonth()
}
</script>
<style scoped>
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
.expand-enter-active, .expand-leave-active {
transition: all 0.25s ease;
overflow: hidden;
}
.expand-enter-from, .expand-leave-to {
opacity: 0;
max-height: 0;
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
}
.expand-enter-to, .expand-leave-from {
max-height: 800px;
}
</style>