feat(admin): 优化后台日期选择器和播出时段交互流程
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Successful in 2m15s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Successful in 9m18s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Successful in 6s

- AdminDatePicker: 新增可展开月历网格 + 6 个快捷日期跳转按钮
- AdminMultiDatePicker: 新增 8 种批量预设 + Shift 范围选择 + 月历网格
- PlayTimeManager: 新增 6 个校园广播快速模板 + 时段复制 + 批量启停
- ScheduleManager: 播出时段从下拉改为标签按钮组 + 增加统计卡片
- RequestTimeManager: datetime-local 拆分为日期+时间双输入 + 6 种快捷预设
This commit is contained in:
2026-09-13 02:04:48 +08:00
parent a481e0ae4c
commit 19ab33b102
5 changed files with 839 additions and 176 deletions
+147 -13
View File
@@ -291,26 +291,64 @@
>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between px-1">
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest">快捷时间填充</label>
<span class="text-[10px] font-bold text-zinc-500">点击可一键套用常见时段</span>
</div>
<div class="flex flex-wrap gap-2">
<button
v-for="p in requestTimeQuickPresets"
:key="p.label"
type="button"
class="px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-xl text-[11px] font-bold text-zinc-400 hover:text-zinc-200 hover:border-zinc-700 hover:bg-zinc-900 transition-all whitespace-nowrap"
@click="applyRequestTimePreset(p)"
>
{{ p.label }}
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>开始日期时间</label
>
<input
v-model="formData.startTime"
type="datetime-local"
class="w-full bg-zinc-950 border border-zinc-800 rounded-2xl px-5 py-3.5 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/30"
>开始时间</label
>
<div class="grid grid-cols-2 gap-2">
<input
v-model="formData.startDate"
type="date"
class="w-full bg-zinc-950 border border-zinc-800 rounded-2xl px-4 py-3.5 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
<input
v-model="formData.startTimeOfDay"
type="time"
class="w-full bg-zinc-950 border border-zinc-800 rounded-2xl px-4 py-3.5 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
</div>
<p class="text-[9px] text-zinc-600 px-1">
实际值: {{ formData.startTime || '尚未设置' }}
</p>
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>结束日期时间</label
>
<input
v-model="formData.endTime"
type="datetime-local"
class="w-full bg-zinc-950 border border-zinc-800 rounded-2xl px-5 py-3.5 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/30"
>结束时间</label
>
<div class="grid grid-cols-2 gap-2">
<input
v-model="formData.endDate"
type="date"
class="w-full bg-zinc-950 border border-zinc-800 rounded-2xl px-4 py-3.5 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
<input
v-model="formData.endTimeOfDay"
type="time"
class="w-full bg-zinc-950 border border-zinc-800 rounded-2xl px-4 py-3.5 text-sm text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
</div>
<p class="text-[9px] text-zinc-600 px-1">
实际值: {{ formData.endTime || '尚未设置' }}
</p>
</div>
</div>
@@ -502,7 +540,7 @@
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref, computed } from 'vue'
import { onMounted, onUnmounted, reactive, ref, computed, watch } from 'vue'
import { useAuth } from '~/composables/useAuth'
import type { RequestTime } from '~~/types'
import {
@@ -521,6 +559,57 @@ import {
Filter
} from '@lucide/vue'
const addMinutes = (d: Date, mins: number) => {
const r = new Date(d); r.setMinutes(r.getMinutes() + mins); return r
}
const addHours = (d: Date, h: number) => addMinutes(d, h * 60)
const addDays = (d: Date, n: number) => {
const r = new Date(d); r.setDate(r.getDate() + n); return r
}
const nextWeekday = (base: Date, target: number) => {
const r = new Date(base)
const diff = (target - r.getDay() + 7) % 7 || 7
r.setDate(r.getDate() + diff)
return r
}
const pad = (n: number) => n.toString().padStart(2, '0')
const fmt = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
const splitDT = (dt: string): [string, string] => {
if (!dt || !dt.includes('T')) return ['', '']
const [d, t] = dt.split('T')
return [d, t.slice(0, 5)]
}
const requestTimeQuickPresets = [
{ label: '立即开放 2 小时', apply: () => {
const n = new Date()
return { startTime: addMinutes(n, 5), endTime: addHours(n, 2) }
}},
{ label: '今晚 18:00-22:00', apply: () => {
const t = new Date(); t.setHours(18, 0, 0, 0)
return { startTime: t, endTime: addHours(t, 4) }
}},
{ label: '明天 12:00-14:00', apply: () => {
const t = addDays(new Date(), 1); t.setHours(12, 0, 0, 0)
return { startTime: t, endTime: addHours(t, 2) }
}},
{ label: '下周一至周五', apply: () => {
const s = nextWeekday(new Date(), 1); s.setHours(0, 5, 0, 0)
const e = addDays(s, 4); e.setHours(23, 59, 0, 0)
return { startTime: s, endTime: e }
}},
{ label: '下周整周 7 天', apply: () => {
const s = nextWeekday(new Date(), 1); s.setHours(0, 5, 0, 0)
const e = addDays(s, 6); e.setHours(23, 59, 0, 0)
return { startTime: s, endTime: e }
}},
{ label: '持续开放 30 天', apply: () => {
const s = addMinutes(new Date(), 5)
const e = addDays(s, 30); e.setHours(23, 59, 0, 0)
return { startTime: s, endTime: e }
}}
]
const { getAuthConfig, isAdmin } = useAuth()
const requestTimes = ref<RequestTime[]>([])
@@ -544,11 +633,51 @@ const formData = reactive({
name: '',
startTime: '',
endTime: '',
startDate: '',
startTimeOfDay: '',
endDate: '',
endTimeOfDay: '',
description: '',
enabled: true,
expected: 0
})
// 日期+时间分离字段 <-> 合并的 datetime-local 字段双向同步
const syncStartFromParts = () => {
if (formData.startDate && formData.startTimeOfDay) {
formData.startTime = `${formData.startDate}T${formData.startTimeOfDay}`
} else if (!formData.startDate && !formData.startTimeOfDay) {
formData.startTime = ''
}
}
const syncEndFromParts = () => {
if (formData.endDate && formData.endTimeOfDay) {
formData.endTime = `${formData.endDate}T${formData.endTimeOfDay}`
} else if (!formData.endDate && !formData.endTimeOfDay) {
formData.endTime = ''
}
}
watch(() => formData.startDate, syncStartFromParts)
watch(() => formData.startTimeOfDay, syncStartFromParts)
watch(() => formData.endDate, syncEndFromParts)
watch(() => formData.endTimeOfDay, syncEndFromParts)
// 将 startTime/endTime 拆分到辅助字段(编辑时调用)
const splitFormDateTime = () => {
const [sd, st] = splitDT(formData.startTime)
formData.startDate = sd; formData.startTimeOfDay = st
const [ed, et] = splitDT(formData.endTime)
formData.endDate = ed; formData.endTimeOfDay = et
}
// 应用快捷时间预设
const applyRequestTimePreset = (p: any) => {
const { startTime, endTime } = p.apply()
formData.startTime = fmt(startTime)
formData.endTime = fmt(endTime)
splitFormDateTime()
}
// 统计数据
const stats = computed(() => [
{
@@ -711,6 +840,7 @@ const editRequestTime = (RequestTime: RequestTime) => {
enabled: RequestTime.enabled,
expected: RequestTime.expected || 0
})
splitFormDateTime()
}
const toggleRequestTimeStatus = async (RequestTime: RequestTime) => {
@@ -827,6 +957,10 @@ const cancelForm = () => {
name: '',
startTime: '',
endTime: '',
startDate: '',
startTimeOfDay: '',
endDate: '',
endTimeOfDay: '',
description: '',
enabled: true,
expected: 0