feat(frontend): 自动回填并支持修改虚拟环境目录
- 虚拟环境目录与解释器移到工作目录正下方,虚拟环境目录优先且更宽 - 探测到虚拟环境时自动填入(工作目录内用相对路径 .venv),仍可手动修改 - 手动改过的值不再被探测结果覆盖;换工作目录只清理上一轮自动填入的值并重新探测 - 防抖期间忽略过期探测结果,避免旧环境被回填
This commit is contained in:
@@ -211,6 +211,11 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 虚拟环境目录通常是一条完整路径,给它更多宽度;解释器名很短 */
|
||||
.venv-grid {
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 0.9fr);
|
||||
}
|
||||
|
||||
.form-select {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -889,6 +894,10 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.venv-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cron-run-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Textarea,
|
||||
TextButton,
|
||||
} from '@stash/ui'
|
||||
import { useEffect, useState, type ReactNode } from 'react'
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import {
|
||||
Controller,
|
||||
type FieldPath,
|
||||
@@ -87,9 +87,13 @@ export function TaskEditorModal({
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [cronPreviewCount, setCronPreviewCount] = useState(5)
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
// 记录「自动填入」的虚拟环境目录:用户手动改过就不再覆盖;
|
||||
// 工作目录变化时只有仍等于该值的字段会被清掉,交给新目录重新探测。
|
||||
const autoVenvRef = useRef('')
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
getValues,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
@@ -181,8 +185,49 @@ export function TaskEditorModal({
|
||||
setCronPreviewCount(5)
|
||||
setSubmitError(null)
|
||||
setPickerOpen(false)
|
||||
autoVenvRef.current = ''
|
||||
}, [open, reset, task])
|
||||
|
||||
// 换工作目录时清掉上一轮自动填入的值(手动填写的值不动)。
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return
|
||||
}
|
||||
if (autoVenvRef.current && getValues('script.venv').trim() === autoVenvRef.current) {
|
||||
setValue('script.venv', '', { shouldDirty: false })
|
||||
}
|
||||
autoVenvRef.current = ''
|
||||
}, [debouncedWorkingDir, getValues, open, setValue])
|
||||
|
||||
// 探测到虚拟环境就自动填进「虚拟环境目录」,但用户自己改过就保留用户的值。
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return
|
||||
}
|
||||
const probe = venvProbe.data
|
||||
if (!probe || probe.status !== 'linked' || !probe.venv_path) {
|
||||
return
|
||||
}
|
||||
const current = getValues('script.venv').trim()
|
||||
// 防抖中的输入可能还没落到查询键上:只有「当前输入」与「本次探测用的输入」
|
||||
// 一致时才回填,否则工作目录刚变化时可能把上一轮探测结果又填回去。
|
||||
if (debouncedVenv.trim() !== current) {
|
||||
return
|
||||
}
|
||||
if (current && current !== autoVenvRef.current) {
|
||||
return
|
||||
}
|
||||
if (current !== '' && current === autoVenvRef.current) {
|
||||
return
|
||||
}
|
||||
const next = venvFieldValue(debouncedWorkingDir, probe.venv_path)
|
||||
autoVenvRef.current = next
|
||||
setValue('script.venv', next, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}, [debouncedVenv, debouncedWorkingDir, getValues, open, setValue, venvProbe.data])
|
||||
|
||||
// 打开后把焦点给第一个可编辑字段:新建时是任务标识,编辑时 ID 已锁定,跳名称。
|
||||
// 原生 dialog 默认聚焦关闭按钮,不符合编辑表单的节奏。
|
||||
useEffect(() => {
|
||||
@@ -442,14 +487,20 @@ export function TaskEditorModal({
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<VenvDetectorBar
|
||||
workingDir={debouncedWorkingDir}
|
||||
probe={venvProbe.data}
|
||||
pending={venvProbe.isPending}
|
||||
failed={venvProbe.isError}
|
||||
<div className="form-grid venv-grid">
|
||||
<FormField
|
||||
label="虚拟环境目录 (可选)"
|
||||
htmlFor="task-venv"
|
||||
hint="检测到虚拟环境会自动填入,可直接修改;留空则按工作目录自动探测"
|
||||
error={errors.script?.venv?.message}
|
||||
>
|
||||
<Input
|
||||
id="task-venv"
|
||||
{...register('script.venv')}
|
||||
className="mono-input"
|
||||
placeholder="例如:.venv"
|
||||
/>
|
||||
|
||||
<div className="form-grid">
|
||||
</FormField>
|
||||
<FormField
|
||||
label="解释器"
|
||||
htmlFor="task-interpreter"
|
||||
@@ -462,21 +513,15 @@ export function TaskEditorModal({
|
||||
placeholder="python.exe"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="虚拟环境目录 (可选)"
|
||||
htmlFor="task-venv"
|
||||
hint="留空时自动探测;填写后优先使用该目录"
|
||||
error={errors.script?.venv?.message}
|
||||
>
|
||||
<Input
|
||||
id="task-venv"
|
||||
{...register('script.venv')}
|
||||
className="mono-input"
|
||||
placeholder="例如:.venv"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<VenvDetectorBar
|
||||
workingDir={debouncedWorkingDir}
|
||||
probe={venvProbe.data}
|
||||
pending={venvProbe.isPending}
|
||||
failed={venvProbe.isError}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
label="脚本文件或相对路径"
|
||||
htmlFor="task-script"
|
||||
@@ -786,6 +831,30 @@ function formatArgumentPreview(argumentsList: KeyValueFormValue[]): string {
|
||||
return values.length ? ` ${values.join(' ')}` : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 探测结果给的是绝对路径;虚拟环境位于工作目录内时写成相对路径,
|
||||
* 任务搬家后仍然可用(与手填 `.venv` 的语义一致)。
|
||||
*/
|
||||
function venvFieldValue(workingDir: string, venvPath: string): string {
|
||||
const base = workingDir.trim().replace(/[\\/]+$/, '')
|
||||
const path = venvPath.trim()
|
||||
if (!base || !path) {
|
||||
return path
|
||||
}
|
||||
const normalizedBase = base.toLowerCase()
|
||||
const normalizedPath = path.toLowerCase()
|
||||
if (normalizedPath === normalizedBase) {
|
||||
return '.'
|
||||
}
|
||||
if (
|
||||
normalizedPath.startsWith(`${normalizedBase}\\`) ||
|
||||
normalizedPath.startsWith(`${normalizedBase}/`)
|
||||
) {
|
||||
return path.slice(base.length + 1).replace(/\//g, '\\')
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
function formatInterpreterPreview(
|
||||
script: TaskFormValues['script'],
|
||||
linkedPythonPath?: string | null,
|
||||
|
||||
Reference in New Issue
Block a user