Files
BYW-VoiceHub/app/components/Admin/AdminMultiDatePicker.vue
T
Henry_Du c4c30de416 feat(admin): 恢复按日期生成排期图片,重构多项体验问题
- **导入酷狗功能恢复**:废弃纯文本解析,重写为通过多选日期加载对应排期,并支持“根据日期分别生成图片”的场景。
- **浅色打印主题**:在 SchedulePrinter 和 ScheduleItemPrint 中应用 !important 强制白底黑字,消除系统深色模式对预览与打印效果的干扰。
- **日期选择器精简**:彻底删除 AdminDatePicker 和 AdminMultiDatePicker 中的横向日期盘滚动功能,改用直观的选定日期展示,配合月历面板实现更稳定快捷的选择。
- **自动排入逻辑修复**:严格过滤并匹配歌曲的 `expectedPlayDate` 和 `preferredPlayTimeId`,修复无期望播出时间的歌曲被错误填入的问题。
2026-09-13 15:49:05 +08:00

327 lines
11 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-2xl 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 gap-3">
<div class="flex flex-col">
<span class="text-sm font-bold text-zinc-200">
已选择 {{ modelValue.length }} 天
</span>
<span class="text-xs text-zinc-500 font-medium truncate max-w-[200px]">
{{ modelValue.join(', ') || '未选择日期' }}
</span>
</div>
</div>
<div class="flex items-center border-l border-zinc-800 pl-3">
<button class="p-2 text-zinc-500 hover:text-emerald-400 transition-colors" title="跳转到今天" @click.stop="scrollToToday">
<CircleDot class="w-5 h-5" />
</button>
<button
class="p-2 text-zinc-500 hover:text-blue-400 transition-colors"
:class="{ 'text-blue-400': showCalendar }"
title="打开月历"
>
<CalendarIcon class="w-5 h-5" />
</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 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>