727 lines
24 KiB
Dart
727 lines
24 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:collection/collection.dart';
|
||
|
||
import '../../config/provider_icons.dart';
|
||
import '../../models/public_model_config.dart';
|
||
import '../../services/api_service/repositories/impl/admin_repository_impl.dart';
|
||
import '../../utils/logger.dart';
|
||
import '../../utils/web_theme.dart';
|
||
import '../../widgets/common/error_view.dart';
|
||
import '../../widgets/common/loading_indicator.dart';
|
||
import 'widgets/add_public_model_dialog.dart';
|
||
import 'widgets/edit_public_model_dialog.dart';
|
||
import 'widgets/public_model_provider_group_card.dart';
|
||
import 'widgets/validation_results_dialog.dart';
|
||
import '../../widgets/common/top_toast.dart';
|
||
|
||
/// 公共模型管理页面
|
||
/// 提供完整的公共AI模型配置管理功能,包括:
|
||
/// - 按供应商分组显示所有可用提供商
|
||
/// - 在每个提供商分组下显示已配置的公共模型
|
||
/// - 添加/编辑/删除模型配置
|
||
/// - API Key池管理
|
||
/// - 模型验证和状态管理
|
||
class PublicModelManagementScreen extends StatefulWidget {
|
||
const PublicModelManagementScreen({Key? key}) : super(key: key);
|
||
|
||
@override
|
||
State<PublicModelManagementScreen> createState() => _PublicModelManagementScreenState();
|
||
}
|
||
|
||
/// 公共模型管理内容主体,可以在不同布局中复用
|
||
class PublicModelManagementBody extends StatefulWidget {
|
||
const PublicModelManagementBody({Key? key}) : super(key: key);
|
||
|
||
@override
|
||
State<PublicModelManagementBody> createState() => _PublicModelManagementBodyState();
|
||
}
|
||
|
||
class _PublicModelManagementScreenState extends State<PublicModelManagementScreen> {
|
||
final GlobalKey<_PublicModelManagementBodyState> _bodyKey = GlobalKey<_PublicModelManagementBodyState>();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
backgroundColor: WebTheme.getBackgroundColor(context),
|
||
foregroundColor: WebTheme.getTextColor(context),
|
||
title: Text(
|
||
'公共模型管理',
|
||
style: TextStyle(color: WebTheme.getTextColor(context)),
|
||
),
|
||
actions: [
|
||
IconButton(
|
||
onPressed: () => _bodyKey.currentState?._refreshData(),
|
||
icon: Icon(Icons.refresh, color: WebTheme.getTextColor(context)),
|
||
tooltip: '刷新',
|
||
),
|
||
IconButton(
|
||
onPressed: () => _showAddModelDialog(context),
|
||
icon: Icon(Icons.add, color: WebTheme.getTextColor(context)),
|
||
tooltip: '添加模型',
|
||
),
|
||
],
|
||
),
|
||
backgroundColor: WebTheme.getBackgroundColor(context),
|
||
body: Align(
|
||
alignment: Alignment.topCenter,
|
||
child: ConstrainedBox(
|
||
constraints: const BoxConstraints(maxWidth: 1600),
|
||
child: PublicModelManagementBody(key: _bodyKey),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showAddModelDialog(BuildContext context) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AddPublicModelDialog(
|
||
onSuccess: () => _bodyKey.currentState?._refreshData(),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PublicModelManagementBodyState extends State<PublicModelManagementBody> {
|
||
List<PublicModelConfigDetails> _modelConfigs = [];
|
||
List<String> _availableProviders = [];
|
||
bool _isLoading = true;
|
||
String? _error;
|
||
String _searchQuery = '';
|
||
String _filterValue = 'all';
|
||
Map<String, bool> _expandedProviders = {};
|
||
|
||
late final AdminRepositoryImpl _adminRepository;
|
||
final String _tag = 'PublicModelManagementScreen';
|
||
|
||
// 缓存机制
|
||
DateTime? _lastLoadTime;
|
||
static const Duration _cacheValidDuration = Duration(minutes: 3);
|
||
bool _isInitialLoad = true;
|
||
|
||
bool get _shouldRefreshConfigs {
|
||
if (_lastLoadTime == null || _isInitialLoad) return true;
|
||
return DateTime.now().difference(_lastLoadTime!) > _cacheValidDuration;
|
||
}
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_adminRepository = AdminRepositoryImpl();
|
||
_loadData();
|
||
}
|
||
|
||
Future<void> _loadData() async {
|
||
// 先加载可用供应商,然后加载模型配置
|
||
await _loadAvailableProviders();
|
||
await _loadModelConfigs();
|
||
}
|
||
|
||
Future<void> _loadAvailableProviders() async {
|
||
if (!mounted) return;
|
||
|
||
// 开始加载可用供应商
|
||
|
||
try {
|
||
AppLogger.d(_tag, '开始加载可用供应商列表');
|
||
final providers = await _adminRepository.getAvailableProviders();
|
||
|
||
if (mounted) {
|
||
setState(() {
|
||
_availableProviders = providers;
|
||
// 默认展开所有供应商
|
||
for (final provider in providers) {
|
||
_expandedProviders[provider] ??= true;
|
||
}
|
||
});
|
||
|
||
AppLogger.d(_tag, '成功加载 ${providers.length} 个供应商');
|
||
}
|
||
} catch (e) {
|
||
AppLogger.e(_tag, '加载供应商列表失败', e);
|
||
// 忽略加载状态更新,无需标记供应商加载中
|
||
}
|
||
}
|
||
|
||
Future<void> _loadModelConfigs() async {
|
||
if (!_shouldRefreshConfigs) {
|
||
AppLogger.d(_tag, '使用缓存数据,跳过重新加载');
|
||
if (mounted) {
|
||
setState(() {
|
||
_isLoading = false;
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
setState(() {
|
||
_isLoading = true;
|
||
_error = null;
|
||
});
|
||
|
||
try {
|
||
AppLogger.d(_tag, '开始加载公共模型配置列表');
|
||
_lastLoadTime = DateTime.now();
|
||
_isInitialLoad = false;
|
||
|
||
final configs = await _adminRepository.getPublicModelConfigDetails();
|
||
|
||
AppLogger.d(_tag, '📊 原始配置数据: ${configs.length} 个');
|
||
for (int i = 0; i < configs.length && i < 3; i++) {
|
||
final config = configs[i];
|
||
AppLogger.d(_tag, '📊 配置 $i: provider=${config.provider}, modelId=${config.modelId}, enabled=${config.enabled}, id=${config.id}');
|
||
}
|
||
|
||
if (mounted) {
|
||
setState(() {
|
||
_modelConfigs = configs;
|
||
_isLoading = false;
|
||
});
|
||
|
||
// 提示:可为公共模型打标签以用于后端选择策略(示例:"jsonify"/"cheap"/"fast")
|
||
// - jsonify:适配“文本→JSON结构化工具”阶段优先选择
|
||
// - cheap:成本优先
|
||
// - fast:时延优先
|
||
// 管理员可在“编辑模型”中为配置添加上述 tags,后端会在第二阶段依据标签和 priority 挑选。
|
||
|
||
AppLogger.d(_tag, '✅ 成功加载 ${configs.length} 个公共模型配置,界面状态已更新');
|
||
|
||
// 检查分组结果
|
||
final grouped = _groupConfigsByProvider();
|
||
AppLogger.d(_tag, '📊 分组结果: ${grouped.length} 个供应商,${grouped.values.expand((list) => list).length} 个配置');
|
||
grouped.forEach((provider, configList) {
|
||
AppLogger.d(_tag, '📊 供应商 $provider: ${configList.length} 个配置');
|
||
});
|
||
}
|
||
} catch (e, stackTrace) {
|
||
AppLogger.e(_tag, '加载公共模型配置失败', e);
|
||
AppLogger.e(_tag, '错误堆栈', stackTrace);
|
||
if (mounted) {
|
||
setState(() {
|
||
_error = '加载公共模型配置失败: ${e.toString()}';
|
||
_isLoading = false;
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
void _handleSearch(String query) {
|
||
setState(() {
|
||
_searchQuery = query.toLowerCase();
|
||
});
|
||
}
|
||
|
||
void _handleFilterChange(String value) {
|
||
setState(() {
|
||
_filterValue = value;
|
||
});
|
||
}
|
||
|
||
void _handleToggleProvider(String provider) {
|
||
setState(() {
|
||
_expandedProviders[provider] = !(_expandedProviders[provider] ?? true);
|
||
});
|
||
}
|
||
|
||
Future<void> _handleValidate(String configId) async {
|
||
try {
|
||
AppLogger.d(_tag, '开始验证模型配置: $configId');
|
||
|
||
TopToast.info(context, '正在验证模型配置...');
|
||
|
||
final withKeys = await _adminRepository.validatePublicModelConfigAndFetchWithKeys(configId);
|
||
|
||
if (!mounted) return;
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => ValidationResultsDialog(config: withKeys),
|
||
);
|
||
|
||
AppLogger.d(_tag, '模型配置验证成功: $configId');
|
||
_refreshData();
|
||
} catch (e) {
|
||
AppLogger.e(_tag, '模型配置验证失败', e);
|
||
TopToast.error(context, '验证失败: ${e.toString()}');
|
||
}
|
||
}
|
||
|
||
Future<void> _handleToggleStatus(String configId, bool enabled) async {
|
||
try {
|
||
AppLogger.d(_tag, '切换模型配置状态: $configId -> $enabled');
|
||
|
||
await _adminRepository.togglePublicModelConfigStatus(configId, enabled);
|
||
|
||
TopToast.success(context, enabled ? '模型已启用' : '模型已禁用');
|
||
|
||
AppLogger.d(_tag, '模型配置状态切换成功: $configId');
|
||
_refreshData();
|
||
} catch (e) {
|
||
AppLogger.e(_tag, '切换模型配置状态失败', e);
|
||
TopToast.error(context, '操作失败: ${e.toString()}');
|
||
}
|
||
}
|
||
|
||
void _handleEdit(String configId) {
|
||
final config = _modelConfigs.firstWhereOrNull((c) => c.id == configId);
|
||
if (config == null) return;
|
||
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => EditPublicModelDialog(
|
||
config: config,
|
||
onSuccess: _refreshData,
|
||
),
|
||
);
|
||
}
|
||
|
||
void _handleCopy(String configId) {
|
||
final config = _modelConfigs.firstWhereOrNull((c) => c.id == configId);
|
||
if (config == null) return;
|
||
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AddPublicModelDialog(
|
||
onSuccess: _refreshData,
|
||
selectedProvider: config.provider,
|
||
sourceConfig: config, // 传递源配置用于复制
|
||
),
|
||
);
|
||
}
|
||
|
||
void _handleDelete(String configId) {
|
||
final config = _modelConfigs.firstWhereOrNull((c) => c.id == configId);
|
||
if (config == null) return;
|
||
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
backgroundColor: WebTheme.getCardColor(context),
|
||
title: Text(
|
||
'确认删除',
|
||
style: TextStyle(color: WebTheme.getTextColor(context)),
|
||
),
|
||
content: Text(
|
||
'确定要删除模型配置 "${config.displayName ?? config.modelId}" 吗?此操作不可恢复。',
|
||
style: TextStyle(color: WebTheme.getTextColor(context)),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
child: Text('取消', style: TextStyle(color: WebTheme.getTextColor(context))),
|
||
),
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
_deleteModelConfig(configId);
|
||
},
|
||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||
child: const Text('删除'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _deleteModelConfig(String configId) async {
|
||
try {
|
||
AppLogger.d(_tag, '开始删除模型配置: $configId');
|
||
|
||
TopToast.info(context, '正在删除模型配置...');
|
||
|
||
await _adminRepository.deletePublicModelConfig(configId);
|
||
|
||
TopToast.success(context, '模型配置删除成功');
|
||
|
||
AppLogger.d(_tag, '模型配置删除成功: $configId');
|
||
_refreshData();
|
||
} catch (e) {
|
||
AppLogger.e(_tag, '删除模型配置失败', e);
|
||
TopToast.error(context, '删除失败: ${e.toString()}');
|
||
}
|
||
}
|
||
|
||
void _handleAddModel(String provider) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AddPublicModelDialog(
|
||
onSuccess: _refreshData,
|
||
selectedProvider: provider,
|
||
),
|
||
);
|
||
}
|
||
|
||
void _refreshData() {
|
||
_lastLoadTime = null; // 使缓存失效
|
||
_loadData();
|
||
}
|
||
|
||
// 按提供商分组配置 - 显示所有可用提供商
|
||
Map<String, List<PublicModelConfigDetails>> _groupConfigsByProvider() {
|
||
final Map<String, List<PublicModelConfigDetails>> grouped = {};
|
||
|
||
// 首先为所有可用提供商创建空列表
|
||
for (final provider in _availableProviders) {
|
||
grouped[provider] = [];
|
||
}
|
||
|
||
// 然后将配置分组到对应的提供商
|
||
for (final config in _modelConfigs) {
|
||
final provider = config.provider;
|
||
if (grouped.containsKey(provider)) {
|
||
grouped[provider]!.add(config);
|
||
} else {
|
||
// 如果配置的提供商不在可用列表中,也要显示
|
||
grouped[provider] = [config];
|
||
}
|
||
}
|
||
|
||
// 应用搜索和过滤
|
||
if (_searchQuery.isNotEmpty || _filterValue != 'all') {
|
||
final filteredGrouped = <String, List<PublicModelConfigDetails>>{};
|
||
|
||
for (final entry in grouped.entries) {
|
||
final provider = entry.key;
|
||
final configs = entry.value;
|
||
|
||
// 检查提供商名称是否匹配搜索
|
||
final providerMatches = _searchQuery.isEmpty ||
|
||
provider.toLowerCase().contains(_searchQuery) ||
|
||
ProviderIcons.getProviderDisplayName(provider).toLowerCase().contains(_searchQuery);
|
||
|
||
// 过滤配置
|
||
final filteredConfigs = configs.where((config) {
|
||
final matchesSearch = _searchQuery.isEmpty ||
|
||
(config.displayName?.toLowerCase().contains(_searchQuery) ?? false) ||
|
||
config.modelId.toLowerCase().contains(_searchQuery);
|
||
|
||
bool matchesFilter = true;
|
||
if (_filterValue == 'enabled') {
|
||
matchesFilter = config.enabled == true;
|
||
} else if (_filterValue == 'disabled') {
|
||
matchesFilter = config.enabled != true;
|
||
} else if (_filterValue == 'validated') {
|
||
matchesFilter = config.isValidated == true;
|
||
} else if (_filterValue == 'unvalidated') {
|
||
matchesFilter = config.isValidated != true;
|
||
}
|
||
|
||
return matchesSearch && matchesFilter;
|
||
}).toList();
|
||
|
||
// 如果提供商匹配搜索或者有匹配的配置,则显示该提供商
|
||
if (providerMatches || filteredConfigs.isNotEmpty) {
|
||
filteredGrouped[provider] = filteredConfigs;
|
||
}
|
||
}
|
||
|
||
return filteredGrouped;
|
||
}
|
||
|
||
return grouped;
|
||
}
|
||
|
||
// 获取提供商信息
|
||
Map<String, dynamic> _getProviderInfo(String provider) {
|
||
return {
|
||
'name': ProviderIcons.getProviderDisplayName(provider),
|
||
'description': _getProviderDescription(provider),
|
||
'color': ProviderIcons.getProviderColor(provider),
|
||
};
|
||
}
|
||
|
||
// 获取提供商描述
|
||
String _getProviderDescription(String provider) {
|
||
switch (provider.toLowerCase()) {
|
||
case 'openai':
|
||
return 'Advanced language models for various applications';
|
||
case 'anthropic':
|
||
return 'Constitutional AI models focused on safety';
|
||
case 'google':
|
||
case 'gemini':
|
||
return 'Gemini models and PaLM-based systems';
|
||
case 'openrouter':
|
||
return 'Unified API for multiple AI models';
|
||
case 'ollama':
|
||
return 'Local AI models runner';
|
||
case 'microsoft':
|
||
case 'azure':
|
||
return 'Microsoft Azure OpenAI Service';
|
||
case 'meta':
|
||
case 'llama':
|
||
return 'Large Language Model Meta AI';
|
||
case 'deepseek':
|
||
return 'DeepSeek AI language models';
|
||
case 'zhipu':
|
||
case 'glm':
|
||
return 'GLM and ChatGLM models';
|
||
case 'qwen':
|
||
case 'tongyi':
|
||
return 'Alibaba Tongyi Qianwen models';
|
||
case 'doubao':
|
||
case 'bytedance':
|
||
return 'ByteDance Doubao AI models';
|
||
case 'mistral':
|
||
return 'Mistral AI language models';
|
||
case 'perplexity':
|
||
return 'Perplexity AI search and reasoning';
|
||
case 'huggingface':
|
||
case 'hf':
|
||
return 'Hugging Face model hub and inference';
|
||
case 'stability':
|
||
return 'Stability AI generative models';
|
||
case 'xai':
|
||
case 'grok':
|
||
return 'xAI Grok conversational AI';
|
||
case 'siliconcloud':
|
||
case 'siliconflow':
|
||
return 'SiliconCloud AI model services';
|
||
default:
|
||
return 'AI model provider';
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
// 搜索和过滤头部
|
||
_buildHeader(),
|
||
|
||
// 内容区域
|
||
Expanded(
|
||
child: _buildContent(),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildHeader() {
|
||
return Container(
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.getCardColor(context),
|
||
border: Border(
|
||
bottom: BorderSide(
|
||
color: WebTheme.getBorderColor(context),
|
||
),
|
||
),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// 搜索框
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: TextField(
|
||
onChanged: _handleSearch,
|
||
style: TextStyle(color: WebTheme.getTextColor(context)),
|
||
decoration: InputDecoration(
|
||
hintText: '搜索模型或提供商...',
|
||
hintStyle: TextStyle(color: WebTheme.getSecondaryTextColor(context)),
|
||
prefixIcon: Icon(Icons.search, color: WebTheme.getSecondaryTextColor(context)),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(color: WebTheme.getBorderColor(context)),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(color: WebTheme.getTextColor(context)),
|
||
),
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
|
||
// 过滤下拉框
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: WebTheme.getBorderColor(context)),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: DropdownButton<String>(
|
||
value: _filterValue,
|
||
onChanged: (value) => _handleFilterChange(value!),
|
||
dropdownColor: WebTheme.getCardColor(context),
|
||
style: TextStyle(color: WebTheme.getTextColor(context)),
|
||
underline: Container(),
|
||
items: const [
|
||
DropdownMenuItem(value: 'all', child: Text('全部')),
|
||
DropdownMenuItem(value: 'enabled', child: Text('已启用')),
|
||
DropdownMenuItem(value: 'disabled', child: Text('已禁用')),
|
||
DropdownMenuItem(value: 'validated', child: Text('已验证')),
|
||
DropdownMenuItem(value: 'unvalidated', child: Text('未验证')),
|
||
],
|
||
),
|
||
),
|
||
|
||
const SizedBox(width: 12),
|
||
|
||
// 刷新按钮
|
||
IconButton(
|
||
onPressed: _refreshData,
|
||
icon: Icon(Icons.refresh, color: WebTheme.getTextColor(context)),
|
||
tooltip: '刷新',
|
||
),
|
||
],
|
||
),
|
||
|
||
const SizedBox(height: 12),
|
||
|
||
// 统计信息
|
||
Row(
|
||
children: [
|
||
_buildStatChip(
|
||
'总配置: ${_modelConfigs.length}',
|
||
Colors.blue,
|
||
),
|
||
const SizedBox(width: 8),
|
||
_buildStatChip(
|
||
'供应商: ${_availableProviders.length}',
|
||
Colors.green,
|
||
),
|
||
const SizedBox(width: 8),
|
||
_buildStatChip(
|
||
'已启用: ${_modelConfigs.where((c) => c.enabled == true).length}',
|
||
Colors.orange,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStatChip(String label, Color color) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||
decoration: BoxDecoration(
|
||
color: color.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(
|
||
color: color.withOpacity(0.3),
|
||
),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(
|
||
color: color,
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildContent() {
|
||
AppLogger.d(_tag, '🎨 构建内容: isLoading=$_isLoading, modelConfigs.length=${_modelConfigs.length}, availableProviders.length=${_availableProviders.length}, error=$_error');
|
||
|
||
if (_isLoading && _modelConfigs.isEmpty) {
|
||
AppLogger.d(_tag, '🎨 显示加载指示器');
|
||
return const Center(child: LoadingIndicator());
|
||
}
|
||
|
||
if (_error != null && _modelConfigs.isEmpty && _availableProviders.isEmpty) {
|
||
AppLogger.d(_tag, '🎨 显示错误视图: $_error');
|
||
return ErrorView(
|
||
error: _error!,
|
||
onRetry: _refreshData,
|
||
);
|
||
}
|
||
|
||
final groupedConfigs = _groupConfigsByProvider();
|
||
AppLogger.d(_tag, '🎨 分组配置: ${groupedConfigs.length} 个供应商');
|
||
|
||
if (groupedConfigs.isEmpty) {
|
||
AppLogger.d(_tag, '🎨 显示空状态 (搜索: $_searchQuery, 过滤: $_filterValue)');
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.cloud_off,
|
||
size: 64,
|
||
color: WebTheme.getSecondaryTextColor(context),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
_searchQuery.isNotEmpty || _filterValue != 'all'
|
||
? '没有找到匹配的供应商或模型配置'
|
||
: '暂无可用的AI供应商',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
color: WebTheme.getSecondaryTextColor(context),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
// 添加调试信息
|
||
if (_modelConfigs.isNotEmpty || _availableProviders.isNotEmpty)
|
||
Column(
|
||
children: [
|
||
Text(
|
||
'调试信息: 模型配置=${_modelConfigs.length}, 供应商=${_availableProviders.length}',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: WebTheme.getSecondaryTextColor(context),
|
||
),
|
||
),
|
||
Text(
|
||
'搜索="$_searchQuery", 过滤="$_filterValue"',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: WebTheme.getSecondaryTextColor(context),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
if (_searchQuery.isEmpty && _filterValue == 'all')
|
||
ElevatedButton.icon(
|
||
onPressed: () => _handleAddModel(''),
|
||
icon: const Icon(Icons.add, size: 16),
|
||
label: const Text('添加公共模型'),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: WebTheme.getTextColor(context),
|
||
foregroundColor: WebTheme.getBackgroundColor(context),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
AppLogger.d(_tag, '🎨 显示供应商列表: ${groupedConfigs.length} 个');
|
||
return ListView.builder(
|
||
padding: const EdgeInsets.all(16),
|
||
itemCount: groupedConfigs.length,
|
||
itemBuilder: (context, index) {
|
||
final provider = groupedConfigs.keys.elementAt(index);
|
||
final configs = groupedConfigs[provider]!;
|
||
final providerInfo = _getProviderInfo(provider);
|
||
final isExpanded = _expandedProviders[provider] ?? true;
|
||
|
||
AppLogger.d(_tag, '🎨 构建供应商卡片 $index: $provider (${configs.length} 个配置)');
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 16),
|
||
child: PublicModelProviderGroupCard(
|
||
provider: provider,
|
||
providerName: providerInfo['name'],
|
||
description: providerInfo['description'],
|
||
configs: configs,
|
||
isExpanded: isExpanded,
|
||
onToggleExpanded: () => _handleToggleProvider(provider),
|
||
onAddModel: () => _handleAddModel(provider),
|
||
onValidate: _handleValidate,
|
||
onEdit: _handleEdit,
|
||
onDelete: _handleDelete,
|
||
onToggleStatus: _handleToggleStatus,
|
||
onCopy: _handleCopy,
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
} |