CreateScheduleModal.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <template>
  2. <a-modal
  3. :visible="visible"
  4. :title="isEdit ? '编辑定时任务' : '创建定时任务'"
  5. :width="600"
  6. :footer="null"
  7. destroyOnClose
  8. @cancel="emit('close')"
  9. class="create-schedule-modal"
  10. >
  11. <div class="schedule-form">
  12. <div class="form-item">
  13. <label class="form-label">任务名称 <span class="required">*</span></label>
  14. <a-input v-model:value="form.name" placeholder="请输入任务名称(1-100字)" :maxlength="100" />
  15. </div>
  16. <div class="form-item">
  17. <label class="form-label">调度方式</label>
  18. <div class="mode-switch">
  19. <div :class="['mode-btn', { active: mode === 'friendly' }]" @click="switchMode('friendly')">简单模式</div>
  20. <div :class="['mode-btn', { active: mode === 'cron' }]" @click="switchMode('cron')">专业模式</div>
  21. </div>
  22. </div>
  23. <!-- 简单模式 -->
  24. <template v-if="mode === 'friendly'">
  25. <div class="form-item">
  26. <label class="form-label">执行频率 <span class="required">*</span></label>
  27. <a-select v-model:value="form.schedule.freq" class="form-select" @change="onFreqChange">
  28. <a-select-option value="hourly">每小时</a-select-option>
  29. <a-select-option value="daily">每天</a-select-option>
  30. <a-select-option value="weekly">每周</a-select-option>
  31. <a-select-option value="monthly">每月</a-select-option>
  32. </a-select>
  33. </div>
  34. <div class="form-item" v-if="form.schedule.freq === 'weekly'">
  35. <label class="form-label">星期 <span class="required">*</span></label>
  36. <a-select v-model:value="form.schedule.day_of_week" class="form-select" placeholder="请选择">
  37. <a-select-option :value="1">周一</a-select-option>
  38. <a-select-option :value="2">周二</a-select-option>
  39. <a-select-option :value="3">周三</a-select-option>
  40. <a-select-option :value="4">周四</a-select-option>
  41. <a-select-option :value="5">周五</a-select-option>
  42. <a-select-option :value="6">周六</a-select-option>
  43. <a-select-option :value="7">周日</a-select-option>
  44. </a-select>
  45. </div>
  46. <div class="form-item" v-if="form.schedule.freq === 'monthly'">
  47. <label class="form-label">日期 <span class="required">*</span></label>
  48. <a-input-number v-model:value="form.schedule.day_of_month" :min="1" :max="31" placeholder="1-31" class="form-input-number" />
  49. </div>
  50. <div class="form-row" v-if="form.schedule.freq !== 'hourly'">
  51. <div class="form-item form-col">
  52. <label class="form-label">小时 <span class="required">*</span></label>
  53. <a-input-number v-model:value="form.schedule.hour" :min="0" :max="23" placeholder="0-23" class="form-input-number" />
  54. </div>
  55. <div class="form-item form-col">
  56. <label class="form-label">分钟 <span class="required">*</span></label>
  57. <a-input-number v-model:value="form.schedule.minute" :min="0" :max="59" placeholder="0-59" class="form-input-number" />
  58. </div>
  59. </div>
  60. <div class="form-item" v-else>
  61. <label class="form-label">分钟 <span class="required">*</span></label>
  62. <a-input-number v-model:value="form.schedule.minute" :min="0" :max="59" placeholder="0-59" class="form-input-number" />
  63. </div>
  64. <div v-if="humanDesc" class="schedule-preview">
  65. <span class="preview-label">执行计划:</span>
  66. <span class="preview-value">{{ humanDesc }}</span>
  67. </div>
  68. </template>
  69. <!-- 专业模式 -->
  70. <template v-if="mode === 'cron'">
  71. <div class="form-item">
  72. <label class="form-label">Cron 表达式 <span class="required">*</span></label>
  73. <a-input v-model:value="form.cron_expr" placeholder="如:30 20 * * * (每天20:30)" class="cron-input" />
  74. <div class="form-hint">标准5段cron:分 时 日 月 周(1-7,1=周一)</div>
  75. </div>
  76. </template>
  77. <div class="form-item">
  78. <label class="form-label">任务指令 <span class="required">*</span></label>
  79. <a-textarea v-model:value="form.prompt" placeholder="到点后智能体执行的具体指令,如:总结今天的工作" :rows="4" :maxlength="2000" />
  80. </div>
  81. <div class="form-item">
  82. <label class="form-label">Webhook 通知 <span class="form-optional">(选填)</span></label>
  83. <a-input v-model:value="form.notify_webhook" placeholder="执行完成后推送通知的 URL" />
  84. </div>
  85. <div class="form-actions">
  86. <a-button @click="emit('close')">取消</a-button>
  87. <a-button type="primary" :loading="submitting" :disabled="!canSubmit" @click="handleSubmit">{{ isEdit ? '保存' : '创建' }}</a-button>
  88. </div>
  89. </div>
  90. </a-modal>
  91. </template>
  92. <script setup lang="ts">
  93. import { ref, reactive, computed, watch } from 'vue';
  94. import { createSchedule, updateSchedule } from '../../api';
  95. import { message } from 'ant-design-vue';
  96. interface ScheduleData {
  97. id: number;
  98. name: string;
  99. cron_expr: string;
  100. prompt: string;
  101. notify_webhook?: string;
  102. schedule?: { freq: string; hour?: number; minute?: number; day_of_week?: number; day_of_month?: number; human?: string };
  103. }
  104. const props = defineProps<{ visible: boolean; editData?: ScheduleData | null }>();
  105. const emit = defineEmits<{
  106. (e: 'close'): void;
  107. (e: 'created'): void;
  108. (e: 'updated'): void;
  109. }>();
  110. const mode = ref<'friendly' | 'cron'>('friendly');
  111. const submitting = ref(false);
  112. const defaultForm = () => ({
  113. name: '',
  114. schedule: {
  115. freq: 'daily' as string,
  116. hour: 20 as number | undefined,
  117. minute: 30 as number | undefined,
  118. day_of_week: undefined as number | undefined,
  119. day_of_month: undefined as number | undefined,
  120. },
  121. cron_expr: '',
  122. prompt: '',
  123. notify_webhook: '',
  124. });
  125. const form = reactive(defaultForm());
  126. const isEdit = computed(() => !!props.editData);
  127. watch(
  128. () => props.visible,
  129. (val) => {
  130. if (val) {
  131. Object.assign(form, defaultForm());
  132. submitting.value = false;
  133. if (props.editData) {
  134. const d = props.editData;
  135. form.name = d.name;
  136. form.prompt = d.prompt;
  137. form.notify_webhook = d.notify_webhook || '';
  138. form.cron_expr = d.cron_expr || '';
  139. if (d.schedule && d.schedule.freq && d.schedule.freq !== 'other') {
  140. mode.value = 'friendly';
  141. form.schedule.freq = d.schedule.freq;
  142. form.schedule.hour = d.schedule.hour;
  143. form.schedule.minute = d.schedule.minute;
  144. form.schedule.day_of_week = d.schedule.day_of_week;
  145. form.schedule.day_of_month = d.schedule.day_of_month;
  146. } else {
  147. mode.value = 'cron';
  148. }
  149. } else {
  150. mode.value = 'friendly';
  151. }
  152. }
  153. },
  154. );
  155. const onFreqChange = (freq: string) => {
  156. form.schedule.day_of_week = undefined;
  157. form.schedule.day_of_month = undefined;
  158. if (freq === 'hourly') {
  159. form.schedule.hour = undefined;
  160. form.schedule.minute = 0;
  161. } else if (freq === 'daily') {
  162. form.schedule.hour = 20;
  163. form.schedule.minute = 30;
  164. } else if (freq === 'weekly') {
  165. form.schedule.day_of_week = 1;
  166. form.schedule.hour = 9;
  167. form.schedule.minute = 0;
  168. } else if (freq === 'monthly') {
  169. form.schedule.day_of_month = 1;
  170. form.schedule.hour = 8;
  171. form.schedule.minute = 0;
  172. }
  173. };
  174. const freqLabelMap: Record<string, string> = {
  175. hourly: '每小时',
  176. daily: '每天',
  177. weekly: '每周',
  178. monthly: '每月',
  179. };
  180. const weekLabelMap: Record<number, string> = {
  181. 1: '周一',
  182. 2: '周二',
  183. 3: '周三',
  184. 4: '周四',
  185. 5: '周五',
  186. 6: '周六',
  187. 7: '周日',
  188. };
  189. const humanDesc = computed(() => {
  190. const f = form.schedule;
  191. const freq = freqLabelMap[f.freq] || f.freq;
  192. if (f.freq === 'hourly') return `${freq}第${f.minute}分钟`;
  193. const time = `${String(f.hour ?? 0).padStart(2, '0')}:${String(f.minute ?? 0).padStart(2, '0')}`;
  194. if (f.freq === 'daily') return `${freq} ${time}`;
  195. if (f.freq === 'weekly') return `${freq}${weekLabelMap[f.day_of_week ?? 1] || ''} ${time}`;
  196. if (f.freq === 'monthly') return `每月${f.day_of_month}日 ${time}`;
  197. return '';
  198. });
  199. const scheduleToCron = (): string => {
  200. const f = form.schedule;
  201. const min = f.minute ?? 0;
  202. const hr = f.hour ?? 0;
  203. if (f.freq === 'hourly') return `${min} * * * *`;
  204. if (f.freq === 'daily') return `${min} ${hr} * * *`;
  205. if (f.freq === 'weekly') return `${min} ${hr} * * ${f.day_of_week ?? 1}`;
  206. if (f.freq === 'monthly') return `${min} ${hr} ${f.day_of_month ?? 1} * *`;
  207. return '';
  208. };
  209. const switchMode = (target: 'friendly' | 'cron') => {
  210. if (target === mode.value) return;
  211. if (target === 'cron' && mode.value === 'friendly') {
  212. form.cron_expr = scheduleToCron();
  213. }
  214. mode.value = target;
  215. };
  216. const canSubmit = computed(() => {
  217. if (!form.name.trim() || !form.prompt.trim()) return false;
  218. if (mode.value === 'cron') return !!form.cron_expr.trim();
  219. return true;
  220. });
  221. const buildPayload = () => {
  222. const payload: Record<string, any> = {
  223. name: form.name.trim(),
  224. prompt: form.prompt.trim(),
  225. };
  226. if (form.notify_webhook.trim()) {
  227. payload.notify_webhook = form.notify_webhook.trim();
  228. } else if (isEdit.value) {
  229. payload.notify_webhook = '';
  230. }
  231. if (mode.value === 'cron') {
  232. payload.cron_expr = form.cron_expr.trim();
  233. } else {
  234. const s: Record<string, any> = { freq: form.schedule.freq };
  235. if (form.schedule.hour != null) s.hour = form.schedule.hour;
  236. if (form.schedule.minute != null) s.minute = form.schedule.minute;
  237. if (form.schedule.day_of_week != null) s.day_of_week = form.schedule.day_of_week;
  238. if (form.schedule.day_of_month != null) s.day_of_month = form.schedule.day_of_month;
  239. payload.schedule = s;
  240. }
  241. return payload;
  242. };
  243. const handleSubmit = async () => {
  244. if (!canSubmit.value) return;
  245. submitting.value = true;
  246. try {
  247. const payload = buildPayload();
  248. if (isEdit.value && props.editData) {
  249. await updateSchedule(props.editData.id, payload);
  250. message.success(`已更新定时任务「${form.name.trim()}」`);
  251. emit('updated');
  252. } else {
  253. await createSchedule(payload as any);
  254. message.success(`已创建定时任务「${form.name.trim()}」`);
  255. emit('created');
  256. }
  257. emit('close');
  258. } catch (err: any) {
  259. const msg = err?.response?.data?.message || err?.message || (isEdit.value ? '更新失败,请重试' : '创建失败,请重试');
  260. message.error(msg);
  261. } finally {
  262. submitting.value = false;
  263. }
  264. };
  265. </script>
  266. <style scoped lang="less">
  267. .schedule-form {
  268. display: flex;
  269. flex-direction: column;
  270. gap: 18px;
  271. }
  272. .form-item {
  273. display: flex;
  274. flex-direction: column;
  275. gap: 6px;
  276. }
  277. .form-label {
  278. font-size: 13px;
  279. color: #e8edf3;
  280. font-weight: 500;
  281. .required {
  282. color: #ff4d4f;
  283. }
  284. .form-optional {
  285. color: #6a7a8a;
  286. font-weight: 400;
  287. }
  288. }
  289. .form-row {
  290. display: flex;
  291. gap: 16px;
  292. .form-col {
  293. flex: 1;
  294. }
  295. }
  296. .form-select {
  297. width: 100%;
  298. :deep(.zxm-select-selector) {
  299. background: rgba(10, 132, 255, 0.08) !important;
  300. border: 1px solid rgba(63, 80, 106, 0.4) !important;
  301. border-radius: 6px !important;
  302. color: #e0e6ed;
  303. height: 36px;
  304. }
  305. :deep(.zxm-select-selection-item) {
  306. line-height: 34px !important;
  307. color: #e0e6ed;
  308. }
  309. }
  310. :deep(.zxm-input),
  311. :deep(.zxm-input-number),
  312. :deep(textarea.zxm-input) {
  313. background: rgba(10, 132, 255, 0.08);
  314. border: 1px solid rgba(63, 80, 106, 0.4);
  315. border-radius: 6px;
  316. color: #e0e6ed;
  317. &:hover,
  318. &:focus {
  319. border-color: rgba(10, 132, 255, 0.5);
  320. }
  321. }
  322. .form-input-number {
  323. width: 100%;
  324. }
  325. .cron-input {
  326. font-family: 'Courier New', monospace;
  327. }
  328. .form-hint {
  329. font-size: 12px;
  330. color: #6a7a8a;
  331. margin-top: 2px;
  332. }
  333. .mode-switch {
  334. display: flex;
  335. gap: 0;
  336. border-radius: 6px;
  337. overflow: hidden;
  338. border: 1px solid rgba(63, 80, 106, 0.4);
  339. width: fit-content;
  340. .mode-btn {
  341. padding: 6px 20px;
  342. font-size: 13px;
  343. color: #8b949e;
  344. cursor: pointer;
  345. transition: all 0.2s;
  346. background: rgba(10, 132, 255, 0.04);
  347. &:hover {
  348. color: #e0e6ed;
  349. }
  350. &.active {
  351. background: rgba(10, 132, 255, 0.2);
  352. color: #5cb8ff;
  353. }
  354. }
  355. }
  356. .schedule-preview {
  357. padding: 10px 14px;
  358. background: rgba(10, 132, 255, 0.06);
  359. border: 1px solid rgba(10, 132, 255, 0.15);
  360. border-radius: 6px;
  361. font-size: 13px;
  362. .preview-label {
  363. color: #6a7a8a;
  364. }
  365. .preview-value {
  366. color: #5cb8ff;
  367. font-weight: 500;
  368. }
  369. }
  370. .form-actions {
  371. display: flex;
  372. justify-content: flex-end;
  373. gap: 10px;
  374. padding-top: 8px;
  375. }
  376. </style>
  377. <style lang="less">
  378. .create-schedule-modal {
  379. .zxm-modal-content {
  380. background: linear-gradient(135deg, #0a1628 0%, #0d2137 50%, #0a1e35 100%);
  381. border: 1px solid rgba(63, 80, 106, 0.4);
  382. border-radius: 12px;
  383. }
  384. .zxm-modal-header {
  385. background: transparent;
  386. border-bottom: 1px solid rgba(63, 80, 106, 0.25);
  387. padding: 18px 24px;
  388. border-radius: 10px 10px 0 0;
  389. }
  390. .zxm-modal-title {
  391. color: #e8edf3;
  392. font-size: 18px;
  393. font-weight: 600;
  394. }
  395. .zxm-modal-body {
  396. padding: 24px !important;
  397. }
  398. .zxm-modal-close {
  399. .zxm-modal-close-x {
  400. color: #8b949e;
  401. }
  402. }
  403. }
  404. </style>