feat: 实现环境变量预设功能 & 移除稀疏检出

## 后端改动
- 添加 Project.envPresets 字段(JSON 格式)
- 移除 Deployment.env 字段,统一使用 envVars
- 更新部署 DTO,支持 envVars (Record<string, string>)
- pipeline-runner 支持解析并注入 envVars 到环境
- 移除稀疏检出模板和相关环境变量
- 优化代码格式(Biome lint & format)

## 前端改动
- 新增 EnvPresetsEditor 组件(支持单选/多选/输入框类型)
- 项目创建/编辑界面集成环境预设编辑器
- 部署界面基于预设动态生成环境变量表单
- 移除稀疏检出表单项
- 项目详情页添加环境变量预设配置 tab
- 优化部署界面布局(基本参数 & 环境变量分区)

## 文档
- 添加完整文档目录结构(docs/)
- 创建设计文档 design-0005(部署流程重构)
- 添加 API 文档、架构设计文档等

## 数据库
- 执行 prisma db push 同步 schema 变更
This commit is contained in:
2026-01-03 22:59:20 +08:00
parent c40532c757
commit d22fdc9618
71 changed files with 9611 additions and 5849 deletions

View File

@@ -0,0 +1,214 @@
import { Button, Checkbox, Input, Select, Space } from '@arco-design/web-react';
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
import { useEffect, useState } from 'react';
export interface EnvPreset {
key: string;
label: string;
type: 'select' | 'multiselect' | 'input';
required?: boolean; // 是否必填
options?: Array<{ label: string; value: string }>;
}
interface EnvPresetsEditorProps {
value?: EnvPreset[];
onChange?: (value: EnvPreset[]) => void;
}
function EnvPresetsEditor({ value = [], onChange }: EnvPresetsEditorProps) {
const [presets, setPresets] = useState<EnvPreset[]>(value);
// 当外部 value 变化时同步到内部状态
useEffect(() => {
setPresets(value);
}, [value]);
const handleAddPreset = () => {
const newPreset: EnvPreset = {
key: '',
label: '',
type: 'select',
options: [{ label: '', value: '' }],
};
const newPresets = [...presets, newPreset];
setPresets(newPresets);
onChange?.(newPresets);
};
const handleRemovePreset = (index: number) => {
const newPresets = presets.filter((_, i) => i !== index);
setPresets(newPresets);
onChange?.(newPresets);
};
const handlePresetChange = (
index: number,
field: keyof EnvPreset,
val: string | boolean | EnvPreset['type'] | EnvPreset['options'],
) => {
const newPresets = [...presets];
newPresets[index] = { ...newPresets[index], [field]: val };
setPresets(newPresets);
onChange?.(newPresets);
};
const handleAddOption = (presetIndex: number) => {
const newPresets = [...presets];
if (!newPresets[presetIndex].options) {
newPresets[presetIndex].options = [];
}
newPresets[presetIndex].options?.push({ label: '', value: '' });
setPresets(newPresets);
onChange?.(newPresets);
};
const handleRemoveOption = (presetIndex: number, optionIndex: number) => {
const newPresets = [...presets];
newPresets[presetIndex].options = newPresets[presetIndex].options?.filter(
(_, i) => i !== optionIndex,
);
setPresets(newPresets);
onChange?.(newPresets);
};
const handleOptionChange = (
presetIndex: number,
optionIndex: number,
field: 'label' | 'value',
val: string,
) => {
const newPresets = [...presets];
if (newPresets[presetIndex].options) {
newPresets[presetIndex].options![optionIndex][field] = val;
setPresets(newPresets);
onChange?.(newPresets);
}
};
return (
<div className="space-y-4">
{presets.map((preset, presetIndex) => (
<div
key={`preset-${preset.key || presetIndex}`}
className="border border-gray-200 rounded p-4"
>
<div className="flex items-start justify-between mb-3">
<div className="font-medium text-gray-700">
#{presetIndex + 1}
</div>
<Button
size="small"
status="danger"
icon={<IconDelete />}
onClick={() => handleRemovePreset(presetIndex)}
>
</Button>
</div>
<Space direction="vertical" style={{ width: '100%' }}>
<div className="grid grid-cols-2 gap-2">
<Input
placeholder="变量名 (key)"
value={preset.key}
onChange={(val) => handlePresetChange(presetIndex, 'key', val)}
/>
<Input
placeholder="显示名称 (label)"
value={preset.label}
onChange={(val) =>
handlePresetChange(presetIndex, 'label', val)
}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<Select
placeholder="选择类型"
value={preset.type}
onChange={(val) => handlePresetChange(presetIndex, 'type', val)}
>
<Select.Option value="select"></Select.Option>
<Select.Option value="multiselect"></Select.Option>
<Select.Option value="input"></Select.Option>
</Select>
<div className="flex items-center">
<Checkbox
checked={preset.required || false}
onChange={(checked) =>
handlePresetChange(presetIndex, 'required', checked)
}
>
</Checkbox>
</div>
</div>
{(preset.type === 'select' || preset.type === 'multiselect') && (
<div className="mt-2">
<div className="text-sm text-gray-600 mb-2"></div>
{preset.options?.map((option, optionIndex) => (
<div
key={`option-${option.value || optionIndex}`}
className="flex items-center gap-2 mb-2"
>
<Input
size="small"
placeholder="显示文本"
value={option.label}
onChange={(val) =>
handleOptionChange(
presetIndex,
optionIndex,
'label',
val,
)
}
/>
<Input
size="small"
placeholder="值"
value={option.value}
onChange={(val) =>
handleOptionChange(
presetIndex,
optionIndex,
'value',
val,
)
}
/>
<Button
size="small"
status="danger"
icon={<IconDelete />}
onClick={() =>
handleRemoveOption(presetIndex, optionIndex)
}
/>
</div>
))}
<Button
size="small"
type="dashed"
long
icon={<IconPlus />}
onClick={() => handleAddOption(presetIndex)}
>
</Button>
</div>
)}
</Space>
</div>
))}
<Button type="dashed" long icon={<IconPlus />} onClick={handleAddPreset}>
</Button>
</div>
);
}
export default EnvPresetsEditor;