From d0c6f836ffe5daae0e69ea6e3b56cbf928bcf780 Mon Sep 17 00:00:00 2001 From: duorameng <2997944583@qq.com> Date: Fri, 5 Jun 2026 19:58:37 +0800 Subject: [PATCH] refactor(web): extract common config components for task and repo dialogs --- mise.toml | 2 + web/src/views/tasks/RepoDialog.vue | 358 ++---------------- web/src/views/tasks/TaskDialog.vue | 287 +------------- .../tasks/components/TaskAdvancedConfig.vue | 195 ++++++++++ .../views/tasks/components/TaskCronConfig.vue | 66 ++++ .../views/tasks/components/TaskLangConfig.vue | 166 ++++++++ .../views/tasks/components/TaskTagsConfig.vue | 57 +++ 7 files changed, 529 insertions(+), 602 deletions(-) create mode 100644 mise.toml create mode 100644 web/src/views/tasks/components/TaskAdvancedConfig.vue create mode 100644 web/src/views/tasks/components/TaskCronConfig.vue create mode 100644 web/src/views/tasks/components/TaskLangConfig.vue create mode 100644 web/src/views/tasks/components/TaskTagsConfig.vue diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..ac7816d --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +go = "1.25" diff --git a/web/src/views/tasks/RepoDialog.vue b/web/src/views/tasks/RepoDialog.vue index d430998..4472e58 100644 --- a/web/src/views/tasks/RepoDialog.vue +++ b/web/src/views/tasks/RepoDialog.vue @@ -8,15 +8,17 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Switch } from '@/components/ui/switch' import { Checkbox } from '@/components/ui/checkbox' import { ScrollArea } from '@/components/ui/scroll-area' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import DirTreeSelect from '@/components/DirTreeSelect.vue' -import { X, Globe, GitBranch, Shield, Zap, Clock, Download, Plus, Search, Check, ChevronsUpDown, Loader2, AlertCircle, Terminal } from 'lucide-vue-next' -import { api, type Task, type RepoConfig, type Agent, type MiseLanguage } from '@/api' +import { Globe, GitBranch, Shield, Zap, Download, AlertCircle, Terminal } from 'lucide-vue-next' +import { api, type Task, type RepoConfig, type Agent } from '@/api' import { toast } from 'vue-sonner' import { cn } from '@/lib/utils' -import { getCronDescription } from '@/utils/cron' + import { parseBaihuCommand, parseQlCommand } from '@/utils/repo-parser' import TaskNotificationConfig from './components/TaskNotificationConfig.vue' +import TaskAdvancedConfig from './components/TaskAdvancedConfig.vue' +import TaskCronConfig from './components/TaskCronConfig.vue' +import TaskTagsConfig from './components/TaskTagsConfig.vue' const notificationConfigRef = ref | null>(null) @@ -31,17 +33,7 @@ const emit = defineEmits<{ 'saved': [] }>() -const cronPresets = [ - { label: '每5秒', value: '*/5 * * * * *' }, - { label: '每30秒', value: '*/30 * * * * *' }, - { label: '每分钟', value: '0 * * * * *' }, - { label: '每5分钟', value: '0 */5 * * * *' }, - { label: '每小时', value: '0 0 * * * *' }, - { label: '每天0点', value: '0 0 0 * * *' }, - { label: '每天8点', value: '0 0 8 * * *' }, - { label: '每周一', value: '0 0 0 * * 1' }, - { label: '每月1号', value: '0 0 0 1 * *' }, -] + const proxyOptions = [ { label: '不使用代理', value: 'none' }, @@ -70,11 +62,10 @@ const repoConfig = ref({ repo_source: '', proxy: '' }) -const cleanType = ref('none') -const cleanKeep = ref(30) + const allAgents = ref([]) const selectedAgentId = ref('local') -const tagInput = ref('') + const autoAddCron = computed({ get: () => !!repoConfig.value.auto_add_cron, @@ -91,66 +82,7 @@ const pullQlConfig = computed({ }) // === 语言环境相关 === -const installedLangs = ref([]) -const loadingLangs = ref(false) const selectedLangs = ref<{ name: string; version: string; availableVersions: string[] }[]>([]) -const availablePlugins = ref([]) -const pluginSearch = ref('') -const versionSearch = ref('') - -const filteredPlugins = computed(() => { - if (!pluginSearch.value) return availablePlugins.value - const s = pluginSearch.value.toLowerCase() - return availablePlugins.value.filter((p: string) => p.toLowerCase().includes(s)) -}) - -function getFilteredVersions(versions: string[]) { - if (!versionSearch.value) return versions - const s = versionSearch.value.toLowerCase() - return versions.filter((v: string) => v.toLowerCase().includes(s)) -} - -async function fetchInstalledLangs() { - loadingLangs.value = true - try { - installedLangs.value = await api.mise.list() - const plugins = new Set() - installedLangs.value.forEach((l: MiseLanguage) => plugins.add(l.plugin)) - availablePlugins.value = Array.from(plugins).sort() - } catch (e) { - console.error('Fetch installed langs failed', e) - } finally { - loadingLangs.value = false - } -} - -import { getLangIcon } from '@/utils/icons' -function updateAvailableVersions(lang: { name: string; version: string; availableVersions: string[] }) { - if (lang.name) { - lang.availableVersions = installedLangs.value - .filter((l: MiseLanguage) => l.plugin === lang.name) - .map((l: MiseLanguage) => l.version) - .sort((a: string, b: string) => b.localeCompare(a, undefined, { numeric: true })) - } else { - lang.availableVersions = [] - } -} - -function addLang() { - selectedLangs.value.push({ name: '', version: '', availableVersions: [] }) -} - -function removeLang(index: number) { - selectedLangs.value.splice(index, 1) -} - -function updateLangName(index: number, name: string) { - const lang = selectedLangs.value[index] - if (!lang) return - lang.name = name - lang.version = '' // reset version - updateAvailableVersions(lang) -} const showQlImportDialog = ref(false) const qlCommandInput = ref('') @@ -220,7 +152,6 @@ function submitBaihuImport() { version: l.version || '', availableVersions: [] })) - selectedLangs.value.forEach(l => updateAvailableVersions(l)) } // toast.success('命令解析成功,已自动填充表单') @@ -251,38 +182,9 @@ function submitQlImport() { showQlImportDialog.value = false } -const cronDescription = computed(() => { - if (!form.value.schedule) return '' - return getCronDescription(form.value.schedule, (navigator as any).language) -}) - -function addTag() { - const val = tagInput.value.trim() - if (!val) return - const currentTags = form.value.tags ? form.value.tags.split(',').filter(Boolean) : [] - if (!currentTags.includes(val)) { - currentTags.push(val) - form.value.tags = currentTags.join(',') - } - tagInput.value = '' -} - -function removeTag(tagToRemove: string) { - const currentTags = form.value.tags ? form.value.tags.split(',').filter(Boolean) : [] - form.value.tags = currentTags.filter((t: string) => t !== tagToRemove).join(',') -} -const concurrencyEnabled = computed({ - get: () => repoConfig.value.concurrency === 1, - set: (val: boolean) => { - repoConfig.value.concurrency = val ? 1 : 0 - } -}) -function onConcurrencyChange(val: boolean) { - concurrencyEnabled.value = val -} const isSingleFile = computed({ get: () => !!repoConfig.value.single_file, @@ -291,11 +193,6 @@ const isSingleFile = computed({ } }) -const cleanConfig = computed(() => { - if (!cleanType.value || cleanType.value === 'none' || cleanKeep.value <= 0) return '' - return JSON.stringify({ type: cleanType.value, keep: cleanKeep.value }) -}) - watch(() => props.open, async (val: boolean) => { if (val) { form.value = { @@ -308,20 +205,7 @@ watch(() => props.open, async (val: boolean) => { post_command: props.task?.post_command ?? '', ...props.task } - // 解析清理配置 - if (props.task?.clean_config) { - try { - const config = JSON.parse(props.task.clean_config) - cleanType.value = config.type || 'none' - cleanKeep.value = config.keep || 30 - } catch { - cleanType.value = 'none' - cleanKeep.value = 30 - } - } else { - cleanType.value = 'none' - cleanKeep.value = 30 - } + // 解析仓库配置 // 解析仓库配置 const defaultConfig: RepoConfig = { @@ -347,12 +231,7 @@ watch(() => props.open, async (val: boolean) => { if (configStr) { try { const parsed = JSON.parse(configStr) - // 兼容旧字段: 优先使用 $task_concurrency, 若无则默认 1 - let concurrency = 1 - if (parsed['$task_concurrency'] !== undefined) { - concurrency = parsed['$task_concurrency'] === 1 ? 1 : 0 - } - repoConfig.value = { ...defaultConfig, ...parsed, concurrency } + repoConfig.value = { ...defaultConfig, ...parsed } } catch { repoConfig.value = defaultConfig } @@ -374,12 +253,6 @@ watch(() => props.open, async (val: boolean) => { selectedAgentId.value = 'local' // 加载 Agent 列表 await loadAgents() - if (selectedAgentId.value === 'local') { - await fetchInstalledLangs() - selectedLangs.value.forEach((lang: { name: string; version: string; availableVersions: string[] }) => { - updateAvailableVersions(lang) - }) - } // 加载通知配置 await notificationConfigRef.value?.loadConfig(props.isEdit ? props.task?.id : undefined) } @@ -400,14 +273,13 @@ async function save() { } try { - form.value.clean_config = cleanConfig.value - form.value.type = 'repo' - // 确保 concurrency 字段被正确保存到 config 中 - // 注意:我们将 concurrency 存储在 config 的 $task_concurrency 字段中 - // 同时也保留在 repoConfig 对象中以便回显 + let existingConfig = {} + if (form.value.config) { + try { existingConfig = JSON.parse(form.value.config) } catch {} + } const configToSave: any = { - ...repoConfig.value, - '$task_concurrency': concurrencyEnabled.value ? 1 : 0 + ...existingConfig, + ...repoConfig.value } // 保存语言环境 @@ -481,28 +353,7 @@ async function save() { -
- -
-
-
- - -
-
-
- - {{ tag }} - - -
-
-
+ @@ -684,87 +535,7 @@ async function save() {

同步后生成的任务将自动继承此运行环境。如果不指定语言版本,某些依赖特定语言的脚本(如 js, py)将无法顺利解析和运行!

-
-
- - - - - -
-
- - -
-
- -
- -
-
- 未找到匹配项 -
- -
-
-
- - - - - - -
-
- - -
-
- -
- 无可用版本 -
- -
-
-
- - -
- - +
@@ -778,78 +549,10 @@ async function save() {
-
- -
- -
- - {{ cronDescription }} -
-
-
- 格式指导: 秒 分 时 日 月 周 -
-
- -
-
-
-
+ -
- -
-
- - -
-
- 避免高频并发,在基准时间点后延迟 0~{{ form.random_range || 0 }}s -
-
-
- -
- -
-
- - 分钟超时 -
-
-
- -
- -
-
- -
- - {{ cleanType === 'day' ? '天' : '条' }} -
-
-
-
- -
- -
- + + +
diff --git a/web/src/views/tasks/TaskDialog.vue b/web/src/views/tasks/TaskDialog.vue index 5daebb1..d129bc3 100644 --- a/web/src/views/tasks/TaskDialog.vue +++ b/web/src/views/tasks/TaskDialog.vue @@ -9,15 +9,18 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { ScrollArea } from '@/components/ui/scroll-area' import DirTreeSelect from '@/components/DirTreeSelect.vue' -import { Plus, ChevronDown, X, Search, Check, ChevronsUpDown, AlertCircle, Terminal, Zap, Loader2, Lock, Variable } from 'lucide-vue-next' +import { Plus, X, ChevronDown, Search, AlertCircle, Terminal, Zap, Lock, Variable } from 'lucide-vue-next' import { Badge } from '@/components/ui/badge' -import TagInput from '@/components/TagInput.vue' import { cn } from '@/lib/utils' -import { api, type Task, type EnvVar, type Agent, type MiseLanguage } from '@/api' +import { api, type Task, type EnvVar, type Agent } from '@/api' import { PATHS, TRIGGER_TYPE } from '@/constants' import { toast } from 'vue-sonner' -import { getCronDescription } from '@/utils/cron' + import TaskNotificationConfig from './components/TaskNotificationConfig.vue' +import TaskAdvancedConfig from './components/TaskAdvancedConfig.vue' +import TaskCronConfig from './components/TaskCronConfig.vue' +import TaskLangConfig from './components/TaskLangConfig.vue' +import TaskTagsConfig from './components/TaskTagsConfig.vue' const props = defineProps<{ open: boolean @@ -30,22 +33,9 @@ const emit = defineEmits<{ 'saved': [] }>() -const cronPresets = [ - { label: '每5秒', value: '*/5 * * * * *' }, - { label: '每30秒', value: '*/30 * * * * *' }, - { label: '每分钟', value: '0 * * * * *' }, - { label: '每5分钟', value: '0 */5 * * * *' }, - { label: '每小时', value: '0 0 * * * *' }, - { label: '每天0点', value: '0 0 0 * * *' }, - { label: '每天8点', value: '0 0 8 * * *' }, - { label: '每周一', value: '0 0 0 * * 1' }, - { label: '每月1号', value: '0 0 0 1 * *' }, -] + const form = ref>({}) -const tagInput = ref('') -const cleanType = ref('none') -const cleanKeep = ref(30) const allEnvVars = ref([]) const allAgents = ref([]) const selectedEnvIds = ref([]) @@ -54,23 +44,10 @@ const selectedTriggerType = ref('cron') const envSearchQuery = ref('') // 为每个执行位置保存独立的工作目录配置 const workDirCache = ref>({}) -const concurrency = ref(0) -const concurrencyEnabled = ref(false) const commentToTaskEnabled = ref(false) const allEnvsEnabled = ref(false) const scriptsDir = ref(PATHS.SCRIPTS_DIR) -const cronDescription = computed(() => { - if (!form.value.schedule) return '' - return getCronDescription(form.value.schedule, (navigator as any).language) -}) - - - -// 监听 concurrencyEnabled 的变化,同步到 concurrency -watch(concurrencyEnabled, (val: boolean) => { - concurrency.value = val ? 1 : 0 -}) @@ -78,21 +55,7 @@ function onAllEnvsChange(val: boolean) { allEnvsEnabled.value = val } -function addTag(passedTag?: string) { - const val = (passedTag || tagInput.value).trim() - if (!val) return - const currentTags = form.value.tags ? form.value.tags.split(',').filter(Boolean) : [] - if (!currentTags.includes(val)) { - currentTags.push(val) - form.value.tags = currentTags.join(',') - } - tagInput.value = '' -} -function removeTag(tagToRemove: string) { - const currentTags = form.value.tags ? form.value.tags.split(',').filter(Boolean) : [] - form.value.tags = currentTags.filter((t: string) => t !== tagToRemove).join(',') -} // 当前显示的工作目录(根据选择的执行位置) const currentWorkDir = computed({ @@ -102,10 +65,6 @@ const currentWorkDir = computed({ } }) -const cleanConfig = computed(() => { - if (!cleanType.value || cleanType.value === 'none' || cleanKeep.value <= 0) return '' - return JSON.stringify({ type: cleanType.value, keep: cleanKeep.value }) -}) const filteredEnvVars = computed(() => { return allEnvVars.value.filter((env: EnvVar) => { @@ -128,72 +87,12 @@ const onlineAgents = computed(() => { return allAgents.value.filter((a: Agent) => a.enabled) }) -// 语言环境相关 -const installedLangs = ref([]) -const loadingLangs = ref(false) + + const selectedLangs = ref<{ name: string; version: string; availableVersions: string[] }[]>([]) -const availablePlugins = ref([]) -const pluginSearch = ref('') -const versionSearch = ref('') - const notificationConfigRef = ref | null>(null) -const filteredPlugins = computed(() => { - if (!pluginSearch.value) return availablePlugins.value - const s = pluginSearch.value.toLowerCase() - return availablePlugins.value.filter((p: string) => p.toLowerCase().includes(s)) -}) - -function getFilteredVersions(versions: string[]) { - if (!versionSearch.value) return versions - const s = versionSearch.value.toLowerCase() - return versions.filter((v: string) => v.toLowerCase().includes(s)) -} - -async function fetchInstalledLangs() { - loadingLangs.value = true - try { - installedLangs.value = await api.mise.list() - const plugins = new Set() - installedLangs.value.forEach((l: MiseLanguage) => plugins.add(l.plugin)) - availablePlugins.value = Array.from(plugins).sort() - } catch (e) { - console.error('Fetch installed langs failed', e) - } finally { - loadingLangs.value = false - } -} - -import { getLangIcon } from '@/utils/icons' - -function updateAvailableVersions(lang: { name: string; version: string; availableVersions: string[] }) { - if (lang.name) { - lang.availableVersions = installedLangs.value - .filter((l: MiseLanguage) => l.plugin === lang.name) - .map((l: MiseLanguage) => l.version) - .sort((a: string, b: string) => b.localeCompare(a, undefined, { numeric: true })) - } else { - lang.availableVersions = [] - } -} - -function addLang() { - selectedLangs.value.push({ name: '', version: '', availableVersions: [] }) -} - -function removeLang(index: number) { - selectedLangs.value.splice(index, 1) -} - -function updateLangName(index: number, name: string) { - const lang = selectedLangs.value[index] - if (!lang) return - lang.name = name - lang.version = '' // reset version - updateAvailableVersions(lang) -} - watch(() => props.open, async (val: boolean) => { if (val) { form.value = { @@ -206,20 +105,7 @@ watch(() => props.open, async (val: boolean) => { post_command: props.task?.post_command ?? '', ...props.task } - // 解析清理配置 - if (props.task?.clean_config) { - try { - const config = JSON.parse(props.task.clean_config) - cleanType.value = config.type || 'none' - cleanKeep.value = config.keep || 30 - } catch { - cleanType.value = 'none' - cleanKeep.value = 30 - } - } else { - cleanType.value = 'none' - cleanKeep.value = 30 - } + // 配置清理会在 TaskAdvancedConfig 中自动处理 // 解析任务配置 try { // 确保 config 是有效的 JSON 对象字符串 @@ -232,30 +118,17 @@ watch(() => props.open, async (val: boolean) => { const parsed = JSON.parse(configStr) // 确保解析结果是对象 if (parsed && typeof parsed === 'object') { - const val = parsed['$task_concurrency'] - if (typeof val === 'number') { - // 如果已存在并发配置,直接使用(0 或 1) - concurrency.value = val - concurrencyEnabled.value = val === 1 - } else { - // 默认值:允许并发 - concurrency.value = 1 - concurrencyEnabled.value = true - } // 解析全部环境变量配置 allEnvsEnabled.value = !!parsed['$task_all_envs'] // 解析注释解析配置 commentToTaskEnabled.value = !!parsed['$task_comment_to_task'] } else { - concurrency.value = 1 - concurrencyEnabled.value = true allEnvsEnabled.value = false commentToTaskEnabled.value = false } } catch { - concurrency.value = 1 - concurrencyEnabled.value = true + // ignore } // 解析环境变量 if (props.task?.envs) { @@ -290,13 +163,7 @@ watch(() => props.open, async (val: boolean) => { ? normalizeLocalWorkDirForDisplay(props.task?.work_dir) : (props.task?.work_dir || '') } - if (selectedAgentId.value === 'local') { - await fetchInstalledLangs() - // 更新所有语言的可用版本 - selectedLangs.value.forEach((lang: { name: string; version: string; availableVersions: string[] }) => { - updateAvailableVersions(lang) - }) - } + // 加载通知配置 await notificationConfigRef.value?.loadConfig(props.isEdit ? props.task?.id : undefined) } @@ -355,7 +222,6 @@ function encodeLocalWorkDir(workDir?: string | null): string { async function save() { try { - form.value.clean_config = cleanConfig.value form.value.envs = selectedEnvIds.value.join(',') form.value.type = 'task' form.value.trigger_type = selectedTriggerType.value @@ -382,8 +248,6 @@ async function save() { } } - // 更新并发控制字段 (1: 开启, 0: 关闭) - config['$task_concurrency'] = concurrencyEnabled.value ? 1 : 0 // 更新注入全部环境变量字段 config['$task_all_envs'] = !!allEnvsEnabled.value // 更新注释解析字段 @@ -443,24 +307,7 @@ async function save() {
-
- -
-
- - {{ tag }} - - -
- -
-
-
-
+
@@ -511,30 +358,10 @@ async function save() {
-
+
-
-
- - - -
- -
- -
-
-
- - - - - - - -
- +
@@ -623,87 +450,11 @@ async function save() {
-
- -
-
- - 分钟超时 -
-
-
-
- -
-
- 重试 - - 次,间隔 - - -
-
-
-
- -
-
- -
- - {{ cleanType === 'day' ? '天' : '条' }} -
-
-
-
-
- -
-
-
-
- - 并发控制 -
- -
-

- {{ concurrencyEnabled ? '允许同时开启多个副本。' : '当前任务未结束时,新触发将被静默忽略。' }} -

-
-
-
+ +
diff --git a/web/src/views/tasks/components/TaskAdvancedConfig.vue b/web/src/views/tasks/components/TaskAdvancedConfig.vue new file mode 100644 index 0000000..89e7dce --- /dev/null +++ b/web/src/views/tasks/components/TaskAdvancedConfig.vue @@ -0,0 +1,195 @@ + + + diff --git a/web/src/views/tasks/components/TaskCronConfig.vue b/web/src/views/tasks/components/TaskCronConfig.vue new file mode 100644 index 0000000..bd68d1b --- /dev/null +++ b/web/src/views/tasks/components/TaskCronConfig.vue @@ -0,0 +1,66 @@ + + + diff --git a/web/src/views/tasks/components/TaskLangConfig.vue b/web/src/views/tasks/components/TaskLangConfig.vue new file mode 100644 index 0000000..33b1f37 --- /dev/null +++ b/web/src/views/tasks/components/TaskLangConfig.vue @@ -0,0 +1,166 @@ + + + diff --git a/web/src/views/tasks/components/TaskTagsConfig.vue b/web/src/views/tasks/components/TaskTagsConfig.vue new file mode 100644 index 0000000..6ae31e1 --- /dev/null +++ b/web/src/views/tasks/components/TaskTagsConfig.vue @@ -0,0 +1,57 @@ + + +