Files
MaliangAINovalWriter/AINoval/lib/screens/settings/widgets/model_provider_group_card.dart
2025-09-10 00:07:52 +08:00

458 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:ainoval/models/user_ai_model_config_model.dart';
import 'package:ainoval/screens/editor/widgets/menu_builder.dart';
import '../../../config/provider_icons.dart';
/// 模型提供商分组卡片
/// 显示提供商信息和其下的模型列表
class ModelProviderGroupCard extends StatelessWidget {
const ModelProviderGroupCard({
super.key,
required this.provider,
required this.providerName,
required this.description,
required this.icon,
required this.color,
required this.configs,
required this.isExpanded,
required this.onToggleExpanded,
required this.onAddModel,
required this.onSetDefault,
required this.onValidate,
required this.onEdit,
required this.onDelete,
});
final String provider;
final String providerName;
final String description;
final IconData icon;
final Color color;
final List<UserAIModelConfigModel> configs;
final bool isExpanded;
final VoidCallback onToggleExpanded;
final VoidCallback onAddModel;
final Function(String) onSetDefault;
final Function(String) onValidate;
final Function(String) onEdit;
final Function(String) onDelete;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
// 统计验证状态
final verifiedCount = configs.where((c) => c.isValidated).length;
final totalCount = configs.length;
// 查找在当前提供商组内的默认模型
final defaultConfig = configs.firstWhere(
(c) => c.isDefault,
orElse: () => UserAIModelConfigModel.empty(),
);
// 只有当默认模型真正在当前组内时才显示
final hasDefaultInThisGroup = defaultConfig.id.isNotEmpty;
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.15),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDark ? 0.3 : 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 提供商头部
InkWell(
onTap: onToggleExpanded,
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// 提供商图标
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.3),
width: 1,
),
),
child: ProviderIcons.getProviderIconForContext(
provider,
iconSize: IconSize.large,
),
),
const SizedBox(width: 16),
// 提供商信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
providerName,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
description,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
],
),
),
const SizedBox(width: 16),
// 右侧状态信息根据HTML样式改进
_buildRightSideInfo(context, verifiedCount, totalCount, defaultConfig, hasDefaultInThisGroup),
],
),
),
),
// 分隔线
if (isExpanded)
Divider(
height: 1,
color: theme.colorScheme.outline.withOpacity(0.2),
indent: 16,
endIndent: 16,
),
// 模型列表
if (isExpanded)
Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 模型项列表
...configs.map((config) => _buildModelItem(context, config)),
const SizedBox(height: 12),
// 添加模型按钮
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: onAddModel,
icon: const Icon(Icons.add, size: 16),
label: const Text('添加模型'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
side: BorderSide(
color: theme.colorScheme.outline.withOpacity(0.5),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
),
),
],
),
);
}
// 构建右侧状态信息参考HTML结构
Widget _buildRightSideInfo(BuildContext context, int verifiedCount, int totalCount,
UserAIModelConfigModel defaultConfig, bool hasDefaultInThisGroup) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Container(
constraints: const BoxConstraints(minWidth: 120),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 桌面端显示sm及以上
LayoutBuilder(
builder: (context, constraints) {
final isSmallScreen = MediaQuery.of(context).size.width < 640;
if (isSmallScreen) {
// 移动端简化显示
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$verifiedCount/$totalCount',
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(width: 8),
_buildChevronIcon(isDark),
],
);
} else {
// 桌面端完整显示
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// 状态显示
Text(
'$verifiedCount/$totalCount 已启用',
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(width: 12),
// 默认模型显示(只有当前组有默认模型时才显示)
if (hasDefaultInThisGroup)
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.3),
width: 1,
),
borderRadius: BorderRadius.circular(20),
color: theme.colorScheme.surface,
),
child: Text(
'默认: ${defaultConfig.alias}',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
),
const SizedBox(width: 8),
_buildChevronIcon(isDark),
],
);
}
},
),
],
),
);
}
// 构建Chevron图标
Widget _buildChevronIcon(bool isDark) {
return AnimatedRotation(
turns: isExpanded ? 0.25 : 0, // 90度旋转
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.chevron_right,
size: 16,
color: isDark ? Colors.white.withOpacity(0.7) : Colors.black.withOpacity(0.7),
),
);
}
Widget _buildModelItem(BuildContext context, UserAIModelConfigModel config) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: config.isDefault
? theme.colorScheme.primary.withOpacity(0.1)
: theme.colorScheme.surfaceContainerHighest.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: config.isDefault
? theme.colorScheme.primary.withOpacity(0.3)
: theme.colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
child: Row(
children: [
// 模型状态图标
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: config.isValidated
? Colors.green.withOpacity(0.1)
: Colors.orange.withOpacity(0.1),
shape: BoxShape.circle,
border: Border.all(
color: config.isValidated
? Colors.green.withOpacity(0.3)
: Colors.orange.withOpacity(0.3),
width: 1,
),
),
child: Icon(
config.isValidated ? Icons.check_circle : Icons.access_time,
color: config.isValidated ? Colors.green : Colors.orange,
size: 16,
),
),
const SizedBox(width: 12),
// 模型信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
config.alias,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: isDark ? Colors.white : Colors.black,
),
),
if (config.isDefault) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'默认',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: theme.colorScheme.onPrimary,
),
),
),
],
],
),
const SizedBox(height: 2),
Text(
config.modelName,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
fontFamily: 'monospace',
),
),
],
),
),
// 价格信息(模拟数据)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'\$0.03',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
Text(
'输入',
style: theme.textTheme.bodySmall?.copyWith(
fontSize: 10,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'\$0.06',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
Text(
'输出',
style: theme.textTheme.bodySmall?.copyWith(
fontSize: 10,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
],
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'每千标记',
style: theme.textTheme.bodySmall?.copyWith(
fontSize: 10,
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
),
const SizedBox(width: 12),
// 操作按钮
MenuBuilder.buildModelMenu(
context: context,
configId: config.id,
isValidated: config.isValidated,
isDefault: config.isDefault,
onValidate: (configId) async => onValidate(configId),
onSetDefault: (configId) async => onSetDefault(configId),
onEdit: (configId) async => onEdit(configId),
onDelete: (configId) async => onDelete(configId),
width: 180,
align: 'right',
),
],
),
);
}
}