| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- <template>
- <a-modal
- :visible="visible"
- :title="isEdit ? '编辑定时任务' : '创建定时任务'"
- :width="600"
- :footer="null"
- destroyOnClose
- @cancel="emit('close')"
- class="create-schedule-modal"
- >
- <div class="schedule-form">
- <div class="form-item">
- <label class="form-label">任务名称 <span class="required">*</span></label>
- <a-input v-model:value="form.name" placeholder="请输入任务名称(1-100字)" :maxlength="100" />
- </div>
- <div class="form-item">
- <label class="form-label">调度方式</label>
- <div class="mode-switch">
- <div :class="['mode-btn', { active: mode === 'friendly' }]" @click="switchMode('friendly')">简单模式</div>
- <div :class="['mode-btn', { active: mode === 'cron' }]" @click="switchMode('cron')">专业模式</div>
- </div>
- </div>
- <!-- 简单模式 -->
- <template v-if="mode === 'friendly'">
- <div class="form-item">
- <label class="form-label">执行频率 <span class="required">*</span></label>
- <a-select v-model:value="form.schedule.freq" class="form-select" @change="onFreqChange">
- <a-select-option value="hourly">每小时</a-select-option>
- <a-select-option value="daily">每天</a-select-option>
- <a-select-option value="weekly">每周</a-select-option>
- <a-select-option value="monthly">每月</a-select-option>
- </a-select>
- </div>
- <div class="form-item" v-if="form.schedule.freq === 'weekly'">
- <label class="form-label">星期 <span class="required">*</span></label>
- <a-select v-model:value="form.schedule.day_of_week" class="form-select" placeholder="请选择">
- <a-select-option :value="1">周一</a-select-option>
- <a-select-option :value="2">周二</a-select-option>
- <a-select-option :value="3">周三</a-select-option>
- <a-select-option :value="4">周四</a-select-option>
- <a-select-option :value="5">周五</a-select-option>
- <a-select-option :value="6">周六</a-select-option>
- <a-select-option :value="7">周日</a-select-option>
- </a-select>
- </div>
- <div class="form-item" v-if="form.schedule.freq === 'monthly'">
- <label class="form-label">日期 <span class="required">*</span></label>
- <a-input-number v-model:value="form.schedule.day_of_month" :min="1" :max="31" placeholder="1-31" class="form-input-number" />
- </div>
- <div class="form-row" v-if="form.schedule.freq !== 'hourly'">
- <div class="form-item form-col">
- <label class="form-label">小时 <span class="required">*</span></label>
- <a-input-number v-model:value="form.schedule.hour" :min="0" :max="23" placeholder="0-23" class="form-input-number" />
- </div>
- <div class="form-item form-col">
- <label class="form-label">分钟 <span class="required">*</span></label>
- <a-input-number v-model:value="form.schedule.minute" :min="0" :max="59" placeholder="0-59" class="form-input-number" />
- </div>
- </div>
- <div class="form-item" v-else>
- <label class="form-label">分钟 <span class="required">*</span></label>
- <a-input-number v-model:value="form.schedule.minute" :min="0" :max="59" placeholder="0-59" class="form-input-number" />
- </div>
- <div v-if="humanDesc" class="schedule-preview">
- <span class="preview-label">执行计划:</span>
- <span class="preview-value">{{ humanDesc }}</span>
- </div>
- </template>
- <!-- 专业模式 -->
- <template v-if="mode === 'cron'">
- <div class="form-item">
- <label class="form-label">Cron 表达式 <span class="required">*</span></label>
- <a-input v-model:value="form.cron_expr" placeholder="如:30 20 * * * (每天20:30)" class="cron-input" />
- <div class="form-hint">标准5段cron:分 时 日 月 周(1-7,1=周一)</div>
- </div>
- </template>
- <div class="form-item">
- <label class="form-label">任务指令 <span class="required">*</span></label>
- <a-textarea v-model:value="form.prompt" placeholder="到点后智能体执行的具体指令,如:总结今天的工作" :rows="4" :maxlength="2000" />
- </div>
- <div class="form-item">
- <label class="form-label">Webhook 通知 <span class="form-optional">(选填)</span></label>
- <a-input v-model:value="form.notify_webhook" placeholder="执行完成后推送通知的 URL" />
- </div>
- <div class="form-actions">
- <a-button @click="emit('close')">取消</a-button>
- <a-button type="primary" :loading="submitting" :disabled="!canSubmit" @click="handleSubmit">{{ isEdit ? '保存' : '创建' }}</a-button>
- </div>
- </div>
- </a-modal>
- </template>
- <script setup lang="ts">
- import { ref, reactive, computed, watch } from 'vue';
- import { createSchedule, updateSchedule } from '../../api';
- import { message } from 'ant-design-vue';
- interface ScheduleData {
- id: number;
- name: string;
- cron_expr: string;
- prompt: string;
- notify_webhook?: string;
- schedule?: { freq: string; hour?: number; minute?: number; day_of_week?: number; day_of_month?: number; human?: string };
- }
- const props = defineProps<{ visible: boolean; editData?: ScheduleData | null }>();
- const emit = defineEmits<{
- (e: 'close'): void;
- (e: 'created'): void;
- (e: 'updated'): void;
- }>();
- const mode = ref<'friendly' | 'cron'>('friendly');
- const submitting = ref(false);
- const defaultForm = () => ({
- name: '',
- schedule: {
- freq: 'daily' as string,
- hour: 20 as number | undefined,
- minute: 30 as number | undefined,
- day_of_week: undefined as number | undefined,
- day_of_month: undefined as number | undefined,
- },
- cron_expr: '',
- prompt: '',
- notify_webhook: '',
- });
- const form = reactive(defaultForm());
- const isEdit = computed(() => !!props.editData);
- watch(
- () => props.visible,
- (val) => {
- if (val) {
- Object.assign(form, defaultForm());
- submitting.value = false;
- if (props.editData) {
- const d = props.editData;
- form.name = d.name;
- form.prompt = d.prompt;
- form.notify_webhook = d.notify_webhook || '';
- form.cron_expr = d.cron_expr || '';
- if (d.schedule && d.schedule.freq && d.schedule.freq !== 'other') {
- mode.value = 'friendly';
- form.schedule.freq = d.schedule.freq;
- form.schedule.hour = d.schedule.hour;
- form.schedule.minute = d.schedule.minute;
- form.schedule.day_of_week = d.schedule.day_of_week;
- form.schedule.day_of_month = d.schedule.day_of_month;
- } else {
- mode.value = 'cron';
- }
- } else {
- mode.value = 'friendly';
- }
- }
- },
- );
- const onFreqChange = (freq: string) => {
- form.schedule.day_of_week = undefined;
- form.schedule.day_of_month = undefined;
- if (freq === 'hourly') {
- form.schedule.hour = undefined;
- form.schedule.minute = 0;
- } else if (freq === 'daily') {
- form.schedule.hour = 20;
- form.schedule.minute = 30;
- } else if (freq === 'weekly') {
- form.schedule.day_of_week = 1;
- form.schedule.hour = 9;
- form.schedule.minute = 0;
- } else if (freq === 'monthly') {
- form.schedule.day_of_month = 1;
- form.schedule.hour = 8;
- form.schedule.minute = 0;
- }
- };
- const freqLabelMap: Record<string, string> = {
- hourly: '每小时',
- daily: '每天',
- weekly: '每周',
- monthly: '每月',
- };
- const weekLabelMap: Record<number, string> = {
- 1: '周一',
- 2: '周二',
- 3: '周三',
- 4: '周四',
- 5: '周五',
- 6: '周六',
- 7: '周日',
- };
- const humanDesc = computed(() => {
- const f = form.schedule;
- const freq = freqLabelMap[f.freq] || f.freq;
- if (f.freq === 'hourly') return `${freq}第${f.minute}分钟`;
- const time = `${String(f.hour ?? 0).padStart(2, '0')}:${String(f.minute ?? 0).padStart(2, '0')}`;
- if (f.freq === 'daily') return `${freq} ${time}`;
- if (f.freq === 'weekly') return `${freq}${weekLabelMap[f.day_of_week ?? 1] || ''} ${time}`;
- if (f.freq === 'monthly') return `每月${f.day_of_month}日 ${time}`;
- return '';
- });
- const scheduleToCron = (): string => {
- const f = form.schedule;
- const min = f.minute ?? 0;
- const hr = f.hour ?? 0;
- if (f.freq === 'hourly') return `${min} * * * *`;
- if (f.freq === 'daily') return `${min} ${hr} * * *`;
- if (f.freq === 'weekly') return `${min} ${hr} * * ${f.day_of_week ?? 1}`;
- if (f.freq === 'monthly') return `${min} ${hr} ${f.day_of_month ?? 1} * *`;
- return '';
- };
- const switchMode = (target: 'friendly' | 'cron') => {
- if (target === mode.value) return;
- if (target === 'cron' && mode.value === 'friendly') {
- form.cron_expr = scheduleToCron();
- }
- mode.value = target;
- };
- const canSubmit = computed(() => {
- if (!form.name.trim() || !form.prompt.trim()) return false;
- if (mode.value === 'cron') return !!form.cron_expr.trim();
- return true;
- });
- const buildPayload = () => {
- const payload: Record<string, any> = {
- name: form.name.trim(),
- prompt: form.prompt.trim(),
- };
- if (form.notify_webhook.trim()) {
- payload.notify_webhook = form.notify_webhook.trim();
- } else if (isEdit.value) {
- payload.notify_webhook = '';
- }
- if (mode.value === 'cron') {
- payload.cron_expr = form.cron_expr.trim();
- } else {
- const s: Record<string, any> = { freq: form.schedule.freq };
- if (form.schedule.hour != null) s.hour = form.schedule.hour;
- if (form.schedule.minute != null) s.minute = form.schedule.minute;
- if (form.schedule.day_of_week != null) s.day_of_week = form.schedule.day_of_week;
- if (form.schedule.day_of_month != null) s.day_of_month = form.schedule.day_of_month;
- payload.schedule = s;
- }
- return payload;
- };
- const handleSubmit = async () => {
- if (!canSubmit.value) return;
- submitting.value = true;
- try {
- const payload = buildPayload();
- if (isEdit.value && props.editData) {
- await updateSchedule(props.editData.id, payload);
- message.success(`已更新定时任务「${form.name.trim()}」`);
- emit('updated');
- } else {
- await createSchedule(payload as any);
- message.success(`已创建定时任务「${form.name.trim()}」`);
- emit('created');
- }
- emit('close');
- } catch (err: any) {
- const msg = err?.response?.data?.message || err?.message || (isEdit.value ? '更新失败,请重试' : '创建失败,请重试');
- message.error(msg);
- } finally {
- submitting.value = false;
- }
- };
- </script>
- <style scoped lang="less">
- .schedule-form {
- display: flex;
- flex-direction: column;
- gap: 18px;
- }
- .form-item {
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
- .form-label {
- font-size: 13px;
- color: #e8edf3;
- font-weight: 500;
- .required {
- color: #ff4d4f;
- }
- .form-optional {
- color: #6a7a8a;
- font-weight: 400;
- }
- }
- .form-row {
- display: flex;
- gap: 16px;
- .form-col {
- flex: 1;
- }
- }
- .form-select {
- width: 100%;
- :deep(.zxm-select-selector) {
- background: rgba(10, 132, 255, 0.08) !important;
- border: 1px solid rgba(63, 80, 106, 0.4) !important;
- border-radius: 6px !important;
- color: #e0e6ed;
- height: 36px;
- }
- :deep(.zxm-select-selection-item) {
- line-height: 34px !important;
- color: #e0e6ed;
- }
- }
- :deep(.zxm-input),
- :deep(.zxm-input-number),
- :deep(textarea.zxm-input) {
- background: rgba(10, 132, 255, 0.08);
- border: 1px solid rgba(63, 80, 106, 0.4);
- border-radius: 6px;
- color: #e0e6ed;
- &:hover,
- &:focus {
- border-color: rgba(10, 132, 255, 0.5);
- }
- }
- .form-input-number {
- width: 100%;
- }
- .cron-input {
- font-family: 'Courier New', monospace;
- }
- .form-hint {
- font-size: 12px;
- color: #6a7a8a;
- margin-top: 2px;
- }
- .mode-switch {
- display: flex;
- gap: 0;
- border-radius: 6px;
- overflow: hidden;
- border: 1px solid rgba(63, 80, 106, 0.4);
- width: fit-content;
- .mode-btn {
- padding: 6px 20px;
- font-size: 13px;
- color: #8b949e;
- cursor: pointer;
- transition: all 0.2s;
- background: rgba(10, 132, 255, 0.04);
- &:hover {
- color: #e0e6ed;
- }
- &.active {
- background: rgba(10, 132, 255, 0.2);
- color: #5cb8ff;
- }
- }
- }
- .schedule-preview {
- padding: 10px 14px;
- background: rgba(10, 132, 255, 0.06);
- border: 1px solid rgba(10, 132, 255, 0.15);
- border-radius: 6px;
- font-size: 13px;
- .preview-label {
- color: #6a7a8a;
- }
- .preview-value {
- color: #5cb8ff;
- font-weight: 500;
- }
- }
- .form-actions {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- padding-top: 8px;
- }
- </style>
- <style lang="less">
- .create-schedule-modal {
- .zxm-modal-content {
- background: linear-gradient(135deg, #0a1628 0%, #0d2137 50%, #0a1e35 100%);
- border: 1px solid rgba(63, 80, 106, 0.4);
- border-radius: 12px;
- }
- .zxm-modal-header {
- background: transparent;
- border-bottom: 1px solid rgba(63, 80, 106, 0.25);
- padding: 18px 24px;
- border-radius: 10px 10px 0 0;
- }
- .zxm-modal-title {
- color: #e8edf3;
- font-size: 18px;
- font-weight: 600;
- }
- .zxm-modal-body {
- padding: 24px !important;
- }
- .zxm-modal-close {
- .zxm-modal-close-x {
- color: #8b949e;
- }
- }
- }
- </style>
|