整个课程和数据UI重新设计版本

This commit is contained in:
shenlei
2026-09-18 17:55:44 +09:00
parent febfd30f49
commit 0a4f97d105
154 changed files with 12366 additions and 4784 deletions
@@ -0,0 +1,171 @@
import 'package:flutter/material.dart';
import '../../core/ai_service.dart';
import '../../core/app_state.dart';
import '../../core/app_theme.dart';
import '../../core/models.dart';
import '../../widgets/app_widgets.dart';
String aiProviderLabel(AiProviderType provider) => switch (provider) {
AiProviderType.mock => '内置练习模式',
AiProviderType.openAi => 'OpenAI API',
AiProviderType.gemini => 'Gemini API',
AiProviderType.compatible => 'OpenAI 兼容',
};
/// AI 服务:服务类型、接口地址、模型与 API Key。
class AiServiceView extends StatefulWidget {
const AiServiceView({super.key, required this.state});
final AppState state;
@override
State<AiServiceView> createState() => _AiServiceViewState();
}
class _AiServiceViewState extends State<AiServiceView> {
late final endpoint = TextEditingController(text: widget.state.aiEndpoint);
late final model = TextEditingController(text: widget.state.aiModel);
final apiKey = TextEditingController();
bool testingConnection = false;
@override
void dispose() {
endpoint.dispose();
model.dispose();
apiKey.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => SpacedColumn(
children: [
Text(
'订阅版 ChatGPT / Gemini 不能直接作为 App API 使用。请使用自己的 API Key,或填写兼容 OpenAI 接口的 CLIProxyAPI 地址。AI 只提供建议,不会直接决定你的掌握程度。',
style: TextStyle(fontSize: 12, color: AppColors.muted),
),
DropdownButtonFormField<AiProviderType>(
initialValue: widget.state.aiProvider,
decoration: const InputDecoration(
labelText: '服务类型',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(
value: AiProviderType.mock,
child: Text('内置练习模式(无需网络)'),
),
DropdownMenuItem(
value: AiProviderType.openAi,
child: Text('OpenAI API'),
),
DropdownMenuItem(
value: AiProviderType.gemini,
child: Text('Gemini API'),
),
DropdownMenuItem(
value: AiProviderType.compatible,
child: Text('OpenAI 兼容 / CLIProxyAPI'),
),
],
onChanged: _changeProvider,
),
TextField(
controller: endpoint,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
labelText: 'Base URL(可选)',
hintText:
'OpenAI / 兼容: https://…/v1;Gemini: https://generativelanguage.googleapis.com/v1beta',
border: OutlineInputBorder(),
),
),
TextField(
controller: model,
decoration: const InputDecoration(
labelText: '模型名称(可选)',
hintText: '例如 gpt-4.1-mini',
border: OutlineInputBorder(),
),
),
TextField(
controller: apiKey,
obscureText: true,
decoration: const InputDecoration(
labelText: 'API Key(仅保存到设备安全存储)',
border: OutlineInputBorder(),
),
),
PrimaryButton(label: '保存服务设置', onPressed: _save),
SecondaryButton(
label: testingConnection ? '正在测试连接...' : '测试连接',
onPressed: testingConnection ? null : _testConnection,
),
SecondaryButton(
label: '从配置文件重载 (ai_config.json)',
onPressed: _reloadFromAsset,
),
],
);
void _changeProvider(AiProviderType? value) {
if (value == null) return;
setState(() {
widget.state.setAiProvider(value);
if (value == AiProviderType.gemini && endpoint.text.trim().isEmpty) {
endpoint.text = 'https://generativelanguage.googleapis.com/v1beta';
if (model.text.trim().isEmpty) model.text = 'gemini-2.5-flash';
}
});
}
Future<void> _save() async {
final messenger = ScaffoldMessenger.of(context);
widget.state.saveAiConfiguration(
endpoint: endpoint.text,
model: model.text,
);
if (apiKey.text.trim().isNotEmpty) {
await AiService.instance.saveApiKey(apiKey.text);
}
messenger.showSnackBar(const SnackBar(content: Text('服务设置已保存。')));
}
Future<void> _testConnection() async {
final messenger = ScaffoldMessenger.of(context);
setState(() => testingConnection = true);
final result = await AiService.instance.testConnection(
provider: widget.state.aiProvider,
endpoint: endpoint.text,
model: model.text,
explicitApiKey: apiKey.text,
);
if (!mounted) return;
setState(() => testingConnection = false);
messenger.showSnackBar(
SnackBar(
content: Text(result.message),
backgroundColor: result.ok ? AppColors.green : Colors.redAccent,
duration: const Duration(seconds: 4),
),
);
}
Future<void> _reloadFromAsset() async {
final messenger = ScaffoldMessenger.of(context);
final ok = await widget.state.reloadAiConfigFromAsset();
if (!mounted) return;
if (ok) {
setState(() {
endpoint.text = widget.state.aiEndpoint;
model.text = widget.state.aiModel;
});
}
messenger.showSnackBar(
SnackBar(
content: Text(
ok ? '已从 assets/config/ai_config.json 载入配置。' : '未找到配置文件或解析失败。',
),
),
);
}
}