207 lines
6.9 KiB
Vue
207 lines
6.9 KiB
Vue
<template>
|
|
<layout>
|
|
<div class="bg-white box-border w-full h-full flex rounded-[16px] overflow-hidden">
|
|
<div class="w-full flex flex-col h-full p-10 pt-12">
|
|
<!-- Header -->
|
|
<div class="flex flex-col md:flex-row md:items-start justify-between mb-6 shrink-0 gap-4">
|
|
<div>
|
|
<h1
|
|
class="text-5xl md:text-6xl font-serif text-[#171717] mb-3 font-normal tracking-tight"
|
|
style="font-family: Georgia, Cambria, 'Times New Roman', Times, serif"
|
|
>
|
|
{{ t('cron.title') }}
|
|
</h1>
|
|
<p class="text-[17px] text-[#171717]/70 font-medium">
|
|
{{ t('cron.subtitle') }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 md:mt-2">
|
|
<button
|
|
@click="store.fetchJobs()"
|
|
class="hover:bg-black/5 transition-colors shrink-0 text-[13px] font-medium px-4 h-9 rounded-full border border-black/10 flex items-center justify-center text-[#171717]/80 hover:text-[#171717]"
|
|
>
|
|
<RiRefreshLine :class="['h-4 w-4 mr-2', store.loading && 'animate-spin']" />
|
|
{{ t('cron.refresh') }}
|
|
</button>
|
|
<button
|
|
@click="openCreateDialog"
|
|
class="shrink-0 text-[13px] font-medium px-4 h-9 rounded-full bg-[#2B7FFF] hover:bg-[#2B7FFF]/90 text-white flex items-center justify-center shadow-none"
|
|
>
|
|
<RiAddLine class="h-4 w-4 mr-2" />
|
|
{{ t('cron.newTask') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Area -->
|
|
<div class="flex-1 overflow-y-auto pr-2 pb-10 min-h-0 -mr-2">
|
|
<!-- Error Display -->
|
|
<div
|
|
v-if="store.error"
|
|
class="mb-4 p-4 rounded-xl border border-red-500/50 bg-red-500/10 text-red-600 text-sm font-medium flex items-center gap-2"
|
|
>
|
|
<RiErrorWarningLine class="h-5 w-5 shrink-0" />
|
|
<span>{{ store.error }}</span>
|
|
</div>
|
|
|
|
<!-- Loading -->
|
|
<div
|
|
v-if="store.loading && store.safeJobs.length === 0"
|
|
class="flex flex-col items-center justify-center h-full text-[#525866]"
|
|
>
|
|
<RiRefreshLine class="h-10 w-10 animate-spin mb-4" />
|
|
<p>{{ t('common.loading', 'Loading...') }}</p>
|
|
</div>
|
|
|
|
<template v-else>
|
|
<!-- Statistics -->
|
|
<CronStats
|
|
:total="store.safeJobs.length"
|
|
:active="store.activeJobs.length"
|
|
:paused="store.pausedJobs.length"
|
|
:failed="store.failedJobs.length"
|
|
/>
|
|
|
|
<!-- Jobs List -->
|
|
<div v-if="store.safeJobs.length === 0" class="flex flex-col items-center justify-center py-20 text-[#525866] bg-[#F4F3EB]/40 rounded-3xl border border-transparent">
|
|
<RiTimeLine class="h-10 w-10 mb-4 opacity-50" />
|
|
<h3 class="text-lg font-medium mb-2 text-[#171717]">{{ t('cron.empty.title') }}</h3>
|
|
<p class="text-[14px] text-center mb-6 max-w-md">
|
|
{{ t('cron.empty.description') }}
|
|
</p>
|
|
<button
|
|
@click="openCreateDialog"
|
|
class="rounded-full px-6 h-10 bg-[#2B7FFF] hover:bg-[#2B7FFF]/90 text-white flex items-center justify-center text-[13px] font-medium"
|
|
>
|
|
<RiAddLine class="h-4 w-4 mr-2" />
|
|
{{ t('cron.empty.create') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
|
<CronJobCard
|
|
v-for="job in store.safeJobs"
|
|
:key="job.id"
|
|
:job="job"
|
|
@toggle="(enabled) => handleToggle(job.id, enabled)"
|
|
@edit="() => openEditDialog(job)"
|
|
@delete="() => confirmDelete(job.id)"
|
|
@trigger="() => handleTrigger(job.id)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create/Edit Dialog -->
|
|
<CronTaskDialog
|
|
v-model="dialogVisible"
|
|
:job="editingJob"
|
|
@save="handleSave"
|
|
@closed="handleDialogClose"
|
|
/>
|
|
</layout>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
import {
|
|
RiRefreshLine,
|
|
RiAddLine,
|
|
RiErrorWarningLine,
|
|
RiTimeLine,
|
|
} from '@remixicon/vue';
|
|
import { useCronStore } from '@src/store/cron';
|
|
import type { CronJob, CronJobCreateInput } from '@lib/cron-types';
|
|
import CronStats from './components/CronStats.vue';
|
|
import CronJobCard from './components/CronJobCard.vue';
|
|
import CronTaskDialog from './components/CronTaskDialog.vue';
|
|
|
|
const { t } = useI18n();
|
|
const store = useCronStore();
|
|
|
|
const dialogVisible = ref(false);
|
|
const editingJob = ref<CronJob | undefined>(undefined);
|
|
|
|
onMounted(() => {
|
|
store.fetchJobs();
|
|
});
|
|
|
|
function openCreateDialog() {
|
|
editingJob.value = undefined;
|
|
dialogVisible.value = true;
|
|
}
|
|
|
|
function openEditDialog(job: CronJob) {
|
|
editingJob.value = job;
|
|
dialogVisible.value = true;
|
|
}
|
|
|
|
function handleDialogClose() {
|
|
editingJob.value = undefined;
|
|
}
|
|
|
|
async function handleSave(input: CronJobCreateInput) {
|
|
try {
|
|
if (editingJob.value) {
|
|
await store.updateJob(editingJob.value.id, input);
|
|
ElMessage.success(t('cron.toast.updated'));
|
|
} else {
|
|
await store.createJob(input);
|
|
ElMessage.success(t('cron.toast.created'));
|
|
}
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
ElMessage.error(msg);
|
|
}
|
|
}
|
|
|
|
async function handleToggle(id: string, enabled: boolean) {
|
|
try {
|
|
await store.toggleJob(id, enabled);
|
|
ElMessage.success(enabled ? t('cron.toast.enabled') : t('cron.toast.paused'));
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
ElMessage.error(msg || t('cron.toast.failedUpdate'));
|
|
}
|
|
}
|
|
|
|
async function handleTrigger(id: string) {
|
|
try {
|
|
await store.triggerJob(id);
|
|
ElMessage.success(t('cron.toast.triggered'));
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
ElMessage.error(t('cron.toast.failedTrigger', { error: msg }));
|
|
}
|
|
}
|
|
|
|
async function confirmDelete(id: string) {
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
t('cron.card.deleteConfirm'),
|
|
t('common.confirm', 'Confirm'),
|
|
{
|
|
confirmButtonText: t('common.delete', 'Delete'),
|
|
cancelButtonText: t('common.cancel', 'Cancel'),
|
|
type: 'warning',
|
|
},
|
|
);
|
|
await store.deleteJob(id);
|
|
ElMessage.success(t('cron.toast.deleted'));
|
|
} catch (err) {
|
|
// User cancelled or error
|
|
if (err !== 'cancel' && !(err instanceof Error && err.message === 'cancel')) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
if (msg && msg !== 'cancel') {
|
|
ElMessage.error(msg || t('cron.toast.failedDelete'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|