mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 重新设计定时任务 UI
- 使用模板化结构渲染定时任务列表,便于扩展 - 统一时间选择器样式:时钟图标 + 独立的时/分下拉框 - 优化卡片布局:开关、图标、标题、时间配置在同一行 - 未启用时图标无背景色,启用时显示主题色背景 - 额外配置项收纳在展开区域中
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user