refactor: 重新设计定时任务 UI

- 使用模板化结构渲染定时任务列表,便于扩展
- 统一时间选择器样式:时钟图标 + 独立的时/分下拉框
- 优化卡片布局:开关、图标、标题、时间配置在同一行
- 未启用时图标无背景色,启用时显示主题色背景
- 额外配置项收纳在展开区域中
This commit is contained in:
fawney19
2026-02-04 13:14:11 +08:00
parent 9ffe4b4ce8
commit d15b294c38

View File

@@ -487,177 +487,141 @@
title="定时任务" title="定时任务"
description="配置系统后台定时任务" description="配置系统后台定时任务"
> >
<template #actions> <div class="space-y-3">
<Button <template
size="sm" v-for="task in scheduledTasks"
:disabled="schedulerConfigLoading || !hasSchedulerConfigChanges" :key="task.id"
@click="handleSchedulerConfigSave"
> >
{{ schedulerConfigLoading ? '保存中...' : '保存' }} <div
</Button> class="group relative rounded-xl border transition-all duration-300"
</template> :class="task.enabled
? 'border-primary/30 bg-primary/[0.02] shadow-sm shadow-primary/5'
: 'border-border bg-card hover:border-border/80'"
>
<!-- 主行 -->
<div class="flex items-center gap-4 p-4">
<!-- 左侧开关 -->
<div class="shrink-0">
<Switch
:id="`enable-${task.id}`"
:model-value="task.enabled"
@update:model-value="task.onToggle"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <!-- 中间图标标题描述 -->
<div class="flex items-center space-x-2"> <div class="flex items-center gap-3 flex-1 min-w-0">
<Switch <div
id="enable-provider-checkin" class="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 transition-colors duration-300"
:model-value="systemConfig.enable_provider_checkin" :class="task.enabled
@update:model-value="handleProviderCheckinToggle" ? 'bg-primary/10 text-primary'
/> : 'text-muted-foreground'"
<div>
<Label
for="enable-provider-checkin"
class="cursor-pointer"
>
启用 Provider 自动签到
</Label>
<p class="text-xs text-muted-foreground">
自动执行已配置 Provider 的签到任务
</p>
</div>
</div>
<div v-if="systemConfig.enable_provider_checkin">
<Label class="block text-sm font-medium">
执行时间
</Label>
<div class="mt-1 flex items-center gap-2">
<Select
v-model:open="checkinHourSelectOpen"
:model-value="checkinHour"
@update:model-value="(val: string) => updateCheckinTime(val, checkinMinute)"
>
<SelectTrigger class="w-20">
<SelectValue placeholder="" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="h in 24"
:key="h - 1"
:value="String(h - 1).padStart(2, '0')"
> >
{{ String(h - 1).padStart(2, '0') }} <component
</SelectItem> :is="task.icon"
</SelectContent> class="w-4.5 h-4.5"
</Select> />
<span class="text-muted-foreground">:</span> </div>
<Select <div class="flex-1 min-w-0">
v-model:open="checkinMinuteSelectOpen" <h4 class="font-medium text-sm">
:model-value="checkinMinute" {{ task.title }}
@update:model-value="(val: string) => updateCheckinTime(checkinHour, val)" </h4>
> <p class="text-xs text-muted-foreground mt-0.5 truncate">
<SelectTrigger class="w-20"> {{ task.description }}
<SelectValue placeholder="" /> </p>
</SelectTrigger> </div>
<SelectContent> </div>
<SelectItem
v-for="m in 60" <!-- 右侧时间选择器 + 保存按钮 -->
:key="m - 1" <div
:value="String(m - 1).padStart(2, '0')" v-if="task.enabled"
class="flex items-center gap-2 shrink-0"
>
<Clock class="w-4 h-4 text-muted-foreground" />
<Select
v-model:open="task.hourSelectOpen.value"
:model-value="task.hour"
@update:model-value="(val: string) => task.updateTime(val, task.minute)"
> >
{{ String(m - 1).padStart(2, '0') }} <SelectTrigger class="w-14 h-8 text-xs">
</SelectItem> <SelectValue placeholder="" />
</SelectContent> </SelectTrigger>
</Select> <SelectContent>
</div> <SelectItem
<p class="mt-1 text-xs text-muted-foreground"> v-for="h in 24"
每天定时执行24小时制 :key="h - 1"
</p> :value="String(h - 1).padStart(2, '0')"
</div> >
</div> {{ String(h - 1).padStart(2, '0') }}
</SelectItem>
</SelectContent>
</Select>
<span class="text-sm text-muted-foreground">:</span>
<Select
v-model:open="task.minuteSelectOpen.value"
:model-value="task.minute"
@update:model-value="(val: string) => task.updateTime(task.hour, val)"
>
<SelectTrigger class="w-14 h-8 text-xs">
<SelectValue placeholder="" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="m in 60"
:key="m - 1"
:value="String(m - 1).padStart(2, '0')"
>
{{ String(m - 1).padStart(2, '0') }}
</SelectItem>
</SelectContent>
</Select>
<Button
v-if="task.hasChanges"
variant="default"
size="sm"
class="h-8 px-2.5 text-xs"
:disabled="task.loading"
@click="task.onSave"
>
<Check
v-if="!task.loading"
class="w-3.5 h-3.5"
/>
<Loader2
v-else
class="w-3.5 h-3.5 animate-spin"
/>
</Button>
</div>
</div>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6"> <!-- 额外配置区域(仅用户配额重置任务有) -->
<div class="flex items-center space-x-2"> <div
<Switch v-if="task.id === 'user-quota-reset' && task.enabled"
id="enable-user-quota-reset" class="px-4 pb-4 pt-0"
:model-value="systemConfig.enable_user_quota_reset"
@update:model-value="handleUserQuotaResetToggle"
/>
<div>
<Label
for="enable-user-quota-reset"
class="cursor-pointer"
> >
启用用户配额自动重置 <div class="flex items-center gap-3 p-3 rounded-lg bg-muted/30 border border-border/50">
</Label> <div class="flex items-center gap-2 text-sm">
<p class="text-xs text-muted-foreground"> <span class="text-muted-foreground">重置周期</span>
每天定时触发,按周期执行 <div class="flex items-center gap-1.5">
</p> <span class="text-muted-foreground">每</span>
</div> <Input
</div> v-model.number="systemConfig.user_quota_reset_interval_days"
type="number"
<div min="1"
v-if="systemConfig.enable_user_quota_reset" step="1"
class="grid grid-cols-1 md:grid-cols-2 gap-6" class="w-14 h-7 text-xs text-center px-2"
> />
<div> <span class="text-muted-foreground">天</span>
<Label class="block text-sm font-medium"> </div>
执行时间 </div>
</Label> </div>
<div class="mt-1 flex items-center gap-2"> <p class="text-[11px] text-muted-foreground mt-2 ml-1">
<Select 滚动计算:距离上次成功执行满 N 天后再次执行
v-model:open="userQuotaResetHourSelectOpen" </p>
:model-value="userQuotaResetHour"
@update:model-value="(val: string) => updateUserQuotaResetTime(val, userQuotaResetMinute)"
>
<SelectTrigger class="w-20">
<SelectValue placeholder="" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="h in 24"
:key="h - 1"
:value="String(h - 1).padStart(2, '0')"
>
{{ String(h - 1).padStart(2, '0') }}
</SelectItem>
</SelectContent>
</Select>
<span class="text-muted-foreground">:</span>
<Select
v-model:open="userQuotaResetMinuteSelectOpen"
:model-value="userQuotaResetMinute"
@update:model-value="(val: string) => updateUserQuotaResetTime(userQuotaResetHour, val)"
>
<SelectTrigger class="w-20">
<SelectValue placeholder="" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="m in 60"
:key="m - 1"
:value="String(m - 1).padStart(2, '0')"
>
{{ String(m - 1).padStart(2, '0') }}
</SelectItem>
</SelectContent>
</Select>
</div> </div>
<p class="mt-1 text-xs text-muted-foreground">
每天定时执行24小时制
</p>
</div> </div>
</template>
<div>
<Label class="block text-sm font-medium">
重置周期
</Label>
<div class="mt-1 flex items-center gap-2 flex-wrap">
<span class="text-xs text-muted-foreground">每</span>
<Input
v-model.number="systemConfig.user_quota_reset_interval_days"
type="number"
min="1"
step="1"
class="w-24"
/>
<span class="text-xs text-muted-foreground">天</span>
</div>
<p class="mt-1 text-xs text-muted-foreground">
滚动计算:距离上次成功执行满 N 天后才会再次执行
</p>
</div>
</div>
</div> </div>
</CardSection> </CardSection>
@@ -1031,7 +995,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { Download, Upload } from 'lucide-vue-next' import { Download, Upload, CalendarCheck, RotateCcw, Clock, Check, Loader2 } from 'lucide-vue-next'
import Button from '@/components/ui/button.vue' import Button from '@/components/ui/button.vue'
import Input from '@/components/ui/input.vue' import Input from '@/components/ui/input.vue'
import Label from '@/components/ui/label.vue' import Label from '@/components/ui/label.vue'
@@ -1413,7 +1377,8 @@ async function handleProviderCheckinToggle(enabled: boolean) {
} }
const previousCheckinTime = ref('') const previousCheckinTime = ref('')
const schedulerConfigLoading = ref(false) const checkinConfigLoading = ref(false)
const quotaResetConfigLoading = ref(false)
const checkinHourSelectOpen = ref(false) const checkinHourSelectOpen = ref(false)
const checkinMinuteSelectOpen = ref(false) const checkinMinuteSelectOpen = ref(false)
@@ -1488,33 +1453,75 @@ const hasUserQuotaResetIntervalChanged = computed(() => {
return systemConfig.value.user_quota_reset_interval_days !== previousUserQuotaResetIntervalDays.value return systemConfig.value.user_quota_reset_interval_days !== previousUserQuotaResetIntervalDays.value
}) })
const hasSchedulerConfigChanges = computed(() => { const hasQuotaResetConfigChanged = computed(() => {
return ( return hasUserQuotaResetTimeChanged.value || hasUserQuotaResetIntervalChanged.value
hasCheckinTimeChanged.value ||
hasUserQuotaResetTimeChanged.value ||
hasUserQuotaResetIntervalChanged.value
)
}) })
async function handleSchedulerConfigSave() { // 定时任务配置列表
const configItems: Array<{ key: string, value: any, description: string, onSuccess: () => void }> = [] const scheduledTasks = computed(() => [
{
id: 'provider-checkin',
icon: CalendarCheck,
title: 'Provider 自动签到',
description: '自动执行已配置 Provider 的签到任务',
enabled: systemConfig.value.enable_provider_checkin,
hour: checkinHour.value,
minute: checkinMinute.value,
hourSelectOpen: checkinHourSelectOpen,
minuteSelectOpen: checkinMinuteSelectOpen,
updateTime: updateCheckinTime,
hasChanges: hasCheckinTimeChanged.value,
loading: checkinConfigLoading.value,
onToggle: handleProviderCheckinToggle,
onSave: handleCheckinTimeSave,
},
{
id: 'user-quota-reset',
icon: RotateCcw,
title: '用户配额自动重置',
description: '定时将用户已使用配额重置为零',
enabled: systemConfig.value.enable_user_quota_reset,
hour: userQuotaResetHour.value,
minute: userQuotaResetMinute.value,
hourSelectOpen: userQuotaResetHourSelectOpen,
minuteSelectOpen: userQuotaResetMinuteSelectOpen,
updateTime: updateUserQuotaResetTime,
hasChanges: hasQuotaResetConfigChanged.value,
loading: quotaResetConfigLoading.value,
onToggle: handleUserQuotaResetToggle,
onSave: handleQuotaResetConfigSave,
},
])
if (hasCheckinTimeChanged.value) { // Provider 签到时间保存
const newTime = systemConfig.value.provider_checkin_time async function handleCheckinTimeSave() {
if (!newTime || !/^\d{2}:\d{2}$/.test(newTime)) { const newTime = systemConfig.value.provider_checkin_time
error('请输入有效的时间格式 (HH:MM)') if (!newTime || !/^\d{2}:\d{2}$/.test(newTime)) {
return error('请输入有效的时间格式 (HH:MM)')
} return
configItems.push({
key: 'provider_checkin_time',
value: newTime,
description: 'Provider 自动签到执行时间HH:MM 格式)',
onSuccess: () => {
previousCheckinTime.value = newTime
}
})
} }
checkinConfigLoading.value = true
try {
await adminApi.updateSystemConfig(
'provider_checkin_time',
newTime,
'Provider 自动签到执行时间HH:MM 格式)'
)
previousCheckinTime.value = newTime
success(`签到时间已设置为 ${newTime}`)
} catch (err) {
error('保存签到时间失败')
log.error('保存签到时间失败:', err)
} finally {
checkinConfigLoading.value = false
}
}
// 用户配额重置配置保存
async function handleQuotaResetConfigSave() {
const configItems: Array<{ key: string, value: any, description: string, onSuccess: () => void }> = []
if (hasUserQuotaResetTimeChanged.value) { if (hasUserQuotaResetTimeChanged.value) {
const newTime = systemConfig.value.user_quota_reset_time const newTime = systemConfig.value.user_quota_reset_time
if (!newTime || !/^\d{2}:\d{2}$/.test(newTime)) { if (!newTime || !/^\d{2}:\d{2}$/.test(newTime)) {
@@ -1550,7 +1557,7 @@ async function handleSchedulerConfigSave() {
if (configItems.length === 0) return if (configItems.length === 0) return
schedulerConfigLoading.value = true quotaResetConfigLoading.value = true
const failedKeys: string[] = [] const failedKeys: string[] = []
try { try {
@@ -1560,7 +1567,7 @@ async function handleSchedulerConfigSave() {
item.onSuccess() item.onSuccess()
} catch (err) { } catch (err) {
failedKeys.push(item.key) failedKeys.push(item.key)
log.error(`保存定时任务配置失败: ${item.key}`, err) log.error(`保存配额重置配置失败: ${item.key}`, err)
} }
} }
@@ -1569,9 +1576,9 @@ async function handleSchedulerConfigSave() {
return return
} }
success('定时任务配置已保存') success('配额重置配置已保存')
} finally { } finally {
schedulerConfigLoading.value = false quotaResetConfigLoading.value = false
} }
} }