541 lines
21 KiB
Vue
541 lines
21 KiB
Vue
<template>
|
||
<div class="space-y-6">
|
||
<div class="flex justify-between items-center">
|
||
<h3 class="text-lg font-bold text-zinc-100 flex items-center gap-2">
|
||
<CalendarDays class="w-5 h-5 text-blue-400" />
|
||
期望播出时段设置
|
||
</h3>
|
||
<button
|
||
class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-bold transition-colors"
|
||
@click="openAddModal"
|
||
>
|
||
新增开放日期
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="loading" class="text-center py-10">
|
||
<Loader2 class="w-6 h-6 animate-spin mx-auto text-zinc-500" />
|
||
</div>
|
||
|
||
<div v-else-if="dates.length === 0" class="text-center py-10 text-zinc-500 bg-zinc-900/50 rounded-xl border border-zinc-800">
|
||
暂未设置任何开放日期
|
||
</div>
|
||
|
||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
<div v-for="date in dates" :key="date.id" class="bg-zinc-900 border border-zinc-800 rounded-xl p-4 flex flex-col gap-3">
|
||
<div class="flex justify-between items-start">
|
||
<div class="font-bold text-zinc-100 flex items-center gap-2">
|
||
{{ date.date }}
|
||
<span class="text-xs font-normal text-zinc-500 bg-zinc-800 px-2 py-0.5 rounded">{{ date.weekday }}</span>
|
||
</div>
|
||
<button @click="openDeleteDialog(date)" class="text-red-400 hover:text-red-300 p-1 transition-colors" title="删除">
|
||
<Trash2 class="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<div class="flex flex-col gap-2.5 bg-zinc-950/50 p-3 rounded-lg border border-zinc-800/50">
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-xs text-zinc-500">开放范围</div>
|
||
<div class="text-xs font-mono text-blue-400 font-medium break-all text-right ml-4">{{ formatSequences(date.availableSequences) }}</div>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-xs text-zinc-500">序号状态</div>
|
||
<div class="text-xs flex gap-2">
|
||
<span class="text-emerald-500 bg-emerald-500/10 px-1.5 py-0.5 rounded border border-emerald-500/20">空闲 {{ getAvailableCount(date) }}</span>
|
||
<span class="text-zinc-400 bg-zinc-800 px-1.5 py-0.5 rounded border border-zinc-700">共 {{ getTotalCount(date) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增详细占用情况列表 -->
|
||
<div v-if="date.takenSequences && date.takenSequences.length > 0" class="mt-1 pt-3 border-t border-zinc-800">
|
||
<div class="text-xs font-bold text-zinc-500 mb-2 flex items-center justify-between">
|
||
<span>已占用明细</span>
|
||
<span class="text-red-400">{{ date.takenSequences.length }} 个</span>
|
||
</div>
|
||
<div class="space-y-1.5 max-h-32 overflow-y-auto pr-1 custom-scrollbar">
|
||
<div
|
||
v-for="seq in date.takenSequences"
|
||
:key="seq"
|
||
class="flex items-center gap-2 text-[11px]"
|
||
>
|
||
<span class="w-5 h-5 rounded flex items-center justify-center bg-red-500/10 text-red-500 font-bold border border-red-500/20 shrink-0">{{ seq }}</span>
|
||
<span class="text-zinc-300 truncate" :title="date.takenDetails?.[seq]?.title">{{ date.takenDetails?.[seq]?.title || '未知歌曲' }}</span>
|
||
<span class="text-zinc-500 truncate" :title="date.takenDetails?.[seq]?.artist">- {{ date.takenDetails?.[seq]?.artist || '未知歌手' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="mt-2 pt-3 border-t border-zinc-800 text-xs text-emerald-500/70 font-bold">
|
||
所有序号目前空闲
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增/编辑弹窗 -->
|
||
<div v-if="showModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||
<div class="bg-zinc-900 border border-zinc-800 rounded-xl shadow-2xl w-full max-w-lg p-6">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h3 class="text-lg font-bold text-zinc-100">新增开放日期</h3>
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="flex p-1 bg-zinc-950 rounded-lg mb-6">
|
||
<button
|
||
@click="activeTab = 'auto'"
|
||
:class="['flex-1 py-2 text-xs font-bold rounded-md transition-colors', activeTab === 'auto' ? 'bg-blue-600 text-white' : 'text-zinc-500 hover:text-zinc-300']"
|
||
>
|
||
批量自动分配
|
||
</button>
|
||
<button
|
||
@click="activeTab = 'manual'; nextTick(() => { setTimeout(() => { setStartSeq(manualFormData.startSeq); setEndSeq(manualFormData.endSeq) }, 100) })"
|
||
:class="['flex-1 py-2 text-xs font-bold rounded-md transition-colors', activeTab === 'manual' ? 'bg-blue-600 text-white' : 'text-zinc-500 hover:text-zinc-300']"
|
||
>
|
||
手动配置单日
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 自动分配表单 -->
|
||
<div v-if="activeTab === 'auto'" class="space-y-4">
|
||
<div>
|
||
<label class="block text-xs font-bold text-zinc-500 mb-2">选择播出日期 (可多选)</label>
|
||
<AdminMultiDatePicker v-model="autoFormData.dates" />
|
||
<p class="text-[10px] text-zinc-600 mt-1">已选择 {{ autoFormData.dates.length }} 天</p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-zinc-500 mb-1">总序号数量</label>
|
||
<input type="number" min="1" v-model="autoFormData.totalSequences" class="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-zinc-200">
|
||
<div class="flex items-center gap-2 mt-2">
|
||
<button @click="autoFormData.totalSequences = 21" class="text-[10px] px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded transition-colors">21</button>
|
||
<button @click="autoFormData.totalSequences = 28" class="text-[10px] px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded transition-colors">28</button>
|
||
<button @click="autoFormData.totalSequences = 35" class="text-[10px] px-2 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded transition-colors">35</button>
|
||
</div>
|
||
</div>
|
||
<div class="p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
|
||
<p class="text-xs text-blue-400 font-bold mb-1">自动分配预览:</p>
|
||
<p class="text-[10px] text-blue-300/80">
|
||
<span v-if="autoFormData.dates.length === 0">请先选择日期</span>
|
||
<span v-else>将会把 1 - {{ autoFormData.totalSequences }} 号平均分配到选中的 {{ autoFormData.dates.length }} 天中。每天约 {{ Math.ceil(autoFormData.totalSequences / autoFormData.dates.length) }} 个序号。</span>
|
||
</p>
|
||
</div>
|
||
<div class="flex gap-3 pt-4">
|
||
<button @click="showModal = false" class="flex-1 py-2 bg-zinc-800 text-zinc-300 rounded-lg text-sm font-bold">取消</button>
|
||
<button @click="saveAuto" :disabled="saving || autoFormData.dates.length === 0" class="flex-1 py-2 bg-blue-600 text-white rounded-lg text-sm font-bold disabled:opacity-50">批量保存</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 手动配置表单 -->
|
||
<div v-else class="space-y-4">
|
||
<div>
|
||
<label class="block text-xs font-bold text-zinc-500 mb-2">选择日期 (单选)</label>
|
||
<AdminDatePicker v-model="manualFormData.date" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-zinc-500 mb-1">开放的空闲序号</label>
|
||
|
||
<div class="flex flex-col items-center bg-zinc-950 border border-zinc-800 rounded-xl p-4">
|
||
<!-- 模拟 iOS 滚轮 (这里用简化的网格选择代替复杂的滚轮组件以保证稳定性和易用性) -->
|
||
<div class="text-sm font-bold text-zinc-400 mb-3 w-full flex justify-between items-center">
|
||
<span>选择开放范围</span>
|
||
<span class="text-blue-400">{{ manualFormData.startSeq }} - {{ manualFormData.endSeq }}</span>
|
||
</div>
|
||
|
||
<div class="flex gap-4 items-center w-full justify-center py-4">
|
||
<div class="flex flex-col items-center flex-1">
|
||
<span class="text-[10px] text-zinc-500 mb-2">起始序号</span>
|
||
<div ref="startScrollRef" class="relative w-full h-32 bg-zinc-900 border border-zinc-700 rounded-lg overflow-hidden snap-y snap-mandatory hide-scrollbar" style="scroll-behavior: smooth;" @scroll="handleStartScroll">
|
||
<div class="absolute top-1/2 left-0 right-0 h-10 -mt-5 bg-blue-500/10 border-y border-blue-500/30 pointer-events-none z-10"></div>
|
||
<div class="h-[44px]"></div> <!-- Padding top -->
|
||
<div
|
||
v-for="n in 100"
|
||
:key="'start-'+n"
|
||
class="h-10 flex items-center justify-center text-lg font-bold snap-center cursor-pointer transition-colors"
|
||
:class="manualFormData.startSeq === n ? 'text-blue-400' : 'text-zinc-500 hover:text-zinc-300'"
|
||
@click="setStartSeq(n)"
|
||
:data-val="n"
|
||
>
|
||
{{ n }}
|
||
</div>
|
||
<div class="h-[44px]"></div> <!-- Padding bottom -->
|
||
</div>
|
||
</div>
|
||
|
||
<span class="text-zinc-500 font-bold">至</span>
|
||
|
||
<div class="flex flex-col items-center flex-1">
|
||
<span class="text-[10px] text-zinc-500 mb-2">结束序号</span>
|
||
<div ref="endScrollRef" class="relative w-full h-32 bg-zinc-900 border border-zinc-700 rounded-lg overflow-hidden snap-y snap-mandatory hide-scrollbar" style="scroll-behavior: smooth;" @scroll="handleEndScroll">
|
||
<div class="absolute top-1/2 left-0 right-0 h-10 -mt-5 bg-blue-500/10 border-y border-blue-500/30 pointer-events-none z-10"></div>
|
||
<div class="h-[44px]"></div> <!-- Padding top -->
|
||
<div
|
||
v-for="n in 100"
|
||
:key="'end-'+n"
|
||
class="h-10 flex items-center justify-center text-lg font-bold snap-center cursor-pointer transition-colors"
|
||
:class="manualFormData.endSeq === n ? 'text-blue-400' : 'text-zinc-500 hover:text-zinc-300'"
|
||
@click="setEndSeq(n)"
|
||
:data-val="n"
|
||
>
|
||
{{ n }}
|
||
</div>
|
||
<div class="h-[44px]"></div> <!-- Padding bottom -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 如果有特定跳过的序号,保留文本框以供进阶编辑 -->
|
||
<div class="w-full mt-4">
|
||
<div
|
||
class="flex justify-between items-center text-[10px] text-zinc-500 cursor-pointer py-2 hover:text-zinc-300 transition-colors"
|
||
@click="showAdvancedManual = !showAdvancedManual"
|
||
>
|
||
<span>进阶:自定义跳过某些序号</span>
|
||
<span :class="{'rotate-180': showAdvancedManual}" class="transition-transform">▼</span>
|
||
</div>
|
||
|
||
<div v-if="showAdvancedManual" class="mt-2">
|
||
<textarea v-model="manualFormData.sequences" rows="2" placeholder="例如: 1,2,3,5 (跳过了4)" class="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-xs text-zinc-300 resize-none focus:outline-none focus:border-blue-500"></textarea>
|
||
<p class="text-[10px] text-zinc-600 mt-1">手动编辑后将以此处文本框内容为准</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="text-[10px] text-zinc-600 mt-2">注意:手动配置会覆盖该日期原有的设置</p>
|
||
</div>
|
||
<div class="flex gap-3 pt-4">
|
||
<button @click="showModal = false" class="flex-1 py-2 bg-zinc-800 text-zinc-300 rounded-lg text-sm font-bold">取消</button>
|
||
<button @click="saveManual" :disabled="saving || !manualFormData.date" class="flex-1 py-2 bg-blue-600 text-white rounded-lg text-sm font-bold disabled:opacity-50">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<ConfirmDialog
|
||
:show="showDeleteDialog"
|
||
title="确认删除"
|
||
:message="deleteDialogMessage"
|
||
type="danger"
|
||
confirm-text="确认删除"
|
||
cancel-text="取消"
|
||
:loading="deleting"
|
||
@confirm="confirmDelete"
|
||
@update:show="showDeleteDialog = $event"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, nextTick, computed } from 'vue'
|
||
import { CalendarDays, Trash2, Loader2 } from '@lucide/vue'
|
||
import AdminMultiDatePicker from './AdminMultiDatePicker.vue'
|
||
import AdminDatePicker from './AdminDatePicker.vue'
|
||
import ConfirmDialog from '~/components/UI/ConfirmDialog.vue'
|
||
|
||
import { useAuth } from '~/composables/useAuth'
|
||
|
||
const auth = useAuth()
|
||
const dates = ref([])
|
||
const loading = ref(true)
|
||
const showModal = ref(false)
|
||
const saving = ref(false)
|
||
const activeTab = ref('auto')
|
||
const showAdvancedManual = ref(false)
|
||
const showDeleteDialog = ref(false)
|
||
const deleteTarget = ref(null)
|
||
const deleting = ref(false)
|
||
|
||
const deleteDialogMessage = computed(() => {
|
||
if (!deleteTarget.value) return '确定要删除该开放日期吗?此操作不可撤销。'
|
||
const dateStr = deleteTarget.value.date || ''
|
||
return `确定要删除开放日期 ${dateStr} 吗?此操作不可撤销。`
|
||
})
|
||
|
||
const formatSequences = (seqStr) => {
|
||
if (!seqStr) return '无'
|
||
const seqs = seqStr.split(',').map(Number).filter(n => !isNaN(n)).sort((a, b) => a - b)
|
||
if (seqs.length === 0) return '无'
|
||
|
||
const ranges = []
|
||
let start = seqs[0]
|
||
let prev = seqs[0]
|
||
|
||
for (let i = 1; i < seqs.length; i++) {
|
||
if (seqs[i] === prev + 1) {
|
||
prev = seqs[i]
|
||
} else {
|
||
ranges.push(start === prev ? `${start}` : `${start}-${prev}`)
|
||
start = seqs[i]
|
||
prev = seqs[i]
|
||
}
|
||
}
|
||
ranges.push(start === prev ? `${start}` : `${start}-${prev}`)
|
||
|
||
return ranges.join(', ')
|
||
}
|
||
|
||
const getTotalCount = (date) => {
|
||
return date.availableSequences ? date.availableSequences.split(',').filter(Boolean).length : 0
|
||
}
|
||
|
||
const getTakenCount = (date) => {
|
||
return date.takenSequences ? date.takenSequences.length : 0
|
||
}
|
||
|
||
const getAvailableCount = (date) => {
|
||
return Math.max(0, getTotalCount(date) - getTakenCount(date))
|
||
}
|
||
|
||
const startScrollRef = ref(null)
|
||
const endScrollRef = ref(null)
|
||
let scrollTimeoutStart = null
|
||
let scrollTimeoutEnd = null
|
||
|
||
const autoFormData = ref({
|
||
dates: [],
|
||
totalSequences: 28
|
||
})
|
||
|
||
const manualFormData = ref({
|
||
date: new Date().toISOString().split('T')[0],
|
||
startSeq: 1,
|
||
endSeq: 28,
|
||
sequences: ''
|
||
})
|
||
|
||
const fetchDates = async () => {
|
||
loading.value = true
|
||
try {
|
||
const res = await $fetch('/api/admin/expected-play-dates', {
|
||
...auth.getAuthConfig(),
|
||
query: { t: Date.now() }
|
||
})
|
||
dates.value = res
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const setStartSeq = (val) => {
|
||
manualFormData.value.startSeq = val
|
||
if (startScrollRef.value) {
|
||
const el = startScrollRef.value.querySelector(`[data-val="${val}"]`)
|
||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||
}
|
||
}
|
||
|
||
const setEndSeq = (val) => {
|
||
manualFormData.value.endSeq = val
|
||
if (endScrollRef.value) {
|
||
const el = endScrollRef.value.querySelector(`[data-val="${val}"]`)
|
||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||
}
|
||
}
|
||
|
||
const handleStartScroll = (e) => {
|
||
clearTimeout(scrollTimeoutStart)
|
||
scrollTimeoutStart = setTimeout(() => {
|
||
if (!startScrollRef.value) return
|
||
const container = startScrollRef.value
|
||
const center = container.scrollTop + container.clientHeight / 2
|
||
const items = Array.from(container.querySelectorAll('[data-val]'))
|
||
let closest = items[0]
|
||
let minDiff = Infinity
|
||
|
||
items.forEach(item => {
|
||
const itemCenter = item.offsetTop + item.clientHeight / 2
|
||
const diff = Math.abs(center - itemCenter)
|
||
if (diff < minDiff) {
|
||
minDiff = diff
|
||
closest = item
|
||
}
|
||
})
|
||
|
||
if (closest) {
|
||
const val = parseInt(closest.getAttribute('data-val'))
|
||
manualFormData.value.startSeq = val
|
||
}
|
||
}, 150)
|
||
}
|
||
|
||
const handleEndScroll = (e) => {
|
||
clearTimeout(scrollTimeoutEnd)
|
||
scrollTimeoutEnd = setTimeout(() => {
|
||
if (!endScrollRef.value) return
|
||
const container = endScrollRef.value
|
||
const center = container.scrollTop + container.clientHeight / 2
|
||
const items = Array.from(container.querySelectorAll('[data-val]'))
|
||
let closest = items[0]
|
||
let minDiff = Infinity
|
||
|
||
items.forEach(item => {
|
||
const itemCenter = item.offsetTop + item.clientHeight / 2
|
||
const diff = Math.abs(center - itemCenter)
|
||
if (diff < minDiff) {
|
||
minDiff = diff
|
||
closest = item
|
||
}
|
||
})
|
||
|
||
if (closest) {
|
||
const val = parseInt(closest.getAttribute('data-val'))
|
||
manualFormData.value.endSeq = val
|
||
}
|
||
}, 150)
|
||
}
|
||
|
||
const openAddModal = () => {
|
||
activeTab.value = 'auto'
|
||
autoFormData.value = { dates: [], totalSequences: 28 }
|
||
manualFormData.value = { date: new Date().toISOString().split('T')[0], startSeq: 1, endSeq: 28, sequences: '' }
|
||
showAdvancedManual.value = false
|
||
showModal.value = true
|
||
|
||
// 延时滚动到对应位置
|
||
nextTick(() => {
|
||
setTimeout(() => {
|
||
setStartSeq(manualFormData.value.startSeq)
|
||
setEndSeq(manualFormData.value.endSeq)
|
||
}, 100)
|
||
})
|
||
}
|
||
|
||
const saveAuto = async () => {
|
||
if (autoFormData.value.dates.length === 0 || !autoFormData.value.totalSequences) return
|
||
saving.value = true
|
||
|
||
const selectedDates = [...autoFormData.value.dates].sort()
|
||
const totalDays = selectedDates.length
|
||
const maxSeq = parseInt(autoFormData.value.totalSequences)
|
||
const seqsPerDay = Math.ceil(maxSeq / totalDays)
|
||
|
||
try {
|
||
for (let i = 0; i < selectedDates.length; i++) {
|
||
const date = selectedDates[i]
|
||
const startSeq = i * seqsPerDay + 1
|
||
const endSeq = Math.min((i + 1) * seqsPerDay, maxSeq)
|
||
|
||
const seqs = []
|
||
for (let j = startSeq; j <= endSeq; j++) {
|
||
seqs.push(j)
|
||
}
|
||
|
||
if (seqs.length > 0) {
|
||
await $fetch('/api/admin/expected-play-dates', {
|
||
method: 'POST',
|
||
body: {
|
||
date: date,
|
||
availableSequences: seqs.join(',')
|
||
},
|
||
...auth.getAuthConfig()
|
||
})
|
||
}
|
||
}
|
||
|
||
showModal.value = false
|
||
fetchDates()
|
||
if (window.$showNotification) {
|
||
window.$showNotification('批量自动分配成功', 'success')
|
||
}
|
||
} catch (e) {
|
||
if (window.$showNotification) {
|
||
window.$showNotification(e.data?.message || '保存失败', 'error')
|
||
}
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
const saveManual = async () => {
|
||
let finalSequences = manualFormData.value.sequences
|
||
if (!showAdvancedManual.value || !finalSequences) {
|
||
const start = Math.min(manualFormData.value.startSeq, manualFormData.value.endSeq)
|
||
const end = Math.max(manualFormData.value.startSeq, manualFormData.value.endSeq)
|
||
finalSequences = Array.from({ length: end - start + 1 }, (_, i) => start + i).join(',')
|
||
}
|
||
|
||
if (!manualFormData.value.date || !finalSequences) return
|
||
saving.value = true
|
||
try {
|
||
await $fetch('/api/admin/expected-play-dates', {
|
||
method: 'POST',
|
||
body: {
|
||
date: manualFormData.value.date,
|
||
availableSequences: finalSequences
|
||
},
|
||
...auth.getAuthConfig()
|
||
})
|
||
showModal.value = false
|
||
fetchDates()
|
||
if (window.$showNotification) {
|
||
window.$showNotification('手动配置成功', 'success')
|
||
}
|
||
} catch (e) {
|
||
if (window.$showNotification) {
|
||
window.$showNotification(e.data?.message || '保存失败', 'error')
|
||
}
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
|
||
|
||
const deleteDate = async (id) => {
|
||
try {
|
||
await $fetch(`/api/admin/expected-play-dates/${id}`, {
|
||
method: 'DELETE',
|
||
...auth.getAuthConfig()
|
||
})
|
||
fetchDates()
|
||
if (window.$showNotification) {
|
||
window.$showNotification('删除成功', 'success')
|
||
}
|
||
} catch (e) {
|
||
console.error(e)
|
||
if (window.$showNotification) {
|
||
window.$showNotification(e.data?.message || '删除失败', 'error')
|
||
}
|
||
}
|
||
}
|
||
|
||
const openDeleteDialog = (date) => {
|
||
deleteTarget.value = date
|
||
showDeleteDialog.value = true
|
||
}
|
||
|
||
const confirmDelete = async () => {
|
||
if (!deleteTarget.value?.id) return
|
||
deleting.value = true
|
||
try {
|
||
await deleteDate(deleteTarget.value.id)
|
||
showDeleteDialog.value = false
|
||
deleteTarget.value = null
|
||
} finally {
|
||
deleting.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(fetchDates)
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 自定义滚动条样式 */
|
||
.custom-scrollbar::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.custom-scrollbar::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||
background: #4a5568;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||
background: #718096;
|
||
}
|
||
</style>
|