feat(flutter): i18n体系(zh/zh_TW/en) + 智能体解聘功能
- 建立完整 flutter_localizations i18n 体系:zh/zh_TW/en 三语言 - l10n.yaml + ARB 文件 (app_zh.arb 约120键作模板,zh_TW/en 对应覆盖) - localeProvider 连接 SharedPreferences language 设置,实时切换语言 - 设置页加入语言选择器(简体中文/繁体中文/English) - 我的智能体页实现解聘(解聘确认弹窗 + DELETE API)与重命名功能 - 全部页面 (~18个) UI 字符串替换为 AppLocalizations.of(context).xxx Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3074ea54a9
commit
6be84617d2
|
|
@ -0,0 +1,5 @@
|
||||||
|
arb-dir: lib/l10n
|
||||||
|
template-arb-file: app_zh.arb
|
||||||
|
output-localization-file: app_localizations.dart
|
||||||
|
output-class: AppLocalizations
|
||||||
|
nullable-getter: false
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'core/router/app_router.dart';
|
import 'core/router/app_router.dart';
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
import 'features/settings/presentation/providers/settings_providers.dart';
|
import 'features/settings/presentation/providers/settings_providers.dart';
|
||||||
|
import 'l10n/app_localizations.dart';
|
||||||
|
|
||||||
class IT0App extends ConsumerWidget {
|
class IT0App extends ConsumerWidget {
|
||||||
const IT0App({super.key});
|
const IT0App({super.key});
|
||||||
|
|
@ -11,9 +12,13 @@ class IT0App extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final router = ref.watch(routerProvider);
|
final router = ref.watch(routerProvider);
|
||||||
final themeMode = ref.watch(themeModeProvider);
|
final themeMode = ref.watch(themeModeProvider);
|
||||||
|
final locale = ref.watch(localeProvider);
|
||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: '我智能体',
|
onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle,
|
||||||
|
locale: locale,
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
theme: AppTheme.lightTheme,
|
theme: AppTheme.lightTheme,
|
||||||
darkTheme: AppTheme.darkTheme,
|
darkTheme: AppTheme.darkTheme,
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../updater/update_service.dart';
|
import '../updater/update_service.dart';
|
||||||
import '../network/connectivity_provider.dart';
|
import '../network/connectivity_provider.dart';
|
||||||
|
|
@ -161,15 +162,15 @@ class _ScaffoldWithNavState extends ConsumerState<ScaffoldWithNav>
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
selectedIndex: currentIndex,
|
selectedIndex: currentIndex,
|
||||||
destinations: [
|
destinations: [
|
||||||
const NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.home_outlined),
|
icon: const Icon(Icons.home_outlined),
|
||||||
selectedIcon: Icon(Icons.home),
|
selectedIcon: const Icon(Icons.home),
|
||||||
label: '主页',
|
label: AppLocalizations.of(context).navHome,
|
||||||
),
|
),
|
||||||
const NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.smart_toy_outlined),
|
icon: const Icon(Icons.smart_toy_outlined),
|
||||||
selectedIcon: Icon(Icons.smart_toy),
|
selectedIcon: const Icon(Icons.smart_toy),
|
||||||
label: '我的智能体',
|
label: AppLocalizations.of(context).navMyAgents,
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Badge(
|
icon: Badge(
|
||||||
|
|
@ -178,12 +179,12 @@ class _ScaffoldWithNavState extends ConsumerState<ScaffoldWithNav>
|
||||||
child: const Icon(Icons.credit_card_outlined),
|
child: const Icon(Icons.credit_card_outlined),
|
||||||
),
|
),
|
||||||
selectedIcon: const Icon(Icons.credit_card),
|
selectedIcon: const Icon(Icons.credit_card),
|
||||||
label: '账单',
|
label: AppLocalizations.of(context).navBilling,
|
||||||
),
|
),
|
||||||
const NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.person_outline),
|
icon: const Icon(Icons.person_outline),
|
||||||
selectedIcon: Icon(Icons.person),
|
selectedIcon: const Icon(Icons.person),
|
||||||
label: '我',
|
label: AppLocalizations.of(context).navProfile,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onDestinationSelected: (index) {
|
onDestinationSelected: (index) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
@ -797,11 +798,11 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text('我智能体',
|
Text(AppLocalizations.of(context).appTitle,
|
||||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
_agentState == 'thinking' ? '思考中...' : _durationLabel,
|
_agentState == 'thinking' ? AppLocalizations.of(context).agentCallThinking : _durationLabel,
|
||||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 13)),
|
style: const TextStyle(color: AppColors.textSecondary, fontSize: 13)),
|
||||||
if (_isReconnecting) ...[
|
if (_isReconnecting) ...[
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
|
|
@ -944,13 +945,13 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
||||||
String get _statusText {
|
String get _statusText {
|
||||||
switch (_phase) {
|
switch (_phase) {
|
||||||
case _CallPhase.ringing:
|
case _CallPhase.ringing:
|
||||||
return '我智能体 语音通话';
|
return AppLocalizations.of(context).agentCallRingingStatus;
|
||||||
case _CallPhase.connecting:
|
case _CallPhase.connecting:
|
||||||
return '连接中...';
|
return AppLocalizations.of(context).agentCallConnectingStatus;
|
||||||
case _CallPhase.active:
|
case _CallPhase.active:
|
||||||
return '我智能体';
|
return AppLocalizations.of(context).agentCallActiveStatus;
|
||||||
case _CallPhase.ended:
|
case _CallPhase.ended:
|
||||||
return '通话结束';
|
return AppLocalizations.of(context).agentCallEndedStatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -964,7 +965,7 @@ class _AgentCallPageState extends ConsumerState<AgentCallPage>
|
||||||
case _CallPhase.connecting:
|
case _CallPhase.connecting:
|
||||||
return '正在建立安全连接';
|
return '正在建立安全连接';
|
||||||
case _CallPhase.active:
|
case _CallPhase.active:
|
||||||
if (_agentState == 'thinking') return '思考中...';
|
if (_agentState == 'thinking') return AppLocalizations.of(context).agentCallThinking;
|
||||||
if (_agentState == 'initializing') return '正在初始化...';
|
if (_agentState == 'initializing') return '正在初始化...';
|
||||||
return '语音通话中';
|
return '语音通话中';
|
||||||
case _CallPhase.ended:
|
case _CallPhase.ended:
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../data/providers/auth_provider.dart';
|
import '../../data/providers/auth_provider.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
|
|
||||||
enum _LoginMode { password, otp }
|
enum _LoginMode { password, otp }
|
||||||
|
|
||||||
|
|
@ -41,7 +42,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
final phone = _phoneController.text.trim();
|
final phone = _phoneController.text.trim();
|
||||||
if (phone.isEmpty) {
|
if (phone.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('请先输入手机号')),
|
SnackBar(content: Text(AppLocalizations.of(context).enterPhoneFirstError)),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -88,9 +89,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset('assets/icons/logo.svg', width: 96, height: 96),
|
SvgPicture.asset('assets/icons/logo.svg', width: 96, height: 96),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
Text(
|
||||||
'我智能体',
|
AppLocalizations.of(context).appTitle,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -98,9 +99,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
const Text(
|
Text(
|
||||||
'服务器集群运维智能体',
|
AppLocalizations.of(context).appSubtitle,
|
||||||
style: TextStyle(color: AppColors.textSecondary, fontSize: 14),
|
style: const TextStyle(color: AppColors.textSecondary, fontSize: 14),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
|
@ -113,8 +114,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _modeButton('密码登录', _LoginMode.password)),
|
Expanded(child: _modeButton(AppLocalizations.of(context).loginPasswordTab, _LoginMode.password)),
|
||||||
Expanded(child: _modeButton('验证码登录', _LoginMode.otp)),
|
Expanded(child: _modeButton(AppLocalizations.of(context).loginOtpTab, _LoginMode.otp)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -124,10 +125,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '邮箱',
|
labelText: AppLocalizations.of(context).emailLabel,
|
||||||
hintText: 'user@example.com',
|
hintText: AppLocalizations.of(context).emailHint,
|
||||||
prefixIcon: Icon(Icons.email_outlined),
|
prefixIcon: const Icon(Icons.email_outlined),
|
||||||
),
|
),
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v == null || v.isEmpty) return '请输入邮箱地址';
|
if (v == null || v.isEmpty) return '请输入邮箱地址';
|
||||||
|
|
@ -139,9 +140,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '密码',
|
labelText: AppLocalizations.of(context).passwordLabel,
|
||||||
prefixIcon: Icon(Icons.lock_outline),
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
),
|
),
|
||||||
validator: (v) => (v == null || v.isEmpty) ? '请输入密码' : null,
|
validator: (v) => (v == null || v.isEmpty) ? '请输入密码' : null,
|
||||||
onFieldSubmitted: (_) => _handlePasswordLogin(),
|
onFieldSubmitted: (_) => _handlePasswordLogin(),
|
||||||
|
|
@ -150,10 +151,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _phoneController,
|
controller: _phoneController,
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '手机号',
|
labelText: AppLocalizations.of(context).phoneLabel,
|
||||||
hintText: '+86 138 0000 0000',
|
hintText: AppLocalizations.of(context).phoneHint,
|
||||||
prefixIcon: Icon(Icons.phone_outlined),
|
prefixIcon: const Icon(Icons.phone_outlined),
|
||||||
),
|
),
|
||||||
validator: (v) => (v == null || v.isEmpty) ? '请输入手机号' : null,
|
validator: (v) => (v == null || v.isEmpty) ? '请输入手机号' : null,
|
||||||
),
|
),
|
||||||
|
|
@ -166,10 +167,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
controller: _otpController,
|
controller: _otpController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
maxLength: 6,
|
maxLength: 6,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '验证码',
|
labelText: AppLocalizations.of(context).otpLabel,
|
||||||
hintText: '6 位数字',
|
hintText: AppLocalizations.of(context).otpHint,
|
||||||
prefixIcon: Icon(Icons.sms_outlined),
|
prefixIcon: const Icon(Icons.sms_outlined),
|
||||||
counterText: '',
|
counterText: '',
|
||||||
),
|
),
|
||||||
validator: (v) => (v == null || v.isEmpty) ? '请输入验证码' : null,
|
validator: (v) => (v == null || v.isEmpty) ? '请输入验证码' : null,
|
||||||
|
|
@ -183,10 +184,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
onPressed: (_smsSending || _smsCooldown > 0) ? null : _sendSmsCode,
|
onPressed: (_smsSending || _smsCooldown > 0) ? null : _sendSmsCode,
|
||||||
child: Text(
|
child: Text(
|
||||||
_smsSending
|
_smsSending
|
||||||
? '发送中'
|
? AppLocalizations.of(context).sendingLabel
|
||||||
: _smsCooldown > 0
|
: _smsCooldown > 0
|
||||||
? '${_smsCooldown}s'
|
? '${_smsCooldown}s'
|
||||||
: '获取验证码',
|
: AppLocalizations.of(context).getOtpButton,
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -234,13 +235,13 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2, color: Colors.white),
|
strokeWidth: 2, color: Colors.white),
|
||||||
)
|
)
|
||||||
: const Text('登录', style: TextStyle(fontSize: 16)),
|
: Text(AppLocalizations.of(context).loginButton, style: const TextStyle(fontSize: 16)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Text(
|
Text(
|
||||||
'账号由管理员在后台创建或通过邀请链接注册',
|
AppLocalizations.of(context).accountCreationNote,
|
||||||
style: TextStyle(color: AppColors.textMuted, fontSize: 12),
|
style: const TextStyle(color: AppColors.textMuted, fontSize: 12),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../providers/billing_provider.dart';
|
import '../providers/billing_provider.dart';
|
||||||
|
|
||||||
|
|
@ -14,7 +15,7 @@ class BillingOverviewPage extends ConsumerWidget {
|
||||||
final cardColor = isDark ? AppColors.surface : Colors.white;
|
final cardColor = isDark ? AppColors.surface : Colors.white;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('订阅与用量')),
|
appBar: AppBar(title: Text(AppLocalizations.of(context).billingTitle)),
|
||||||
body: billingAsync.when(
|
body: billingAsync.when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (e, _) => Center(child: Text('加载失败: $e')),
|
error: (e, _) => Center(child: Text('加载失败: $e')),
|
||||||
|
|
@ -58,7 +59,7 @@ class BillingOverviewPage extends ConsumerWidget {
|
||||||
_showUpgradeDialog(context);
|
_showUpgradeDialog(context);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.upgrade),
|
icon: const Icon(Icons.upgrade),
|
||||||
label: const Text('升级套餐'),
|
label: Text(AppLocalizations.of(context).upgradeButton),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: const Size(double.infinity, 48),
|
minimumSize: const Size(double.infinity, 48),
|
||||||
backgroundColor: AppColors.primary,
|
backgroundColor: AppColors.primary,
|
||||||
|
|
@ -78,12 +79,12 @@ class BillingOverviewPage extends ConsumerWidget {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('升级套餐'),
|
title: Text(AppLocalizations.of(ctx).upgradeDialogTitle),
|
||||||
content: const Text('请前往 Web 管理后台 → 账单 → 套餐 完成升级。'),
|
content: Text(AppLocalizations.of(ctx).upgradeDialogMessage),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(ctx).pop(),
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
child: const Text('知道了'),
|
child: Text(AppLocalizations.of(ctx).acknowledgeButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -113,14 +114,17 @@ class _SubscriptionCard extends StatelessWidget {
|
||||||
_ => Colors.grey,
|
_ => Colors.grey,
|
||||||
};
|
};
|
||||||
|
|
||||||
String _statusLabel(String status) => switch (status) {
|
String _statusLabel(String status, BuildContext context) {
|
||||||
'active' => '正常',
|
final l = AppLocalizations.of(context);
|
||||||
'trialing' => '试用期',
|
return switch (status) {
|
||||||
'past_due' => '待付款',
|
'active' => l.billingStatusActive,
|
||||||
'cancelled' => '已取消',
|
'trialing' => l.billingStatusTrialing,
|
||||||
'expired' => '已过期',
|
'past_due' => l.billingStatusPastDue,
|
||||||
|
'cancelled' => l.billingStatusCancelled,
|
||||||
|
'expired' => l.billingStatusExpired,
|
||||||
_ => status,
|
_ => status,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -140,7 +144,7 @@ class _SubscriptionCard extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.credit_card, size: 20, color: AppColors.primary),
|
const Icon(Icons.credit_card, size: 20, color: AppColors.primary),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('当前套餐', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)),
|
Text(AppLocalizations.of(context).currentPlanLabel, style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
@ -159,7 +163,7 @@ class _SubscriptionCard extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
_statusLabel(status),
|
_statusLabel(status, context),
|
||||||
style: TextStyle(fontSize: 12, color: _statusColor(status), fontWeight: FontWeight.w600),
|
style: TextStyle(fontSize: 12, color: _statusColor(status), fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -168,7 +172,7 @@ class _SubscriptionCard extends StatelessWidget {
|
||||||
if (periodEnd != null) ...[
|
if (periodEnd != null) ...[
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'当期结束:${_formatDate(periodEnd!)}',
|
'${AppLocalizations.of(context).periodEndLabel}${_formatDate(periodEnd!)}',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey),
|
style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -222,7 +226,7 @@ class _UsageCard extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.bolt, size: 20, color: AppColors.primary),
|
const Icon(Icons.bolt, size: 20, color: AppColors.primary),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('本月 Token 用量', style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)),
|
Text(AppLocalizations.of(context).tokenUsageLabel, style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -234,7 +238,7 @@ class _UsageCard extends StatelessWidget {
|
||||||
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
isUnlimited ? '不限量' : _formatTokens(limitTokens),
|
isUnlimited ? AppLocalizations.of(context).unlimitedLabel : _formatTokens(limitTokens),
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(color: Colors.grey),
|
style: theme.textTheme.bodyMedium?.copyWith(color: Colors.grey),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -297,7 +301,7 @@ class _InvoiceCard extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
invoice.status == 'paid' ? '已付款' : '待付款',
|
invoice.status == 'paid' ? AppLocalizations.of(context).invoicePaidStatus : AppLocalizations.of(context).invoiceUnpaidStatus,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
|
|
@ -60,7 +61,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
Future<void> _transcribeToInput(String audioPath) async {
|
Future<void> _transcribeToInput(String audioPath) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_sttLoading = true;
|
_sttLoading = true;
|
||||||
_messageController.text = '识别中…';
|
_messageController.text = AppLocalizations.of(context).chatRecognizingLabel;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
final text = await ref.read(chatProvider.notifier).transcribeAudio(audioPath);
|
final text = await ref.read(chatProvider.notifier).transcribeAudio(audioPath);
|
||||||
|
|
@ -76,7 +77,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _messageController.text = '');
|
setState(() => _messageController.text = '');
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('语音识别失败,请重试')),
|
SnackBar(content: Text(AppLocalizations.of(context).chatSpeechRecognitionError)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -125,18 +126,18 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.photo_library),
|
leading: const Icon(Icons.photo_library),
|
||||||
title: const Text('从相册选择'),
|
title: Text(AppLocalizations.of(context).chatSelectFromAlbum),
|
||||||
subtitle: const Text('支持多选'),
|
subtitle: const Text('支持多选'),
|
||||||
onTap: () { Navigator.pop(ctx); _pickMultipleImages(); },
|
onTap: () { Navigator.pop(ctx); _pickMultipleImages(); },
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.camera_alt),
|
leading: const Icon(Icons.camera_alt),
|
||||||
title: const Text('拍照'),
|
title: Text(AppLocalizations.of(context).chatTakePhoto),
|
||||||
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); },
|
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); },
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.attach_file),
|
leading: const Icon(Icons.attach_file),
|
||||||
title: const Text('选择文件'),
|
title: Text(AppLocalizations.of(context).chatSelectFile),
|
||||||
subtitle: const Text('图片、PDF'),
|
subtitle: const Text('图片、PDF'),
|
||||||
onTap: () { Navigator.pop(ctx); _pickFile(); },
|
onTap: () { Navigator.pop(ctx); _pickFile(); },
|
||||||
),
|
),
|
||||||
|
|
@ -400,7 +401,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
case MessageType.thinking:
|
case MessageType.thinking:
|
||||||
return TimelineEventNode(
|
return TimelineEventNode(
|
||||||
status: isStreamingNow ? NodeStatus.active : NodeStatus.completed,
|
status: isStreamingNow ? NodeStatus.active : NodeStatus.completed,
|
||||||
label: '思考中...',
|
label: AppLocalizations.of(context).chatThinkingLabel,
|
||||||
isFirst: isFirst,
|
isFirst: isFirst,
|
||||||
isLast: isLast,
|
isLast: isLast,
|
||||||
content: _CollapsibleThinking(
|
content: _CollapsibleThinking(
|
||||||
|
|
@ -429,7 +430,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
final isError = tool?.status == ToolStatus.error;
|
final isError = tool?.status == ToolStatus.error;
|
||||||
return TimelineEventNode(
|
return TimelineEventNode(
|
||||||
status: isError ? NodeStatus.error : NodeStatus.completed,
|
status: isError ? NodeStatus.error : NodeStatus.completed,
|
||||||
label: isError ? '执行失败' : '执行结果',
|
label: isError ? AppLocalizations.of(context).chatExecutionFailedLabel : AppLocalizations.of(context).chatExecutionResultLabel,
|
||||||
isFirst: isFirst,
|
isFirst: isFirst,
|
||||||
isLast: isLast,
|
isLast: isLast,
|
||||||
icon: isError ? Icons.cancel_outlined : Icons.check_circle_outline,
|
icon: isError ? Icons.cancel_outlined : Icons.check_circle_outline,
|
||||||
|
|
@ -444,7 +445,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
case MessageType.approval:
|
case MessageType.approval:
|
||||||
return TimelineEventNode(
|
return TimelineEventNode(
|
||||||
status: NodeStatus.warning,
|
status: NodeStatus.warning,
|
||||||
label: '需要审批',
|
label: AppLocalizations.of(context).chatNeedsApprovalLabel,
|
||||||
isFirst: isFirst,
|
isFirst: isFirst,
|
||||||
isLast: isLast,
|
isLast: isLast,
|
||||||
icon: Icons.shield_outlined,
|
icon: Icons.shield_outlined,
|
||||||
|
|
@ -462,7 +463,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
case MessageType.standingOrderDraft:
|
case MessageType.standingOrderDraft:
|
||||||
return TimelineEventNode(
|
return TimelineEventNode(
|
||||||
status: NodeStatus.warning,
|
status: NodeStatus.warning,
|
||||||
label: '常驻指令草案',
|
label: AppLocalizations.of(context).chatStandingOrderDraftLabel,
|
||||||
isFirst: isFirst,
|
isFirst: isFirst,
|
||||||
isLast: isLast,
|
isLast: isLast,
|
||||||
icon: Icons.schedule,
|
icon: Icons.schedule,
|
||||||
|
|
@ -486,7 +487,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
default:
|
default:
|
||||||
return TimelineEventNode(
|
return TimelineEventNode(
|
||||||
status: isStreamingNow ? NodeStatus.active : NodeStatus.completed,
|
status: isStreamingNow ? NodeStatus.active : NodeStatus.completed,
|
||||||
label: isStreamingNow ? '回复中...' : '回复',
|
label: isStreamingNow ? AppLocalizations.of(context).chatReplyingLabel : AppLocalizations.of(context).chatReplyLabel,
|
||||||
isFirst: isFirst,
|
isFirst: isFirst,
|
||||||
isLast: isLast,
|
isLast: isLast,
|
||||||
icon: isStreamingNow ? null : Icons.check_circle_outline,
|
icon: isStreamingNow ? null : Icons.check_circle_outline,
|
||||||
|
|
@ -527,8 +528,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
children: [
|
children: [
|
||||||
RobotAvatar(state: robotState, size: 32),
|
RobotAvatar(state: robotState, size: 32),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text('我智能体',
|
Text(AppLocalizations.of(context).appTitle,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16, fontWeight: FontWeight.w600)),
|
fontSize: 16, fontWeight: FontWeight.w600)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -537,20 +538,20 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit_outlined, size: 20),
|
icon: const Icon(Icons.edit_outlined, size: 20),
|
||||||
tooltip: '新对话',
|
tooltip: AppLocalizations.of(context).chatNewConversationTooltip,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: () => ref.read(chatProvider.notifier).startNewChat(),
|
onPressed: () => ref.read(chatProvider.notifier).startNewChat(),
|
||||||
),
|
),
|
||||||
if (chatState.isStreaming)
|
if (chatState.isStreaming)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.stop_circle_outlined, size: 20),
|
icon: const Icon(Icons.stop_circle_outlined, size: 20),
|
||||||
tooltip: '停止',
|
tooltip: AppLocalizations.of(context).chatStopTooltip,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: () => ref.read(chatProvider.notifier).cancelCurrentTask(),
|
onPressed: () => ref.read(chatProvider.notifier).cancelCurrentTask(),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.call, size: 20),
|
icon: const Icon(Icons.call, size: 20),
|
||||||
tooltip: '语音通话',
|
tooltip: AppLocalizations.of(context).chatVoiceCallTooltip,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: _openVoiceCall,
|
onPressed: _openVoiceCall,
|
||||||
),
|
),
|
||||||
|
|
@ -603,7 +604,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
_needsWorkingNode(chatState)) {
|
_needsWorkingNode(chatState)) {
|
||||||
return TimelineEventNode(
|
return TimelineEventNode(
|
||||||
status: NodeStatus.active,
|
status: NodeStatus.active,
|
||||||
label: '处理中...',
|
label: AppLocalizations.of(context).chatProcessingLabel,
|
||||||
isFirst: false,
|
isFirst: false,
|
||||||
isLast: false,
|
isLast: false,
|
||||||
);
|
);
|
||||||
|
|
@ -644,13 +645,13 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
Icon(Icons.smart_toy_outlined, size: 64, color: AppColors.textMuted),
|
Icon(Icons.smart_toy_outlined, size: 64, color: AppColors.textMuted),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'开始与 我智能体 对话',
|
AppLocalizations.of(context).chatStartConversationPrompt,
|
||||||
style: TextStyle(color: AppColors.textSecondary, fontSize: 16),
|
style: const TextStyle(color: AppColors.textSecondary, fontSize: 16),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'输入指令或拨打语音通话',
|
AppLocalizations.of(context).chatInputInstructionHint,
|
||||||
style: TextStyle(color: AppColors.textMuted, fontSize: 13),
|
style: const TextStyle(color: AppColors.textMuted, fontSize: 13),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
|
|
@ -700,7 +701,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
padding: const EdgeInsets.only(left: 4),
|
padding: const EdgeInsets.only(left: 4),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.add_circle_outline, size: 22),
|
icon: const Icon(Icons.add_circle_outline, size: 22),
|
||||||
tooltip: '添加图片',
|
tooltip: AppLocalizations.of(context).chatAddImageTooltip,
|
||||||
onPressed: isAwaitingApproval ? null : _showAttachmentOptions,
|
onPressed: isAwaitingApproval ? null : _showAttachmentOptions,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -708,7 +709,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _messageController,
|
controller: _messageController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: isStreaming ? '追加指令...' : '输入指令...',
|
hintText: isStreaming ? AppLocalizations.of(context).chatAdditionalInstructionHint : AppLocalizations.of(context).chatInstructionHint,
|
||||||
hintStyle: TextStyle(color: AppColors.textMuted),
|
hintStyle: TextStyle(color: AppColors.textMuted),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.only(
|
contentPadding: EdgeInsets.only(
|
||||||
|
|
@ -729,14 +730,14 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.send, color: AppColors.info, size: 20),
|
icon: const Icon(Icons.send, color: AppColors.info, size: 20),
|
||||||
tooltip: '追加指令',
|
tooltip: AppLocalizations.of(context).chatInjectionTooltip,
|
||||||
onPressed: _inject,
|
onPressed: _inject,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 4),
|
padding: const EdgeInsets.only(right: 4),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.stop_circle_outlined, color: AppColors.error, size: 20),
|
icon: const Icon(Icons.stop_circle_outlined, color: AppColors.error, size: 20),
|
||||||
tooltip: '停止',
|
tooltip: AppLocalizations.of(context).chatStopTooltip,
|
||||||
onPressed: () => ref.read(chatProvider.notifier).cancelCurrentTask(),
|
onPressed: () => ref.read(chatProvider.notifier).cancelCurrentTask(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -829,7 +830,7 @@ class _CollapsibleCodeBlockState extends State<_CollapsibleCodeBlock> {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
_expanded ? '收起' : '展开 ($lineCount 行)',
|
_expanded ? AppLocalizations.of(context).chatCollapseLabel : '展开 ($lineCount 行)',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.info,
|
color: AppColors.info,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -1000,7 +1001,7 @@ class _StandingOrderContent extends StatelessWidget {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||||
minimumSize: Size.zero,
|
minimumSize: Size.zero,
|
||||||
),
|
),
|
||||||
child: const Text('取消', style: TextStyle(fontSize: 12)),
|
child: Text(AppLocalizations.of(context).cancelButton, style: const TextStyle(fontSize: 12)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../../../core/widgets/robot_avatar.dart';
|
import '../../../../core/widgets/robot_avatar.dart';
|
||||||
import '../../../chat/presentation/providers/chat_providers.dart';
|
import '../../../chat/presentation/providers/chat_providers.dart';
|
||||||
|
|
@ -47,9 +48,9 @@ class HomePage extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
const Text(
|
Text(
|
||||||
'我智能体 随时为你服务',
|
AppLocalizations.of(context).homeSubtitle,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
),
|
),
|
||||||
|
|
@ -103,13 +104,13 @@ class HomePage extends ConsumerWidget {
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// ── IT0 official agent cards ─────────────────────────────
|
// ── IT0 official agent cards ─────────────────────────────
|
||||||
const _SectionHeader(title: 'IT0 官方智能体'),
|
_SectionHeader(title: AppLocalizations.of(context).officialAgentsSection),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const _OfficialAgentsRow(),
|
const _OfficialAgentsRow(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// ── Guide if nothing created yet ─────────────────────────
|
// ── Guide if nothing created yet ─────────────────────────
|
||||||
const _SectionHeader(title: '我的智能体'),
|
_SectionHeader(title: AppLocalizations.of(context).myAgentsSection),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const _MyAgentsPlaceholder(),
|
const _MyAgentsPlaceholder(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
@ -190,9 +191,9 @@ class _AgentStatusCard extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'我智能体',
|
AppLocalizations.of(context).appTitle,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
|
|
@ -271,39 +272,40 @@ class _SectionHeader extends StatelessWidget {
|
||||||
class _OfficialAgentsRow extends StatelessWidget {
|
class _OfficialAgentsRow extends StatelessWidget {
|
||||||
const _OfficialAgentsRow();
|
const _OfficialAgentsRow();
|
||||||
|
|
||||||
static const _agents = [
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context);
|
||||||
|
final agents = [
|
||||||
_AgentCard(
|
_AgentCard(
|
||||||
name: '我智能体 运维助手',
|
name: l.officialAgent1Name,
|
||||||
desc: '服务器管理、SSH 执行、日志分析',
|
desc: l.officialAgent1Desc,
|
||||||
icon: Icons.dns_outlined,
|
icon: Icons.dns_outlined,
|
||||||
color: Color(0xFF6366F1),
|
color: const Color(0xFF6366F1),
|
||||||
isOfficial: true,
|
isOfficial: true,
|
||||||
),
|
),
|
||||||
_AgentCard(
|
_AgentCard(
|
||||||
name: '安全审计助手',
|
name: l.officialAgent2Name,
|
||||||
desc: '漏洞扫描、权限审查、合规检查',
|
desc: l.officialAgent2Desc,
|
||||||
icon: Icons.security_outlined,
|
icon: Icons.security_outlined,
|
||||||
color: Color(0xFF22C55E),
|
color: const Color(0xFF22C55E),
|
||||||
isOfficial: true,
|
isOfficial: true,
|
||||||
),
|
),
|
||||||
_AgentCard(
|
_AgentCard(
|
||||||
name: '数据库巡检',
|
name: l.officialAgent3Name,
|
||||||
desc: '慢查询分析、索引优化、备份验证',
|
desc: l.officialAgent3Desc,
|
||||||
icon: Icons.storage_outlined,
|
icon: Icons.storage_outlined,
|
||||||
color: Color(0xFF0EA5E9),
|
color: const Color(0xFF0EA5E9),
|
||||||
isOfficial: true,
|
isOfficial: true,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 130,
|
height: 130,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: _agents.length,
|
itemCount: agents.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
||||||
itemBuilder: (context, index) => _agents[index],
|
itemBuilder: (context, index) => agents[index],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -358,7 +360,7 @@ class _AgentCard extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'官方',
|
AppLocalizations.of(context).officialBadge,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: color,
|
color: color,
|
||||||
|
|
@ -423,18 +425,18 @@ class _MyAgentsPlaceholder extends StatelessWidget {
|
||||||
color: AppColors.primary,
|
color: AppColors.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
Text(
|
||||||
'还没有自己的智能体',
|
AppLocalizations.of(context).noOwnAgentsTitle,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
const Text(
|
Text(
|
||||||
'点击下方机器人按钮,告诉 我智能体\n"帮我招募一个 OpenClaw 智能体"',
|
AppLocalizations.of(context).noOwnAgentsDesc,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
|
|
@ -454,15 +456,16 @@ class _MyAgentsPlaceholder extends StatelessWidget {
|
||||||
class _QuickTipsCard extends StatelessWidget {
|
class _QuickTipsCard extends StatelessWidget {
|
||||||
const _QuickTipsCard();
|
const _QuickTipsCard();
|
||||||
|
|
||||||
static const _tips = [
|
|
||||||
'💬 "帮我招募一个监控 GitHub Actions 的智能体"',
|
|
||||||
'🔧 "把我的 OpenClaw 配置导出为 JSON"',
|
|
||||||
'📊 "分析我的服务器最近7天的负载情况"',
|
|
||||||
'🛡️ "帮我设置每天凌晨2点自动备份数据库"',
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context);
|
||||||
|
final tips = [
|
||||||
|
l.quickTip1,
|
||||||
|
l.quickTip2,
|
||||||
|
l.quickTip3,
|
||||||
|
l.quickTip4,
|
||||||
|
];
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -472,16 +475,16 @@ class _QuickTipsCard extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'你可以这样说...',
|
l.quickTipsHeader,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
..._tips.map(
|
...tips.map(
|
||||||
(tip) => Padding(
|
(tip) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/config/api_endpoints.dart';
|
import '../../../../core/config/api_endpoints.dart';
|
||||||
|
import '../../../../core/errors/error_handler.dart';
|
||||||
import '../../../../core/network/dio_client.dart';
|
import '../../../../core/network/dio_client.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../../../core/utils/date_formatter.dart';
|
import '../../../../core/utils/date_formatter.dart';
|
||||||
|
|
@ -79,12 +81,16 @@ const _statusColors = {
|
||||||
'error': Color(0xFFEF4444),
|
'error': Color(0xFFEF4444),
|
||||||
};
|
};
|
||||||
|
|
||||||
const _statusLabels = {
|
String _statusLabel(BuildContext context, String status) {
|
||||||
'running': '运行中',
|
final l = AppLocalizations.of(context);
|
||||||
'deploying': '部署中',
|
return switch (status) {
|
||||||
'stopped': '已停止',
|
'running' => l.statusRunning,
|
||||||
'error': '错误',
|
'deploying' => l.statusDeploying,
|
||||||
};
|
'stopped' => l.statusStopped,
|
||||||
|
'error' => l.statusError,
|
||||||
|
_ => status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const _statusIcons = {
|
const _statusIcons = {
|
||||||
'running': Icons.circle,
|
'running': Icons.circle,
|
||||||
|
|
@ -108,7 +114,7 @@ class MyAgentsPage extends ConsumerWidget {
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
title: const Text('我的智能体'),
|
title: Text(AppLocalizations.of(context).myAgentsTitle),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh_outlined),
|
icon: const Icon(Icons.refresh_outlined),
|
||||||
|
|
@ -145,22 +151,22 @@ class MyAgentsPage extends ConsumerWidget {
|
||||||
child: const Icon(Icons.smart_toy_outlined, size: 60, color: AppColors.primary),
|
child: const Icon(Icons.smart_toy_outlined, size: 60, color: AppColors.primary),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text(
|
Text(
|
||||||
'招募你的专属智能体',
|
AppLocalizations.of(context).myAgentsEmptyTitle,
|
||||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: AppColors.textPrimary),
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: AppColors.textPrimary),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
const Text(
|
Text(
|
||||||
'通过与 我智能体 对话,你可以招募各种智能体:\nOpenClaw 编程助手、运维机器人、数据分析师...',
|
AppLocalizations.of(context).myAgentsEmptyDesc,
|
||||||
style: TextStyle(fontSize: 14, color: AppColors.textSecondary, height: 1.6),
|
style: const TextStyle(fontSize: 14, color: AppColors.textSecondary, height: 1.6),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 36),
|
const SizedBox(height: 36),
|
||||||
_StepCard(step: '1', title: '点击下方机器人', desc: '打开与 我智能体 的对话窗口', icon: Icons.smart_toy_outlined, color: AppColors.primary),
|
_StepCard(step: '1', title: AppLocalizations.of(context).myAgentsStep1Title, desc: AppLocalizations.of(context).myAgentsStep1Desc, icon: Icons.smart_toy_outlined, color: AppColors.primary),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_StepCard(step: '2', title: '描述你想要的智能体', desc: '例如:"帮我招募一个 OpenClaw 编程助手"', icon: Icons.record_voice_over_outlined, color: const Color(0xFF0EA5E9)),
|
_StepCard(step: '2', title: AppLocalizations.of(context).myAgentsStep2Title, desc: AppLocalizations.of(context).myAgentsStep2Desc, icon: Icons.record_voice_over_outlined, color: const Color(0xFF0EA5E9)),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_StepCard(step: '3', title: '我智能体 自动部署', desc: '部署完成后出现在这里,通过 Telegram/WhatsApp 等渠道与它对话', icon: Icons.check_circle_outline, color: AppColors.success),
|
_StepCard(step: '3', title: AppLocalizations.of(context).myAgentsStep3Title, desc: AppLocalizations.of(context).myAgentsStep3Desc, icon: Icons.check_circle_outline, color: AppColors.success),
|
||||||
const SizedBox(height: 36),
|
const SizedBox(height: 36),
|
||||||
const _TemplatesSection(),
|
const _TemplatesSection(),
|
||||||
const SizedBox(height: 100),
|
const SizedBox(height: 100),
|
||||||
|
|
@ -180,11 +186,11 @@ class MyAgentsPage extends ConsumerWidget {
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_SummaryChip(label: '总计 ${instances.length}', color: AppColors.textMuted),
|
_SummaryChip(label: AppLocalizations.of(context).summaryTotal(instances.length), color: AppColors.textMuted),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_SummaryChip(label: '运行中 $running', color: const Color(0xFF22C55E)),
|
_SummaryChip(label: AppLocalizations.of(context).summaryRunning(running), color: const Color(0xFF22C55E)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_SummaryChip(label: '已停止 ${instances.length - running}', color: const Color(0xFFF59E0B)),
|
_SummaryChip(label: AppLocalizations.of(context).summaryStopped(instances.length - running), color: const Color(0xFFF59E0B)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -196,7 +202,12 @@ class MyAgentsPage extends ConsumerWidget {
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(context, index) {
|
(context, index) {
|
||||||
if (index.isOdd) return const SizedBox(height: 10);
|
if (index.isOdd) return const SizedBox(height: 10);
|
||||||
return _InstanceCard(instance: instances[index ~/ 2]);
|
final inst = instances[index ~/ 2];
|
||||||
|
return _InstanceCard(
|
||||||
|
instance: inst,
|
||||||
|
onDismiss: () => _handleDismiss(context, ref, inst),
|
||||||
|
onRename: () => _handleRename(context, ref, inst),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
childCount: instances.length * 2 - 1,
|
childCount: instances.length * 2 - 1,
|
||||||
),
|
),
|
||||||
|
|
@ -208,6 +219,69 @@ class MyAgentsPage extends ConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Dismiss / Rename helpers (top-level functions for ConsumerWidget access)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Future<void> _handleDismiss(
|
||||||
|
BuildContext context, WidgetRef ref, AgentInstance instance) async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => _DismissConfirmDialog(name: instance.name),
|
||||||
|
);
|
||||||
|
if (confirmed != true) return;
|
||||||
|
try {
|
||||||
|
final dio = ref.read(dioClientProvider);
|
||||||
|
await dio.delete('${ApiEndpoints.agentInstances}/${instance.id}');
|
||||||
|
ref.invalidate(myInstancesProvider);
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(AppLocalizations.of(context).dismissSuccessMessage(instance.name))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context).dismissErrorMessage(ErrorHandler.friendlyMessage(e))),
|
||||||
|
backgroundColor: AppColors.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleRename(
|
||||||
|
BuildContext context, WidgetRef ref, AgentInstance instance) async {
|
||||||
|
final newName = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => _RenameDialog(currentName: instance.name),
|
||||||
|
);
|
||||||
|
if (newName == null || newName.trim().isEmpty) return;
|
||||||
|
try {
|
||||||
|
final dio = ref.read(dioClientProvider);
|
||||||
|
await dio.put(
|
||||||
|
'${ApiEndpoints.agentInstances}/${instance.id}/name',
|
||||||
|
data: {'name': newName.trim()},
|
||||||
|
);
|
||||||
|
ref.invalidate(myInstancesProvider);
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(AppLocalizations.of(context).renameSuccessMessage)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context).renameErrorMessage(ErrorHandler.friendlyMessage(e))),
|
||||||
|
backgroundColor: AppColors.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Summary chip
|
// Summary chip
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -237,13 +311,52 @@ class _SummaryChip extends StatelessWidget {
|
||||||
|
|
||||||
class _InstanceCard extends StatelessWidget {
|
class _InstanceCard extends StatelessWidget {
|
||||||
final AgentInstance instance;
|
final AgentInstance instance;
|
||||||
|
final VoidCallback? onDismiss;
|
||||||
|
final VoidCallback? onRename;
|
||||||
|
|
||||||
const _InstanceCard({required this.instance});
|
const _InstanceCard({required this.instance, this.onDismiss, this.onRename});
|
||||||
|
|
||||||
|
void _showActions(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (ctx) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.textMuted.withOpacity(0.4),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.drive_file_rename_outline),
|
||||||
|
title: Text(AppLocalizations.of(ctx).renameButton),
|
||||||
|
onTap: () { Navigator.pop(ctx); onRename?.call(); },
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.person_remove_outlined, color: AppColors.error),
|
||||||
|
title: Text(AppLocalizations.of(ctx).dismissButton, style: const TextStyle(color: AppColors.error)),
|
||||||
|
onTap: () { Navigator.pop(ctx); onDismiss?.call(); },
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final statusColor = _statusColors[instance.status] ?? AppColors.textMuted;
|
final statusColor = _statusColors[instance.status] ?? AppColors.textMuted;
|
||||||
final statusLabel = _statusLabels[instance.status] ?? instance.status;
|
final statusLabel = _statusLabel(context, instance.status);
|
||||||
final statusIcon = _statusIcons[instance.status] ?? Icons.help_outline;
|
final statusIcon = _statusIcons[instance.status] ?? Icons.help_outline;
|
||||||
final timeLabel = DateFormatter.timeAgo(instance.createdAt);
|
final timeLabel = DateFormatter.timeAgo(instance.createdAt);
|
||||||
final isDeploying = instance.status == 'deploying';
|
final isDeploying = instance.status == 'deploying';
|
||||||
|
|
@ -310,6 +423,14 @@ class _InstanceCard extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// More options
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.more_vert, size: 18),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
onPressed: () => _showActions(context),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -433,7 +554,7 @@ class _TemplatesSection extends StatelessWidget {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('热门模板(告诉 我智能体 你想要哪种)', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textSecondary)),
|
Text(AppLocalizations.of(context).myAgentsTemplatesHeader, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textSecondary)),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
GridView.count(
|
GridView.count(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
|
@ -488,3 +609,95 @@ class _TemplateChip extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Dismiss confirm dialog
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _DismissConfirmDialog extends StatelessWidget {
|
||||||
|
final String name;
|
||||||
|
const _DismissConfirmDialog({required this.name});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.warning_amber_rounded, color: AppColors.error),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(AppLocalizations.of(context).dismissTitle),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
AppLocalizations.of(context).dismissConfirmContent(name),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: Text(AppLocalizations.of(context).cancelButton),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
style: FilledButton.styleFrom(backgroundColor: AppColors.error),
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: Text(AppLocalizations.of(context).dismissButton),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Rename dialog
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _RenameDialog extends StatefulWidget {
|
||||||
|
final String currentName;
|
||||||
|
const _RenameDialog({required this.currentName});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RenameDialog> createState() => _RenameDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RenameDialogState extends State<_RenameDialog> {
|
||||||
|
late final TextEditingController _ctrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ctrl = TextEditingController(text: widget.currentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
title: Text(AppLocalizations.of(context).renameTitle),
|
||||||
|
content: TextField(
|
||||||
|
controller: _ctrl,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: AppLocalizations.of(context).renameHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onSubmitted: (_) => Navigator.of(context).pop(_ctrl.text),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(null),
|
||||||
|
child: Text(AppLocalizations.of(context).cancelButton),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(_ctrl.text),
|
||||||
|
child: Text(AppLocalizations.of(context).confirmButton),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import '../../data/in_site_notification.dart';
|
import '../../data/in_site_notification.dart';
|
||||||
import '../../data/in_site_notification_repository.dart';
|
import '../../data/in_site_notification_repository.dart';
|
||||||
import '../providers/notification_providers.dart';
|
import '../providers/notification_providers.dart';
|
||||||
|
|
@ -72,7 +73,7 @@ class _NotificationInboxPageState
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('操作失败,请重试')),
|
SnackBar(content: Text(AppLocalizations.of(context).operationFailedError)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +160,7 @@ class _NotificationInboxPageState
|
||||||
if (item.linkUrl != null) ...[
|
if (item.linkUrl != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'链接:${item.linkUrl}',
|
'${AppLocalizations.of(ctx).linkLabel}${item.linkUrl}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13, color: Colors.blue),
|
fontSize: 13, color: Colors.blue),
|
||||||
),
|
),
|
||||||
|
|
@ -180,12 +181,12 @@ class _NotificationInboxPageState
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('站内消息'),
|
title: Text(AppLocalizations.of(context).notificationInboxTitle),
|
||||||
actions: [
|
actions: [
|
||||||
if (hasUnread)
|
if (hasUnread)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _markAllRead,
|
onPressed: _markAllRead,
|
||||||
child: const Text('全部已读'),
|
child: Text(AppLocalizations.of(context).notificationMarkAllRead),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -199,24 +200,24 @@ class _NotificationInboxPageState
|
||||||
const Icon(Icons.error_outline,
|
const Icon(Icons.error_outline,
|
||||||
size: 48, color: Colors.grey),
|
size: 48, color: Colors.grey),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text('加载失败', style: TextStyle(color: Colors.grey[600])),
|
Text(AppLocalizations.of(context).notificationLoadingFailed, style: TextStyle(color: Colors.grey[600])),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _refresh, child: const Text('重试')),
|
onPressed: _refresh, child: Text(AppLocalizations.of(context).retryButton)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
data: (items) {
|
data: (items) {
|
||||||
if (items.isEmpty) {
|
if (items.isEmpty) {
|
||||||
return const Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.notifications_none,
|
const Icon(Icons.notifications_none,
|
||||||
size: 64, color: Colors.grey),
|
size: 64, color: Colors.grey),
|
||||||
SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text('暂无消息',
|
Text(AppLocalizations.of(context).noMessagesTitle,
|
||||||
style: TextStyle(color: Colors.grey)),
|
style: const TextStyle(color: Colors.grey)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import '../../data/notification_channel.dart';
|
import '../../data/notification_channel.dart';
|
||||||
import '../../../../core/network/dio_client.dart';
|
import '../../../../core/network/dio_client.dart';
|
||||||
|
|
||||||
|
|
@ -43,7 +44,7 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
|
||||||
ref.invalidate(_channelPrefsProvider);
|
ref.invalidate(_channelPrefsProvider);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('通知偏好已保存'), duration: Duration(seconds: 2)),
|
SnackBar(content: Text(AppLocalizations.of(context).preferencesSavedMessage), duration: const Duration(seconds: 2)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -64,14 +65,14 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('通知偏好设置'),
|
title: Text(AppLocalizations.of(context).notificationPreferencesTitle),
|
||||||
actions: [
|
actions: [
|
||||||
if (_pendingChanges.isNotEmpty)
|
if (_pendingChanges.isNotEmpty)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _saving ? null : _saveChanges,
|
onPressed: _saving ? null : _saveChanges,
|
||||||
child: _saving
|
child: _saving
|
||||||
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
|
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
: const Text('保存'),
|
: Text(AppLocalizations.of(context).saveButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -81,18 +82,18 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text('加载失败', style: theme.textTheme.titleMedium),
|
Text(AppLocalizations.of(context).notificationLoadingFailed, style: theme.textTheme.titleMedium),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => ref.invalidate(_channelPrefsProvider),
|
onPressed: () => ref.invalidate(_channelPrefsProvider),
|
||||||
child: const Text('重试'),
|
child: Text(AppLocalizations.of(context).retryButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
data: (prefs) {
|
data: (prefs) {
|
||||||
if (prefs.isEmpty) {
|
if (prefs.isEmpty) {
|
||||||
return const Center(child: Text('暂无可配置的通知频道'));
|
return Center(child: Text(AppLocalizations.of(context).noNotificationChannels));
|
||||||
}
|
}
|
||||||
|
|
||||||
final mandatory = prefs.where((p) => p.isMandatory).toList();
|
final mandatory = prefs.where((p) => p.isMandatory).toList();
|
||||||
|
|
@ -114,7 +115,7 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'您可以选择接收哪些类型的通知。强制通知(如安全告警)无法关闭。',
|
AppLocalizations.of(context).notificationPreferencesInfo,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||||
),
|
),
|
||||||
|
|
@ -129,7 +130,7 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
'重要通知(不可关闭)',
|
AppLocalizations.of(context).mandatoryNotificationsSection,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
|
|
@ -148,7 +149,7 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
'可选通知',
|
AppLocalizations.of(context).optionalNotificationsSection,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
|
|
@ -179,7 +180,7 @@ class _NotificationPreferencesPageState extends ConsumerState<NotificationPrefer
|
||||||
width: 20, height: 20,
|
width: 20, height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||||
)
|
)
|
||||||
: const Text('保存偏好设置'),
|
: Text(AppLocalizations.of(context).savePreferencesButton),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -217,7 +218,7 @@ class _ChannelTile extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'必需',
|
AppLocalizations.of(context).requiredLabel,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: Colors.amber.shade800,
|
color: Colors.amber.shade800,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../../../core/updater/update_service.dart';
|
import '../../../../core/updater/update_service.dart';
|
||||||
import '../../../auth/data/providers/auth_provider.dart';
|
import '../../../auth/data/providers/auth_provider.dart';
|
||||||
|
|
@ -42,6 +43,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final settings = ref.watch(settingsProvider);
|
final settings = ref.watch(settingsProvider);
|
||||||
final profile = ref.watch(accountProfileProvider);
|
final profile = ref.watch(accountProfileProvider);
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
@ -53,7 +55,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
title: const Text('我'),
|
title: Text(l10n.navProfile),
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
|
@ -69,7 +71,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.workspace_premium_outlined,
|
icon: Icons.workspace_premium_outlined,
|
||||||
iconBg: const Color(0xFF10B981),
|
iconBg: const Color(0xFF10B981),
|
||||||
title: '订阅套餐与用量',
|
title: l10n.profileSubscriptionLabel,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
'Free',
|
'Free',
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14),
|
style: TextStyle(color: subtitleColor, fontSize: 14),
|
||||||
|
|
@ -79,9 +81,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.card_giftcard_outlined,
|
icon: Icons.card_giftcard_outlined,
|
||||||
iconBg: const Color(0xFF6366F1),
|
iconBg: const Color(0xFF6366F1),
|
||||||
title: '邀请有礼',
|
title: l10n.profileReferralLabel,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
'推荐赚积分',
|
l10n.profileReferralHint,
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14),
|
style: TextStyle(color: subtitleColor, fontSize: 14),
|
||||||
),
|
),
|
||||||
onTap: () => context.push('/referral'),
|
onTap: () => context.push('/referral'),
|
||||||
|
|
@ -90,7 +92,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.tune_outlined,
|
icon: Icons.tune_outlined,
|
||||||
iconBg: const Color(0xFF8B5CF6),
|
iconBg: const Color(0xFF8B5CF6),
|
||||||
title: '通知偏好设置',
|
title: l10n.notificationPreferencesTitle,
|
||||||
onTap: () => context.push('/notifications/preferences'),
|
onTap: () => context.push('/notifications/preferences'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -104,15 +106,15 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.palette_outlined,
|
icon: Icons.palette_outlined,
|
||||||
iconBg: const Color(0xFF6366F1),
|
iconBg: const Color(0xFF6366F1),
|
||||||
title: '外观主题',
|
title: l10n.appearanceThemeLabel,
|
||||||
trailing: _ThemeLabel(mode: settings.themeMode),
|
trailing: _ThemeLabel(mode: settings.themeMode),
|
||||||
onTap: () => _showThemePicker(settings.themeMode),
|
onTap: () => _showThemePicker(settings.themeMode),
|
||||||
),
|
),
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.language,
|
icon: Icons.language,
|
||||||
iconBg: const Color(0xFF3B82F6),
|
iconBg: const Color(0xFF3B82F6),
|
||||||
title: '语言',
|
title: l10n.languageLabel,
|
||||||
trailing: Text('简体中文',
|
trailing: Text(l10n.languageZh,
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -129,7 +131,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
if (false) _SettingsRow(
|
if (false) _SettingsRow(
|
||||||
icon: Icons.psychology,
|
icon: Icons.psychology,
|
||||||
iconBg: const Color(0xFF7C3AED),
|
iconBg: const Color(0xFF7C3AED),
|
||||||
title: '对话引擎',
|
title: l10n.conversationEngineLabel,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
settings.engineType == 'claude_agent_sdk'
|
settings.engineType == 'claude_agent_sdk'
|
||||||
? 'Agent SDK'
|
? 'Agent SDK'
|
||||||
|
|
@ -141,9 +143,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.record_voice_over,
|
icon: Icons.record_voice_over,
|
||||||
iconBg: const Color(0xFF0EA5E9),
|
iconBg: const Color(0xFF0EA5E9),
|
||||||
title: '语音音色',
|
title: l10n.ttsVoiceLabel,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
_voiceDisplayLabel(settings.ttsVoice),
|
_voiceDisplayLabel(context, settings.ttsVoice),
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14),
|
style: TextStyle(color: subtitleColor, fontSize: 14),
|
||||||
),
|
),
|
||||||
onTap: () => _showVoicePicker(settings.ttsVoice),
|
onTap: () => _showVoicePicker(settings.ttsVoice),
|
||||||
|
|
@ -151,9 +153,9 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.tune,
|
icon: Icons.tune,
|
||||||
iconBg: const Color(0xFFF97316),
|
iconBg: const Color(0xFFF97316),
|
||||||
title: '语音风格',
|
title: l10n.ttsStyleLabel,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
_styleDisplayName(settings.ttsStyle),
|
_styleDisplayName(context, settings.ttsStyle),
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14),
|
style: TextStyle(color: subtitleColor, fontSize: 14),
|
||||||
),
|
),
|
||||||
onTap: () => _showStylePicker(settings.ttsStyle),
|
onTap: () => _showStylePicker(settings.ttsStyle),
|
||||||
|
|
@ -169,7 +171,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsToggleRow(
|
_SettingsToggleRow(
|
||||||
icon: Icons.notifications_outlined,
|
icon: Icons.notifications_outlined,
|
||||||
iconBg: const Color(0xFFEF4444),
|
iconBg: const Color(0xFFEF4444),
|
||||||
title: '推送通知',
|
title: l10n.pushNotificationsLabel,
|
||||||
value: settings.notificationsEnabled,
|
value: settings.notificationsEnabled,
|
||||||
onChanged: (v) => ref
|
onChanged: (v) => ref
|
||||||
.read(settingsProvider.notifier)
|
.read(settingsProvider.notifier)
|
||||||
|
|
@ -178,7 +180,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsToggleRow(
|
_SettingsToggleRow(
|
||||||
icon: Icons.volume_up_outlined,
|
icon: Icons.volume_up_outlined,
|
||||||
iconBg: const Color(0xFFF59E0B),
|
iconBg: const Color(0xFFF59E0B),
|
||||||
title: '提示音',
|
title: l10n.soundLabel,
|
||||||
value: settings.soundEnabled,
|
value: settings.soundEnabled,
|
||||||
onChanged: settings.notificationsEnabled
|
onChanged: settings.notificationsEnabled
|
||||||
? (v) =>
|
? (v) =>
|
||||||
|
|
@ -196,7 +198,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.lock_outline,
|
icon: Icons.lock_outline,
|
||||||
iconBg: const Color(0xFF8B5CF6),
|
iconBg: const Color(0xFF8B5CF6),
|
||||||
title: '修改密码',
|
title: l10n.changePasswordLabel,
|
||||||
onTap: _showChangePasswordSheet,
|
onTap: _showChangePasswordSheet,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -210,7 +212,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.info_outline,
|
icon: Icons.info_outline,
|
||||||
iconBg: const Color(0xFF64748B),
|
iconBg: const Color(0xFF64748B),
|
||||||
title: '版本',
|
title: l10n.versionLabel,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
_appVersion.isNotEmpty ? _appVersion : 'v1.0.0',
|
_appVersion.isNotEmpty ? _appVersion : 'v1.0.0',
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14),
|
style: TextStyle(color: subtitleColor, fontSize: 14),
|
||||||
|
|
@ -243,7 +245,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.business_outlined,
|
icon: Icons.business_outlined,
|
||||||
iconBg: const Color(0xFF0EA5E9),
|
iconBg: const Color(0xFF0EA5E9),
|
||||||
title: '租户',
|
title: l10n.tenantLabel,
|
||||||
trailing: Text(settings.selectedTenantName!,
|
trailing: Text(settings.selectedTenantName!,
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
||||||
),
|
),
|
||||||
|
|
@ -263,8 +265,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
child: const Text('退出登录',
|
child: Text(l10n.logoutButton,
|
||||||
style: TextStyle(fontSize: 16)),
|
style: const TextStyle(fontSize: 16)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
|
|
@ -277,6 +279,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
|
|
||||||
Widget _buildProfileCard(
|
Widget _buildProfileCard(
|
||||||
AccountProfile profile, Color cardColor, Color? subtitleColor) {
|
AccountProfile profile, Color cardColor, Color? subtitleColor) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final initial = profile.displayName.isNotEmpty
|
final initial = profile.displayName.isNotEmpty
|
||||||
? profile.displayName[0].toUpperCase()
|
? profile.displayName[0].toUpperCase()
|
||||||
: '?';
|
: '?';
|
||||||
|
|
@ -323,7 +326,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
Text(
|
Text(
|
||||||
profile.displayName.isNotEmpty
|
profile.displayName.isNotEmpty
|
||||||
? profile.displayName
|
? profile.displayName
|
||||||
: '加载中...',
|
: l10n.loadingLabel,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|
@ -350,6 +353,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
// ── Pickers ───────────────────────────────────────────────────────────────
|
// ── Pickers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void _showThemePicker(ThemeMode current) {
|
void _showThemePicker(ThemeMode current) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
|
|
@ -362,14 +366,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_handle(),
|
_handle(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text('选择主题',
|
Text(l10n.selectThemeTitle,
|
||||||
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
)),
|
)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_ThemeOption(
|
_ThemeOption(
|
||||||
icon: Icons.dark_mode,
|
icon: Icons.dark_mode,
|
||||||
label: '深色模式',
|
label: l10n.darkModeLabel,
|
||||||
selected: current == ThemeMode.dark,
|
selected: current == ThemeMode.dark,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref
|
ref
|
||||||
|
|
@ -380,7 +384,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
),
|
),
|
||||||
_ThemeOption(
|
_ThemeOption(
|
||||||
icon: Icons.light_mode,
|
icon: Icons.light_mode,
|
||||||
label: '浅色模式',
|
label: l10n.lightModeLabel,
|
||||||
selected: current == ThemeMode.light,
|
selected: current == ThemeMode.light,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref
|
ref
|
||||||
|
|
@ -391,7 +395,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
),
|
),
|
||||||
_ThemeOption(
|
_ThemeOption(
|
||||||
icon: Icons.settings_brightness,
|
icon: Icons.settings_brightness,
|
||||||
label: '跟随系统',
|
label: l10n.followSystemLabel,
|
||||||
selected: current == ThemeMode.system,
|
selected: current == ThemeMode.system,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref
|
ref
|
||||||
|
|
@ -407,19 +411,22 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const _voices = [
|
List<(String, String, String)> _voiceList(BuildContext context) {
|
||||||
('coral', 'Coral', '女 · 温暖'),
|
final l10n = AppLocalizations.of(context);
|
||||||
('nova', 'Nova', '女 · 活泼'),
|
return [
|
||||||
('sage', 'Sage', '女 · 知性'),
|
('coral', 'Coral', l10n.voiceCoralDesc),
|
||||||
('shimmer', 'Shimmer', '女 · 柔和'),
|
('nova', 'Nova', l10n.voiceNovaDesc),
|
||||||
('ash', 'Ash', '男 · 沉稳'),
|
('sage', 'Sage', l10n.voiceSageDesc),
|
||||||
('echo', 'Echo', '男 · 清朗'),
|
('shimmer', 'Shimmer', l10n.voiceShimmerDesc),
|
||||||
('onyx', 'Onyx', '男 · 低沉'),
|
('ash', 'Ash', l10n.voiceAshDesc),
|
||||||
('alloy', 'Alloy', '中性'),
|
('echo', 'Echo', l10n.voiceEchoDesc),
|
||||||
|
('onyx', 'Onyx', l10n.voiceOnyxDesc),
|
||||||
|
('alloy', 'Alloy', l10n.voiceAlloyDesc),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
String _voiceDisplayLabel(String voice) {
|
String _voiceDisplayLabel(BuildContext context, String voice) {
|
||||||
for (final v in _voices) {
|
for (final v in _voiceList(context)) {
|
||||||
if (v.$1 == voice) return '${v.$2} · ${v.$3}';
|
if (v.$1 == voice) return '${v.$2} · ${v.$3}';
|
||||||
}
|
}
|
||||||
return voice.isNotEmpty
|
return voice.isNotEmpty
|
||||||
|
|
@ -428,6 +435,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showVoicePicker(String current) {
|
void _showVoicePicker(String current) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final voices = _voiceList(context);
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
|
|
@ -440,7 +449,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_handle(),
|
_handle(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text('选择语音音色',
|
Text(l10n.selectVoiceTitle,
|
||||||
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
)),
|
)),
|
||||||
|
|
@ -448,7 +457,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: _voices
|
children: voices
|
||||||
.map((v) => ListTile(
|
.map((v) => ListTile(
|
||||||
leading: Icon(Icons.record_voice_over,
|
leading: Icon(Icons.record_voice_over,
|
||||||
color: current == v.$1
|
color: current == v.$1
|
||||||
|
|
@ -492,6 +501,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
];
|
];
|
||||||
|
|
||||||
void _showEngineTypePicker(String current) {
|
void _showEngineTypePicker(String current) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
|
|
@ -504,7 +514,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
children: [
|
children: [
|
||||||
_handle(),
|
_handle(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text('选择对话引擎',
|
Text(l10n.selectEngineTitle,
|
||||||
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
)),
|
)),
|
||||||
|
|
@ -542,25 +552,31 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const _stylePresets = [
|
List<(String, String)> _stylePresetList(BuildContext context) {
|
||||||
('专业干练', '用专业、简洁、干练的语气说话,不拖泥带水。'),
|
final l10n = AppLocalizations.of(context);
|
||||||
('温柔耐心', '用温柔、耐心的语气说话,像一个贴心的朋友。'),
|
return [
|
||||||
('轻松活泼', '用轻松、活泼的语气说话,带一点幽默感。'),
|
(l10n.styleProfessionalName, '用专业、简洁、干练的语气说话,不拖泥带水。'),
|
||||||
('严肃正式', '用严肃、正式的语气说话,像在正式会议中发言。'),
|
(l10n.styleGentleName, '用温柔、耐心的语气说话,像一个贴心的朋友。'),
|
||||||
('科幻AI', '用科幻电影中AI的语气说话,冷静、理性、略带未来感。'),
|
(l10n.styleRelaxedName, '用轻松、活泼的语气说话,带一点幽默感。'),
|
||||||
|
(l10n.styleFormalName, '用严肃、正式的语气说话,像在正式会议中发言。'),
|
||||||
|
(l10n.styleScifiName, '用科幻电影中AI的语气说话,冷静、理性、略带未来感。'),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
String _styleDisplayName(String style) {
|
String _styleDisplayName(BuildContext context, String style) {
|
||||||
if (style.isEmpty) return '默认';
|
final l10n = AppLocalizations.of(context);
|
||||||
for (final p in _stylePresets) {
|
if (style.isEmpty) return l10n.defaultStyleLabel;
|
||||||
|
for (final p in _stylePresetList(context)) {
|
||||||
if (p.$2 == style) return p.$1;
|
if (p.$2 == style) return p.$1;
|
||||||
}
|
}
|
||||||
return style.length > 6 ? '${style.substring(0, 6)}...' : style;
|
return style.length > 6 ? '${style.substring(0, 6)}...' : style;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showStylePicker(String current) {
|
void _showStylePicker(String current) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final stylePresets = _stylePresetList(context);
|
||||||
final controller = TextEditingController(
|
final controller = TextEditingController(
|
||||||
text: _stylePresets.any((p) => p.$2 == current) ? '' : current,
|
text: stylePresets.any((p) => p.$2 == current) ? '' : current,
|
||||||
);
|
);
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -576,7 +592,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
children: [
|
children: [
|
||||||
_handle(),
|
_handle(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text('选择语音风格',
|
Text(l10n.selectStyleTitle,
|
||||||
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
)),
|
)),
|
||||||
|
|
@ -584,7 +600,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: _stylePresets
|
children: stylePresets
|
||||||
.map((p) => ChoiceChip(
|
.map((p) => ChoiceChip(
|
||||||
label: Text(p.$1),
|
label: Text(p.$1),
|
||||||
selected: current == p.$2,
|
selected: current == p.$2,
|
||||||
|
|
@ -601,8 +617,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '自定义风格',
|
labelText: l10n.customStyleLabel,
|
||||||
hintText: '例如:用东北话说话,幽默风趣',
|
hintText: l10n.customStyleHint,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
|
|
@ -619,7 +635,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
.setTtsStyle('');
|
.setTtsStyle('');
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
},
|
},
|
||||||
child: const Text('恢复默认'),
|
child: Text(l10n.resetToDefaultButton),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
@ -646,18 +662,20 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showEditNameDialog(String currentName) {
|
void _showEditNameDialog(String currentName) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final controller = TextEditingController(text: currentName);
|
final controller = TextEditingController(text: currentName);
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
shape:
|
shape:
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
title: const Text('修改显示名称'),
|
title: Text(l10n.editNameDialogTitle),
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '显示名称',
|
labelText: l10n.displayNameLabel,
|
||||||
|
hintText: l10n.displayNameHint,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
|
|
@ -665,7 +683,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(ctx).pop(),
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
child: const Text('取消'),
|
child: Text(l10n.cancelButton),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|
@ -678,11 +696,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(success ? '名称已更新' : '更新失败')),
|
content: Text(success ? l10n.nameUpdatedMessage : l10n.updateFailedMessage)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('保存'),
|
child: Text(l10n.saveButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -690,6 +708,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showChangePasswordSheet() {
|
void _showChangePasswordSheet() {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final currentCtrl = TextEditingController();
|
final currentCtrl = TextEditingController();
|
||||||
final newCtrl = TextEditingController();
|
final newCtrl = TextEditingController();
|
||||||
final confirmCtrl = TextEditingController();
|
final confirmCtrl = TextEditingController();
|
||||||
|
|
@ -709,7 +728,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
children: [
|
children: [
|
||||||
_handle(),
|
_handle(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text('修改密码',
|
Text(l10n.changePasswordTitle,
|
||||||
style: Theme.of(ctx)
|
style: Theme.of(ctx)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleLarge
|
.titleLarge
|
||||||
|
|
@ -719,7 +738,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
controller: currentCtrl,
|
controller: currentCtrl,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '当前密码',
|
labelText: l10n.currentPasswordLabel,
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
|
@ -730,7 +749,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
controller: newCtrl,
|
controller: newCtrl,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '新密码',
|
labelText: l10n.newPasswordLabel,
|
||||||
prefixIcon: const Icon(Icons.lock_reset),
|
prefixIcon: const Icon(Icons.lock_reset),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
|
@ -741,7 +760,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
controller: confirmCtrl,
|
controller: confirmCtrl,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '确认新密码',
|
labelText: l10n.confirmPasswordLabel,
|
||||||
prefixIcon: const Icon(Icons.lock_reset),
|
prefixIcon: const Icon(Icons.lock_reset),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
|
@ -778,13 +797,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(result.success
|
content: Text(result.success
|
||||||
? '密码已修改'
|
? l10n.passwordChangedMessage
|
||||||
: result.message ?? '修改失败'),
|
: result.message ?? l10n.changeFailedMessage),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('确认修改', style: TextStyle(fontSize: 16)),
|
child: Text(l10n.confirmChangeButton, style: const TextStyle(fontSize: 16)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -793,23 +812,24 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _confirmLogout() async {
|
void _confirmLogout() async {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
shape:
|
shape:
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
title: const Text('退出登录'),
|
title: Text(l10n.logoutDialogTitle),
|
||||||
content: const Text('确定要退出登录吗?'),
|
content: Text(l10n.logoutConfirmMessage),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(ctx).pop(false),
|
onPressed: () => Navigator.of(ctx).pop(false),
|
||||||
child: const Text('取消'),
|
child: Text(l10n.cancelButton),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
style:
|
style:
|
||||||
FilledButton.styleFrom(backgroundColor: AppColors.error),
|
FilledButton.styleFrom(backgroundColor: AppColors.error),
|
||||||
onPressed: () => Navigator.of(ctx).pop(true),
|
onPressed: () => Navigator.of(ctx).pop(true),
|
||||||
child: const Text('退出'),
|
child: Text(l10n.logoutConfirmButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -953,10 +973,11 @@ class _ThemeLabel extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final label = switch (mode) {
|
final label = switch (mode) {
|
||||||
ThemeMode.dark => '深色',
|
ThemeMode.dark => l10n.darkModeLabel,
|
||||||
ThemeMode.light => '浅色',
|
ThemeMode.light => l10n.lightModeLabel,
|
||||||
ThemeMode.system => '跟随系统',
|
ThemeMode.system => l10n.followSystemLabel,
|
||||||
};
|
};
|
||||||
return Text(label,
|
return Text(label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
@ -971,6 +992,7 @@ class _InSiteMessageRow extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final unreadAsync = ref.watch(inSiteUnreadCountProvider);
|
final unreadAsync = ref.watch(inSiteUnreadCountProvider);
|
||||||
final unread = unreadAsync.valueOrNull ?? 0;
|
final unread = unreadAsync.valueOrNull ?? 0;
|
||||||
|
|
||||||
|
|
@ -985,7 +1007,7 @@ class _InSiteMessageRow extends ConsumerWidget {
|
||||||
child: const Icon(Icons.notifications_outlined,
|
child: const Icon(Icons.notifications_outlined,
|
||||||
color: Colors.white, size: 18),
|
color: Colors.white, size: 18),
|
||||||
),
|
),
|
||||||
title: const Text('站内消息'),
|
title: Text(l10n.profileInSiteMessagesLabel),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -1005,7 +1027,7 @@ class _InSiteMessageRow extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Text('查看消息',
|
Text(l10n.profileViewMessagesLabel,
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Icon(Icons.chevron_right,
|
Icon(Icons.chevron_right,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../providers/referral_providers.dart';
|
import '../providers/referral_providers.dart';
|
||||||
import '../../domain/models/referral_info.dart';
|
import '../../domain/models/referral_info.dart';
|
||||||
|
|
@ -10,6 +11,7 @@ class ReferralScreen extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final infoAsync = ref.watch(referralInfoProvider);
|
final infoAsync = ref.watch(referralInfoProvider);
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final cardColor = isDark ? AppColors.surface : Colors.white;
|
final cardColor = isDark ? AppColors.surface : Colors.white;
|
||||||
|
|
@ -18,7 +20,7 @@ class ReferralScreen extends ConsumerWidget {
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
title: const Text('邀请有礼'),
|
title: Text(l10n.referralScreenTitle),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
|
|
@ -47,6 +49,7 @@ class _ReferralBody extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -66,13 +69,13 @@ class _ReferralBody extends ConsumerWidget {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'推荐记录',
|
l10n.referralRecordsSection,
|
||||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => _showReferralList(context),
|
onPressed: () => _showReferralList(context),
|
||||||
child: const Text('查看全部 >'),
|
child: Text(l10n.viewAllReferralsLink),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -83,13 +86,13 @@ class _ReferralBody extends ConsumerWidget {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'待领积分',
|
l10n.pendingRewardsSection,
|
||||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => _showRewardList(context),
|
onPressed: () => _showRewardList(context),
|
||||||
child: const Text('查看全部 >'),
|
child: Text(l10n.viewAllRewardsLink),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -124,6 +127,7 @@ class _ReferralCodeCard extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
return Card(
|
return Card(
|
||||||
color: cardColor,
|
color: cardColor,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|
@ -133,9 +137,9 @@ class _ReferralCodeCard extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'你的推荐码',
|
l10n.yourReferralCodeLabel,
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 13),
|
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -153,7 +157,7 @@ class _ReferralCodeCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.copy, color: AppColors.primary),
|
icon: const Icon(Icons.copy, color: AppColors.primary),
|
||||||
tooltip: '复制推荐码',
|
tooltip: l10n.copyReferralCodeTooltip,
|
||||||
onPressed: () => _copy(context, info.referralCode),
|
onPressed: () => _copy(context, info.referralCode),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -164,7 +168,7 @@ class _ReferralCodeCard extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
icon: const Icon(Icons.link, size: 18),
|
icon: const Icon(Icons.link, size: 18),
|
||||||
label: const Text('复制邀请链接'),
|
label: Text(l10n.copyInviteLinkButton),
|
||||||
onPressed: () => _copy(context, info.shareUrl),
|
onPressed: () => _copy(context, info.shareUrl),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: AppColors.primary,
|
foregroundColor: AppColors.primary,
|
||||||
|
|
@ -178,7 +182,7 @@ class _ReferralCodeCard extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
icon: const Icon(Icons.share, size: 18),
|
icon: const Icon(Icons.share, size: 18),
|
||||||
label: const Text('分享'),
|
label: Text(l10n.shareButton),
|
||||||
onPressed: () => _share(context, info),
|
onPressed: () => _share(context, info),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: AppColors.primary,
|
backgroundColor: AppColors.primary,
|
||||||
|
|
@ -196,9 +200,10 @@ class _ReferralCodeCard extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _copy(BuildContext context, String text) {
|
void _copy(BuildContext context, String text) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
Clipboard.setData(ClipboardData(text: text));
|
Clipboard.setData(ClipboardData(text: text));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('已复制到剪贴板'), duration: Duration(seconds: 2)),
|
SnackBar(content: Text(l10n.copiedToClipboard), duration: const Duration(seconds: 2)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,27 +225,28 @@ class _StatsRow extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
_StatCard(
|
_StatCard(
|
||||||
cardColor: cardColor,
|
cardColor: cardColor,
|
||||||
label: '已推荐',
|
label: l10n.referredLabel,
|
||||||
value: '${info.directCount}',
|
value: '${info.directCount}',
|
||||||
unit: '人',
|
unit: l10n.peopleUnit,
|
||||||
color: const Color(0xFF6366F1),
|
color: const Color(0xFF6366F1),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
_StatCard(
|
_StatCard(
|
||||||
cardColor: cardColor,
|
cardColor: cardColor,
|
||||||
label: '已激活',
|
label: l10n.activatedLabel,
|
||||||
value: '${info.activeCount}',
|
value: '${info.activeCount}',
|
||||||
unit: '人',
|
unit: l10n.peopleUnit,
|
||||||
color: const Color(0xFF10B981),
|
color: const Color(0xFF10B981),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
_StatCard(
|
_StatCard(
|
||||||
cardColor: cardColor,
|
cardColor: cardColor,
|
||||||
label: '待领积分',
|
label: l10n.pendingCreditsLabel,
|
||||||
value: info.pendingCreditFormatted,
|
value: info.pendingCreditFormatted,
|
||||||
unit: '',
|
unit: '',
|
||||||
color: const Color(0xFFF59E0B),
|
color: const Color(0xFFF59E0B),
|
||||||
|
|
@ -317,6 +323,7 @@ class _RewardRulesCard extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
return Card(
|
return Card(
|
||||||
color: cardColor,
|
color: cardColor,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|
@ -326,13 +333,13 @@ class _RewardRulesCard extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.card_giftcard, color: Color(0xFFF59E0B), size: 20),
|
const Icon(Icons.card_giftcard, color: Color(0xFFF59E0B), size: 20),
|
||||||
SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'奖励规则',
|
l10n.rewardRulesTitle,
|
||||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -397,6 +404,7 @@ class _ReferralPreviewList extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final async = ref.watch(referralListProvider);
|
final async = ref.watch(referralListProvider);
|
||||||
return async.when(
|
return async.when(
|
||||||
loading: () => const SizedBox(
|
loading: () => const SizedBox(
|
||||||
|
|
@ -411,12 +419,12 @@ class _ReferralPreviewList extends ConsumerWidget {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape:
|
shape:
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
child: const Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'暂无推荐记录,分享推荐码邀请好友吧',
|
l10n.noReferralsMessage,
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 13),
|
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -444,16 +452,17 @@ class _ReferralTile extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final statusColor = item.isActive
|
final statusColor = item.isActive
|
||||||
? const Color(0xFF10B981)
|
? const Color(0xFF10B981)
|
||||||
: item.status == 'EXPIRED'
|
: item.status == 'EXPIRED'
|
||||||
? Colors.grey
|
? Colors.grey
|
||||||
: const Color(0xFFF59E0B);
|
: const Color(0xFFF59E0B);
|
||||||
final statusLabel = switch (item.status) {
|
final statusLabel = switch (item.status) {
|
||||||
'PENDING' => '待付款',
|
'PENDING' => l10n.pendingPaymentStatus,
|
||||||
'ACTIVE' => '已激活',
|
'ACTIVE' => l10n.activeStatus,
|
||||||
'REWARDED' => '已奖励',
|
'REWARDED' => l10n.rewardedStatus,
|
||||||
'EXPIRED' => '已过期',
|
'EXPIRED' => l10n.expiredStatus,
|
||||||
_ => item.status,
|
_ => item.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -473,7 +482,7 @@ class _ReferralTile extends StatelessWidget {
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'注册于 ${_formatDate(item.registeredAt)}',
|
'${l10n.registeredAt} ${_formatDate(item.registeredAt)}',
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
trailing: Container(
|
trailing: Container(
|
||||||
|
|
@ -501,6 +510,7 @@ class _RewardPreviewList extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final async = ref.watch(pendingRewardsProvider);
|
final async = ref.watch(pendingRewardsProvider);
|
||||||
return async.when(
|
return async.when(
|
||||||
loading: () => const SizedBox(
|
loading: () => const SizedBox(
|
||||||
|
|
@ -513,12 +523,12 @@ class _RewardPreviewList extends ConsumerWidget {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape:
|
shape:
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
child: const Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'暂无待领积分',
|
l10n.noPendingRewardsMessage,
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 13),
|
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -548,6 +558,7 @@ class _RewardTile extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: const Color(0xFFF59E0B).withAlpha(30),
|
backgroundColor: const Color(0xFFF59E0B).withAlpha(30),
|
||||||
|
|
@ -569,9 +580,9 @@ class _RewardTile extends StatelessWidget {
|
||||||
color: const Color(0xFFF59E0B).withAlpha(20),
|
color: const Color(0xFFF59E0B).withAlpha(20),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'待抵扣',
|
l10n.pendingDeductionStatus,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFFF59E0B),
|
color: Color(0xFFF59E0B),
|
||||||
fontWeight: FontWeight.w500),
|
fontWeight: FontWeight.w500),
|
||||||
|
|
@ -588,14 +599,15 @@ class _ReferralListPage extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
final async = ref.watch(referralListProvider);
|
final async = ref.watch(referralListProvider);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('推荐记录')),
|
appBar: AppBar(title: Text(l10n.referralRecordsSection)),
|
||||||
body: async.when(
|
body: async.when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (e, _) => Center(child: Text('加载失败: $e')),
|
error: (e, _) => Center(child: Text('加载失败: $e')),
|
||||||
data: (result) => result.items.isEmpty
|
data: (result) => result.items.isEmpty
|
||||||
? const Center(child: Text('暂无推荐记录'))
|
? Center(child: Text(l10n.noReferralsMessage))
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
itemCount: result.items.length,
|
itemCount: result.items.length,
|
||||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/config/api_endpoints.dart';
|
import '../../../../core/config/api_endpoints.dart';
|
||||||
import '../../../../core/network/dio_client.dart';
|
import '../../../../core/network/dio_client.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
|
|
@ -48,7 +49,7 @@ class _ServersPageState extends ConsumerState<ServersPage> {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('服务器'),
|
title: Text(AppLocalizations.of(context).serversPageTitle),
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () async => ref.invalidate(serversProvider),
|
onRefresh: () async => ref.invalidate(serversProvider),
|
||||||
|
|
@ -62,10 +63,10 @@ class _ServersPageState extends ConsumerState<ServersPage> {
|
||||||
data: (servers) {
|
data: (servers) {
|
||||||
final filtered = _filterServers(servers);
|
final filtered = _filterServers(servers);
|
||||||
if (filtered.isEmpty) {
|
if (filtered.isEmpty) {
|
||||||
return const EmptyState(
|
return EmptyState(
|
||||||
icon: Icons.dns_outlined,
|
icon: Icons.dns_outlined,
|
||||||
title: '未找到服务器',
|
title: AppLocalizations.of(context).noServersTitle,
|
||||||
subtitle: '没有匹配当前筛选条件的服务器',
|
subtitle: AppLocalizations.of(context).noServersFiltered,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
|
@ -106,7 +107,7 @@ class _ServersPageState extends ConsumerState<ServersPage> {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
padding: const EdgeInsets.only(right: 8),
|
||||||
child: FilterChip(
|
child: FilterChip(
|
||||||
label: Text(env == 'all' ? '全部' : env),
|
label: Text(env == 'all' ? AppLocalizations.of(context).allEnvironments : env),
|
||||||
selected: selected,
|
selected: selected,
|
||||||
onSelected: (_) => setState(() => _envFilter = env),
|
onSelected: (_) => setState(() => _envFilter = env),
|
||||||
selectedColor: _envColor(env).withOpacity(0.25),
|
selectedColor: _envColor(env).withOpacity(0.25),
|
||||||
|
|
@ -142,7 +143,7 @@ class _ServersPageState extends ConsumerState<ServersPage> {
|
||||||
void _showServerDetails(
|
void _showServerDetails(
|
||||||
BuildContext context, Map<String, dynamic> server) {
|
BuildContext context, Map<String, dynamic> server) {
|
||||||
final hostname =
|
final hostname =
|
||||||
server['hostname'] as String? ?? server['name'] as String? ?? '未知';
|
server['hostname'] as String? ?? server['name'] as String? ?? AppLocalizations.of(context).unknownLabel;
|
||||||
final ip = server['ip_address'] as String? ??
|
final ip = server['ip_address'] as String? ??
|
||||||
server['ipAddress'] as String? ??
|
server['ipAddress'] as String? ??
|
||||||
server['ip'] as String? ??
|
server['ip'] as String? ??
|
||||||
|
|
@ -222,17 +223,17 @@ class _ServersPageState extends ConsumerState<ServersPage> {
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Detail rows
|
// Detail rows
|
||||||
_DetailRow(label: 'IP 地址', value: ip),
|
_DetailRow(label: AppLocalizations.of(context).ipAddressLabel, value: ip),
|
||||||
if (os.isNotEmpty) _DetailRow(label: '操作系统', value: os),
|
if (os.isNotEmpty) _DetailRow(label: AppLocalizations.of(context).osLabel, value: os),
|
||||||
if (cpu.isNotEmpty) _DetailRow(label: 'CPU', value: cpu),
|
if (cpu.isNotEmpty) _DetailRow(label: 'CPU', value: cpu),
|
||||||
if (memory.isNotEmpty)
|
if (memory.isNotEmpty)
|
||||||
_DetailRow(label: '内存', value: memory),
|
_DetailRow(label: AppLocalizations.of(context).memoryLabel, value: memory),
|
||||||
if (region.isNotEmpty)
|
if (region.isNotEmpty)
|
||||||
_DetailRow(label: '区域', value: region),
|
_DetailRow(label: AppLocalizations.of(context).regionLabel, value: region),
|
||||||
if (provider.isNotEmpty)
|
if (provider.isNotEmpty)
|
||||||
_DetailRow(label: '云厂商', value: provider),
|
_DetailRow(label: AppLocalizations.of(context).cloudProviderLabel, value: provider),
|
||||||
if (createdAt.isNotEmpty)
|
if (createdAt.isNotEmpty)
|
||||||
_DetailRow(label: '创建时间', value: createdAt),
|
_DetailRow(label: AppLocalizations.of(context).createdAtLabel, value: createdAt),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -269,7 +270,7 @@ class _ServerCard extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final hostname =
|
final hostname =
|
||||||
server['hostname'] as String? ?? server['name'] as String? ?? '未知';
|
server['hostname'] as String? ?? server['name'] as String? ?? AppLocalizations.of(context).unknownLabel;
|
||||||
final ip = server['ip_address'] as String? ??
|
final ip = server['ip_address'] as String? ??
|
||||||
server['ipAddress'] as String? ??
|
server['ipAddress'] as String? ??
|
||||||
server['ip'] as String? ??
|
server['ip'] as String? ??
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../../../core/updater/update_service.dart';
|
import '../../../../core/updater/update_service.dart';
|
||||||
|
|
@ -44,7 +45,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
final subtitleColor = isDark ? AppColors.textSecondary : Colors.grey[600];
|
final subtitleColor = isDark ? AppColors.textSecondary : Colors.grey[600];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('设置')),
|
appBar: AppBar(title: Text(AppLocalizations.of(context).settingsPageTitle)),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -59,16 +60,19 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.palette_outlined,
|
icon: Icons.palette_outlined,
|
||||||
iconBg: const Color(0xFF6366F1),
|
iconBg: const Color(0xFF6366F1),
|
||||||
title: '外观主题',
|
title: AppLocalizations.of(context).appearanceThemeLabel,
|
||||||
trailing: _ThemeLabel(mode: settings.themeMode),
|
trailing: _ThemeLabel(mode: settings.themeMode),
|
||||||
onTap: () => _showThemePicker(settings.themeMode),
|
onTap: () => _showThemePicker(settings.themeMode),
|
||||||
),
|
),
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.language,
|
icon: Icons.language,
|
||||||
iconBg: const Color(0xFF3B82F6),
|
iconBg: const Color(0xFF3B82F6),
|
||||||
title: '语言',
|
title: AppLocalizations.of(context).languageLabel,
|
||||||
trailing: Text('简体中文',
|
trailing: Text(
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
_languageDisplayLabel(settings.language),
|
||||||
|
style: TextStyle(color: subtitleColor, fontSize: 14),
|
||||||
|
),
|
||||||
|
onTap: () => _showLanguagePicker(settings.language),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -81,7 +85,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsToggleRow(
|
_SettingsToggleRow(
|
||||||
icon: Icons.notifications_outlined,
|
icon: Icons.notifications_outlined,
|
||||||
iconBg: const Color(0xFFEF4444),
|
iconBg: const Color(0xFFEF4444),
|
||||||
title: '推送通知',
|
title: AppLocalizations.of(context).pushNotificationsLabel,
|
||||||
value: settings.notificationsEnabled,
|
value: settings.notificationsEnabled,
|
||||||
onChanged: (v) =>
|
onChanged: (v) =>
|
||||||
ref.read(settingsProvider.notifier).setNotificationsEnabled(v),
|
ref.read(settingsProvider.notifier).setNotificationsEnabled(v),
|
||||||
|
|
@ -89,7 +93,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsToggleRow(
|
_SettingsToggleRow(
|
||||||
icon: Icons.volume_up_outlined,
|
icon: Icons.volume_up_outlined,
|
||||||
iconBg: const Color(0xFFF59E0B),
|
iconBg: const Color(0xFFF59E0B),
|
||||||
title: '提示音',
|
title: AppLocalizations.of(context).soundLabel,
|
||||||
value: settings.soundEnabled,
|
value: settings.soundEnabled,
|
||||||
onChanged: settings.notificationsEnabled
|
onChanged: settings.notificationsEnabled
|
||||||
? (v) =>
|
? (v) =>
|
||||||
|
|
@ -99,7 +103,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsToggleRow(
|
_SettingsToggleRow(
|
||||||
icon: Icons.vibration,
|
icon: Icons.vibration,
|
||||||
iconBg: const Color(0xFF22C55E),
|
iconBg: const Color(0xFF22C55E),
|
||||||
title: '震动反馈',
|
title: AppLocalizations.of(context).hapticFeedbackLabel,
|
||||||
value: settings.hapticFeedback,
|
value: settings.hapticFeedback,
|
||||||
onChanged: settings.notificationsEnabled
|
onChanged: settings.notificationsEnabled
|
||||||
? (v) =>
|
? (v) =>
|
||||||
|
|
@ -117,7 +121,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.psychology,
|
icon: Icons.psychology,
|
||||||
iconBg: const Color(0xFF7C3AED),
|
iconBg: const Color(0xFF7C3AED),
|
||||||
title: '对话引擎',
|
title: AppLocalizations.of(context).conversationEngineLabel,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
settings.engineType == 'claude_agent_sdk' ? 'Agent SDK' : 'Claude API',
|
settings.engineType == 'claude_agent_sdk' ? 'Agent SDK' : 'Claude API',
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14),
|
style: TextStyle(color: subtitleColor, fontSize: 14),
|
||||||
|
|
@ -127,7 +131,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.record_voice_over,
|
icon: Icons.record_voice_over,
|
||||||
iconBg: const Color(0xFF0EA5E9),
|
iconBg: const Color(0xFF0EA5E9),
|
||||||
title: '语音音色',
|
title: AppLocalizations.of(context).ttsVoiceLabel,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
_voiceDisplayLabel(settings.ttsVoice),
|
_voiceDisplayLabel(settings.ttsVoice),
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14),
|
style: TextStyle(color: subtitleColor, fontSize: 14),
|
||||||
|
|
@ -137,7 +141,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.tune,
|
icon: Icons.tune,
|
||||||
iconBg: const Color(0xFFF97316),
|
iconBg: const Color(0xFFF97316),
|
||||||
title: '语音风格',
|
title: AppLocalizations.of(context).ttsStyleLabel,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
_styleDisplayName(settings.ttsStyle),
|
_styleDisplayName(settings.ttsStyle),
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14),
|
style: TextStyle(color: subtitleColor, fontSize: 14),
|
||||||
|
|
@ -155,7 +159,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.credit_card_outlined,
|
icon: Icons.credit_card_outlined,
|
||||||
iconBg: const Color(0xFF10B981),
|
iconBg: const Color(0xFF10B981),
|
||||||
title: '订阅与用量',
|
title: AppLocalizations.of(context).subscriptionLabel,
|
||||||
onTap: () => context.push('/settings/billing'),
|
onTap: () => context.push('/settings/billing'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -169,7 +173,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.lock_outline,
|
icon: Icons.lock_outline,
|
||||||
iconBg: const Color(0xFF8B5CF6),
|
iconBg: const Color(0xFF8B5CF6),
|
||||||
title: '修改密码',
|
title: AppLocalizations.of(context).changePasswordLabel,
|
||||||
onTap: _showChangePasswordSheet,
|
onTap: _showChangePasswordSheet,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -183,7 +187,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.info_outline,
|
icon: Icons.info_outline,
|
||||||
iconBg: const Color(0xFF64748B),
|
iconBg: const Color(0xFF64748B),
|
||||||
title: '版本',
|
title: AppLocalizations.of(context).versionLabel,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
_appVersion.isNotEmpty ? _appVersion : 'v1.0.0',
|
_appVersion.isNotEmpty ? _appVersion : 'v1.0.0',
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
||||||
|
|
@ -191,14 +195,14 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.system_update_outlined,
|
icon: Icons.system_update_outlined,
|
||||||
iconBg: const Color(0xFF22C55E),
|
iconBg: const Color(0xFF22C55E),
|
||||||
title: '检查更新',
|
title: AppLocalizations.of(context).checkUpdateLabel,
|
||||||
onTap: () => UpdateService().manualCheckUpdate(context),
|
onTap: () => UpdateService().manualCheckUpdate(context),
|
||||||
),
|
),
|
||||||
if (settings.selectedTenantName != null)
|
if (settings.selectedTenantName != null)
|
||||||
_SettingsRow(
|
_SettingsRow(
|
||||||
icon: Icons.business_outlined,
|
icon: Icons.business_outlined,
|
||||||
iconBg: const Color(0xFF0EA5E9),
|
iconBg: const Color(0xFF0EA5E9),
|
||||||
title: '租户',
|
title: AppLocalizations.of(context).tenantLabel,
|
||||||
trailing: Text(settings.selectedTenantName!,
|
trailing: Text(settings.selectedTenantName!,
|
||||||
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
style: TextStyle(color: subtitleColor, fontSize: 14)),
|
||||||
),
|
),
|
||||||
|
|
@ -237,7 +241,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
child: const Text('退出登录', style: TextStyle(fontSize: 16)),
|
child: Text(AppLocalizations.of(context).logoutButton, style: const TextStyle(fontSize: 16)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
|
|
@ -319,6 +323,61 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Language Picker ------------------------------------------------------
|
||||||
|
|
||||||
|
String _languageDisplayLabel(String lang) => switch (lang) {
|
||||||
|
'zh_TW' => '繁體中文',
|
||||||
|
'en' => 'English',
|
||||||
|
_ => '简体中文',
|
||||||
|
};
|
||||||
|
|
||||||
|
void _showLanguagePicker(String current) {
|
||||||
|
const options = [
|
||||||
|
('zh', '简体中文'),
|
||||||
|
('zh_TW', '繁體中文'),
|
||||||
|
('en', 'English'),
|
||||||
|
];
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (ctx) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withOpacity(0.4),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: Text('选择语言 / 語言 / Language',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
...options.map((o) => ListTile(
|
||||||
|
title: Text(o.$2),
|
||||||
|
trailing: o.$1 == current
|
||||||
|
? const Icon(Icons.check, color: Colors.blue)
|
||||||
|
: null,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
ref.read(settingsProvider.notifier).setLanguage(o.$1);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Theme Picker ---------------------------------------------------------
|
// ---- Theme Picker ---------------------------------------------------------
|
||||||
|
|
||||||
void _showThemePicker(ThemeMode current) {
|
void _showThemePicker(ThemeMode current) {
|
||||||
|
|
@ -342,14 +401,14 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('选择主题',
|
Text(AppLocalizations.of(context).selectThemeTitle,
|
||||||
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
)),
|
)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_ThemeOption(
|
_ThemeOption(
|
||||||
icon: Icons.dark_mode,
|
icon: Icons.dark_mode,
|
||||||
label: '深色模式',
|
label: AppLocalizations.of(context).darkModeLabel,
|
||||||
selected: current == ThemeMode.dark,
|
selected: current == ThemeMode.dark,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.dark);
|
ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.dark);
|
||||||
|
|
@ -358,7 +417,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
),
|
),
|
||||||
_ThemeOption(
|
_ThemeOption(
|
||||||
icon: Icons.light_mode,
|
icon: Icons.light_mode,
|
||||||
label: '浅色模式',
|
label: AppLocalizations.of(context).lightModeLabel,
|
||||||
selected: current == ThemeMode.light,
|
selected: current == ThemeMode.light,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.light);
|
ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.light);
|
||||||
|
|
@ -367,7 +426,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
),
|
),
|
||||||
_ThemeOption(
|
_ThemeOption(
|
||||||
icon: Icons.settings_brightness,
|
icon: Icons.settings_brightness,
|
||||||
label: '跟随系统',
|
label: AppLocalizations.of(context).followSystemLabel,
|
||||||
selected: current == ThemeMode.system,
|
selected: current == ThemeMode.system,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.system);
|
ref.read(settingsProvider.notifier).setThemeMode(ThemeMode.system);
|
||||||
|
|
@ -384,26 +443,30 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
|
|
||||||
// ---- Voice Picker ----------------------------------------------------------
|
// ---- Voice Picker ----------------------------------------------------------
|
||||||
|
|
||||||
static const _voices = [
|
List<(String, String, String)> _voices(BuildContext context) {
|
||||||
('coral', 'Coral', '女 · 温暖'),
|
final l = AppLocalizations.of(context);
|
||||||
('nova', 'Nova', '女 · 活泼'),
|
return [
|
||||||
('sage', 'Sage', '女 · 知性'),
|
('coral', 'Coral', l.voiceCoralDesc),
|
||||||
('shimmer', 'Shimmer', '女 · 柔和'),
|
('nova', 'Nova', l.voiceNovaDesc),
|
||||||
('marin', 'Marin', '女 · 清澈'),
|
('sage', 'Sage', l.voiceSageDesc),
|
||||||
('ash', 'Ash', '男 · 沉稳'),
|
('shimmer', 'Shimmer', l.voiceShimmerDesc),
|
||||||
('echo', 'Echo', '男 · 清朗'),
|
('marin', 'Marin', l.voiceMarinDesc),
|
||||||
('onyx', 'Onyx', '男 · 低沉'),
|
('ash', 'Ash', l.voiceAshDesc),
|
||||||
('verse', 'Verse', '男 · 磁性'),
|
('echo', 'Echo', l.voiceEchoDesc),
|
||||||
('ballad', 'Ballad', '男 · 浑厚'),
|
('onyx', 'Onyx', l.voiceOnyxDesc),
|
||||||
('cedar', 'Cedar', '男 · 自然'),
|
('verse', 'Verse', l.voiceVerseDesc),
|
||||||
('alloy', 'Alloy', '中性'),
|
('ballad', 'Ballad', l.voiceBalladDesc),
|
||||||
('fable', 'Fable', '中性 · 叙事'),
|
('cedar', 'Cedar', l.voiceCedarDesc),
|
||||||
|
('alloy', 'Alloy', l.voiceAlloyDesc),
|
||||||
|
('fable', 'Fable', l.voiceFableDesc),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
void _showEngineTypePicker(String current) {
|
void _showEngineTypePicker(String current) {
|
||||||
|
final l = AppLocalizations.of(context);
|
||||||
final engines = [
|
final engines = [
|
||||||
('claude_agent_sdk', 'Agent SDK', '支持工具审批、技能注入、会话恢复'),
|
('claude_agent_sdk', 'Agent SDK', l.agentSdkDesc),
|
||||||
('claude_api', 'Claude API', '直连 API,响应更快'),
|
('claude_api', 'Claude API', l.claudeApiDesc),
|
||||||
];
|
];
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -424,7 +487,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('选择对话引擎',
|
Text(AppLocalizations.of(context).selectEngineTitle,
|
||||||
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
)),
|
)),
|
||||||
|
|
@ -475,7 +538,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('选择语音音色',
|
Text(AppLocalizations.of(context).selectVoiceTitle,
|
||||||
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
)),
|
)),
|
||||||
|
|
@ -483,7 +546,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: _voices
|
children: _voices(context)
|
||||||
.map((v) => ListTile(
|
.map((v) => ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Icons.record_voice_over,
|
Icons.record_voice_over,
|
||||||
|
|
@ -532,23 +595,26 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
// ---- Style Picker ---------------------------------------------------------
|
// ---- Style Picker ---------------------------------------------------------
|
||||||
|
|
||||||
String _voiceDisplayLabel(String voice) {
|
String _voiceDisplayLabel(String voice) {
|
||||||
for (final v in _voices) {
|
for (final v in _voices(context)) {
|
||||||
if (v.$1 == voice) return '${v.$2} · ${v.$3}';
|
if (v.$1 == voice) return '${v.$2} · ${v.$3}';
|
||||||
}
|
}
|
||||||
return voice[0].toUpperCase() + voice.substring(1);
|
return voice[0].toUpperCase() + voice.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const _stylePresets = [
|
List<(String, String)> _stylePresets(BuildContext context) {
|
||||||
('专业干练', '用专业、简洁、干练的语气说话,不拖泥带水。'),
|
final l = AppLocalizations.of(context);
|
||||||
('温柔耐心', '用温柔、耐心的语气说话,像一个贴心的朋友。'),
|
return [
|
||||||
('轻松活泼', '用轻松、活泼的语气说话,带一点幽默感。'),
|
(l.styleProfessionalName, '用专业、简洁、干练的语气说话,不拖泥带水。'),
|
||||||
('严肃正式', '用严肃、正式的语气说话,像在正式会议中发言。'),
|
(l.styleGentleName, '用温柔、耐心的语气说话,像一个贴心的朋友。'),
|
||||||
('科幻AI', '用科幻电影中AI的语气说话,冷静、理性、略带未来感。'),
|
(l.styleRelaxedName, '用轻松、活泼的语气说话,带一点幽默感。'),
|
||||||
|
(l.styleFormalName, '用严肃、正式的语气说话,像在正式会议中发言。'),
|
||||||
|
(l.styleScifiName, '用科幻电影中AI的语气说话,冷静、理性、略带未来感。'),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
String _styleDisplayName(String style) {
|
String _styleDisplayName(String style) {
|
||||||
if (style.isEmpty) return '默认';
|
if (style.isEmpty) return AppLocalizations.of(context).defaultStyleLabel;
|
||||||
for (final p in _stylePresets) {
|
for (final p in _stylePresets(context)) {
|
||||||
if (p.$2 == style) return p.$1;
|
if (p.$2 == style) return p.$1;
|
||||||
}
|
}
|
||||||
return style.length > 6 ? '${style.substring(0, 6)}...' : style;
|
return style.length > 6 ? '${style.substring(0, 6)}...' : style;
|
||||||
|
|
@ -556,7 +622,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
|
|
||||||
void _showStylePicker(String current) {
|
void _showStylePicker(String current) {
|
||||||
final controller = TextEditingController(
|
final controller = TextEditingController(
|
||||||
text: _stylePresets.any((p) => p.$2 == current) ? '' : current,
|
text: _stylePresets(context).any((p) => p.$2 == current) ? '' : current,
|
||||||
);
|
);
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
|
|
@ -581,7 +647,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('选择语音风格',
|
Text(AppLocalizations.of(context).selectStyleTitle,
|
||||||
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
style: Theme.of(ctx).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
)),
|
)),
|
||||||
|
|
@ -589,7 +655,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: _stylePresets
|
children: _stylePresets(context)
|
||||||
.map((p) => ChoiceChip(
|
.map((p) => ChoiceChip(
|
||||||
label: Text(p.$1),
|
label: Text(p.$1),
|
||||||
selected: current == p.$2,
|
selected: current == p.$2,
|
||||||
|
|
@ -606,8 +672,8 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '自定义风格',
|
labelText: AppLocalizations.of(context).customStyleLabel,
|
||||||
hintText: '例如:用东北话说话,幽默风趣',
|
hintText: AppLocalizations.of(context).customStyleHint,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
|
|
@ -622,7 +688,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
ref.read(settingsProvider.notifier).setTtsStyle('');
|
ref.read(settingsProvider.notifier).setTtsStyle('');
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
},
|
},
|
||||||
child: const Text('恢复默认'),
|
child: Text(AppLocalizations.of(context).resetToDefaultButton),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
@ -637,7 +703,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
}
|
}
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
},
|
},
|
||||||
child: const Text('确认'),
|
child: Text(AppLocalizations.of(context).confirmButton),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -657,13 +723,13 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
title: const Text('修改显示名称'),
|
title: Text(AppLocalizations.of(context).editNameDialogTitle),
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '显示名称',
|
labelText: AppLocalizations.of(context).displayNameLabel,
|
||||||
hintText: '输入新的显示名称',
|
hintText: AppLocalizations.of(context).displayNameHint,
|
||||||
border:
|
border:
|
||||||
OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
|
|
@ -671,7 +737,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(ctx).pop(),
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
child: const Text('取消'),
|
child: Text(AppLocalizations.of(context).cancelButton),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|
@ -683,11 +749,11 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
.updateDisplayName(name);
|
.updateDisplayName(name);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(success ? '名称已更新' : '更新失败')),
|
SnackBar(content: Text(success ? AppLocalizations.of(context).nameUpdatedMessage : AppLocalizations.of(context).updateFailedMessage)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('保存'),
|
child: Text(AppLocalizations.of(context).saveButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -725,7 +791,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('修改密码',
|
Text(AppLocalizations.of(context).changePasswordTitle,
|
||||||
style: Theme.of(ctx)
|
style: Theme.of(ctx)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleLarge
|
.titleLarge
|
||||||
|
|
@ -735,7 +801,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
controller: currentCtrl,
|
controller: currentCtrl,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '当前密码',
|
labelText: AppLocalizations.of(context).currentPasswordLabel,
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
|
@ -746,7 +812,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
controller: newCtrl,
|
controller: newCtrl,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '新密码',
|
labelText: AppLocalizations.of(context).newPasswordLabel,
|
||||||
prefixIcon: const Icon(Icons.lock_reset),
|
prefixIcon: const Icon(Icons.lock_reset),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
|
@ -757,7 +823,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
controller: confirmCtrl,
|
controller: confirmCtrl,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '确认新密码',
|
labelText: AppLocalizations.of(context).confirmPasswordLabel,
|
||||||
prefixIcon: const Icon(Icons.lock_reset),
|
prefixIcon: const Icon(Icons.lock_reset),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
|
@ -794,13 +860,13 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(result.success
|
content: Text(result.success
|
||||||
? '密码已修改'
|
? AppLocalizations.of(context).passwordChangedMessage
|
||||||
: result.message ?? '修改失败'),
|
: result.message ?? AppLocalizations.of(context).changeFailedMessage),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('确认修改', style: TextStyle(fontSize: 16)),
|
child: Text(AppLocalizations.of(context).confirmChangeButton, style: const TextStyle(fontSize: 16)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -815,17 +881,17 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
title: const Text('退出登录'),
|
title: Text(AppLocalizations.of(context).logoutDialogTitle),
|
||||||
content: const Text('确定要退出登录吗?'),
|
content: Text(AppLocalizations.of(context).logoutConfirmMessage),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(ctx).pop(false),
|
onPressed: () => Navigator.of(ctx).pop(false),
|
||||||
child: const Text('取消'),
|
child: Text(AppLocalizations.of(context).cancelButton),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
style: FilledButton.styleFrom(backgroundColor: AppColors.error),
|
style: FilledButton.styleFrom(backgroundColor: AppColors.error),
|
||||||
onPressed: () => Navigator.of(ctx).pop(true),
|
onPressed: () => Navigator.of(ctx).pop(true),
|
||||||
child: const Text('退出'),
|
child: Text(AppLocalizations.of(context).logoutConfirmButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -224,3 +224,12 @@ final themeModeProvider = Provider<ThemeMode>((ref) {
|
||||||
final notificationsEnabledProvider = Provider<bool>((ref) {
|
final notificationsEnabledProvider = Provider<bool>((ref) {
|
||||||
return ref.watch(settingsProvider).notificationsEnabled;
|
return ref.watch(settingsProvider).notificationsEnabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final localeProvider = Provider<Locale>((ref) {
|
||||||
|
final lang = ref.watch(settingsProvider).language;
|
||||||
|
return switch (lang) {
|
||||||
|
'zh_TW' => const Locale('zh', 'TW'),
|
||||||
|
'en' => const Locale('en'),
|
||||||
|
_ => const Locale('zh'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/config/api_endpoints.dart';
|
import '../../../../core/config/api_endpoints.dart';
|
||||||
import '../../../../core/network/dio_client.dart';
|
import '../../../../core/network/dio_client.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
|
|
@ -41,17 +42,17 @@ class StandingOrdersPage extends ConsumerWidget {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('常驻指令'),
|
title: Text(AppLocalizations.of(context).standingOrdersPageTitle),
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () async => ref.invalidate(standingOrdersProvider),
|
onRefresh: () async => ref.invalidate(standingOrdersProvider),
|
||||||
child: ordersAsync.when(
|
child: ordersAsync.when(
|
||||||
data: (orders) {
|
data: (orders) {
|
||||||
if (orders.isEmpty) {
|
if (orders.isEmpty) {
|
||||||
return const EmptyState(
|
return EmptyState(
|
||||||
icon: Icons.rule_outlined,
|
icon: Icons.rule_outlined,
|
||||||
title: '暂无常驻指令',
|
title: AppLocalizations.of(context).noStandingOrdersTitle,
|
||||||
subtitle: '配置后常驻指令将显示在此处',
|
subtitle: AppLocalizations.of(context).standingOrdersEmptyHint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
|
@ -93,7 +94,7 @@ class StandingOrderCardState extends ConsumerState<StandingOrderCard> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final order = widget.order;
|
final order = widget.order;
|
||||||
final name = order['name'] as String? ?? '未命名指令';
|
final name = order['name'] as String? ?? AppLocalizations.of(context).unnamedOrderName;
|
||||||
final triggerType = order['trigger_type'] as String? ??
|
final triggerType = order['trigger_type'] as String? ??
|
||||||
order['triggerType'] as String? ??
|
order['triggerType'] as String? ??
|
||||||
'unknown';
|
'unknown';
|
||||||
|
|
@ -113,7 +114,7 @@ class StandingOrderCardState extends ConsumerState<StandingOrderCard> {
|
||||||
|
|
||||||
final lastExecLabel = lastExecution != null
|
final lastExecLabel = lastExecution != null
|
||||||
? DateFormatter.timeAgo(DateTime.parse(lastExecution))
|
? DateFormatter.timeAgo(DateTime.parse(lastExecution))
|
||||||
: '从未执行';
|
: AppLocalizations.of(context).neverExecuted;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
color: AppColors.surface,
|
color: AppColors.surface,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import '../../../../core/config/api_endpoints.dart';
|
import '../../../../core/config/api_endpoints.dart';
|
||||||
import '../../../../core/errors/error_handler.dart';
|
import '../../../../core/errors/error_handler.dart';
|
||||||
import '../../../../core/network/dio_client.dart';
|
import '../../../../core/network/dio_client.dart';
|
||||||
|
|
@ -74,12 +75,12 @@ class _TasksPageState extends ConsumerState<TasksPage>
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('任务'),
|
title: Text(AppLocalizations.of(context).tasksPageTitle),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
tabs: const [
|
tabs: [
|
||||||
Tab(text: '运维任务'),
|
Tab(text: AppLocalizations.of(context).opsTasksTab),
|
||||||
Tab(text: '常驻指令'),
|
Tab(text: AppLocalizations.of(context).standingOrdersTab),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -130,10 +131,10 @@ class _TasksListBody extends StatelessWidget {
|
||||||
child: tasksAsync.when(
|
child: tasksAsync.when(
|
||||||
data: (tasks) {
|
data: (tasks) {
|
||||||
if (tasks.isEmpty) {
|
if (tasks.isEmpty) {
|
||||||
return const EmptyState(
|
return EmptyState(
|
||||||
icon: Icons.assignment_outlined,
|
icon: Icons.assignment_outlined,
|
||||||
title: '暂无任务',
|
title: AppLocalizations.of(context).noTasksTitle,
|
||||||
subtitle: '点击 + 创建新任务',
|
subtitle: AppLocalizations.of(context).createNewTaskHint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
|
@ -172,10 +173,10 @@ class _StandingOrdersListBody extends StatelessWidget {
|
||||||
child: ordersAsync.when(
|
child: ordersAsync.when(
|
||||||
data: (orders) {
|
data: (orders) {
|
||||||
if (orders.isEmpty) {
|
if (orders.isEmpty) {
|
||||||
return const EmptyState(
|
return EmptyState(
|
||||||
icon: Icons.rule_outlined,
|
icon: Icons.rule_outlined,
|
||||||
title: '暂无常驻指令',
|
title: AppLocalizations.of(context).noStandingOrdersTitle,
|
||||||
subtitle: '通过 我智能体 对话新增常驻指令',
|
subtitle: AppLocalizations.of(context).standingOrdersHint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
|
@ -354,19 +355,19 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
'新建任务',
|
AppLocalizations.of(context).createTaskTitle,
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _titleController,
|
controller: _titleController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '标题',
|
labelText: AppLocalizations.of(context).taskTitleLabel,
|
||||||
hintText: '例如: 重启 web-01 的 nginx',
|
hintText: AppLocalizations.of(context).taskTitleHint,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
validator: (v) => (v == null || v.trim().isEmpty) ? '请输入标题' : null,
|
validator: (v) => (v == null || v.trim().isEmpty) ? '请输入标题' : null,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
|
|
@ -376,10 +377,10 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
|
||||||
// Description
|
// Description
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _descriptionController,
|
controller: _descriptionController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '描述',
|
labelText: AppLocalizations.of(context).taskDescriptionLabel,
|
||||||
hintText: '可选详情...',
|
hintText: AppLocalizations.of(context).taskDescriptionHint,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
|
|
@ -389,9 +390,9 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
|
||||||
// Priority dropdown
|
// Priority dropdown
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _priority,
|
value: _priority,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '优先级',
|
labelText: AppLocalizations.of(context).taskPriorityLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: _priorities
|
items: _priorities
|
||||||
.map((p) => DropdownMenuItem(
|
.map((p) => DropdownMenuItem(
|
||||||
|
|
@ -411,12 +412,12 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
|
||||||
if (servers.isEmpty) return const SizedBox.shrink();
|
if (servers.isEmpty) return const SizedBox.shrink();
|
||||||
return DropdownButtonFormField<String?>(
|
return DropdownButtonFormField<String?>(
|
||||||
value: _selectedServerId,
|
value: _selectedServerId,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: '服务器(可选)',
|
labelText: AppLocalizations.of(context).taskServerOptionalLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
const DropdownMenuItem(value: null, child: Text('不指定')),
|
DropdownMenuItem(value: null, child: Text(AppLocalizations.of(context).taskNoServerSelection)),
|
||||||
...servers.map((s) {
|
...servers.map((s) {
|
||||||
final id = s['id']?.toString() ?? '';
|
final id = s['id']?.toString() ?? '';
|
||||||
final name = s['hostname'] as String? ??
|
final name = s['hostname'] as String? ??
|
||||||
|
|
@ -442,7 +443,7 @@ class _CreateTaskSheetState extends State<_CreateTaskSheet> {
|
||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||||
)
|
)
|
||||||
: const Text('创建任务'),
|
: Text(AppLocalizations.of(context).createTaskButton),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:it0_app/l10n/app_localizations.dart';
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
import 'package:xterm/xterm.dart';
|
import 'package:xterm/xterm.dart';
|
||||||
import '../../../../core/config/api_endpoints.dart';
|
import '../../../../core/config/api_endpoints.dart';
|
||||||
|
|
@ -182,14 +183,14 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String get _statusLabel {
|
String _statusLabel(BuildContext context) {
|
||||||
switch (_status) {
|
switch (_status) {
|
||||||
case _ConnectionStatus.connected:
|
case _ConnectionStatus.connected:
|
||||||
return '已连接';
|
return AppLocalizations.of(context).terminalConnectedLabel;
|
||||||
case _ConnectionStatus.connecting:
|
case _ConnectionStatus.connecting:
|
||||||
return '连接中...';
|
return AppLocalizations.of(context).terminalConnectingLabel;
|
||||||
case _ConnectionStatus.disconnected:
|
case _ConnectionStatus.disconnected:
|
||||||
return '未连接';
|
return AppLocalizations.of(context).terminalDisconnectedLabel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,7 +202,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('远程终端'),
|
title: Text(AppLocalizations.of(context).terminalTitle),
|
||||||
actions: [
|
actions: [
|
||||||
// Connection status indicator
|
// Connection status indicator
|
||||||
Padding(
|
Padding(
|
||||||
|
|
@ -219,7 +220,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
_statusLabel,
|
_statusLabel(context),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _statusColor,
|
color: _statusColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -261,9 +262,9 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||||
child: serversAsync.when(
|
child: serversAsync.when(
|
||||||
data: (servers) {
|
data: (servers) {
|
||||||
if (servers.isEmpty) {
|
if (servers.isEmpty) {
|
||||||
return const Text(
|
return Text(
|
||||||
'暂无可用服务器',
|
AppLocalizations.of(context).terminalNoAvailableServers,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.textMuted,
|
color: AppColors.textMuted,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
|
|
@ -272,9 +273,9 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||||
return DropdownButtonHideUnderline(
|
return DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<String>(
|
child: DropdownButton<String>(
|
||||||
value: _selectedServerId,
|
value: _selectedServerId,
|
||||||
hint: const Text(
|
hint: Text(
|
||||||
'选择服务器...',
|
AppLocalizations.of(context).terminalSelectServerHint,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.textMuted,
|
color: AppColors.textMuted,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
|
|
@ -317,7 +318,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'加载服务器失败',
|
AppLocalizations.of(context).terminalLoadServersError,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.error,
|
color: AppColors.error,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|
@ -355,10 +356,10 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
_status == _ConnectionStatus.connected
|
_status == _ConnectionStatus.connected
|
||||||
? '断开'
|
? AppLocalizations.of(context).terminalDisconnectButton
|
||||||
: _status == _ConnectionStatus.connecting
|
: _status == _ConnectionStatus.connecting
|
||||||
? '连接中...'
|
? AppLocalizations.of(context).terminalConnectingMessage
|
||||||
: '连接',
|
: AppLocalizations.of(context).terminalConnectButton,
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,404 @@
|
||||||
|
{
|
||||||
|
"@@locale": "en",
|
||||||
|
|
||||||
|
"appTitle": "My Agent",
|
||||||
|
"appSubtitle": "Server Cluster Operations AI Agent",
|
||||||
|
|
||||||
|
"navHome": "Home",
|
||||||
|
"navMyAgents": "My Agents",
|
||||||
|
"navBilling": "Billing",
|
||||||
|
"navProfile": "Me",
|
||||||
|
|
||||||
|
"cancelButton": "Cancel",
|
||||||
|
"confirmButton": "Confirm",
|
||||||
|
"saveButton": "Save",
|
||||||
|
"retryButton": "Retry",
|
||||||
|
"loadingLabel": "Loading...",
|
||||||
|
"unknownLabel": "Unknown",
|
||||||
|
"unnamedLabel": "Unnamed",
|
||||||
|
|
||||||
|
"homeGreeting": "{greeting}, {name}",
|
||||||
|
"@homeGreeting": {
|
||||||
|
"placeholders": {
|
||||||
|
"greeting": { "type": "String" },
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeSubtitle": "My Agent is always here for you",
|
||||||
|
"greetingEarlyMorning": "Good morning",
|
||||||
|
"greetingNoon": "Good noon",
|
||||||
|
"greetingAfternoon": "Good afternoon",
|
||||||
|
"greetingEvening": "Good evening",
|
||||||
|
"greetingLateNight": "Late night",
|
||||||
|
|
||||||
|
"agentStatusIdle": "Idle",
|
||||||
|
"agentStatusThinking": "Thinking...",
|
||||||
|
"agentStatusExecuting": "Executing...",
|
||||||
|
"agentStatusAwaitingApproval": "Awaiting approval",
|
||||||
|
"agentStatusError": "Error occurred",
|
||||||
|
|
||||||
|
"officialAgentsSection": "IT0 Official Agents",
|
||||||
|
"myAgentsSection": "My Agents",
|
||||||
|
"officialBadge": "Official",
|
||||||
|
"officialAgent1Name": "My Agent Ops Assistant",
|
||||||
|
"officialAgent1Desc": "Server management, SSH execution, log analysis",
|
||||||
|
"officialAgent2Name": "Security Audit Assistant",
|
||||||
|
"officialAgent2Desc": "Vulnerability scanning, permission review, compliance",
|
||||||
|
"officialAgent3Name": "Database Inspector",
|
||||||
|
"officialAgent3Desc": "Slow query analysis, index optimization, backup verification",
|
||||||
|
|
||||||
|
"noOwnAgentsTitle": "No agents yet",
|
||||||
|
"noOwnAgentsDesc": "Tap the robot button below and tell My Agent:\n\"Recruit an OpenClaw agent for me\"",
|
||||||
|
"quickTipsHeader": "You can say...",
|
||||||
|
"quickTip1": "💬 \"Recruit an agent to monitor GitHub Actions\"",
|
||||||
|
"quickTip2": "🔧 \"Export my OpenClaw config as JSON\"",
|
||||||
|
"quickTip3": "📊 \"Analyze server load for the past 7 days\"",
|
||||||
|
"quickTip4": "🛡️ \"Set up automatic database backup at 2AM daily\"",
|
||||||
|
|
||||||
|
"myAgentsTitle": "My Agents",
|
||||||
|
"myAgentsEmptyTitle": "Recruit your own agent",
|
||||||
|
"myAgentsEmptyDesc": "Chat with My Agent to recruit various agents:\nOpenClaw coding assistant, ops bot, data analyst...",
|
||||||
|
"myAgentsStep1Title": "Tap the robot button",
|
||||||
|
"myAgentsStep1Desc": "Open a conversation with My Agent",
|
||||||
|
"myAgentsStep2Title": "Describe the agent you want",
|
||||||
|
"myAgentsStep2Desc": "e.g. \"Recruit an OpenClaw coding assistant for me\"",
|
||||||
|
"myAgentsStep3Title": "My Agent auto-deploys",
|
||||||
|
"myAgentsStep3Desc": "It appears here after deployment. Chat via Telegram/WhatsApp.",
|
||||||
|
"myAgentsTemplatesHeader": "Popular templates (tell My Agent which one you want)",
|
||||||
|
|
||||||
|
"summaryTotal": "Total {count}",
|
||||||
|
"@summaryTotal": {
|
||||||
|
"placeholders": { "count": { "type": "int" } }
|
||||||
|
},
|
||||||
|
"summaryRunning": "Running {count}",
|
||||||
|
"@summaryRunning": {
|
||||||
|
"placeholders": { "count": { "type": "int" } }
|
||||||
|
},
|
||||||
|
"summaryStopped": "Stopped {count}",
|
||||||
|
"@summaryStopped": {
|
||||||
|
"placeholders": { "count": { "type": "int" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"statusRunning": "Running",
|
||||||
|
"statusDeploying": "Deploying",
|
||||||
|
"statusStopped": "Stopped",
|
||||||
|
"statusError": "Error",
|
||||||
|
|
||||||
|
"dismissTitle": "Dismiss Agent",
|
||||||
|
"dismissConfirmContent": "Confirm dismissal of \"{name}\"?\n\nThe agent container will be stopped and deleted. This cannot be undone.",
|
||||||
|
"@dismissConfirmContent": {
|
||||||
|
"placeholders": { "name": { "type": "String" } }
|
||||||
|
},
|
||||||
|
"dismissButton": "Dismiss",
|
||||||
|
"renameButton": "Rename",
|
||||||
|
"renameTitle": "Rename",
|
||||||
|
"renameHint": "Enter new name",
|
||||||
|
"dismissSuccessMessage": "Dismissed \"{name}\"",
|
||||||
|
"@dismissSuccessMessage": {
|
||||||
|
"placeholders": { "name": { "type": "String" } }
|
||||||
|
},
|
||||||
|
"dismissErrorMessage": "Dismiss failed: {error}",
|
||||||
|
"@dismissErrorMessage": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
|
"renameSuccessMessage": "Renamed successfully",
|
||||||
|
"renameErrorMessage": "Rename failed: {error}",
|
||||||
|
"@renameErrorMessage": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"loginPasswordTab": "Password",
|
||||||
|
"loginOtpTab": "OTP",
|
||||||
|
"emailLabel": "Email",
|
||||||
|
"emailHint": "user@example.com",
|
||||||
|
"emailRequiredError": "Please enter email",
|
||||||
|
"invalidEmailError": "Please enter a valid email",
|
||||||
|
"passwordLabel": "Password",
|
||||||
|
"passwordRequiredError": "Please enter password",
|
||||||
|
"phoneLabel": "Phone",
|
||||||
|
"phoneHint": "+1 234 567 8900",
|
||||||
|
"phoneRequiredError": "Please enter phone number",
|
||||||
|
"otpLabel": "OTP",
|
||||||
|
"otpHint": "6-digit code",
|
||||||
|
"otpRequiredError": "Please enter OTP",
|
||||||
|
"sendingLabel": "Sending",
|
||||||
|
"getOtpButton": "Get OTP",
|
||||||
|
"enterPhoneFirstError": "Please enter phone number first",
|
||||||
|
"loginButton": "Login",
|
||||||
|
"accountCreationNote": "Accounts are created by admin or via invitation link",
|
||||||
|
|
||||||
|
"chatNewConversationTooltip": "New conversation",
|
||||||
|
"chatStopTooltip": "Stop",
|
||||||
|
"chatVoiceCallTooltip": "Voice call",
|
||||||
|
"chatSelectFromAlbum": "Select from album",
|
||||||
|
"chatMultiSelectSupport": "Multi-select supported",
|
||||||
|
"chatTakePhoto": "Take photo",
|
||||||
|
"chatSelectFile": "Select file",
|
||||||
|
"chatImagesPdfLabel": "Images, PDF",
|
||||||
|
"chatThinkingLabel": "Thinking...",
|
||||||
|
"chatNeedsApprovalLabel": "Needs approval",
|
||||||
|
"chatExecutionFailedLabel": "Execution failed",
|
||||||
|
"chatExecutionResultLabel": "Execution result",
|
||||||
|
"chatStandingOrderDraftLabel": "Standing order draft",
|
||||||
|
"chatProcessingLabel": "Processing...",
|
||||||
|
"chatReplyingLabel": "Replying...",
|
||||||
|
"chatReplyLabel": "Reply",
|
||||||
|
"chatStartConversationPrompt": "Start chatting with My Agent",
|
||||||
|
"chatInputInstructionHint": "Enter command or make a voice call",
|
||||||
|
"chatAdditionalInstructionHint": "Additional instruction...",
|
||||||
|
"chatInstructionHint": "Enter instruction...",
|
||||||
|
"chatAddImageTooltip": "Add image",
|
||||||
|
"chatInjectionTooltip": "Inject instruction",
|
||||||
|
"chatCollapseLabel": "Collapse",
|
||||||
|
"chatExpandLabel": "Expand ({lineCount} lines)",
|
||||||
|
"@chatExpandLabel": {
|
||||||
|
"placeholders": { "lineCount": { "type": "int" } }
|
||||||
|
},
|
||||||
|
"chatRecognizingLabel": "Recognizing…",
|
||||||
|
"chatSpeechRecognitionError": "Speech recognition failed, please retry",
|
||||||
|
"chatTargetsLabel": "Targets: ",
|
||||||
|
|
||||||
|
"agentCallVoiceCallTitle": "Voice Call",
|
||||||
|
"agentCallRingingStatus": "My Agent Voice Call",
|
||||||
|
"agentCallActiveStatus": "My Agent",
|
||||||
|
"agentCallConnectingStatus": "Connecting...",
|
||||||
|
"agentCallEndedStatus": "Call ended",
|
||||||
|
"agentCallThinking": "Thinking...",
|
||||||
|
|
||||||
|
"terminalTitle": "Remote Terminal",
|
||||||
|
"terminalInitMessage": "My Agent Remote Terminal",
|
||||||
|
"terminalSelectServerMessage": "Select a server and click Connect.",
|
||||||
|
"terminalSelectServerFirst": "Please select a server first",
|
||||||
|
"terminalConnectingMessage": "Connecting to server",
|
||||||
|
"terminalConnectedLabel": "Connected",
|
||||||
|
"terminalConnectingLabel": "Connecting...",
|
||||||
|
"terminalDisconnectedLabel": "Disconnected",
|
||||||
|
"terminalSelectServerHint": "Select server...",
|
||||||
|
"terminalNoAvailableServers": "No servers available",
|
||||||
|
"terminalLoadServersError": "Failed to load servers",
|
||||||
|
"terminalConnectButton": "Connect",
|
||||||
|
"terminalDisconnectButton": "Disconnect",
|
||||||
|
"terminalDisconnectMessage": "Disconnected",
|
||||||
|
|
||||||
|
"tasksPageTitle": "Tasks",
|
||||||
|
"opsTasksTab": "Ops Tasks",
|
||||||
|
"standingOrdersTab": "Standing Orders",
|
||||||
|
"noTasksTitle": "No tasks",
|
||||||
|
"createNewTaskHint": "Tap + to create a new task",
|
||||||
|
"noStandingOrdersTitle": "No standing orders",
|
||||||
|
"standingOrdersHint": "Chat with My Agent to add standing orders",
|
||||||
|
"createTaskTitle": "New Task",
|
||||||
|
"taskTitleLabel": "Title",
|
||||||
|
"taskTitleHint": "e.g. Restart nginx on web-01",
|
||||||
|
"taskDescriptionLabel": "Description",
|
||||||
|
"taskDescriptionHint": "Optional details...",
|
||||||
|
"taskPriorityLabel": "Priority",
|
||||||
|
"taskServerOptionalLabel": "Server (optional)",
|
||||||
|
"taskNoServerSelection": "Any",
|
||||||
|
"createTaskButton": "Create Task",
|
||||||
|
"createTaskError": "Failed to create task: {error}",
|
||||||
|
"@createTaskError": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"notificationInboxTitle": "Inbox",
|
||||||
|
"notificationMarkAllRead": "Mark all read",
|
||||||
|
"notificationLoadingFailed": "Failed to load",
|
||||||
|
"noMessagesTitle": "No messages",
|
||||||
|
"operationFailedError": "Operation failed, please retry",
|
||||||
|
"linkLabel": "Link: ",
|
||||||
|
|
||||||
|
"notificationPreferencesTitle": "Notification Preferences",
|
||||||
|
"noNotificationChannels": "No configurable notification channels",
|
||||||
|
"notificationPreferencesInfo": "Choose which notifications to receive. Mandatory notifications (e.g. security alerts) cannot be disabled.",
|
||||||
|
"mandatoryNotificationsSection": "Mandatory notifications",
|
||||||
|
"optionalNotificationsSection": "Optional notifications",
|
||||||
|
"savePreferencesButton": "Save preferences",
|
||||||
|
"requiredLabel": "Required",
|
||||||
|
"preferencesSavedMessage": "Preferences saved",
|
||||||
|
"saveFailedMessage": "Save failed: {error}",
|
||||||
|
"@saveFailedMessage": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"referralScreenTitle": "Refer & Earn",
|
||||||
|
"yourReferralCodeLabel": "Your referral code",
|
||||||
|
"copyReferralCodeTooltip": "Copy referral code",
|
||||||
|
"copyInviteLinkButton": "Copy invite link",
|
||||||
|
"shareButton": "Share",
|
||||||
|
"copiedToClipboard": "Copied to clipboard",
|
||||||
|
"referralRecordsSection": "Referral records",
|
||||||
|
"viewAllReferralsLink": "View all >",
|
||||||
|
"pendingRewardsSection": "Pending credits",
|
||||||
|
"viewAllRewardsLink": "View all >",
|
||||||
|
"referredLabel": "Referred",
|
||||||
|
"peopleUnit": " people",
|
||||||
|
"activatedLabel": "Activated",
|
||||||
|
"pendingCreditsLabel": "Pending credits",
|
||||||
|
"rewardRulesTitle": "Reward rules",
|
||||||
|
"proReferralReward": "Refer Pro plan: you get USD15, they get USD5",
|
||||||
|
"enterpriseReferralReward": "Refer Enterprise plan: you get USD50, they get USD20",
|
||||||
|
"renewalBonusReward": "Earn 10% of their monthly payments for up to 12 months",
|
||||||
|
"creditDeductionNote": "Credits automatically applied to your next invoice",
|
||||||
|
"noReferralsMessage": "No referrals yet. Share your code!",
|
||||||
|
"pendingPaymentStatus": "Pending",
|
||||||
|
"activeStatus": "Active",
|
||||||
|
"rewardedStatus": "Rewarded",
|
||||||
|
"expiredStatus": "Expired",
|
||||||
|
"registeredAt": "Registered",
|
||||||
|
"noPendingRewardsMessage": "No pending credits",
|
||||||
|
"noReferralRecordsMessage": "No referral records",
|
||||||
|
"noRewardRecordsMessage": "No reward records",
|
||||||
|
"pendingDeductionStatus": "Pending deduction",
|
||||||
|
|
||||||
|
"billingTitle": "Subscription & Usage",
|
||||||
|
"upgradeButton": "Upgrade",
|
||||||
|
"upgradeDialogTitle": "Upgrade Plan",
|
||||||
|
"upgradeDialogMessage": "Go to Web Admin → Billing → Plans to complete the upgrade.",
|
||||||
|
"acknowledgeButton": "Got it",
|
||||||
|
"currentPlanLabel": "Current plan",
|
||||||
|
"periodEndLabel": "Period ends: ",
|
||||||
|
"tokenUsageLabel": "Token usage this month",
|
||||||
|
"unlimitedLabel": "Unlimited",
|
||||||
|
"billingStatusActive": "Active",
|
||||||
|
"billingStatusTrialing": "Trial",
|
||||||
|
"billingStatusPastDue": "Past due",
|
||||||
|
"billingStatusCancelled": "Cancelled",
|
||||||
|
"billingStatusExpired": "Expired",
|
||||||
|
"invoicePaidStatus": "Paid",
|
||||||
|
"invoiceUnpaidStatus": "Unpaid",
|
||||||
|
|
||||||
|
"serversPageTitle": "Servers",
|
||||||
|
"noServersTitle": "No servers found",
|
||||||
|
"noServersFiltered": "No servers match the current filter",
|
||||||
|
"allEnvironments": "All",
|
||||||
|
"ipAddressLabel": "IP Address",
|
||||||
|
"osLabel": "OS",
|
||||||
|
"cpuLabel": "CPU",
|
||||||
|
"memoryLabel": "Memory",
|
||||||
|
"regionLabel": "Region",
|
||||||
|
"cloudProviderLabel": "Cloud provider",
|
||||||
|
"createdAtLabel": "Created at",
|
||||||
|
|
||||||
|
"standingOrdersPageTitle": "Standing Orders",
|
||||||
|
"standingOrdersEmptyHint": "Standing orders will appear here after configuration",
|
||||||
|
"executionHistoryLabel": "Execution history ({count})",
|
||||||
|
"@executionHistoryLabel": {
|
||||||
|
"placeholders": { "count": { "type": "int" } }
|
||||||
|
},
|
||||||
|
"unnamedOrderName": "Unnamed order",
|
||||||
|
"neverExecuted": "Never executed",
|
||||||
|
"updateStatusError": "Failed to update status: {error}",
|
||||||
|
"@updateStatusError": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"settingsPageTitle": "Settings",
|
||||||
|
"appearanceThemeLabel": "Appearance",
|
||||||
|
"languageLabel": "Language",
|
||||||
|
"languageZh": "简体中文",
|
||||||
|
"languageZhTW": "繁體中文",
|
||||||
|
"languageEn": "English",
|
||||||
|
"selectLanguageTitle": "Select Language",
|
||||||
|
"pushNotificationsLabel": "Push notifications",
|
||||||
|
"soundLabel": "Sound",
|
||||||
|
"hapticFeedbackLabel": "Haptic feedback",
|
||||||
|
"conversationEngineLabel": "AI engine",
|
||||||
|
"ttsVoiceLabel": "Voice",
|
||||||
|
"ttsStyleLabel": "Voice style",
|
||||||
|
"subscriptionLabel": "Subscription & Usage",
|
||||||
|
"changePasswordLabel": "Change password",
|
||||||
|
"versionLabel": "Version",
|
||||||
|
"checkUpdateLabel": "Check for updates",
|
||||||
|
"tenantLabel": "Tenant",
|
||||||
|
"logoutButton": "Log out",
|
||||||
|
|
||||||
|
"selectThemeTitle": "Select Theme",
|
||||||
|
"darkModeLabel": "Dark",
|
||||||
|
"lightModeLabel": "Light",
|
||||||
|
"followSystemLabel": "System",
|
||||||
|
|
||||||
|
"selectVoiceTitle": "Select Voice",
|
||||||
|
"voiceCoralDesc": "Female · Warm",
|
||||||
|
"voiceNovaDesc": "Female · Lively",
|
||||||
|
"voiceSageDesc": "Female · Intellectual",
|
||||||
|
"voiceShimmerDesc": "Female · Soft",
|
||||||
|
"voiceMarinDesc": "Female · Clear",
|
||||||
|
"voiceAshDesc": "Male · Steady",
|
||||||
|
"voiceEchoDesc": "Male · Bright",
|
||||||
|
"voiceOnyxDesc": "Male · Deep",
|
||||||
|
"voiceVerseDesc": "Male · Magnetic",
|
||||||
|
"voiceBalladDesc": "Male · Rich",
|
||||||
|
"voiceCedarDesc": "Male · Natural",
|
||||||
|
"voiceAlloyDesc": "Neutral",
|
||||||
|
"voiceFableDesc": "Neutral · Narrative",
|
||||||
|
|
||||||
|
"selectEngineTitle": "Select AI Engine",
|
||||||
|
"agentSdkDesc": "Supports tool approval, skill injection, session restore",
|
||||||
|
"claudeApiDesc": "Direct API, faster response",
|
||||||
|
|
||||||
|
"selectStyleTitle": "Select Voice Style",
|
||||||
|
"defaultStyleLabel": "Default",
|
||||||
|
"customStyleLabel": "Custom style",
|
||||||
|
"customStyleHint": "e.g. Speak like a pirate, with humor",
|
||||||
|
"resetToDefaultButton": "Reset to default",
|
||||||
|
"styleProfessionalName": "Professional",
|
||||||
|
"styleProfessionalDesc": "Speak in a professional, concise, and efficient tone.",
|
||||||
|
"styleGentleName": "Gentle & Patient",
|
||||||
|
"styleGentleDesc": "Speak in a warm and patient tone, like a caring friend.",
|
||||||
|
"styleRelaxedName": "Relaxed & Lively",
|
||||||
|
"styleRelaxedDesc": "Speak in a relaxed, lively tone with a bit of humor.",
|
||||||
|
"styleFormalName": "Formal",
|
||||||
|
"styleFormalDesc": "Speak in a serious, formal tone, like in a business meeting.",
|
||||||
|
"styleScifiName": "Sci-Fi AI",
|
||||||
|
"styleScifiDesc": "Speak like an AI in a sci-fi movie — calm, rational, futuristic.",
|
||||||
|
|
||||||
|
"editNameDialogTitle": "Edit Display Name",
|
||||||
|
"displayNameLabel": "Display name",
|
||||||
|
"displayNameHint": "Enter new display name",
|
||||||
|
"changePasswordTitle": "Change Password",
|
||||||
|
"currentPasswordLabel": "Current password",
|
||||||
|
"newPasswordLabel": "New password",
|
||||||
|
"confirmPasswordLabel": "Confirm new password",
|
||||||
|
"passwordMismatchError": "Passwords do not match",
|
||||||
|
"passwordMinLengthError": "New password must be at least 6 characters",
|
||||||
|
"confirmChangeButton": "Confirm",
|
||||||
|
"passwordChangedMessage": "Password changed",
|
||||||
|
"nameUpdatedMessage": "Name updated",
|
||||||
|
"updateFailedMessage": "Update failed",
|
||||||
|
"changeFailedMessage": "Change failed",
|
||||||
|
"logoutDialogTitle": "Log out",
|
||||||
|
"logoutConfirmMessage": "Are you sure you want to log out?",
|
||||||
|
"logoutConfirmButton": "Log out",
|
||||||
|
|
||||||
|
"profileSubscriptionLabel": "Subscription & Usage",
|
||||||
|
"profileFreePlanLabel": "Free",
|
||||||
|
"profileReferralLabel": "Refer & Earn",
|
||||||
|
"profileReferralHint": "Earn credits by referring",
|
||||||
|
"profileInSiteMessagesLabel": "Inbox",
|
||||||
|
"profileViewMessagesLabel": "View messages",
|
||||||
|
|
||||||
|
"errorNetworkError": "Cannot connect to server, check your network",
|
||||||
|
"errorDataFormat": "Invalid data format",
|
||||||
|
"errorUnknown": "Unknown error occurred",
|
||||||
|
"errorConnectionTimeout": "Connection timed out",
|
||||||
|
"errorSendTimeout": "Request timed out, check your network",
|
||||||
|
"errorReceiveTimeout": "Response timed out, please retry",
|
||||||
|
"errorBadCertificate": "SSL certificate verification failed",
|
||||||
|
"errorRequestCancelled": "Request cancelled",
|
||||||
|
"errorBadRequest": "Invalid request parameters",
|
||||||
|
"errorPermissionDenied": "Permission denied",
|
||||||
|
"errorNotFound": "Resource not found",
|
||||||
|
"errorConflict": "Data conflict, please refresh",
|
||||||
|
"errorInvalidData": "Invalid data submitted",
|
||||||
|
"errorTooManyRequests": "Too many requests, please slow down",
|
||||||
|
"errorInternalServer": "Server error, please retry",
|
||||||
|
"errorBadGateway": "Gateway error, please retry",
|
||||||
|
"errorServiceUnavailable": "Service unavailable, please retry",
|
||||||
|
"errorConnectionReset": "Connection reset, please retry",
|
||||||
|
"errorConnectionRefused": "Connection refused, check if service is running",
|
||||||
|
"errorConnectionClosed": "Connection closed, please retry",
|
||||||
|
"errorSocketException": "Network error, check your connection",
|
||||||
|
"errorTlsException": "Secure connection failed, check your network",
|
||||||
|
"errorNetworkRequestFailed": "Network request failed, check your connection"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,404 @@
|
||||||
|
{
|
||||||
|
"@@locale": "zh",
|
||||||
|
|
||||||
|
"appTitle": "我智能体",
|
||||||
|
"appSubtitle": "服务器集群运维智能体",
|
||||||
|
|
||||||
|
"navHome": "主页",
|
||||||
|
"navMyAgents": "我的智能体",
|
||||||
|
"navBilling": "账单",
|
||||||
|
"navProfile": "我",
|
||||||
|
|
||||||
|
"cancelButton": "取消",
|
||||||
|
"confirmButton": "确认",
|
||||||
|
"saveButton": "保存",
|
||||||
|
"retryButton": "重试",
|
||||||
|
"loadingLabel": "加载中...",
|
||||||
|
"unknownLabel": "未知",
|
||||||
|
"unnamedLabel": "未命名",
|
||||||
|
|
||||||
|
"homeGreeting": "{greeting},{name}",
|
||||||
|
"@homeGreeting": {
|
||||||
|
"placeholders": {
|
||||||
|
"greeting": { "type": "String" },
|
||||||
|
"name": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homeSubtitle": "我智能体 随时为你服务",
|
||||||
|
"greetingEarlyMorning": "早上好",
|
||||||
|
"greetingNoon": "中午好",
|
||||||
|
"greetingAfternoon": "下午好",
|
||||||
|
"greetingEvening": "晚上好",
|
||||||
|
"greetingLateNight": "夜深了",
|
||||||
|
|
||||||
|
"agentStatusIdle": "空闲中",
|
||||||
|
"agentStatusThinking": "正在思考...",
|
||||||
|
"agentStatusExecuting": "执行指令中...",
|
||||||
|
"agentStatusAwaitingApproval": "等待审批",
|
||||||
|
"agentStatusError": "发生错误",
|
||||||
|
|
||||||
|
"officialAgentsSection": "IT0 官方智能体",
|
||||||
|
"myAgentsSection": "我的智能体",
|
||||||
|
"officialBadge": "官方",
|
||||||
|
"officialAgent1Name": "我智能体 运维助手",
|
||||||
|
"officialAgent1Desc": "服务器管理、SSH 执行、日志分析",
|
||||||
|
"officialAgent2Name": "安全审计助手",
|
||||||
|
"officialAgent2Desc": "漏洞扫描、权限审查、合规检查",
|
||||||
|
"officialAgent3Name": "数据库巡检",
|
||||||
|
"officialAgent3Desc": "慢查询分析、索引优化、备份验证",
|
||||||
|
|
||||||
|
"noOwnAgentsTitle": "还没有自己的智能体",
|
||||||
|
"noOwnAgentsDesc": "点击下方机器人按钮,告诉 我智能体\n\"帮我招募一个 OpenClaw 智能体\"",
|
||||||
|
"quickTipsHeader": "你可以这样说...",
|
||||||
|
"quickTip1": "💬 \"帮我招募一个监控 GitHub Actions 的智能体\"",
|
||||||
|
"quickTip2": "🔧 \"把我的 OpenClaw 配置导出为 JSON\"",
|
||||||
|
"quickTip3": "📊 \"分析我的服务器最近7天的负载情况\"",
|
||||||
|
"quickTip4": "🛡️ \"帮我设置每天凌晨2点自动备份数据库\"",
|
||||||
|
|
||||||
|
"myAgentsTitle": "我的智能体",
|
||||||
|
"myAgentsEmptyTitle": "招募你的专属智能体",
|
||||||
|
"myAgentsEmptyDesc": "通过与 我智能体 对话,你可以招募各种智能体:\nOpenClaw 编程助手、运维机器人、数据分析师...",
|
||||||
|
"myAgentsStep1Title": "点击下方机器人",
|
||||||
|
"myAgentsStep1Desc": "打开与 我智能体 的对话窗口",
|
||||||
|
"myAgentsStep2Title": "描述你想要的智能体",
|
||||||
|
"myAgentsStep2Desc": "例如:\"帮我招募一个 OpenClaw 编程助手\"",
|
||||||
|
"myAgentsStep3Title": "我智能体 自动部署",
|
||||||
|
"myAgentsStep3Desc": "部署完成后出现在这里,通过 Telegram/WhatsApp 等渠道与它对话",
|
||||||
|
"myAgentsTemplatesHeader": "热门模板(告诉 我智能体 你想要哪种)",
|
||||||
|
|
||||||
|
"summaryTotal": "总计 {count}",
|
||||||
|
"@summaryTotal": {
|
||||||
|
"placeholders": { "count": { "type": "int" } }
|
||||||
|
},
|
||||||
|
"summaryRunning": "运行中 {count}",
|
||||||
|
"@summaryRunning": {
|
||||||
|
"placeholders": { "count": { "type": "int" } }
|
||||||
|
},
|
||||||
|
"summaryStopped": "已停止 {count}",
|
||||||
|
"@summaryStopped": {
|
||||||
|
"placeholders": { "count": { "type": "int" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"statusRunning": "运行中",
|
||||||
|
"statusDeploying": "部署中",
|
||||||
|
"statusStopped": "已停止",
|
||||||
|
"statusError": "错误",
|
||||||
|
|
||||||
|
"dismissTitle": "解聘智能体",
|
||||||
|
"dismissConfirmContent": "确认要解聘「{name}」吗?\n\n解聘后将停止并删除该智能体容器,此操作不可撤销。",
|
||||||
|
"@dismissConfirmContent": {
|
||||||
|
"placeholders": { "name": { "type": "String" } }
|
||||||
|
},
|
||||||
|
"dismissButton": "解聘",
|
||||||
|
"renameButton": "重命名",
|
||||||
|
"renameTitle": "重命名",
|
||||||
|
"renameHint": "输入新名称",
|
||||||
|
"dismissSuccessMessage": "已解聘「{name}」",
|
||||||
|
"@dismissSuccessMessage": {
|
||||||
|
"placeholders": { "name": { "type": "String" } }
|
||||||
|
},
|
||||||
|
"dismissErrorMessage": "解聘失败:{error}",
|
||||||
|
"@dismissErrorMessage": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
|
"renameSuccessMessage": "重命名成功",
|
||||||
|
"renameErrorMessage": "重命名失败:{error}",
|
||||||
|
"@renameErrorMessage": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"loginPasswordTab": "密码登录",
|
||||||
|
"loginOtpTab": "验证码登录",
|
||||||
|
"emailLabel": "邮箱",
|
||||||
|
"emailHint": "user@example.com",
|
||||||
|
"emailRequiredError": "请输入邮箱地址",
|
||||||
|
"invalidEmailError": "请输入有效的邮箱地址",
|
||||||
|
"passwordLabel": "密码",
|
||||||
|
"passwordRequiredError": "请输入密码",
|
||||||
|
"phoneLabel": "手机号",
|
||||||
|
"phoneHint": "+86 138 0000 0000",
|
||||||
|
"phoneRequiredError": "请输入手机号",
|
||||||
|
"otpLabel": "验证码",
|
||||||
|
"otpHint": "6 位数字",
|
||||||
|
"otpRequiredError": "请输入验证码",
|
||||||
|
"sendingLabel": "发送中",
|
||||||
|
"getOtpButton": "获取验证码",
|
||||||
|
"enterPhoneFirstError": "请先输入手机号",
|
||||||
|
"loginButton": "登录",
|
||||||
|
"accountCreationNote": "账号由管理员在后台创建或通过邀请链接注册",
|
||||||
|
|
||||||
|
"chatNewConversationTooltip": "新对话",
|
||||||
|
"chatStopTooltip": "停止",
|
||||||
|
"chatVoiceCallTooltip": "语音通话",
|
||||||
|
"chatSelectFromAlbum": "从相册选择",
|
||||||
|
"chatMultiSelectSupport": "支持多选",
|
||||||
|
"chatTakePhoto": "拍照",
|
||||||
|
"chatSelectFile": "选择文件",
|
||||||
|
"chatImagesPdfLabel": "图片、PDF",
|
||||||
|
"chatThinkingLabel": "思考中...",
|
||||||
|
"chatNeedsApprovalLabel": "需要审批",
|
||||||
|
"chatExecutionFailedLabel": "执行失败",
|
||||||
|
"chatExecutionResultLabel": "执行结果",
|
||||||
|
"chatStandingOrderDraftLabel": "常驻指令草案",
|
||||||
|
"chatProcessingLabel": "处理中...",
|
||||||
|
"chatReplyingLabel": "回复中...",
|
||||||
|
"chatReplyLabel": "回复",
|
||||||
|
"chatStartConversationPrompt": "开始与 我智能体 对话",
|
||||||
|
"chatInputInstructionHint": "输入指令或拨打语音通话",
|
||||||
|
"chatAdditionalInstructionHint": "追加指令...",
|
||||||
|
"chatInstructionHint": "输入指令...",
|
||||||
|
"chatAddImageTooltip": "添加图片",
|
||||||
|
"chatInjectionTooltip": "追加指令",
|
||||||
|
"chatCollapseLabel": "收起",
|
||||||
|
"chatExpandLabel": "展开 ({lineCount} 行)",
|
||||||
|
"@chatExpandLabel": {
|
||||||
|
"placeholders": { "lineCount": { "type": "int" } }
|
||||||
|
},
|
||||||
|
"chatRecognizingLabel": "识别中…",
|
||||||
|
"chatSpeechRecognitionError": "语音识别失败,请重试",
|
||||||
|
"chatTargetsLabel": "目标: ",
|
||||||
|
|
||||||
|
"agentCallVoiceCallTitle": "语音通话",
|
||||||
|
"agentCallRingingStatus": "我智能体 语音通话",
|
||||||
|
"agentCallActiveStatus": "我智能体",
|
||||||
|
"agentCallConnectingStatus": "连接中...",
|
||||||
|
"agentCallEndedStatus": "通话结束",
|
||||||
|
"agentCallThinking": "思考中...",
|
||||||
|
|
||||||
|
"terminalTitle": "远程终端",
|
||||||
|
"terminalInitMessage": "我智能体 远程终端",
|
||||||
|
"terminalSelectServerMessage": "请选择服务器并点击连接。",
|
||||||
|
"terminalSelectServerFirst": "请先选择服务器",
|
||||||
|
"terminalConnectingMessage": "正在连接服务器",
|
||||||
|
"terminalConnectedLabel": "已连接",
|
||||||
|
"terminalConnectingLabel": "连接中...",
|
||||||
|
"terminalDisconnectedLabel": "未连接",
|
||||||
|
"terminalSelectServerHint": "选择服务器...",
|
||||||
|
"terminalNoAvailableServers": "暂无可用服务器",
|
||||||
|
"terminalLoadServersError": "加载服务器失败",
|
||||||
|
"terminalConnectButton": "连接",
|
||||||
|
"terminalDisconnectButton": "断开",
|
||||||
|
"terminalDisconnectMessage": "已断开连接",
|
||||||
|
|
||||||
|
"tasksPageTitle": "任务",
|
||||||
|
"opsTasksTab": "运维任务",
|
||||||
|
"standingOrdersTab": "常驻指令",
|
||||||
|
"noTasksTitle": "暂无任务",
|
||||||
|
"createNewTaskHint": "点击 + 创建新任务",
|
||||||
|
"noStandingOrdersTitle": "暂无常驻指令",
|
||||||
|
"standingOrdersHint": "通过 我智能体 对话新增常驻指令",
|
||||||
|
"createTaskTitle": "新建任务",
|
||||||
|
"taskTitleLabel": "标题",
|
||||||
|
"taskTitleHint": "例如: 重启 web-01 的 nginx",
|
||||||
|
"taskDescriptionLabel": "描述",
|
||||||
|
"taskDescriptionHint": "可选详情...",
|
||||||
|
"taskPriorityLabel": "优先级",
|
||||||
|
"taskServerOptionalLabel": "服务器(可选)",
|
||||||
|
"taskNoServerSelection": "不指定",
|
||||||
|
"createTaskButton": "创建任务",
|
||||||
|
"createTaskError": "创建任务失败: {error}",
|
||||||
|
"@createTaskError": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"notificationInboxTitle": "站内消息",
|
||||||
|
"notificationMarkAllRead": "全部已读",
|
||||||
|
"notificationLoadingFailed": "加载失败",
|
||||||
|
"noMessagesTitle": "暂无消息",
|
||||||
|
"operationFailedError": "操作失败,请重试",
|
||||||
|
"linkLabel": "链接:",
|
||||||
|
|
||||||
|
"notificationPreferencesTitle": "通知偏好设置",
|
||||||
|
"noNotificationChannels": "暂无可配置的通知频道",
|
||||||
|
"notificationPreferencesInfo": "您可以选择接收哪些类型的通知。强制通知(如安全告警)无法关闭。",
|
||||||
|
"mandatoryNotificationsSection": "重要通知(不可关闭)",
|
||||||
|
"optionalNotificationsSection": "可选通知",
|
||||||
|
"savePreferencesButton": "保存偏好设置",
|
||||||
|
"requiredLabel": "必需",
|
||||||
|
"preferencesSavedMessage": "通知偏好已保存",
|
||||||
|
"saveFailedMessage": "保存失败: {error}",
|
||||||
|
"@saveFailedMessage": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"referralScreenTitle": "邀请有礼",
|
||||||
|
"yourReferralCodeLabel": "你的推荐码",
|
||||||
|
"copyReferralCodeTooltip": "复制推荐码",
|
||||||
|
"copyInviteLinkButton": "复制邀请链接",
|
||||||
|
"shareButton": "分享",
|
||||||
|
"copiedToClipboard": "已复制到剪贴板",
|
||||||
|
"referralRecordsSection": "推荐记录",
|
||||||
|
"viewAllReferralsLink": "查看全部 >",
|
||||||
|
"pendingRewardsSection": "待领积分",
|
||||||
|
"viewAllRewardsLink": "查看全部 >",
|
||||||
|
"referredLabel": "已推荐",
|
||||||
|
"peopleUnit": "人",
|
||||||
|
"activatedLabel": "已激活",
|
||||||
|
"pendingCreditsLabel": "待领积分",
|
||||||
|
"rewardRulesTitle": "奖励规则",
|
||||||
|
"proReferralReward": "推荐 Pro 套餐:你获得 $15 积分,对方获得 $5 积分",
|
||||||
|
"enterpriseReferralReward": "推荐 Enterprise 套餐:你获得 $50 积分,对方获得 $20 积分",
|
||||||
|
"renewalBonusReward": "对方续订后,你持续获得每月付款额 10% 的积分,最长 12 个月",
|
||||||
|
"creditDeductionNote": "积分自动抵扣你的下期账单",
|
||||||
|
"noReferralsMessage": "暂无推荐记录,分享推荐码邀请好友吧",
|
||||||
|
"pendingPaymentStatus": "待付款",
|
||||||
|
"activeStatus": "已激活",
|
||||||
|
"rewardedStatus": "已奖励",
|
||||||
|
"expiredStatus": "已过期",
|
||||||
|
"registeredAt": "注册于",
|
||||||
|
"noPendingRewardsMessage": "暂无待领积分",
|
||||||
|
"noReferralRecordsMessage": "暂无推荐记录",
|
||||||
|
"noRewardRecordsMessage": "暂无奖励记录",
|
||||||
|
"pendingDeductionStatus": "待抵扣",
|
||||||
|
|
||||||
|
"billingTitle": "订阅与用量",
|
||||||
|
"upgradeButton": "升级套餐",
|
||||||
|
"upgradeDialogTitle": "升级套餐",
|
||||||
|
"upgradeDialogMessage": "请前往 Web 管理后台 → 账单 → 套餐 完成升级。",
|
||||||
|
"acknowledgeButton": "知道了",
|
||||||
|
"currentPlanLabel": "当前套餐",
|
||||||
|
"periodEndLabel": "当期结束:",
|
||||||
|
"tokenUsageLabel": "本月 Token 用量",
|
||||||
|
"unlimitedLabel": "不限量",
|
||||||
|
"billingStatusActive": "正常",
|
||||||
|
"billingStatusTrialing": "试用期",
|
||||||
|
"billingStatusPastDue": "待付款",
|
||||||
|
"billingStatusCancelled": "已取消",
|
||||||
|
"billingStatusExpired": "已过期",
|
||||||
|
"invoicePaidStatus": "已付款",
|
||||||
|
"invoiceUnpaidStatus": "待付款",
|
||||||
|
|
||||||
|
"serversPageTitle": "服务器",
|
||||||
|
"noServersTitle": "未找到服务器",
|
||||||
|
"noServersFiltered": "没有匹配当前筛选条件的服务器",
|
||||||
|
"allEnvironments": "全部",
|
||||||
|
"ipAddressLabel": "IP 地址",
|
||||||
|
"osLabel": "操作系统",
|
||||||
|
"cpuLabel": "CPU",
|
||||||
|
"memoryLabel": "内存",
|
||||||
|
"regionLabel": "区域",
|
||||||
|
"cloudProviderLabel": "云厂商",
|
||||||
|
"createdAtLabel": "创建时间",
|
||||||
|
|
||||||
|
"standingOrdersPageTitle": "常驻指令",
|
||||||
|
"standingOrdersEmptyHint": "配置后常驻指令将显示在此处",
|
||||||
|
"executionHistoryLabel": "执行历史 ({count})",
|
||||||
|
"@executionHistoryLabel": {
|
||||||
|
"placeholders": { "count": { "type": "int" } }
|
||||||
|
},
|
||||||
|
"unnamedOrderName": "未命名指令",
|
||||||
|
"neverExecuted": "从未执行",
|
||||||
|
"updateStatusError": "更新状态失败: {error}",
|
||||||
|
"@updateStatusError": {
|
||||||
|
"placeholders": { "error": { "type": "String" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"settingsPageTitle": "设置",
|
||||||
|
"appearanceThemeLabel": "外观主题",
|
||||||
|
"languageLabel": "语言",
|
||||||
|
"languageZh": "简体中文",
|
||||||
|
"languageZhTW": "繁體中文",
|
||||||
|
"languageEn": "English",
|
||||||
|
"selectLanguageTitle": "选择语言",
|
||||||
|
"pushNotificationsLabel": "推送通知",
|
||||||
|
"soundLabel": "提示音",
|
||||||
|
"hapticFeedbackLabel": "震动反馈",
|
||||||
|
"conversationEngineLabel": "对话引擎",
|
||||||
|
"ttsVoiceLabel": "语音音色",
|
||||||
|
"ttsStyleLabel": "语音风格",
|
||||||
|
"subscriptionLabel": "订阅与用量",
|
||||||
|
"changePasswordLabel": "修改密码",
|
||||||
|
"versionLabel": "版本",
|
||||||
|
"checkUpdateLabel": "检查更新",
|
||||||
|
"tenantLabel": "租户",
|
||||||
|
"logoutButton": "退出登录",
|
||||||
|
|
||||||
|
"selectThemeTitle": "选择主题",
|
||||||
|
"darkModeLabel": "深色模式",
|
||||||
|
"lightModeLabel": "浅色模式",
|
||||||
|
"followSystemLabel": "跟随系统",
|
||||||
|
|
||||||
|
"selectVoiceTitle": "选择语音音色",
|
||||||
|
"voiceCoralDesc": "女 · 温暖",
|
||||||
|
"voiceNovaDesc": "女 · 活泼",
|
||||||
|
"voiceSageDesc": "女 · 知性",
|
||||||
|
"voiceShimmerDesc": "女 · 柔和",
|
||||||
|
"voiceMarinDesc": "女 · 清澈",
|
||||||
|
"voiceAshDesc": "男 · 沉稳",
|
||||||
|
"voiceEchoDesc": "男 · 清朗",
|
||||||
|
"voiceOnyxDesc": "男 · 低沉",
|
||||||
|
"voiceVerseDesc": "男 · 磁性",
|
||||||
|
"voiceBalladDesc": "男 · 浑厚",
|
||||||
|
"voiceCedarDesc": "男 · 自然",
|
||||||
|
"voiceAlloyDesc": "中性",
|
||||||
|
"voiceFableDesc": "中性 · 叙事",
|
||||||
|
|
||||||
|
"selectEngineTitle": "选择对话引擎",
|
||||||
|
"agentSdkDesc": "支持工具审批、技能注入、会话恢复",
|
||||||
|
"claudeApiDesc": "直连 API,响应更快",
|
||||||
|
|
||||||
|
"selectStyleTitle": "选择语音风格",
|
||||||
|
"defaultStyleLabel": "默认",
|
||||||
|
"customStyleLabel": "自定义风格",
|
||||||
|
"customStyleHint": "例如:用东北话说话,幽默风趣",
|
||||||
|
"resetToDefaultButton": "恢复默认",
|
||||||
|
"styleProfessionalName": "专业干练",
|
||||||
|
"styleProfessionalDesc": "用专业、简洁、干练的语气说话,不拖泥带水。",
|
||||||
|
"styleGentleName": "温柔耐心",
|
||||||
|
"styleGentleDesc": "用温柔、耐心的语气说话,像一个贴心的朋友。",
|
||||||
|
"styleRelaxedName": "轻松活泼",
|
||||||
|
"styleRelaxedDesc": "用轻松、活泼的语气说话,带一点幽默感。",
|
||||||
|
"styleFormalName": "严肃正式",
|
||||||
|
"styleFormalDesc": "用严肃、正式的语气说话,像在正式会议中发言。",
|
||||||
|
"styleScifiName": "科幻AI",
|
||||||
|
"styleScifiDesc": "用科幻电影中AI的语气说话,冷静、理性、略带未来感。",
|
||||||
|
|
||||||
|
"editNameDialogTitle": "修改显示名称",
|
||||||
|
"displayNameLabel": "显示名称",
|
||||||
|
"displayNameHint": "输入新的显示名称",
|
||||||
|
"changePasswordTitle": "修改密码",
|
||||||
|
"currentPasswordLabel": "当前密码",
|
||||||
|
"newPasswordLabel": "新密码",
|
||||||
|
"confirmPasswordLabel": "确认新密码",
|
||||||
|
"passwordMismatchError": "两次输入的密码不一致",
|
||||||
|
"passwordMinLengthError": "新密码至少6个字符",
|
||||||
|
"confirmChangeButton": "确认修改",
|
||||||
|
"passwordChangedMessage": "密码已修改",
|
||||||
|
"nameUpdatedMessage": "名称已更新",
|
||||||
|
"updateFailedMessage": "更新失败",
|
||||||
|
"changeFailedMessage": "修改失败",
|
||||||
|
"logoutDialogTitle": "退出登录",
|
||||||
|
"logoutConfirmMessage": "确定要退出登录吗?",
|
||||||
|
"logoutConfirmButton": "退出",
|
||||||
|
|
||||||
|
"profileSubscriptionLabel": "订阅套餐与用量",
|
||||||
|
"profileFreePlanLabel": "Free",
|
||||||
|
"profileReferralLabel": "邀请有礼",
|
||||||
|
"profileReferralHint": "推荐赚积分",
|
||||||
|
"profileInSiteMessagesLabel": "站内消息",
|
||||||
|
"profileViewMessagesLabel": "查看消息",
|
||||||
|
|
||||||
|
"errorNetworkError": "无法连接到服务器,请检查网络",
|
||||||
|
"errorDataFormat": "数据格式异常",
|
||||||
|
"errorUnknown": "发生未知错误",
|
||||||
|
"errorConnectionTimeout": "连接超时,服务器无响应",
|
||||||
|
"errorSendTimeout": "发送请求超时,请检查网络",
|
||||||
|
"errorReceiveTimeout": "等待响应超时,请稍后重试",
|
||||||
|
"errorBadCertificate": "安全证书验证失败",
|
||||||
|
"errorRequestCancelled": "请求已取消",
|
||||||
|
"errorBadRequest": "请求参数错误",
|
||||||
|
"errorPermissionDenied": "没有权限执行此操作",
|
||||||
|
"errorNotFound": "请求的资源不存在",
|
||||||
|
"errorConflict": "数据冲突,请刷新后重试",
|
||||||
|
"errorInvalidData": "提交的数据不合法",
|
||||||
|
"errorTooManyRequests": "请求过于频繁,请稍后重试",
|
||||||
|
"errorInternalServer": "服务器内部错误,请稍后重试",
|
||||||
|
"errorBadGateway": "服务器网关错误,请稍后重试",
|
||||||
|
"errorServiceUnavailable": "服务器暂时不可用,请稍后重试",
|
||||||
|
"errorConnectionReset": "连接被服务器重置,请稍后重试",
|
||||||
|
"errorConnectionRefused": "服务器拒绝连接,请确认服务是否启动",
|
||||||
|
"errorConnectionClosed": "连接已关闭,请稍后重试",
|
||||||
|
"errorSocketException": "网络连接异常,请检查网络设置",
|
||||||
|
"errorTlsException": "安全连接失败,请检查网络环境",
|
||||||
|
"errorNetworkRequestFailed": "网络请求失败,请检查网络后重试"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
{
|
||||||
|
"@@locale": "zh_TW",
|
||||||
|
|
||||||
|
"appTitle": "我智能體",
|
||||||
|
"appSubtitle": "伺服器叢集運維智能體",
|
||||||
|
|
||||||
|
"navMyAgents": "我的智能體",
|
||||||
|
|
||||||
|
"homeSubtitle": "我智能體 隨時為你服務",
|
||||||
|
"officialAgent1Name": "我智能體 運維助手",
|
||||||
|
"officialAgent1Desc": "伺服器管理、SSH 執行、日誌分析",
|
||||||
|
"myAgentsSection": "我的智能體",
|
||||||
|
"noOwnAgentsTitle": "還沒有自己的智能體",
|
||||||
|
"noOwnAgentsDesc": "點擊下方機器人按鈕,告訴 我智能體\n\"幫我招募一個 OpenClaw 智能體\"",
|
||||||
|
"quickTipsHeader": "你可以這樣說...",
|
||||||
|
"quickTip1": "💬 \"幫我招募一個監控 GitHub Actions 的智能體\"",
|
||||||
|
"quickTip2": "🔧 \"把我的 OpenClaw 配置匯出為 JSON\"",
|
||||||
|
"quickTip3": "📊 \"分析我的伺服器最近7天的負載情況\"",
|
||||||
|
"quickTip4": "🛡️ \"幫我設置每天凌晨2點自動備份資料庫\"",
|
||||||
|
|
||||||
|
"myAgentsTitle": "我的智能體",
|
||||||
|
"myAgentsEmptyTitle": "招募你的專屬智能體",
|
||||||
|
"myAgentsEmptyDesc": "透過與 我智能體 對話,你可以招募各種智能體:\nOpenClaw 程式助手、運維機器人、資料分析師...",
|
||||||
|
"myAgentsStep1Desc": "打開與 我智能體 的對話視窗",
|
||||||
|
"myAgentsStep2Desc": "例如:\"幫我招募一個 OpenClaw 程式助手\"",
|
||||||
|
"myAgentsStep3Title": "我智能體 自動部署",
|
||||||
|
"myAgentsStep3Desc": "部署完成後出現在這裡,透過 Telegram/WhatsApp 等渠道與它對話",
|
||||||
|
"myAgentsTemplatesHeader": "熱門模板(告訴 我智能體 你想要哪種)",
|
||||||
|
|
||||||
|
"statusRunning": "運行中",
|
||||||
|
"statusDeploying": "部署中",
|
||||||
|
"statusStopped": "已停止",
|
||||||
|
|
||||||
|
"dismissTitle": "解聘智能體",
|
||||||
|
"dismissConfirmContent": "確認要解聘「{name}」嗎?\n\n解聘後將停止並刪除該智能體容器,此操作不可撤銷。",
|
||||||
|
"@dismissConfirmContent": {
|
||||||
|
"placeholders": { "name": { "type": "String" } }
|
||||||
|
},
|
||||||
|
"dismissSuccessMessage": "已解聘「{name}」",
|
||||||
|
"@dismissSuccessMessage": {
|
||||||
|
"placeholders": { "name": { "type": "String" } }
|
||||||
|
},
|
||||||
|
|
||||||
|
"languageZh": "簡體中文",
|
||||||
|
"languageZhTW": "繁體中文",
|
||||||
|
"languageEn": "English",
|
||||||
|
"selectLanguageTitle": "選擇語言",
|
||||||
|
|
||||||
|
"chatStartConversationPrompt": "開始與 我智能體 對話",
|
||||||
|
"chatStandingOrderDraftLabel": "常駐指令草稿",
|
||||||
|
|
||||||
|
"terminalInitMessage": "我智能體 遠端終端機",
|
||||||
|
"terminalTitle": "遠端終端機",
|
||||||
|
|
||||||
|
"tasksPageTitle": "任務",
|
||||||
|
"standingOrdersTab": "常駐指令",
|
||||||
|
"noStandingOrdersTitle": "暫無常駐指令",
|
||||||
|
"standingOrdersHint": "透過 我智能體 對話新增常駐指令",
|
||||||
|
|
||||||
|
"agentCallRingingStatus": "我智能體 語音通話",
|
||||||
|
"agentCallActiveStatus": "我智能體",
|
||||||
|
|
||||||
|
"settingsPageTitle": "設置",
|
||||||
|
"appearanceThemeLabel": "外觀主題",
|
||||||
|
"languageLabel": "語言",
|
||||||
|
"pushNotificationsLabel": "推送通知",
|
||||||
|
"soundLabel": "提示音",
|
||||||
|
"hapticFeedbackLabel": "震動反饋",
|
||||||
|
"conversationEngineLabel": "對話引擎",
|
||||||
|
"ttsVoiceLabel": "語音音色",
|
||||||
|
"ttsStyleLabel": "語音風格",
|
||||||
|
"subscriptionLabel": "訂閱與用量",
|
||||||
|
"changePasswordLabel": "修改密碼",
|
||||||
|
"tenantLabel": "租戶",
|
||||||
|
"logoutButton": "退出登入",
|
||||||
|
|
||||||
|
"selectThemeTitle": "選擇主題",
|
||||||
|
"darkModeLabel": "深色模式",
|
||||||
|
"lightModeLabel": "淺色模式",
|
||||||
|
"followSystemLabel": "跟隨系統",
|
||||||
|
|
||||||
|
"selectVoiceTitle": "選擇語音音色",
|
||||||
|
"selectEngineTitle": "選擇對話引擎",
|
||||||
|
"selectStyleTitle": "選擇語音風格",
|
||||||
|
"defaultStyleLabel": "預設",
|
||||||
|
"customStyleHint": "例如:用臺灣腔說話,親切自然",
|
||||||
|
"resetToDefaultButton": "恢復預設",
|
||||||
|
|
||||||
|
"editNameDialogTitle": "修改顯示名稱",
|
||||||
|
"displayNameLabel": "顯示名稱",
|
||||||
|
"displayNameHint": "輸入新的顯示名稱",
|
||||||
|
"changePasswordTitle": "修改密碼",
|
||||||
|
"currentPasswordLabel": "目前密碼",
|
||||||
|
"newPasswordLabel": "新密碼",
|
||||||
|
"confirmPasswordLabel": "確認新密碼",
|
||||||
|
"passwordMismatchError": "兩次輸入的密碼不一致",
|
||||||
|
"passwordMinLengthError": "新密碼至少6個字元",
|
||||||
|
"confirmChangeButton": "確認修改",
|
||||||
|
"passwordChangedMessage": "密碼已修改",
|
||||||
|
"logoutDialogTitle": "退出登入",
|
||||||
|
"logoutConfirmMessage": "確定要退出登入嗎?",
|
||||||
|
"logoutConfirmButton": "退出",
|
||||||
|
|
||||||
|
"billingTitle": "訂閱與用量",
|
||||||
|
"upgradeButton": "升級方案",
|
||||||
|
"upgradeDialogTitle": "升級方案",
|
||||||
|
"currentPlanLabel": "目前方案",
|
||||||
|
"tokenUsageLabel": "本月 Token 用量",
|
||||||
|
|
||||||
|
"serversPageTitle": "伺服器",
|
||||||
|
"noServersTitle": "未找到伺服器",
|
||||||
|
"ipAddressLabel": "IP 位址",
|
||||||
|
"cloudProviderLabel": "雲端廠商",
|
||||||
|
"createdAtLabel": "建立時間",
|
||||||
|
|
||||||
|
"notificationInboxTitle": "站內訊息",
|
||||||
|
"notificationMarkAllRead": "全部已讀",
|
||||||
|
"noMessagesTitle": "暫無訊息",
|
||||||
|
|
||||||
|
"notificationPreferencesTitle": "通知偏好設定",
|
||||||
|
"notificationPreferencesInfo": "您可以選擇接收哪些類型的通知。強制通知(如安全告警)無法關閉。",
|
||||||
|
"mandatoryNotificationsSection": "重要通知(不可關閉)",
|
||||||
|
"savePreferencesButton": "儲存偏好設定",
|
||||||
|
"preferencesSavedMessage": "通知偏好已儲存",
|
||||||
|
|
||||||
|
"referralScreenTitle": "邀請有禮",
|
||||||
|
"yourReferralCodeLabel": "你的推薦碼",
|
||||||
|
"copyReferralCodeTooltip": "複製推薦碼",
|
||||||
|
"copyInviteLinkButton": "複製邀請連結",
|
||||||
|
"referralRecordsSection": "推薦記錄",
|
||||||
|
"pendingRewardsSection": "待領積分",
|
||||||
|
"rewardRulesTitle": "獎勵規則",
|
||||||
|
"copiedToClipboard": "已複製到剪貼簿",
|
||||||
|
|
||||||
|
"profileSubscriptionLabel": "訂閱方案與用量",
|
||||||
|
"profileReferralLabel": "邀請有禮",
|
||||||
|
"profileReferralHint": "推薦賺積分",
|
||||||
|
"profileInSiteMessagesLabel": "站內訊息",
|
||||||
|
"profileViewMessagesLabel": "查看訊息"
|
||||||
|
}
|
||||||
|
|
@ -454,6 +454,11 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.0.0"
|
version: "8.0.0"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_math_fork:
|
flutter_math_fork:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -756,10 +761,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.19.0"
|
version: "0.20.2"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
|
|
||||||
# State Management
|
# State Management
|
||||||
|
|
@ -58,7 +60,7 @@ dependencies:
|
||||||
xterm: ^4.0.0
|
xterm: ^4.0.0
|
||||||
|
|
||||||
# Utils
|
# Utils
|
||||||
intl: ^0.19.0
|
intl: ^0.20.2
|
||||||
logger: ^2.2.0
|
logger: ^2.2.0
|
||||||
uuid: ^4.3.0
|
uuid: ^4.3.0
|
||||||
url_launcher: ^6.2.0
|
url_launcher: ^6.2.0
|
||||||
|
|
@ -100,6 +102,7 @@ flutter_launcher_icons:
|
||||||
generate: false
|
generate: false
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
generate: true
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
assets:
|
assets:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue