From 00a608c149e1477cfbcb1773cc66715b3da8578a Mon Sep 17 00:00:00 2001 From: CNWei Date: Sun, 18 Oct 2026 22:52:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/e2e/dashboard.spec.ts | 23 ++- frontend/src/App.css | 68 ++++++-- frontend/src/App.test.tsx | 13 +- frontend/src/components/TaskEditorModal.tsx | 172 ++++++++++++++------ frontend/src/lib/taskForm.test.ts | 2 +- frontend/src/lib/taskForm.ts | 39 +++-- 6 files changed, 235 insertions(+), 82 deletions(-) diff --git a/frontend/e2e/dashboard.spec.ts b/frontend/e2e/dashboard.spec.ts index 64e7484..da15bf9 100644 --- a/frontend/e2e/dashboard.spec.ts +++ b/frontend/e2e/dashboard.spec.ts @@ -56,6 +56,27 @@ test('manages a task and reads a real run log', async ({ page, request }) => { path: 'test-results/dashboard-task-editor.png', fullPage: true, }) + const addArgumentButton = newTaskDialog.getByRole('button', { + name: '+ 添加参数', + }) + await addArgumentButton.focus() + await page.keyboard.press('Enter') + await newTaskDialog + .getByPlaceholder('参数名 (如 --mode 或 -v)') + .fill('--mode') + await newTaskDialog + .getByPlaceholder('参数值 (可选,无值可留空)') + .fill('full') + await newTaskDialog + .getByText('调用预览 (Runtime Preview)') + .scrollIntoViewIfNeeded() + await expect( + newTaskDialog.getByText('--mode full', { exact: false }), + ).toBeVisible() + await page.screenshot({ + path: 'test-results/dashboard-task-editor-runtime.png', + fullPage: true, + }) await page.keyboard.press('Escape') await expect(newTaskDialog).toBeHidden() @@ -75,7 +96,7 @@ test('manages a task and reads a real run log', async ({ page, request }) => { name: '编辑任务 [e2e_echo]', }) await expect(editor).toBeVisible() - const nameInput = editor.getByLabel('任务名称') + const nameInput = editor.getByLabel('名称') await nameInput.fill('E2E updated') await editor.getByRole('button', { name: '保存修改' }).click() await expect(page.getByText('E2E updated')).toBeVisible() diff --git a/frontend/src/App.css b/frontend/src/App.css index 2de5b02..97944d8 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -648,6 +648,12 @@ code { max-height: 75vh; } +.task-editor .modal-body { + min-height: 0; + flex: 1; + max-height: none; +} + .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -659,6 +665,10 @@ code { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 0.7fr); } +.form-grid-single { + grid-template-columns: 1fr; +} + .cron-header-row { display: flex; align-items: center; @@ -804,7 +814,7 @@ code { font-size: 10px; } -.action-panel { +.dynamic-fields { margin: 22px 0 0; border: 1px solid #eeeff2; border-radius: 12px; @@ -812,15 +822,6 @@ code { background: #fafafa; } -.action-panel legend { - padding: 0 8px; - color: #374151; - font-family: var(--font-sans); - font-size: 13px; - font-weight: 600; - letter-spacing: 0; -} - .collection-header { display: flex; align-items: flex-start; @@ -853,7 +854,7 @@ code { } .argument-row { - grid-template-columns: 1fr auto; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr) auto; } .key-value-row { @@ -866,11 +867,46 @@ code { font-size: 12px; } -.form-actions { +.cli-preview { + margin-top: 18px; + border-radius: 8px; + padding: 14px 18px; + background: #18191c; +} + +.cli-preview-header { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + border-bottom: 1px solid #282a30; + padding-bottom: 6px; + color: #71767f; + font-size: 11px; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.cli-preview-body { + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.6; +} + +.cli-line-env { + margin-bottom: 4px; + color: #9da3af; +} + +.cli-line-cmd { + color: #34d399; + font-weight: 500; + overflow-wrap: anywhere; +} + +.modal-footer { display: flex; justify-content: flex-end; gap: 8px; - margin: 24px -28px -24px; border-top: 1px solid var(--border); padding: 16px 28px; background: #fafafb; @@ -1205,8 +1241,7 @@ code { padding: 18px; } - .form-actions { - margin: 24px -18px -18px; + .modal-footer { padding: 16px 18px; } @@ -1215,7 +1250,8 @@ code { grid-template-columns: 1fr; } - .key-value-row { + .key-value-row, + .argument-row { grid-template-columns: 1fr; } diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index cc10ca5..62b301f 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -126,9 +126,16 @@ describe('TaskDashboard', () => { renderApp() await user.click(screen.getByRole('button', { name: '+ 新建任务' })) - await user.clear(screen.getByLabelText('任务标识')) - await user.type(screen.getByLabelText('任务标识'), 'new_task') - await user.type(screen.getByLabelText('任务名称'), 'New task') + await user.click(screen.getByRole('button', { name: '+ 添加参数' })) + expect( + screen.getByPlaceholderText('参数名 (如 --mode 或 -v)'), + ).toBeInTheDocument() + expect( + screen.getByPlaceholderText('参数值 (可选,无值可留空)'), + ).toBeInTheDocument() + await user.clear(screen.getByLabelText(/任务标识/)) + await user.type(screen.getByLabelText(/任务标识/), 'new_task') + await user.type(screen.getByLabelText('名称'), 'New task') await user.type(screen.getByLabelText('工作目录'), 'D:\\Projects\\new') await user.click( diff --git a/frontend/src/components/TaskEditorModal.tsx b/frontend/src/components/TaskEditorModal.tsx index ee2f062..4aeb5d9 100644 --- a/frontend/src/components/TaskEditorModal.tsx +++ b/frontend/src/components/TaskEditorModal.tsx @@ -13,6 +13,7 @@ import { mapServerFieldToFormField, taskFormSchema, taskToFormValues, + type KeyValueFormValue, type TaskFormValues, } from '../lib/taskForm' import { useFocusTrap } from '../lib/useFocusTrap' @@ -53,6 +54,7 @@ export function TaskEditorModal({ const actionType = watch('actionType') const cronValue = watch('cron') const timezoneValue = watch('timezone') + const scriptPreview = watch('script') const debouncedCron = useDebouncedValue(cronValue, 300) const debouncedTimezone = useDebouncedValue(timezoneValue, 300) const cronPreview = useQuery({ @@ -169,7 +171,12 @@ export function TaskEditorModal({ -
+ {submitError ? (
{submitError} @@ -177,7 +184,10 @@ export function TaskEditorModal({ ) : null}
- + - + )}
- +
-
+
- +
- - {actionType === 'script' ? ( -
- 脚本配置 +
scriptArgs.append({ value: '' })} + onClick={() => + scriptArgs.append({ key: '', value: '' }) + } > + 添加参数 @@ -352,10 +357,15 @@ export function TaskEditorModal({
{scriptArgs.fields.map((field, index) => (
+
+ +
+
+ 调用预览 (Runtime Preview) + Windows 原生环境 +
+
+ {scriptPreview.env + .filter((item) => item.key.trim()) + .map((item, index) => ( +
+ set {item.key.trim()}= + {isSensitiveName(item.key) + ? '[REDACTED]' + : item.value || '""'} +
+ ))} +
+ {scriptPreview.interpreter.trim() || 'python.exe'}{' '} + {scriptPreview.script.trim() || 'main.py'} + {formatArgumentPreview(scriptPreview.args)} +
+
+
+
) : null} {actionType === 'http' ? ( -
- HTTP 配置 +
httpHeaders.append({ key: '', value: '' })} onRemove={httpHeaders.remove} /> -
+ ) : null} {actionType === 'process' ? ( -
- 进程配置 +
processArgs.append({ value: '' })} + onClick={() => + processArgs.append({ key: '', value: '' }) + } > + 添加参数 @@ -477,10 +514,15 @@ export function TaskEditorModal({
{processArgs.fields.map((field, index) => (
+
+ ) : null} -
- - -
+
+ + ) } +function formatArgumentPreview(argumentsList: KeyValueFormValue[]): string { + const values = argumentsList.flatMap(({ key, value }) => { + const argument = key.trim() + const argumentValue = value.trim() + return [ + argument, + argumentValue.includes(' ') ? `"${argumentValue}"` : argumentValue, + ].filter(Boolean) + }) + return values.length ? ` ${values.join(' ')}` : '' +} + +function isSensitiveName(name: string): boolean { + const normalized = name.toUpperCase() + return [ + 'SECRET', + 'TOKEN', + 'PASSWORD', + 'PASS', + 'API_KEY', + 'PRIVATE_KEY', + 'CREDENTIAL', + 'AUTHORIZATION', + 'AUTH', + ].some((marker) => normalized.includes(marker)) +} + interface FieldProps { label: string error?: string diff --git a/frontend/src/lib/taskForm.test.ts b/frontend/src/lib/taskForm.test.ts index 22b30ae..e185823 100644 --- a/frontend/src/lib/taskForm.test.ts +++ b/frontend/src/lib/taskForm.test.ts @@ -35,7 +35,7 @@ describe('task form payload', () => { values.name = 'Worker' values.actionType = 'process' values.process.executable = 'C:\\Tools\\worker.exe' - values.process.args = [{ value: '--port' }, { value: '8080' }] + values.process.args = [{ key: '--port', value: '8080' }] values.process.env = [{ key: 'WORKER_MODE', value: 'local' }] const payload = valuesToCreateRequest(values) diff --git a/frontend/src/lib/taskForm.ts b/frontend/src/lib/taskForm.ts index 38fdcd8..395127d 100644 --- a/frontend/src/lib/taskForm.ts +++ b/frontend/src/lib/taskForm.ts @@ -12,10 +12,6 @@ export interface KeyValueFormValue { value: string } -export interface ArgumentFormValue { - value: string -} - export interface TaskFormValues { id: string name: string @@ -28,7 +24,7 @@ export interface TaskFormValues { working_dir: string interpreter: string script: string - args: ArgumentFormValue[] + args: KeyValueFormValue[] env: KeyValueFormValue[] } http: { @@ -41,7 +37,7 @@ export interface TaskFormValues { process: { executable: string working_dir: string - args: ArgumentFormValue[] + args: KeyValueFormValue[] env: KeyValueFormValue[] } } @@ -92,7 +88,7 @@ export const taskFormSchema = z working_dir: z.string(), interpreter: z.string(), script: z.string(), - args: z.array(z.object({ value: z.string() })), + args: z.array(keyValueSchema), env: z.array(keyValueSchema), }), http: z.object({ @@ -105,7 +101,7 @@ export const taskFormSchema = z process: z.object({ executable: z.string(), working_dir: z.string(), - args: z.array(z.object({ value: z.string() })), + args: z.array(keyValueSchema), env: z.array(keyValueSchema), }), }) @@ -208,7 +204,7 @@ export function taskToFormValues(task: Task): TaskFormValues { working_dir: task.action.working_dir, interpreter: task.action.interpreter, script: task.action.script, - args: (task.action.args ?? []).map((value) => ({ value })), + args: argsToRows(task.action.args ?? []), env: Object.entries(task.action.env ?? {}).map(([key, value]) => ({ key, value, @@ -228,7 +224,7 @@ export function taskToFormValues(task: Task): TaskFormValues { values.process = { executable: task.action.executable, working_dir: task.action.working_dir ?? '', - args: (task.action.args ?? []).map((value) => ({ value })), + args: argsToRows(task.action.args ?? []), env: Object.entries(task.action.env ?? {}).map(([key, value]) => ({ key, value, @@ -246,7 +242,7 @@ export function valuesToAction(values: TaskFormValues): TaskAction { working_dir: values.script.working_dir.trim(), interpreter: values.script.interpreter.trim(), script: values.script.script.trim(), - args: cleanList(values.script.args.map((argument) => argument.value)), + args: argumentsToArray(values.script.args), env: keyValuesToObject(values.script.env), } } @@ -271,7 +267,7 @@ export function valuesToAction(values: TaskFormValues): TaskAction { type: 'process', executable: values.process.executable.trim(), working_dir: values.process.working_dir.trim() || null, - args: cleanList(values.process.args.map((argument) => argument.value)), + args: argumentsToArray(values.process.args), env: keyValuesToObject(values.process.env), } } @@ -318,8 +314,23 @@ export function mapServerFieldToFormField( return `${actionType}.${actionField}` } -function cleanList(values: string[]): string[] { - return values.map((value) => value.trim()).filter(Boolean) +function argumentsToArray(values: KeyValueFormValue[]): string[] { + return values.flatMap(({ key, value }) => { + const argument = key.trim() + const argumentValue = value.trim() + return [argument, argumentValue].filter(Boolean) + }) +} + +function argsToRows(values: string[]): KeyValueFormValue[] { + const rows: KeyValueFormValue[] = [] + for (let index = 0; index < values.length; index += 2) { + rows.push({ + key: values[index] ?? '', + value: values[index + 1] ?? '', + }) + } + return rows } function keyValuesToObject(