1306 lines
66 KiB
Dart
1306 lines
66 KiB
Dart
import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart';
|
||
import 'package:ainoval/models/ai_model_group.dart';
|
||
import 'package:ainoval/models/user_ai_model_config_model.dart';
|
||
import 'package:ainoval/screens/settings/widgets/custom_model_dialog.dart';
|
||
import 'package:ainoval/screens/settings/widgets/model_group_list.dart';
|
||
import 'package:ainoval/screens/settings/widgets/provider_list.dart';
|
||
import 'package:ainoval/screens/settings/widgets/searchable_model_dropdown.dart';
|
||
import 'package:ainoval/widgets/common/top_toast.dart';
|
||
import 'package:ainoval/utils/logger.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
// Removed import causing linter error
|
||
// import 'package:ai_config_repository/ai_config_repository.dart';
|
||
// Placeholder for localization, replace with your actual import
|
||
// import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||
|
||
class AiConfigForm extends StatefulWidget {
|
||
// Callback when cancel is pressed
|
||
// Optional: Callback on successful save if specific action needed besides hiding form
|
||
// final VoidCallback? onSaveSuccess;
|
||
|
||
const AiConfigForm({
|
||
super.key,
|
||
required this.userId,
|
||
required this.onCancel,
|
||
this.configToEdit,
|
||
// this.onSaveSuccess,
|
||
});
|
||
final UserAIModelConfigModel? configToEdit;
|
||
final String userId;
|
||
final VoidCallback onCancel;
|
||
|
||
@override
|
||
State<AiConfigForm> createState() => _AiConfigFormState();
|
||
}
|
||
|
||
class _AiConfigFormState extends State<AiConfigForm> {
|
||
final _formKey = GlobalKey<FormState>();
|
||
late TextEditingController _aliasController;
|
||
late TextEditingController _apiKeyController;
|
||
late TextEditingController _apiEndpointController;
|
||
|
||
String? _selectedProvider;
|
||
String? _selectedModel;
|
||
ModelListingCapability? _providerCapability; // New: Store capability
|
||
bool _isLoadingProviders = false;
|
||
bool _isLoadingModels = false;
|
||
bool _isTestingApiKey = false; // New: Track API key testing
|
||
bool _apiKeyTestSuccess = false; // New: Track API key test success for current provider
|
||
bool _isSaving = false; // Track internal saving state
|
||
bool _showApiKey = false; // 控制API Key是否显示
|
||
|
||
List<String> _providers = [];
|
||
List<String> _models = [];
|
||
|
||
// 校验错误状态
|
||
bool _providerError = false;
|
||
bool _modelError = false;
|
||
bool _apiKeyError = false;
|
||
String? _providerErrorText;
|
||
String? _modelErrorText;
|
||
String? _apiKeyErrorText;
|
||
|
||
bool get _isEditMode => widget.configToEdit != null;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
// Initialize controllers
|
||
_aliasController =
|
||
TextEditingController(text: widget.configToEdit?.alias ?? '');
|
||
// 编辑模式下回显API Key,新增模式下保持空白
|
||
_apiKeyController = TextEditingController(
|
||
text: _isEditMode ? (widget.configToEdit?.apiKey ?? '') : ''
|
||
);
|
||
_apiEndpointController =
|
||
TextEditingController(text: widget.configToEdit?.apiEndpoint ?? '');
|
||
|
||
// Initialize state based on edit mode
|
||
if (_isEditMode) {
|
||
_selectedProvider = widget.configToEdit?.provider;
|
||
_selectedModel = widget.configToEdit?.modelName;
|
||
// Don't prefill API Key from edit mode
|
||
_apiEndpointController.text = widget.configToEdit?.apiEndpoint ?? '';
|
||
_aliasController.text = widget.configToEdit?.alias ?? '';
|
||
} else {
|
||
_selectedProvider = null;
|
||
_selectedModel = null;
|
||
_providers = [];
|
||
_models = [];
|
||
_apiEndpointController.text = '';
|
||
_apiKeyController.text = '';
|
||
_aliasController.text = '';
|
||
}
|
||
|
||
// Use context safely after first frame
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (!mounted) return; // Check mount status
|
||
final bloc = context.read<AiConfigBloc>();
|
||
|
||
// Pre-populate provider list from bloc state if available
|
||
if (_providers.isEmpty) {
|
||
_providers = bloc.state.availableProviders;
|
||
}
|
||
|
||
// Always load providers on init
|
||
_loadProviders();
|
||
|
||
// If a provider is selected (edit mode or restored state), load its capability
|
||
// 在编辑模式下跳过能力加载和自动填充,避免不必要的API Key验证
|
||
if (_selectedProvider != null && !_isEditMode) {
|
||
print("InitState: Provider '$_selectedProvider' selected, loading capability.");
|
||
bloc.add(LoadProviderCapability(providerName: _selectedProvider!));
|
||
// Model loading will now be handled by the BlocListener based on capability
|
||
// Also try to load default config info
|
||
_autoFillApiInfo(_selectedProvider!);
|
||
}
|
||
|
||
// --- Trigger loading ---
|
||
// Always try to load providers when the form inits,
|
||
// as the list might be stale or empty (especially in add mode).
|
||
// The BlocListener will handle the loading indicator state.
|
||
//_loadProviders(); // Moved up
|
||
|
||
// Model loading logic is now primarily driven by provider capability
|
||
// and API key testing results handled in the BlocListener.
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_aliasController.dispose();
|
||
_apiKeyController.dispose();
|
||
_apiEndpointController.dispose();
|
||
// Don't clear models here as the Bloc state might be needed elsewhere
|
||
// context.read<AiConfigBloc>().add(ClearProviderModels());
|
||
super.dispose();
|
||
}
|
||
|
||
void _loadProviders() {
|
||
if (!mounted) return; // Check if widget is still in the tree
|
||
setState(() {
|
||
_isLoadingProviders = true;
|
||
});
|
||
context.read<AiConfigBloc>().add(const LoadAvailableProviders()); // Corrected call
|
||
}
|
||
|
||
void _loadModels(String provider) {
|
||
if (!mounted) return;
|
||
print("UI triggered _loadModels for $provider"); // Debug log
|
||
setState(() {
|
||
_isLoadingModels = true;
|
||
// Reset model only if provider actually changes (don't reset in edit mode init)
|
||
if (_selectedProvider != provider) {
|
||
_selectedModel = null;
|
||
}
|
||
_models = []; // Clear previous models for the dropdown
|
||
});
|
||
context.read<AiConfigBloc>().add(LoadModelsForProvider(provider: provider));
|
||
}
|
||
|
||
void _submitForm() {
|
||
// 清除之前的错误状态
|
||
setState(() {
|
||
_providerError = false;
|
||
_modelError = false;
|
||
_apiKeyError = false;
|
||
_providerErrorText = null;
|
||
_modelErrorText = null;
|
||
_apiKeyErrorText = null;
|
||
});
|
||
|
||
// 执行校验
|
||
bool hasError = false;
|
||
|
||
// 在添加模式下校验提供商选择
|
||
if (!_isEditMode && _selectedProvider == null) {
|
||
setState(() {
|
||
_providerError = true;
|
||
_providerErrorText = '请选择一个提供商';
|
||
});
|
||
hasError = true;
|
||
}
|
||
|
||
// 在添加模式下校验模型选择
|
||
if (!_isEditMode && _selectedModel == null) {
|
||
setState(() {
|
||
_modelError = true;
|
||
_modelErrorText = '请选择一个模型';
|
||
});
|
||
hasError = true;
|
||
}
|
||
|
||
// 校验API Key(在添加模式下,如果提供商需要API Key)
|
||
if (!_isEditMode &&
|
||
_providerCapability == ModelListingCapability.listingWithKey &&
|
||
_apiKeyController.text.trim().isEmpty) {
|
||
setState(() {
|
||
_apiKeyError = true;
|
||
_apiKeyErrorText = '该提供商需要 API 密钥';
|
||
});
|
||
hasError = true;
|
||
}
|
||
|
||
// 检查重复的已验证配置(仅在添加模式)
|
||
if (!_isEditMode && _selectedProvider != null && _selectedModel != null) {
|
||
final existingConfigs = context.read<AiConfigBloc>().state.configs;
|
||
final isDuplicateValidated = existingConfigs.any((config) =>
|
||
config.provider == _selectedProvider &&
|
||
config.modelName == _selectedModel &&
|
||
config.isValidated);
|
||
|
||
if (isDuplicateValidated) {
|
||
setState(() {
|
||
_modelError = true;
|
||
_modelErrorText = '已存在该模型的已验证配置,无法重复添加';
|
||
});
|
||
hasError = true;
|
||
}
|
||
}
|
||
|
||
// 如果有错误,不执行提交
|
||
if (hasError) {
|
||
return;
|
||
}
|
||
|
||
// 执行表单验证
|
||
if (_formKey.currentState!.validate()) {
|
||
setState(() {
|
||
_isSaving = true;
|
||
});
|
||
final bloc = context.read<AiConfigBloc>();
|
||
|
||
// 处理API密钥
|
||
String? apiKey = _apiKeyController.text.trim();
|
||
if (apiKey.isEmpty) {
|
||
apiKey = null;
|
||
}
|
||
|
||
if (_isEditMode) {
|
||
bloc.add(UpdateAiConfig(
|
||
userId: widget.userId,
|
||
configId: widget.configToEdit!.id,
|
||
alias: _aliasController.text.trim().isEmpty
|
||
? null
|
||
: _aliasController.text.trim(),
|
||
apiKey: apiKey, // Pass null if empty to potentially clear/not update
|
||
apiEndpoint:
|
||
_apiEndpointController.text.trim(), // Send empty string to clear
|
||
));
|
||
} else {
|
||
bloc.add(AddAiConfig(
|
||
userId: widget.userId,
|
||
provider: _selectedProvider!,
|
||
modelName: _selectedModel!,
|
||
apiKey: apiKey ?? "", // Backend likely expects non-null, pass empty string
|
||
alias: _aliasController.text.trim().isEmpty
|
||
? _selectedModel // Default alias to model name if empty
|
||
: _aliasController.text.trim(),
|
||
apiEndpoint: _apiEndpointController.text.trim(),
|
||
));
|
||
}
|
||
// The BlocListener in SettingsPanel will handle hiding the form on success/error
|
||
}
|
||
}
|
||
|
||
// 处理模型选择
|
||
void _handleModelSelected(String model) {
|
||
setState(() {
|
||
_selectedModel = model;
|
||
// 清除模型选择错误状态
|
||
_modelError = false;
|
||
_modelErrorText = null;
|
||
// 设置别名默认为模型名称
|
||
if (_aliasController.text.isEmpty) {
|
||
_aliasController.text = model;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 处理自定义模型添加
|
||
void _handleAddCustomModel() {
|
||
if (_selectedProvider == null) return;
|
||
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => CustomModelDialog(
|
||
providerName: _selectedProvider!,
|
||
onConfirm: (modelName, modelAlias, apiEndpoint) async {
|
||
// 检查 API 密钥
|
||
final apiKey = _apiKeyController.text.trim();
|
||
if (apiKey.isEmpty) {
|
||
TopToast.warning(context, '请先输入 API 密钥再添加自定义模型');
|
||
return;
|
||
}
|
||
|
||
// 立即保存配置到后端
|
||
try {
|
||
AppLogger.i('AiConfigForm', '开始添加自定义模型: $_selectedProvider/$modelName');
|
||
|
||
// 显示加载状态
|
||
setState(() {
|
||
_isSaving = true;
|
||
});
|
||
|
||
// 触发添加自定义模型并验证事件
|
||
context.read<AiConfigBloc>().add(AddCustomModelAndValidate(
|
||
userId: widget.userId,
|
||
provider: _selectedProvider!,
|
||
modelName: modelName,
|
||
apiKey: apiKey,
|
||
alias: modelAlias,
|
||
apiEndpoint: apiEndpoint?.isEmpty == true ? null : apiEndpoint,
|
||
));
|
||
|
||
// 显示成功提示
|
||
TopToast.info(context, '自定义模型 $modelName 已添加,正在验证连接...');
|
||
|
||
} catch (e) {
|
||
AppLogger.e('AiConfigForm', '添加自定义模型失败', e);
|
||
setState(() {
|
||
_isSaving = false;
|
||
});
|
||
|
||
TopToast.error(context, '添加自定义模型失败: ${e.toString()}');
|
||
}
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
// 检查是否存在已验证的相同模型
|
||
bool _isDuplicateValidatedModel() {
|
||
if (_isEditMode || _selectedProvider == null || _selectedModel == null) {
|
||
return false;
|
||
}
|
||
|
||
final existingConfigs = context.read<AiConfigBloc>().state.configs;
|
||
return existingConfigs.any((config) =>
|
||
config.provider == _selectedProvider &&
|
||
config.modelName == _selectedModel &&
|
||
config.isValidated);
|
||
}
|
||
|
||
// 获取已验证模型列表
|
||
List<String> _getVerifiedModels(String provider) {
|
||
final existingConfigs = context.read<AiConfigBloc>().state.configs;
|
||
return existingConfigs
|
||
.where((config) => config.provider == provider && config.isValidated)
|
||
.map((config) => config.modelName)
|
||
.toList();
|
||
}
|
||
|
||
// Modify provider selection handler
|
||
void _handleProviderSelected(String provider) {
|
||
print('️Provider selected: $provider');
|
||
if (provider != _selectedProvider) {
|
||
setState(() {
|
||
_selectedProvider = provider;
|
||
_selectedModel = null; // Reset model selection
|
||
_providerCapability = null; // Reset capability
|
||
_apiKeyTestSuccess = false; // Reset API key test status
|
||
_isTestingApiKey = false; // Reset testing flag
|
||
_models = []; // Clear model list
|
||
_isLoadingModels = false; // Reset loading models flag
|
||
// 清除错误状态
|
||
_providerError = false;
|
||
_modelError = false;
|
||
_apiKeyError = false;
|
||
_providerErrorText = null;
|
||
_modelErrorText = null;
|
||
_apiKeyErrorText = null;
|
||
// Clear previous provider's info only in Add mode
|
||
if (!_isEditMode) {
|
||
_apiEndpointController.text = '';
|
||
_apiKeyController.text = '';
|
||
_aliasController.text = ''; // Clear alias too
|
||
}
|
||
_showApiKey = false; // Hide API key on provider change
|
||
});
|
||
|
||
// Trigger loading capability for the new provider
|
||
context.read<AiConfigBloc>().add(LoadProviderCapability(providerName: provider));
|
||
|
||
// Trigger auto-fill for the new provider
|
||
// 编辑模式下不触发自动填充
|
||
if (!_isEditMode) {
|
||
_autoFillApiInfo(provider);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 切换API Key的显示/隐藏
|
||
void _toggleApiKeyVisibility() {
|
||
setState(() {
|
||
_showApiKey = !_showApiKey;
|
||
});
|
||
}
|
||
|
||
// 自动填充API信息
|
||
void _autoFillApiInfo(String provider) {
|
||
// 编辑模式下不需要自动填充,避免触发不必要的API Key验证
|
||
if (_isEditMode) return;
|
||
|
||
// 发送获取该提供商默认配置的事件
|
||
// 实际的填充操作会在BlocListener中根据状态变化处理
|
||
print('⚠️ 调用_autoFillApiInfo,provider=$provider');
|
||
context.read<AiConfigBloc>().add(GetProviderDefaultConfig(provider: provider));
|
||
}
|
||
|
||
// New method to handle API Key test button press
|
||
void _testApiKey() {
|
||
final apiKey = _apiKeyController.text.trim();
|
||
final apiEndpoint = _apiEndpointController.text.trim().isEmpty
|
||
? null
|
||
: _apiEndpointController.text.trim();
|
||
|
||
if (_selectedProvider != null && apiKey.isNotEmpty) {
|
||
// Set testing state in UI immediately
|
||
// No need to call setState here as BlocListener will handle it
|
||
context.read<AiConfigBloc>().add(TestApiKey(
|
||
providerName: _selectedProvider!,
|
||
apiKey: apiKey,
|
||
apiEndpoint: apiEndpoint,
|
||
));
|
||
} else {
|
||
// Show feedback if provider or key is missing
|
||
TopToast.warning(context, '请先选择提供商并输入 API 密钥');
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return BlocListener<AiConfigBloc, AiConfigState>(
|
||
listener: (context, state) {
|
||
if (!mounted) return;
|
||
|
||
bool needsSetState = false;
|
||
|
||
// --- Provider Loading & List Update ---
|
||
if (_isLoadingProviders &&
|
||
(state.availableProviders.isNotEmpty ||
|
||
state.errorMessage != null && state.status != AiConfigStatus.loading)) {
|
||
_isLoadingProviders = false;
|
||
needsSetState = true;
|
||
}
|
||
if (!listEquals(_providers, state.availableProviders)) {
|
||
_providers = state.availableProviders;
|
||
needsSetState = true;
|
||
}
|
||
|
||
// --- Provider Capability Update ---
|
||
if (state.providerCapability != _providerCapability && state.selectedProviderForModels == _selectedProvider) {
|
||
_providerCapability = state.providerCapability;
|
||
print("Listener: Capability updated for $_selectedProvider: $_providerCapability");
|
||
needsSetState = true;
|
||
// Note: Model loading based on capability is handled in the BLoC event handler itself now.
|
||
}
|
||
|
||
// --- Model Loading & List Update ---
|
||
if (state.selectedProviderForModels == _selectedProvider) {
|
||
if (_isLoadingModels &&
|
||
(state.modelsForProvider.isNotEmpty ||
|
||
state.errorMessage != null)) {
|
||
_isLoadingModels = false;
|
||
needsSetState = true;
|
||
}
|
||
if (!listEquals(_models, state.modelsForProvider)) {
|
||
_models = state.modelsForProvider;
|
||
// If models updated (e.g., after API test), re-validate selected model
|
||
if (!_models.contains(_selectedModel)) {
|
||
_selectedModel = null;
|
||
}
|
||
needsSetState = true;
|
||
}
|
||
} else if (_isLoadingModels) {
|
||
// If the selected provider changed while models were loading, stop loading indicator
|
||
_isLoadingModels = false;
|
||
needsSetState = true;
|
||
}
|
||
|
||
// --- API 密钥测试状态更新 ---
|
||
if (state.isTestingApiKey != _isTestingApiKey) {
|
||
_isTestingApiKey = state.isTestingApiKey;
|
||
// If we *start* testing (used for loading models with key), set loading state
|
||
if (_isTestingApiKey) {
|
||
_isLoadingModels = true;
|
||
}
|
||
needsSetState = true;
|
||
}
|
||
// Check if success is for the *currently selected* provider and non-null
|
||
final testSuccessForCurrentProvider = state.apiKeyTestSuccessProvider != null && state.apiKeyTestSuccessProvider == _selectedProvider;
|
||
if (testSuccessForCurrentProvider != _apiKeyTestSuccess) {
|
||
_apiKeyTestSuccess = testSuccessForCurrentProvider;
|
||
if (_apiKeyTestSuccess) {
|
||
_isLoadingModels = false; // <-- Reset loading state on success
|
||
}
|
||
needsSetState = true;
|
||
}
|
||
// 处理 API 密钥测试错误
|
||
if (state.apiKeyTestError != null) {
|
||
print("Listener: API Key test FAILED for $_selectedProvider: ${state.apiKeyTestError}");
|
||
_isLoadingModels = false; // <-- Reset loading state on error
|
||
TopToast.error(context, 'API 密钥测试失败: ${state.apiKeyTestError}');
|
||
// Clear the error in the bloc state? This should maybe be done in the bloc itself after emitting.
|
||
// context.read<AiConfigBloc>().add(ClearApiKeyTestError()); // Need this event/logic in BLoC
|
||
needsSetState = true; // Need to rebuild to potentially remove loading indicator
|
||
}
|
||
|
||
// --- Default Config Auto-fill Update ---
|
||
if (_selectedProvider != null) {
|
||
final defaultConfig = state.providerDefaultConfigs[_selectedProvider!];
|
||
if (defaultConfig != null && defaultConfig.id.isNotEmpty) {
|
||
if (!_isEditMode) {
|
||
bool filledSomething = false;
|
||
if (_apiEndpointController.text.isEmpty && defaultConfig.apiEndpoint.isNotEmpty) {
|
||
_apiEndpointController.text = defaultConfig.apiEndpoint;
|
||
filledSomething = true;
|
||
}
|
||
if (_apiKeyController.text.isEmpty && (defaultConfig.apiKey?.isNotEmpty ?? false)) {
|
||
_apiKeyController.text = defaultConfig.apiKey!;
|
||
_showApiKey = true;
|
||
filledSomething = true;
|
||
// If auto-filled API key, consider it "tested" for UI purposes,
|
||
// but a real test might still be needed depending on workflow.
|
||
// _apiKeyTestSuccess = true; // Maybe set this? Or require manual test? Let's require manual test for now.
|
||
}
|
||
if(filledSomething) needsSetState = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Saving State Update ---
|
||
if (_isSaving && state.actionStatus != AiConfigActionStatus.loading) {
|
||
if (state.actionStatus == AiConfigActionStatus.success) {
|
||
widget.onCancel();
|
||
}
|
||
// Error toast is handled by the listener in SettingsPanel
|
||
_isSaving = false;
|
||
needsSetState = true;
|
||
}
|
||
|
||
if (needsSetState) {
|
||
setState(() {});
|
||
}
|
||
},
|
||
child: Scaffold(
|
||
backgroundColor: Colors.transparent,
|
||
body: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||
child: Form(
|
||
key: _formKey,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 标题
|
||
Text(
|
||
_isEditMode ? '编辑模型服务' : '添加新模型服务',
|
||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||
fontWeight: FontWeight.bold,
|
||
color: Theme.of(context).colorScheme.primary,
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// 主要内容区域 - 左右布局
|
||
Expanded(
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 左侧提供商列表
|
||
if (!_isEditMode) // 在编辑模式下不显示左侧列表
|
||
SizedBox(
|
||
width: 180,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(
|
||
child: ProviderList(
|
||
providers: _providers,
|
||
selectedProvider: _selectedProvider,
|
||
onProviderSelected: _handleProviderSelected,
|
||
),
|
||
),
|
||
// 提供商选择错误提示
|
||
if (_providerError && _providerErrorText != null)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 4, left: 8),
|
||
child: Text(
|
||
_providerErrorText!,
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.error,
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
if (!_isEditMode)
|
||
const SizedBox(width: 16),
|
||
|
||
// 右侧配置区域
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// Provider显示(仅编辑模式)
|
||
if (_isEditMode && _selectedProvider != null)
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 16),
|
||
child: Text(
|
||
'提供商: $_selectedProvider',
|
||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
|
||
// 模型搜索框(仅添加模式)
|
||
if (!_isEditMode && _selectedProvider != null)
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'选择模型',
|
||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||
fontWeight: FontWeight.w500,
|
||
color: _modelError
|
||
? Theme.of(context).colorScheme.error
|
||
: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Container(
|
||
height: 36,
|
||
decoration: _modelError ? BoxDecoration(
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: Theme.of(context).colorScheme.error,
|
||
width: 1.5,
|
||
),
|
||
) : null,
|
||
child: SearchableModelDropdown(
|
||
models: _models,
|
||
onModelSelected: _handleModelSelected,
|
||
hintText: '搜索可用模型',
|
||
),
|
||
),
|
||
// 模型选择错误提示
|
||
if (_modelError && _modelErrorText != null)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 4),
|
||
child: Text(
|
||
_modelErrorText!,
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.error,
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// 已选模型显示(仅添加模式)
|
||
if (!_isEditMode && _selectedModel != null)
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: Text(
|
||
'已选模型: $_selectedModel',
|
||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||
color: Theme.of(context).colorScheme.primary,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
|
||
// 模型显示(仅编辑模式)
|
||
if (_isEditMode && _selectedModel != null)
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: Text(
|
||
'模型: $_selectedModel',
|
||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
|
||
// 别名输入框
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 6),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'别名 (可选)',
|
||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||
fontWeight: FontWeight.w500,
|
||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||
),
|
||
),
|
||
const SizedBox(height: 6),
|
||
SizedBox(
|
||
height: 36,
|
||
child: TextFormField(
|
||
controller: _aliasController,
|
||
style: Theme.of(context).textTheme.bodyMedium,
|
||
decoration: InputDecoration(
|
||
hintText: '例如:我的 ${_selectedModel ?? '模型'}',
|
||
hintStyle: TextStyle(
|
||
fontSize: 13,
|
||
color: Theme.of(context).hintColor.withOpacity(0.7),
|
||
),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(
|
||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||
width: 1.0,
|
||
),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(
|
||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||
width: 1.0,
|
||
),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(
|
||
color: Theme.of(context).colorScheme.primary,
|
||
width: 1.5,
|
||
),
|
||
),
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 0
|
||
),
|
||
filled: true,
|
||
fillColor: Theme.of(context).brightness == Brightness.dark
|
||
? Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.3)
|
||
: Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.7),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// API Key输入框
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'API 密钥',
|
||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||
fontWeight: FontWeight.w500,
|
||
color: _apiKeyError
|
||
? Theme.of(context).colorScheme.error
|
||
: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center, // Align items vertically
|
||
children: [
|
||
Expanded(
|
||
child: SizedBox(
|
||
height: 36, // Keep height consistent
|
||
child: TextFormField(
|
||
controller: _apiKeyController,
|
||
obscureText: !_showApiKey,
|
||
style: Theme.of(context).textTheme.bodyMedium,
|
||
decoration: InputDecoration(
|
||
hintText: _isEditMode ? '当前API密钥 (可修改)' : '输入您的API密钥',
|
||
hintStyle: TextStyle(
|
||
fontSize: 13,
|
||
color: Theme.of(context).hintColor.withOpacity(0.7),
|
||
),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(
|
||
color: _apiKeyError
|
||
? Theme.of(context).colorScheme.error
|
||
: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||
width: _apiKeyError ? 1.5 : 1.0,
|
||
),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(
|
||
color: _apiKeyError
|
||
? Theme.of(context).colorScheme.error
|
||
: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||
width: _apiKeyError ? 1.5 : 1.0,
|
||
),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(
|
||
color: _apiKeyError
|
||
? Theme.of(context).colorScheme.error
|
||
: Theme.of(context).colorScheme.primary,
|
||
width: 1.5,
|
||
),
|
||
),
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 0, // Adjust vertical padding if needed
|
||
),
|
||
filled: true,
|
||
fillColor: Theme.of(context).brightness == Brightness.dark
|
||
? Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.3)
|
||
: Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.7),
|
||
suffixIcon: IconButton(
|
||
icon: Icon(
|
||
_showApiKey ? Icons.visibility_off : Icons.visibility,
|
||
size: 18,
|
||
),
|
||
onPressed: _toggleApiKeyVisibility,
|
||
),
|
||
),
|
||
validator: (value) {
|
||
// Require API key in add mode only if provider capability mandates it
|
||
if (!_isEditMode &&
|
||
_providerCapability == ModelListingCapability.listingWithKey &&
|
||
(value == null || value.trim().isEmpty)) {
|
||
return '需要 API 密钥';
|
||
}
|
||
return null;
|
||
},
|
||
onChanged: (_) {
|
||
// Reset test success status if key changes
|
||
if (_apiKeyTestSuccess) {
|
||
setState(() { _apiKeyTestSuccess = false; });
|
||
}
|
||
// 清除API Key错误状态
|
||
if (_apiKeyError) {
|
||
setState(() {
|
||
_apiKeyError = false;
|
||
_apiKeyErrorText = null;
|
||
});
|
||
}
|
||
// Trigger rebuild to potentially enable/disable test button
|
||
setState(() {});
|
||
},
|
||
),
|
||
),
|
||
),
|
||
|
||
// API 密钥测试按钮与状态
|
||
if (_selectedProvider != null && _providerCapability == ModelListingCapability.listingWithKey)
|
||
Padding(
|
||
padding: const EdgeInsets.only(left: 8.0),
|
||
child: SizedBox(
|
||
height: 36, // Match TextFormField height
|
||
child: _isTestingApiKey
|
||
? const SizedBox( // Show loading indicator, but don't rebuild on test success, to avoid flicker
|
||
width: 36, height: 36,
|
||
child: Center(child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)))
|
||
)
|
||
: (_apiKeyTestSuccess
|
||
? const Tooltip(
|
||
message: 'API 密钥已验证',
|
||
child: Icon( // Show success icon
|
||
Icons.check_circle, color: Colors.green, size: 24),
|
||
)
|
||
: TextButton( // Show test button
|
||
// Disable button if API key field is empty
|
||
onPressed: _apiKeyController.text.trim().isEmpty ? null : _testApiKey,
|
||
style: TextButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
minimumSize: Size(0, 36), // Ensure height matches
|
||
// Dim text color if disabled
|
||
foregroundColor: _apiKeyController.text.trim().isEmpty ? Theme.of(context).disabledColor : null,
|
||
),
|
||
child: const Text('测试', style: TextStyle(fontSize: 13)),
|
||
)
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
// API 密钥错误提示
|
||
if (_apiKeyError && _apiKeyErrorText != null)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 4),
|
||
child: Text(
|
||
_apiKeyErrorText!,
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.error,
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
),
|
||
|
||
// Auto-fill prompt adjusted
|
||
if (!_isEditMode && _selectedProvider != null && _providerCapability == ModelListingCapability.listingWithKey && !_apiKeyError)
|
||
BlocBuilder<AiConfigBloc, AiConfigState>(
|
||
builder: (context, state) {
|
||
final defaultConfig = state.getProviderDefaultConfig(_selectedProvider!);
|
||
final hasFilledApiKey = _apiKeyController.text.isNotEmpty;
|
||
// Show prompt only if key was auto-filled AND not yet tested successfully
|
||
if (defaultConfig != null && defaultConfig.id.isNotEmpty && (defaultConfig.apiKey?.isNotEmpty ?? false) && hasFilledApiKey && !_apiKeyTestSuccess) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: 4),
|
||
child: Row(
|
||
children: [
|
||
Icon(Icons.info_outline, size: 14, color: Colors.blue),
|
||
const SizedBox(width: 4),
|
||
Expanded(
|
||
child: Text('已自动填充API密钥,请测试连接', style: TextStyle(fontSize: 12, color: Colors.blue)),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
return const SizedBox.shrink();
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// API Endpoint输入框
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'API 接口地址(可选)',
|
||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||
fontWeight: FontWeight.w500,
|
||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: SizedBox(
|
||
height: 36,
|
||
child: TextFormField(
|
||
controller: _apiEndpointController,
|
||
style: Theme.of(context).textTheme.bodyMedium,
|
||
decoration: InputDecoration(
|
||
hintText: '例如:https://api.openai.com/v1',
|
||
hintStyle: TextStyle(
|
||
fontSize: 13,
|
||
color: Theme.of(context).hintColor.withOpacity(0.7),
|
||
),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(
|
||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||
width: 1.0,
|
||
),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(
|
||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||
width: 1.0,
|
||
),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(
|
||
color: Theme.of(context).colorScheme.primary,
|
||
width: 1.5,
|
||
),
|
||
),
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 0
|
||
),
|
||
filled: true,
|
||
fillColor: Theme.of(context).brightness == Brightness.dark
|
||
? Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.3)
|
||
: Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.7),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
// 如果有该提供商的已配置API地址,显示提示
|
||
if (!_isEditMode && _selectedProvider != null)
|
||
BlocBuilder<AiConfigBloc, AiConfigState>(
|
||
builder: (context, state) {
|
||
final defaultConfig = state.getProviderDefaultConfig(_selectedProvider!);
|
||
if (defaultConfig != null && defaultConfig.id.isNotEmpty && defaultConfig.apiEndpoint.isNotEmpty) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: 4),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
Icons.info_outline,
|
||
size: 14,
|
||
color: Theme.of(context).colorScheme.primary,
|
||
),
|
||
const SizedBox(width: 4),
|
||
Expanded(
|
||
child: Text(
|
||
'已使用该提供商的API地址: ${defaultConfig.apiEndpoint}',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: Theme.of(context).colorScheme.primary,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
return const SizedBox.shrink();
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// 模型分组列表(仅在选择了提供商时显示)
|
||
if (!_isEditMode && _selectedProvider != null && !_isLoadingModels)
|
||
Expanded( // 将原来的Column改为Expanded
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 10.0, bottom: 4.0),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text('可用模型', style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||
fontWeight: FontWeight.w500,
|
||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||
fontSize: 13,
|
||
)),
|
||
Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// 刷新按钮
|
||
IconButton(
|
||
icon: const Icon(Icons.refresh, size: 16),
|
||
onPressed: () {
|
||
if (_selectedProvider != null) {
|
||
// 清除该提供商的缓存并重新加载
|
||
context.read<AiConfigBloc>().add(ClearModelsCache(provider: _selectedProvider));
|
||
context.read<AiConfigBloc>().add(LoadModelsForProvider(provider: _selectedProvider!));
|
||
}
|
||
},
|
||
tooltip: '刷新模型列表',
|
||
style: IconButton.styleFrom(
|
||
padding: const EdgeInsets.all(4),
|
||
minimumSize: const Size(24, 24),
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
),
|
||
),
|
||
const SizedBox(width: 4),
|
||
// 添加自定义模型按钮
|
||
TextButton.icon(
|
||
icon: const Icon(Icons.add, size: 14),
|
||
label: const Text('添加自定义模型', style: TextStyle(fontSize: 12)),
|
||
style: TextButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
minimumSize: const Size(0, 30),
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
),
|
||
onPressed: _handleAddCustomModel,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// Container to provide background and border for the list area
|
||
Expanded( // 将Container包装在Expanded中,使其填充剩余空间
|
||
child: Container(
|
||
width: double.infinity,
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).brightness == Brightness.dark
|
||
? Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.5)
|
||
: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.7),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||
width: 0.5,
|
||
),
|
||
),
|
||
child: BlocBuilder<AiConfigBloc, AiConfigState>(
|
||
builder: (context, state) {
|
||
// Safely access model group using the selected provider key
|
||
// NOTE: Assuming AIModelGroup and ModelListingCapability are correctly defined/imported elsewhere
|
||
// after resolving the 'ai_config_repository' dependency.
|
||
final modelGroup = _selectedProvider != null ? state.modelGroups[_selectedProvider!] : null;
|
||
final currentCapability = _providerCapability; // Use local capability state
|
||
// 获取该提供商下已经验证的模型列表
|
||
final verifiedModels = _selectedProvider != null ? _getVerifiedModels(_selectedProvider!) : <String>[];
|
||
|
||
if (_isLoadingModels) {
|
||
return const Center(
|
||
child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator()),
|
||
);
|
||
}
|
||
|
||
// Check if model groups are available
|
||
if (modelGroup != null && modelGroup.groups.isNotEmpty) {
|
||
return SingleChildScrollView(
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||
child: ModelGroupList(
|
||
modelGroup: modelGroup,
|
||
onModelSelected: _handleModelSelected,
|
||
selectedModel: _selectedModel,
|
||
verifiedModels: verifiedModels, // 传递已验证模型列表
|
||
),
|
||
),
|
||
);
|
||
} else {
|
||
// Show message if no models are available or conditions not met
|
||
String message = '该提供商没有可用的模型。';
|
||
// Check if capability requires API key and if it has been tested successfully
|
||
// if (currentCapability == ModelListingCapability.listingWithKey && !_apiKeyTestSuccess) {
|
||
// message = '请先成功测试 API Key 以加载模型列表。';
|
||
// } else if (currentCapability == ModelListingCapability.noListing) {
|
||
// message = '该提供商不支持自动获取模型列表。';
|
||
// }
|
||
// ^^^ Commented out capability check as ModelListingCapability might be undefined now
|
||
|
||
// Fallback message if capability check is removed/unavailable
|
||
if (modelGroup == null || modelGroup.groups.isEmpty) {
|
||
if (_selectedProvider != null) {
|
||
// More specific message if provider selected but no models
|
||
message = '未能加载模型列表。如果需要 API Key,请确保已成功测试。';
|
||
} else {
|
||
message = '请先选择一个提供商。';
|
||
}
|
||
}
|
||
|
||
|
||
return Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
message,
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontSize: 13, color: Theme.of(context).hintColor)
|
||
),
|
||
const SizedBox(height: 16),
|
||
FilledButton.icon(
|
||
icon: const Icon(Icons.add, size: 16),
|
||
label: const Text('添加自定义模型'),
|
||
onPressed: _handleAddCustomModel,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
},
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// 加载指示器
|
||
if (!_isEditMode && _selectedProvider != null && _isLoadingModels)
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 10.0, bottom: 4.0),
|
||
child: Text('可用模型', style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||
fontWeight: FontWeight.w500,
|
||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||
fontSize: 13,
|
||
)),
|
||
),
|
||
Expanded(
|
||
child: Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const CircularProgressIndicator(),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'正在加载 $_selectedProvider 的模型列表...',
|
||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
], // End of right column children
|
||
),
|
||
), // End of Expanded (Right Side)
|
||
], // End of Row children
|
||
), // End of Row
|
||
), // End of Expanded (Main Content Area)
|
||
|
||
// 底部按钮区域
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 16),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: [
|
||
// 调试按钮
|
||
if (!kReleaseMode) // 只在调试模式显示
|
||
TextButton(
|
||
onPressed: () {
|
||
final configs = context.read<AiConfigBloc>().state.providerDefaultConfigs;
|
||
print('⚠️ 当前所有提供商默认配置:');
|
||
configs.forEach((provider, config) {
|
||
print('⚠️ 提供商=$provider, configId=${config.id}, hasApiKey=${config.apiKey != null}');
|
||
});
|
||
},
|
||
child: const Text('打印配置', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||
),
|
||
|
||
const Spacer(),
|
||
|
||
OutlinedButton(
|
||
onPressed: _isSaving ? null : widget.onCancel,
|
||
style: OutlinedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
side: BorderSide(
|
||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||
width: 1.0,
|
||
),
|
||
),
|
||
child: const Text('取消', style: TextStyle(fontSize: 13)),
|
||
),
|
||
const SizedBox(width: 12),
|
||
ElevatedButton(
|
||
onPressed: _isSaving ? null : _submitForm,
|
||
style: ElevatedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||
elevation: 1,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
),
|
||
child: _isSaving
|
||
? const SizedBox(height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||
: Text(_isEditMode ? '保存更改' : '添加', style: const TextStyle(fontSize: 13)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
], // End of Form children
|
||
), // End of Form
|
||
), // End of Container
|
||
), // End of Body
|
||
), // End of Scaffold
|
||
);
|
||
}
|
||
}
|
||
|
||
// Helper function to get model initial (potentially update logic if needed)
|
||
String _getModelInitial(String modelName) {
|
||
if (modelName.isEmpty) return '?';
|
||
// Simple initial, might need refinement for complex names
|
||
return modelName[0].toUpperCase();
|
||
}
|
||
|
||
// Helper function to get model color (can stay based on id or name)
|
||
Color _getModelColor(String modelId) {
|
||
// Use a hash of the model ID to generate a consistent color
|
||
final int hash = modelId.hashCode;
|
||
// Use HSLColor for better control over saturation and lightness
|
||
return HSLColor.fromAHSL(
|
||
1.0, // Alpha
|
||
(hash % 360).toDouble(), // Hue (0-360)
|
||
0.6, // Saturation (adjust as needed, 0.6 is moderately saturated)
|
||
0.5, // Lightness (adjust as needed, 0.5 is mid-lightness)
|
||
).toColor();
|
||
}
|