finished presets

This commit is contained in:
musistudio
2025-12-30 16:49:16 +08:00
parent 06a18c0734
commit 559f5024c4
14 changed files with 869 additions and 588 deletions

View File

@@ -18,7 +18,7 @@ import { Upload, Link, Trash2, Info, Download, CheckCircle2, AlertCircle, Loader
import { Toast } from "@/components/ui/toast";
import { DynamicConfigForm } from "./preset/DynamicConfigForm";
// Schema 类型
// Schema types
interface InputOption {
label: string;
value: string | number | boolean;
@@ -87,6 +87,7 @@ interface PresetDetail extends PresetMetadata {
schema?: RequiredInput[];
template?: any;
configMappings?: any[];
userValues?: Record<string, any>;
}
interface MarketPreset {
@@ -126,7 +127,7 @@ export function Presets() {
navigate('/dashboard');
};
// 加载市场预设
// Load market presets
const loadMarketPresets = async () => {
setMarketLoading(true);
try {
@@ -140,44 +141,51 @@ export function Presets() {
}
};
// 从市场安装预设
// Install preset from market
const handleInstallFromMarket = async (preset: MarketPreset) => {
try {
setInstallingFromMarket(preset.id);
// 第一步:安装预设(解压到目录)
await api.installPresetFromGitHub(preset.repo, preset.name);
// Step 1: Install preset (extract to directory)
const installResult = await api.installPresetFromGitHub(preset.repo);
// 第二步:获取预设详情(检查是否需要配置)
// Step 2: Get preset details (check if configuration is required)
try {
const detail = await api.getPreset(preset.name);
const presetDetail: PresetDetail = { ...preset, ...detail };
const installedPresetName = installResult.presetName || preset.name;
const detail = await api.getPreset(installedPresetName);
const presetDetail: PresetDetail = { ...preset, ...detail, id: installedPresetName };
// 检查是否需要配置
// Check if configuration is required
if (detail.schema && detail.schema.length > 0) {
// 需要配置,打开配置对话框
// Configuration required, open configuration dialog
setSelectedPreset(presetDetail);
// 初始化默认值
// Initialize form values: prefer saved userValues, otherwise use defaultValue
const initialValues: Record<string, any> = {};
for (const input of detail.schema) {
initialValues[input.id] = input.defaultValue ?? '';
// Prefer saved values
if (detail.userValues && detail.userValues[input.id] !== undefined) {
initialValues[input.id] = detail.userValues[input.id];
} else {
// Otherwise use default value
initialValues[input.id] = input.defaultValue ?? '';
}
}
setSecrets(initialValues);
// 关闭市场对话框,打开详情对话框
// Close market dialog, open details dialog
setMarketDialogOpen(false);
setDetailDialogOpen(true);
setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' });
} else {
// 不需要配置,直接完成
// No configuration required, complete directly
setToast({ message: t('presets.preset_installed'), type: 'success' });
setMarketDialogOpen(false);
await loadPresets();
}
} catch (error) {
// 获取详情失败,但安装成功了,刷新列表
// Failed to get details, but installation succeeded, refresh list
console.error('Failed to get preset details after installation:', error);
setToast({ message: t('presets.preset_installed'), type: 'success' });
setMarketDialogOpen(false);
@@ -191,21 +199,21 @@ export function Presets() {
}
};
// 打开市场对话框时加载预设
// Load presets when opening market dialog
useEffect(() => {
if (marketDialogOpen && marketPresets.length === 0) {
loadMarketPresets();
}
}, [marketDialogOpen]);
// 过滤市场预设
// Filter market presets
const filteredMarketPresets = marketPresets.filter(preset =>
preset.name.toLowerCase().includes(marketSearch.toLowerCase()) ||
preset.description?.toLowerCase().includes(marketSearch.toLowerCase()) ||
preset.author?.toLowerCase().includes(marketSearch.toLowerCase())
);
// 加载预设列表
// Load presets list
const loadPresets = async () => {
try {
setLoading(true);
@@ -223,18 +231,24 @@ export function Presets() {
loadPresets();
}, []);
// 查看预设详情
// View preset details
const handleViewDetail = async (preset: PresetMetadata) => {
try {
const detail = await api.getPreset(preset.id);
setSelectedPreset({ ...preset, ...detail });
setDetailDialogOpen(true);
// 初始化默认值
// 初始化表单值:优先使用已保存的 userValues否则使用 defaultValue
if (detail.schema && detail.schema.length > 0) {
const initialValues: Record<string, any> = {};
for (const input of detail.schema) {
initialValues[input.id] = input.defaultValue ?? '';
// 优先使用已保存的值
if (detail.userValues && detail.userValues[input.id] !== undefined) {
initialValues[input.id] = detail.userValues[input.id];
} else {
// Otherwise use default value
initialValues[input.id] = input.defaultValue ?? '';
}
}
setSecrets(initialValues);
}
@@ -266,38 +280,47 @@ export function Presets() {
: installUrl!.split('/').pop()!.replace('.ccrsets', '')
);
// 第一步:安装预设(解压到目录)
// Step 1: Install preset (extract to directory)
let installResult;
if (installMethod === 'url' && installUrl) {
await api.installPresetFromUrl(installUrl, presetName);
installResult = await api.installPresetFromUrl(installUrl, presetName);
} else if (installMethod === 'file' && installFile) {
await api.uploadPresetFile(installFile, presetName);
installResult = await api.uploadPresetFile(installFile, presetName);
} else {
return;
}
// 第二步:获取预设详情(检查是否需要配置)
// Step 2: Get preset details (check if configuration is required)
try {
const detail = await api.getPreset(presetName);
// 使用服务器返回的实际预设名称
const actualPresetName = installResult?.presetName || presetName;
const detail = await api.getPreset(actualPresetName);
// 检查是否需要配置
// Check if configuration is required
if (detail.schema && detail.schema.length > 0) {
// 需要配置,打开配置对话框
// Configuration required, open configuration dialog
setSelectedPreset({
id: presetName,
name: presetName,
id: actualPresetName,
name: detail.name || actualPresetName,
version: detail.version || '1.0.0',
installed: true,
...detail
});
// 初始化默认值
// Initialize form values: prefer saved userValues, otherwise use defaultValue
const initialValues: Record<string, any> = {};
for (const input of detail.schema) {
initialValues[input.id] = input.defaultValue ?? '';
// Prefer saved values
if (detail.userValues && detail.userValues[input.id] !== undefined) {
initialValues[input.id] = detail.userValues[input.id];
} else {
// Otherwise use default value
initialValues[input.id] = input.defaultValue ?? '';
}
}
setSecrets(initialValues);
// 关闭安装对话框,打开详情对话框
// Close installation dialog, open details dialog
setInstallDialogOpen(false);
setInstallUrl('');
setInstallFile(null);
@@ -306,7 +329,7 @@ export function Presets() {
setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' });
} else {
// 不需要配置,直接完成
// No configuration required, complete directly
setToast({ message: t('presets.preset_installed'), type: 'success' });
setInstallDialogOpen(false);
setInstallUrl('');
@@ -315,7 +338,7 @@ export function Presets() {
await loadPresets();
}
} catch (error) {
// 获取详情失败,但安装成功了,刷新列表
// Failed to get details, but installation succeeded, refresh list
console.error('Failed to get preset details after installation:', error);
setToast({ message: t('presets.preset_installed'), type: 'success' });
setInstallDialogOpen(false);
@@ -332,20 +355,24 @@ export function Presets() {
}
};
// 应用预设(配置敏感信息)
// Apply preset (configure sensitive information)
const handleApplyPreset = async (values?: Record<string, any>) => {
try {
setIsApplying(true);
// 使用传入的values或现有的secrets
// Use passed values or existing secrets
const inputValues = values || secrets;
// 验证所有必填项都已填写
// Verify all required fields are filled
if (selectedPreset?.schema && selectedPreset.schema.length > 0) {
// 验证在 DynamicConfigForm 中已完成
// 这里只做简单检查
// Validation completed in DynamicConfigForm
// 这里只做简单检查(对于 confirm 类型false 是有效值)
for (const input of selectedPreset.schema) {
if (input.required !== false && !inputValues[input.id]) {
const value = inputValues[input.id];
const isEmpty = value === undefined || value === null || value === '' ||
(Array.isArray(value) && value.length === 0);
if (input.required !== false && isEmpty) {
setToast({ message: t('presets.please_fill_field', { field: input.label || input.id }), type: 'warning' });
setIsApplying(false);
return;
@@ -353,11 +380,11 @@ export function Presets() {
}
}
await api.applyPreset(selectedPreset!.name, inputValues);
await api.applyPreset(selectedPreset!.id, inputValues);
setToast({ message: t('presets.preset_applied'), type: 'success' });
setDetailDialogOpen(false);
setSecrets({});
// 刷新预设列表
// Refresh presets list
await loadPresets();
} catch (error: any) {
console.error('Failed to apply preset:', error);
@@ -367,7 +394,7 @@ export function Presets() {
}
};
// 删除预设
// Delete preset
const handleDelete = async () => {
if (!presetToDelete) return;
@@ -576,7 +603,7 @@ export function Presets() {
</div>
)}
{/* 配置表单 */}
{/* Configuration form */}
{selectedPreset?.schema && selectedPreset.schema.length > 0 && (
<div className="mt-6">
<h4 className="font-medium text-sm mb-4">{t('presets.required_information')}</h4>
@@ -591,11 +618,6 @@ export function Presets() {
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDetailDialogOpen(false)}>
{t('presets.close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
@@ -11,9 +12,9 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { CheckCircle2, Loader2 } from 'lucide-react';
import { Loader2 } from 'lucide-react';
// 类型定义
// Type definitions
interface InputOption {
label: string;
value: string | number | boolean;
@@ -77,11 +78,12 @@ export function DynamicConfigForm({
isSubmitting = false,
initialValues = {},
}: DynamicConfigFormProps) {
const { t } = useTranslation();
const [values, setValues] = useState<Record<string, any>>(initialValues);
const [errors, setErrors] = useState<Record<string, string>>({});
const [visibleFields, setVisibleFields] = useState<Set<string>>(new Set());
// 计算可见字段
// Calculate visible fields
useEffect(() => {
const updateVisibility = () => {
const visible = new Set<string>();
@@ -98,7 +100,7 @@ export function DynamicConfigForm({
updateVisibility();
}, [values, schema]);
// 评估条件
// Evaluate condition
const evaluateCondition = (condition: Condition): boolean => {
const actualValue = values[condition.field];
@@ -132,7 +134,7 @@ export function DynamicConfigForm({
}
};
// 判断字段是否应该显示
// Determine if field should be displayed
const shouldShowField = (field: RequiredInput): boolean => {
if (!field.when) {
return true;
@@ -142,7 +144,7 @@ export function DynamicConfigForm({
return conditions.every(condition => evaluateCondition(condition));
};
// 获取选项列表
// Get options list
const getOptions = (field: RequiredInput): InputOption[] => {
if (!field.options) {
return [];
@@ -197,13 +199,13 @@ export function DynamicConfigForm({
return [];
};
// 更新字段值
// Update field value
const updateValue = (fieldId: string, value: any) => {
setValues((prev) => ({
...prev,
[fieldId]: value,
}));
// 清除该字段的错误
// Clear errors for this field
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[fieldId];
@@ -211,44 +213,44 @@ export function DynamicConfigForm({
});
};
// 验证单个字段
// Validate single field
const validateField = (field: RequiredInput): string | null => {
const value = values[field.id];
const fieldName = field.label || field.id;
// 检查必填
if (field.required !== false && (value === undefined || value === null || value === '')) {
return `${field.label || field.id} is required`;
// Check required (for confirm type, false is a valid value)
const isEmpty = value === undefined || value === null || value === '' ||
(Array.isArray(value) && value.length === 0);
if (field.required !== false && isEmpty) {
return t('presets.form.field_required', { field: fieldName });
}
if (!value && field.required === false) {
return null;
}
// 类型检查
if (field.type === 'number' && isNaN(Number(value))) {
return `${field.label || field.id} must be a number`;
// Type check
if (field.type === 'number' && value !== '' && isNaN(Number(value))) {
return t('presets.form.must_be_number', { field: fieldName });
}
if (field.type === 'number') {
const numValue = Number(value);
if (field.min !== undefined && numValue < field.min) {
return `${field.label || field.id} must be at least ${field.min}`;
return t('presets.form.must_be_at_least', { field: fieldName, min: field.min });
}
if (field.max !== undefined && numValue > field.max) {
return `${field.label || field.id} must be at most ${field.max}`;
return t('presets.form.must_be_at_most', { field: fieldName, max: field.max });
}
}
// 自定义验证器
if (field.validator) {
// Custom validator
if (field.validator && value !== '') {
if (field.validator instanceof RegExp) {
if (!field.validator.test(String(value))) {
return `${field.label || field.id} format is invalid`;
return t('presets.form.format_invalid', { field: fieldName });
}
} else if (typeof field.validator === 'string') {
const regex = new RegExp(field.validator);
if (!regex.test(String(value))) {
return `${field.label || field.id} format is invalid`;
return t('presets.form.format_invalid', { field: fieldName });
}
}
}
@@ -256,11 +258,11 @@ export function DynamicConfigForm({
return null;
};
// 提交表单
// Submit form
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 验证所有可见字段
// Validate all visible fields
const newErrors: Record<string, string> = {};
for (const field of schema) {
@@ -338,7 +340,7 @@ export function DynamicConfigForm({
disabled={isSubmitting}
>
<SelectTrigger id={`field-${field.id}`}>
<SelectValue placeholder={field.placeholder || `Select ${label}`} />
<SelectValue placeholder={field.placeholder || t('presets.form.select', { label })} />
</SelectTrigger>
<SelectContent>
{getOptions(field).map((option) => (
@@ -432,19 +434,16 @@ export function DynamicConfigForm({
onClick={onCancel}
disabled={isSubmitting}
>
Cancel
{t('app.cancel')}
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Applying...
{t('presets.form.saving')}
</>
) : (
<>
<CheckCircle2 className="mr-2 h-4 w-4" />
Apply
</>
t('app.save')
)}
</Button>
</div>

View File

@@ -43,12 +43,12 @@ export function Toast({ message, type, onClose }: ToastProps) {
};
return (
<div className={`fixed top-4 right-4 z-50 flex items-center justify-between p-4 rounded-lg border shadow-lg ${getBackgroundColor()} transition-all duration-300 ease-in-out`}>
<div className={`fixed top-4 right-4 z-[100] flex items-center justify-between p-4 rounded-lg border shadow-lg ${getBackgroundColor()} transition-all duration-300 ease-in-out`}>
<div className="flex items-center space-x-2">
{getIcon()}
<span className="text-sm font-medium">{message}</span>
</div>
<button
<button
onClick={onClose}
className="ml-4 text-gray-500 hover:text-gray-700 focus:outline-none"
>

View File

@@ -285,6 +285,17 @@
"load_market_failed": "Failed to load market presets",
"preset_installed_config_required": "Preset installed, please complete configuration",
"please_provide_file": "Please provide a preset file",
"please_provide_url": "Please provide a preset URL"
"please_provide_url": "Please provide a preset URL",
"form": {
"field_required": "{{field}} is required",
"must_be_number": "{{field}} must be a number",
"must_be_at_least": "{{field}} must be at least {{min}}",
"must_be_at_most": "{{field}} must be at most {{max}}",
"format_invalid": "{{field}} format is invalid",
"select": "Select {{label}}",
"applying": "Applying...",
"apply": "Apply",
"cancel": "Cancel"
}
}
}

View File

@@ -285,6 +285,17 @@
"load_market_failed": "加载市场预设失败",
"preset_installed_config_required": "预设已安装,请完成配置",
"please_provide_file": "请提供预设文件",
"please_provide_url": "请提供预设 URL"
"please_provide_url": "请提供预设 URL",
"form": {
"field_required": "{{field}} 为必填项",
"must_be_number": "{{field}} 必须是数字",
"must_be_at_least": "{{field}} 至少为 {{min}}",
"must_be_at_most": "{{field}} 最多为 {{max}}",
"format_invalid": "{{field}} 格式无效",
"select": "选择 {{label}}",
"applying": "应用中...",
"apply": "应用",
"cancel": "取消"
}
}
}