feat: v6.10.3 多语言翻译补全 + 17项功能修复
- 引导页协议多语言支持(languageId传递) - 登录页双书名号修复 + 注册页协议勾选 - 个人中心页面多语言(18个翻译键) - 网络断开提示增加关闭/刷新按钮 - 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索 - iOS快捷按钮重复修复(删除Info.plist静态定义) - 测试账号123456警告提示 - 扫码登录自动跳转(HTTP轮询+WebSocket双通道) - 登录页老用户按钮改次要色 - Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0) - macOS标题栏跟随软件夜间模式 - 平台兼容分发渠道弹窗 - 软件著作权图片+交叉水印 - 桌面小部件平台兼容说明默认收起 - iOS/macOS图标更新+名称确认为闲言 - 12个语言文件补全roleNative+7个分发渠道翻译字段
97
CHANGELOG.md
@@ -4,9 +4,104 @@
|
||||
|
||||
***
|
||||
|
||||
## [v6.10.3] - 2026-06-02
|
||||
|
||||
### 🌐 多语言翻译字段补全
|
||||
|
||||
**问题:** 新增 `roleNative`(原生栈)和7个分发渠道翻译字段后,10个非中英语言文件缺少这些字段导致编译 Error。
|
||||
|
||||
**修复方案:**
|
||||
- ✅ 10个语言文件(es, fr, it, pt, ru, ar, hi, bn, ko, ja, zh_tw, de)补全8个 TAbout 字段
|
||||
- ✅ `roleNative` — 各语言原生栈翻译
|
||||
- ✅ `distributionChannel` — 分发渠道标题
|
||||
- ✅ `distAndroid/distIOS/distMacOS/distHarmony/distWeb/distWindows` — 各平台分发说明
|
||||
- ✅ fr.dart 双引号 lint 修复
|
||||
|
||||
**涉及文件:**
|
||||
- `lib/l10n/languages/es.dart` — 西班牙语
|
||||
- `lib/l10n/languages/fr.dart` — 法语
|
||||
- `lib/l10n/languages/it.dart` — 意大利语
|
||||
- `lib/l10n/languages/pt.dart` — 葡萄牙语
|
||||
- `lib/l10n/languages/ru.dart` — 俄语
|
||||
- `lib/l10n/languages/ar.dart` — 阿拉伯语
|
||||
- `lib/l10n/languages/hi.dart` — 印地语
|
||||
- `lib/l10n/languages/bn.dart` — 孟加拉语
|
||||
- `lib/l10n/languages/ko.dart` — 韩语
|
||||
- `lib/l10n/languages/ja.dart` — 日语
|
||||
- `lib/l10n/languages/zh_tw.dart` — 繁体中文
|
||||
- `lib/l10n/languages/de.dart` — 德语
|
||||
|
||||
***
|
||||
|
||||
## [v6.10.2] - 2026-06-02
|
||||
|
||||
### 🔐 二维码扫码登录自动跳转
|
||||
|
||||
**问题:** Device A 扫描 Device B(登录页)的二维码后,Device A 显示"扫码成功"但 Device B 无响应,不会自动登录。
|
||||
|
||||
**根因:** 生成二维码后缺少轮询/WebSocket 监听机制,无法感知扫码确认状态变化。
|
||||
|
||||
**修复方案:**
|
||||
- ✅ `qrcode_login_provider.dart` — 新增 `waiting`/`scanned`/`logging` 步骤,生成二维码后自动启动 HTTP 轮询(3秒间隔) + WebSocket 双通道监听
|
||||
- ✅ 收到 `confirmed` + `token` 后自动调用 `AuthService.tokenLogin(token)` 完成登录
|
||||
- ✅ `qrcode_ws_service.dart` — 处理 confirmed 状态携带 token 的推送,终态自动取消订阅
|
||||
- ✅ `qrcode_login_page.dart` — 新增状态指示器(等待扫码/已扫码/正在登录),登录成功后自动跳转首页
|
||||
- ✅ 切换 Tab / 退出页面时自动停止轮询,防止资源泄漏
|
||||
|
||||
**涉及文件:**
|
||||
- `features/auth/providers/qrcode_login_provider.dart` — 核心轮询+自动登录逻辑
|
||||
- `features/auth/services/qrcode_ws_service.dart` — WS终态自动取消订阅
|
||||
- `features/auth/presentation/qrcode_login_page.dart` — UI状态指示+自动跳转
|
||||
|
||||
***
|
||||
|
||||
## [v6.10.1] - 2026-06-02
|
||||
|
||||
### 🐛 修复 iOS 离线模式 Syncfusion Chart 崩溃
|
||||
|
||||
**问题:** 点击"离线模式"或导航到含图表页面时,Syncfusion Chart 在 build/layout 阶段调用 `markNeedsLayout`,导致 "build during layout" 错误,App 卡死/闪退。
|
||||
|
||||
**根因:** Syncfusion Flutter Charts 已知 Bug — `ChartSeriesRenderer.markNeedsLayout` 在 widget 树构建期间被调用,违反 Flutter 渲染协议。
|
||||
|
||||
**修复方案:**
|
||||
- ✅ 增强 `DeferredBuilder` — 添加 `RepaintBoundary` 隔离重绘 + 可选 `placeholder` 参数
|
||||
- ✅ 全项目 **21个文件、约35处** Syncfusion 图表全部用 `DeferredBuilder` 包裹,延迟到 `postFrameCallback` 渲染
|
||||
- ✅ 所有图表系列补齐 `animationDuration: 0`,防止动画触发 `markNeedsLayout`
|
||||
|
||||
**涉及文件:**
|
||||
- `shared/widgets/containers/deferred_builder.dart` — 增加 RepaintBoundary + placeholder
|
||||
- `features/mine/user_center/` — learning_charts, learning_center_page, coin_log_page, learning_progress_page
|
||||
- `features/tool_center/statistics/` — learning_stats_tab, coin_stats_tab, favorite_stats_tab, statistics_page
|
||||
- `features/reading_report/` — trend_chart
|
||||
- `features/file_transfer/` — transfer_speed_chart, transfer_stats_page
|
||||
- `features/mine/achievement/` — achievement_page
|
||||
- `features/check/` — check_page
|
||||
- `features/discover/` — readlater_stats_page
|
||||
- `features/home/` — history_page, favorite_page
|
||||
- `features/tool_center/leisure/` — leisure_settings_sections
|
||||
- `features/mine/settings/` — data_management_page, translate_plugin_page, permission_management_page
|
||||
|
||||
***
|
||||
|
||||
## [v6.10.0] - 2026-06-02
|
||||
|
||||
### 🏗️ 架构优化 + 灵动岛增强 + Lint规则 + 应用图标
|
||||
### 🏗️ 架构优化 + 灵动岛增强 + Lint规则 + 应用图标 + 引导页协议多语言
|
||||
|
||||
**0. 协议页软件著作权证书图片 📜:**
|
||||
- 🔄 协议内容"2.1 平台版权"→"2.1 软件著作权"(中文+英文同步修改)
|
||||
- ✅ 新增 `WatermarkedCopyrightImage` 组件 — 展示软件著作权证书图片
|
||||
- ✅ 对角线"闲言"水印覆盖(CustomPaint + 旋转文字)
|
||||
- ✅ 点击图片弹出全屏查看(InteractiveViewer 支持缩放)
|
||||
- ✅ 通用协议页 + 引导页协议页 均支持图片展示
|
||||
- ✅ 图片右上角全屏提示标签
|
||||
|
||||
**1. 引导页协议多语言修复 🌐:**
|
||||
- 🐛 修复引导页协议内容始终显示中文的问题
|
||||
- ✅ `AgreementData.getContent/getUpdateDate` 传入当前语言ID
|
||||
- ✅ `agreementType.title` → `agreementType.titleFor(languageId)` 显示本地化标题
|
||||
- ✅ `_parseChapters` 章节解析兼容英文罗马数字编号(I. II. III. / Zero.)
|
||||
- ✅ 新增 `_localeToLanguageId` 方法将 Flutter Locale 映射为协议语言ID
|
||||
- ✅ 支持14种语言的协议内容、标题、更新日期自动切换
|
||||
|
||||
**1. MacosPlatformService 统一 🏗️:**
|
||||
- 🔄 分散在多文件的 MethodChannel 统一为 `MacosPlatformService`
|
||||
|
||||
@@ -104,24 +104,5 @@
|
||||
<array>
|
||||
<string>group.apps.xy.xianyan.share</string>
|
||||
</array>
|
||||
<key>UIApplicationShortcutItems</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UIApplicationShortcutItemType</key>
|
||||
<string>action_theme</string>
|
||||
<key>UIApplicationShortcutItemTitle</key>
|
||||
<string>主题个性化</string>
|
||||
<key>UIApplicationShortcutItemIconName</key>
|
||||
<string>palette</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UIApplicationShortcutItemType</key>
|
||||
<string>action_general_settings</string>
|
||||
<key>UIApplicationShortcutItemTitle</key>
|
||||
<string>通用设置</string>
|
||||
<key>UIApplicationShortcutItemIconName</key>
|
||||
<string>settings</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -24,6 +24,7 @@ import 'package:flutter_quill/flutter_quill.dart'
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../core/services/device/quick_actions_service.dart';
|
||||
import '../core/services/device/macos_platform_service.dart';
|
||||
import '../core/services/data/home_widget_service.dart';
|
||||
import '../core/services/ui/status_bar_service.dart';
|
||||
import '../core/router/app_router.dart' show appRouter, rootNavigatorKey;
|
||||
@@ -222,6 +223,18 @@ class _XianyanAppState extends ConsumerState<XianyanApp>
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangePlatformBrightness() {
|
||||
super.didChangePlatformBrightness();
|
||||
final settings = ref.read(themeSettingsProvider);
|
||||
if (settings.themeMode == AppThemeMode.system) {
|
||||
final isDark =
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
|
||||
Brightness.dark;
|
||||
MacosPlatformService.syncTheme(isDark);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeLocales(List<Locale>? locales) {
|
||||
super.didChangeLocales(locales);
|
||||
@@ -316,6 +329,15 @@ class _XianyanAppState extends ConsumerState<XianyanApp>
|
||||
|
||||
final themeMode = _resolveThemeMode(settings.themeMode);
|
||||
|
||||
final effectiveIsDark = switch (themeMode) {
|
||||
ThemeMode.dark => true,
|
||||
ThemeMode.light => false,
|
||||
ThemeMode.system =>
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
|
||||
Brightness.dark,
|
||||
};
|
||||
MacosPlatformService.syncTheme(effectiveIsDark);
|
||||
|
||||
return Directionality(
|
||||
textDirection: textDirection,
|
||||
child: _LocaleTransitionWrapper(
|
||||
|
||||
@@ -872,7 +872,7 @@ class AgreementData {
|
||||
|
||||
二、内容版权归属
|
||||
|
||||
2.1 平台版权
|
||||
2.1 软件著作权
|
||||
• **闲言APP**的软件著作权归**弥勒市朋普镇微风暴网络科技工作室**所有
|
||||
• 软件著作权登记号:【2020SR0421982】
|
||||
• 应用的界面设计、图标、代码等受【著作权法】保护
|
||||
@@ -1996,7 +1996,7 @@ I. Disclaimers
|
||||
|
||||
II. Content Copyright
|
||||
|
||||
2.1 Platform Copyright
|
||||
2.1 Software Copyright
|
||||
• The software copyright of **Xianyan APP** belongs to **Mile City Pengpu Town Weifengbao Network Technology Studio**
|
||||
• Software Copyright Registration Number: 2020SR0421982
|
||||
• The interface design, icons, code, etc. of the app are protected by copyright law
|
||||
|
||||
@@ -16,6 +16,7 @@ import '../../../core/theme/app_typography.dart';
|
||||
import '../../../core/theme/app_radius.dart';
|
||||
import '../../../l10n/app_locale.dart';
|
||||
import '../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../../shared/widgets/media/watermarked_copyright_image.dart';
|
||||
import '../data/agreement_types.dart';
|
||||
import '../data/agreement_data.dart';
|
||||
import '../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
@@ -199,6 +200,10 @@ class _AgreementSection extends StatelessWidget {
|
||||
final AppThemeExtension ext;
|
||||
final _Section section;
|
||||
|
||||
bool get _isCopyrightSection =>
|
||||
section.title.contains('软件著作权') ||
|
||||
section.title.contains('Software Copyright');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
@@ -217,6 +222,10 @@ class _AgreementSection extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
],
|
||||
if (section.body.isNotEmpty) _RichBody(ext: ext, text: section.body),
|
||||
if (_isCopyrightSection) ...[
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
const WatermarkedCopyrightImage(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -489,7 +489,7 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
ext,
|
||||
icon: CupertinoIcons.person_2,
|
||||
label: auth.legacyUser,
|
||||
accentColor: Theme.of(context).colorScheme.secondary,
|
||||
accentColor: ext.accentLight,
|
||||
onTap: () => _switchLoginMode(_LoginMode.legacy),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.lg),
|
||||
@@ -678,7 +678,9 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
child: GestureDetector(
|
||||
onTap: () => _showAgreement(true),
|
||||
child: Text(
|
||||
'《${auth.userAgreement}》',
|
||||
auth.userAgreement.startsWith('《')
|
||||
? auth.userAgreement
|
||||
: '《${auth.userAgreement}》',
|
||||
style: AppTypography.footnote.copyWith(
|
||||
color: ext.accent,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -696,7 +698,9 @@ class _LoginPageState extends ConsumerState<LoginPage>
|
||||
child: GestureDetector(
|
||||
onTap: () => _showAgreement(false),
|
||||
child: Text(
|
||||
'《${auth.privacyPolicy}》',
|
||||
auth.privacyPolicy.startsWith('《')
|
||||
? auth.privacyPolicy
|
||||
: '《${auth.privacyPolicy}》',
|
||||
style: AppTypography.footnote.copyWith(
|
||||
color: ext.accent,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 二维码登录页面
|
||||
/// 创建时间: 2026-05-10
|
||||
/// 更新时间: 2026-05-10
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 扫码登录(扫描Web端二维码确认登录)+ 生成二维码(Web端扫码登录)
|
||||
/// 上次更新: 初始创建
|
||||
/// 上次更新: 生成二维码后自动轮询监听,confirmed后自动tokenLogin跳转首页
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:async';
|
||||
@@ -15,11 +15,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '../../../core/router/app_routes.dart';
|
||||
import '../../../core/router/app_nav_extension.dart';
|
||||
import '../../../core/theme/app_radius.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../providers/qrcode_login_provider.dart';
|
||||
import '../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
import '../../../shared/widgets/feedback/app_toast.dart';
|
||||
@@ -45,6 +48,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
bool _hasScanned = false;
|
||||
StreamSubscription<Object>? _subscription;
|
||||
Timer? _expireTimer;
|
||||
bool _hasNavigated = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -59,6 +63,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
_subscription?.cancel();
|
||||
_expireTimer?.cancel();
|
||||
_scannerController.dispose();
|
||||
ref.read(qrcodeLoginProvider.notifier).stopPolling();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -148,10 +153,11 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
final qrcodeState = ref.watch(qrcodeLoginProvider);
|
||||
|
||||
ref.listen<QrcodeLoginState>(qrcodeLoginProvider, (prev, next) {
|
||||
if (next.isSuccess && prev?.isSuccess != true) {
|
||||
_showSuccessDialog(ext);
|
||||
if (next.isSuccess && prev?.isSuccess != true && !_hasNavigated) {
|
||||
_onLoginSuccess(ext, next);
|
||||
}
|
||||
if (next.isError && next.errorMessage != null) {
|
||||
if (next.isError && next.errorMessage != null &&
|
||||
prev?.errorMessage != next.errorMessage) {
|
||||
AppToast.showError(next.errorMessage!);
|
||||
}
|
||||
});
|
||||
@@ -188,6 +194,91 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 登录成功处理
|
||||
// ============================================================
|
||||
|
||||
void _onLoginSuccess(AppThemeExtension ext, QrcodeLoginState state) {
|
||||
_hasNavigated = true;
|
||||
ref.invalidate(authProvider);
|
||||
|
||||
if (_currentTab == 1) {
|
||||
_showQrcodeLoginSuccessDialog(ext);
|
||||
} else {
|
||||
_showScanSuccessDialog(ext);
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成二维码Tab: 扫码自动登录成功 → 跳转首页
|
||||
void _showQrcodeLoginSuccessDialog(AppThemeExtension ext) {
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => CupertinoAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('✅ '),
|
||||
Text(
|
||||
'登录成功',
|
||||
style: AppTypography.headline.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: AppSpacing.sm),
|
||||
child: Text(
|
||||
'扫码登录成功,即将进入首页',
|
||||
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
context.appGo(AppRoutes.home);
|
||||
}
|
||||
}).catchError((_) {});
|
||||
}
|
||||
|
||||
/// 扫码Tab: 确认成功 → 返回上一页
|
||||
void _showScanSuccessDialog(AppThemeExtension ext) {
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => CupertinoAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('✅ '),
|
||||
Text(
|
||||
'登录成功',
|
||||
style: AppTypography.headline.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: AppSpacing.sm),
|
||||
child: Text(
|
||||
'已成功在Web端登录,即将返回',
|
||||
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}).catchError((_) {});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Tab栏
|
||||
// ============================================================
|
||||
@@ -222,6 +313,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
_generateQrcode();
|
||||
} else {
|
||||
_hasScanned = false;
|
||||
ref.read(qrcodeLoginProvider.notifier).stopPolling();
|
||||
_startScanner();
|
||||
}
|
||||
},
|
||||
@@ -631,7 +723,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
children: [
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
'Web端扫码登录',
|
||||
'扫码登录',
|
||||
style: AppTypography.headline.copyWith(
|
||||
color: ext.textPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -639,13 +731,15 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
).animate().fadeIn(duration: 400.ms),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'在Web端扫描下方二维码即可登录',
|
||||
'使用已登录的设备扫描下方二维码即可登录',
|
||||
style: AppTypography.subhead.copyWith(color: ext.textHint),
|
||||
textAlign: TextAlign.center,
|
||||
).animate().fadeIn(duration: 400.ms, delay: 100.ms),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildQrcodeCard(ext, state),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildQrcodeStatusIndicator(ext, state),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildQrcodeActions(ext, state),
|
||||
],
|
||||
),
|
||||
@@ -710,7 +804,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
] else if (isExpired) ...[
|
||||
] else if (isExpired || state.isError) ...[
|
||||
_buildExpiredPlaceholder(ext),
|
||||
] else ...[
|
||||
_buildLoadingPlaceholder(ext),
|
||||
@@ -723,6 +817,77 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
.slideY(begin: 0.05, end: 0, duration: 500.ms, delay: 200.ms);
|
||||
}
|
||||
|
||||
/// 二维码状态指示器
|
||||
Widget _buildQrcodeStatusIndicator(
|
||||
AppThemeExtension ext,
|
||||
QrcodeLoginState state,
|
||||
) {
|
||||
if (state.isLogging) {
|
||||
return _buildStatusPill(
|
||||
ext,
|
||||
icon: CupertinoActivityIndicator(radius: 10, color: ext.accent),
|
||||
text: '正在登录…',
|
||||
bgColor: ext.accent.withValues(alpha: 0.1),
|
||||
textColor: ext.accent,
|
||||
);
|
||||
}
|
||||
|
||||
if (state.isScanned) {
|
||||
return _buildStatusPill(
|
||||
ext,
|
||||
icon: const Icon(CupertinoIcons.checkmark_circle_fill, size: 20, color: CupertinoColors.systemGreen),
|
||||
text: '已扫码,等待确认…',
|
||||
bgColor: CupertinoColors.systemGreen.withValues(alpha: 0.1),
|
||||
textColor: CupertinoColors.systemGreen,
|
||||
);
|
||||
}
|
||||
|
||||
if (state.isWaiting) {
|
||||
return _buildStatusPill(
|
||||
ext,
|
||||
icon: CupertinoActivityIndicator(radius: 10, color: ext.accent),
|
||||
text: '等待扫码…',
|
||||
bgColor: ext.accent.withValues(alpha: 0.08),
|
||||
textColor: ext.textSecondary,
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildStatusPill(
|
||||
AppThemeExtension ext, {
|
||||
required Widget icon,
|
||||
required String text,
|
||||
required Color bgColor,
|
||||
required Color textColor,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
text,
|
||||
style: AppTypography.footnote.copyWith(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(duration: 300.ms);
|
||||
}
|
||||
|
||||
Widget _buildExpiredPlaceholder(AppThemeExtension ext) {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -735,6 +900,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
CupertinoIcons.exclamationmark_triangle,
|
||||
@@ -807,6 +973,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
}
|
||||
|
||||
Widget _buildQrcodeActions(AppThemeExtension ext, QrcodeLoginState state) {
|
||||
final isLogging = state.isLogging;
|
||||
return GlassContainer(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
@@ -817,7 +984,7 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
child: CupertinoButton(
|
||||
color: ext.accent,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
onPressed: _generateQrcode,
|
||||
onPressed: isLogging ? null : _generateQrcode,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -858,44 +1025,6 @@ class _QrcodeLoginPageState extends ConsumerState<QrcodeLoginPage> {
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms, delay: 400.ms);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 成功弹窗
|
||||
// ============================================================
|
||||
|
||||
void _showSuccessDialog(AppThemeExtension ext) {
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => CupertinoAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('✅ '),
|
||||
Text(
|
||||
'登录成功',
|
||||
style: AppTypography.headline.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: AppSpacing.sm),
|
||||
child: Text(
|
||||
'已成功在Web端登录,即将返回',
|
||||
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}).catchError((_) {});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/router/app_nav_extension.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../core/theme/app_typography.dart';
|
||||
@@ -43,6 +45,7 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
|
||||
int _regStep = 1;
|
||||
bool _obscureRegPassword = true;
|
||||
bool _subscribeEmail = false;
|
||||
bool _agreedToTerms = false;
|
||||
bool _emsSending = false;
|
||||
int _emsCountdown = 0;
|
||||
bool _showSecQuestion = false;
|
||||
@@ -623,6 +626,8 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildAgreementRow(ext, auth),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -666,7 +671,8 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
|
||||
color: ext.accent,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
onPressed:
|
||||
(_regPasswordController.text.isNotEmpty &&
|
||||
(_agreedToTerms &&
|
||||
_regPasswordController.text.isNotEmpty &&
|
||||
_regConfirmPasswordController.text.isNotEmpty &&
|
||||
!authState.isLoading)
|
||||
? _handleRegister
|
||||
@@ -707,6 +713,98 @@ class _RegisterSectionState extends ConsumerState<RegisterSection> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAgreementRow(AppThemeExtension ext, TAuth auth) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => setState(() => _agreedToTerms = !_agreedToTerms),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
margin: const EdgeInsets.only(top: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: _agreedToTerms ? ext.accent : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(
|
||||
color: _agreedToTerms ? ext.accent : ext.textHint,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: _agreedToTerms
|
||||
? Icon(
|
||||
CupertinoIcons.checkmark,
|
||||
size: 13,
|
||||
color: ext.textOnAccent,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: auth.registerAgreePrefix,
|
||||
style: AppTypography.footnote.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: GestureDetector(
|
||||
onTap: () => _showAgreement(true),
|
||||
child: Text(
|
||||
auth.userAgreement.startsWith('《')
|
||||
? auth.userAgreement
|
||||
: '《${auth.userAgreement}》',
|
||||
style: AppTypography.footnote.copyWith(
|
||||
color: ext.accent,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: auth.and,
|
||||
style: AppTypography.footnote.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: GestureDetector(
|
||||
onTap: () => _showAgreement(false),
|
||||
child: Text(
|
||||
auth.privacyPolicy.startsWith('《')
|
||||
? auth.privacyPolicy
|
||||
: '《${auth.privacyPolicy}》',
|
||||
style: AppTypography.footnote.copyWith(
|
||||
color: ext.accent,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showAgreement(bool isUserAgreement) {
|
||||
if (isUserAgreement) {
|
||||
context.appPush('/agreement/user-service-agreement');
|
||||
} else {
|
||||
context.appPush('/agreement/privacy-policy');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRegister() async {
|
||||
final t = ref.read(translationsProvider);
|
||||
final username = _usernameController.text.trim();
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 二维码登录状态管理
|
||||
/// 创建时间: 2026-05-10
|
||||
/// 更新时间: 2026-05-10
|
||||
/// 作用: 管理扫码登录流程状态(扫描/确认/成功/错误)
|
||||
/// 上次更新: 初始创建
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 管理扫码登录流程状态(扫描/确认/成功/错误)+ 轮询/WebSocket监听自动登录
|
||||
/// 上次更新: 新增轮询+WS双通道监听,confirmed时自动tokenLogin完成登录
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/utils/logger.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/qrcode_ws_service.dart';
|
||||
import '../services/user_security_service.dart';
|
||||
|
||||
// ============================================================
|
||||
@@ -20,7 +24,10 @@ enum QrcodeLoginStep {
|
||||
idle,
|
||||
scanning,
|
||||
confirming,
|
||||
waiting,
|
||||
scanned,
|
||||
success,
|
||||
logging,
|
||||
error,
|
||||
}
|
||||
|
||||
@@ -31,12 +38,14 @@ class QrcodeLoginState {
|
||||
this.qrCode = '',
|
||||
this.qrcodeResult,
|
||||
this.errorMessage,
|
||||
this.loginToken,
|
||||
});
|
||||
|
||||
final QrcodeLoginStep step;
|
||||
final String qrCode;
|
||||
final QrcodeGenerateResult? qrcodeResult;
|
||||
final String? errorMessage;
|
||||
final String? loginToken;
|
||||
|
||||
QrcodeLoginState copyWith({
|
||||
QrcodeLoginStep? step,
|
||||
@@ -45,19 +54,27 @@ class QrcodeLoginState {
|
||||
bool clearQrcodeResult = false,
|
||||
String? errorMessage,
|
||||
bool clearError = false,
|
||||
String? loginToken,
|
||||
bool clearLoginToken = false,
|
||||
}) {
|
||||
return QrcodeLoginState(
|
||||
step: step ?? this.step,
|
||||
qrCode: qrCode ?? this.qrCode,
|
||||
qrcodeResult: clearQrcodeResult ? null : (qrcodeResult ?? this.qrcodeResult),
|
||||
qrcodeResult: clearQrcodeResult
|
||||
? null
|
||||
: (qrcodeResult ?? this.qrcodeResult),
|
||||
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
|
||||
loginToken: clearLoginToken ? null : (loginToken ?? this.loginToken),
|
||||
);
|
||||
}
|
||||
|
||||
bool get isIdle => step == QrcodeLoginStep.idle;
|
||||
bool get isScanning => step == QrcodeLoginStep.scanning;
|
||||
bool get isConfirming => step == QrcodeLoginStep.confirming;
|
||||
bool get isWaiting => step == QrcodeLoginStep.waiting;
|
||||
bool get isScanned => step == QrcodeLoginStep.scanned;
|
||||
bool get isSuccess => step == QrcodeLoginStep.success;
|
||||
bool get isLogging => step == QrcodeLoginStep.logging;
|
||||
bool get isError => step == QrcodeLoginStep.error;
|
||||
}
|
||||
|
||||
@@ -68,8 +85,17 @@ class QrcodeLoginState {
|
||||
/// 二维码登录状态管理器
|
||||
class QrcodeLoginNotifier extends Notifier<QrcodeLoginState> {
|
||||
@override
|
||||
QrcodeLoginState build() => const QrcodeLoginState();
|
||||
QrcodeLoginNotifier();
|
||||
QrcodeLoginState build() {
|
||||
ref.onDispose(_cleanup);
|
||||
return const QrcodeLoginState();
|
||||
}
|
||||
|
||||
Timer? _pollTimer;
|
||||
bool _polling = false;
|
||||
|
||||
// ============================================================
|
||||
// 扫码确认登录 (Device A 扫码后调用)
|
||||
// ============================================================
|
||||
|
||||
/// 扫码确认登录
|
||||
Future<void> confirmLogin(String code) async {
|
||||
@@ -99,16 +125,23 @@ class QrcodeLoginNotifier extends Notifier<QrcodeLoginState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成二维码(Web端登录用)
|
||||
// ============================================================
|
||||
// 生成二维码 + 启动轮询 (Device B 生成二维码)
|
||||
// ============================================================
|
||||
|
||||
/// 生成二维码并启动轮询监听
|
||||
Future<void> generateQrcode() async {
|
||||
state = state.copyWith(clearError: true);
|
||||
stopPolling();
|
||||
state = state.copyWith(clearError: true, clearQrcodeResult: true);
|
||||
try {
|
||||
final result = await UserSecurityService.qrcodeGenerate();
|
||||
state = state.copyWith(
|
||||
step: QrcodeLoginStep.waiting,
|
||||
qrCode: result.code,
|
||||
qrcodeResult: result,
|
||||
);
|
||||
Log.i('二维码生成成功: ${result.code}');
|
||||
_startPolling(result.code);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
step: QrcodeLoginStep.error,
|
||||
@@ -118,13 +151,142 @@ class QrcodeLoginNotifier extends Notifier<QrcodeLoginState> {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 轮询 + WebSocket 双通道监听
|
||||
// ============================================================
|
||||
|
||||
/// 启动轮询 + WebSocket 订阅
|
||||
void _startPolling(String code) {
|
||||
_subscribeWs(code);
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = Timer.periodic(
|
||||
const Duration(seconds: 3),
|
||||
(_) => _pollOnce(code),
|
||||
);
|
||||
_pollOnce(code);
|
||||
Log.i('二维码轮询已启动: $code');
|
||||
}
|
||||
|
||||
/// 停止轮询 + 取消WS订阅
|
||||
void stopPolling() {
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = null;
|
||||
_polling = false;
|
||||
QrcodeWsService.instance.unsubscribe();
|
||||
Log.i('二维码轮询已停止');
|
||||
}
|
||||
|
||||
/// 订阅WebSocket推送
|
||||
void _subscribeWs(String code) {
|
||||
QrcodeWsService.instance.subscribe(code, (data) {
|
||||
final status = data['status'] as String? ?? '';
|
||||
Log.i('WS推送: status=$status');
|
||||
_handleStatus(status, token: data['token'] as String?);
|
||||
});
|
||||
}
|
||||
|
||||
/// 单次轮询
|
||||
Future<void> _pollOnce(String code) async {
|
||||
if (_polling) return;
|
||||
if (state.step == QrcodeLoginStep.success ||
|
||||
state.step == QrcodeLoginStep.logging ||
|
||||
state.step == QrcodeLoginStep.error) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
_polling = true;
|
||||
try {
|
||||
final result = await UserSecurityService.qrcodePoll(code: code);
|
||||
_handleStatus(result.status, token: result.token);
|
||||
} catch (e) {
|
||||
Log.w('轮询异常: $e');
|
||||
} finally {
|
||||
_polling = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一处理状态变更
|
||||
void _handleStatus(String status, {String? token}) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
if (state.step != QrcodeLoginStep.waiting) {
|
||||
state = state.copyWith(step: QrcodeLoginStep.waiting);
|
||||
}
|
||||
break;
|
||||
case 'scanned':
|
||||
if (state.step != QrcodeLoginStep.scanned) {
|
||||
state = state.copyWith(step: QrcodeLoginStep.scanned);
|
||||
Log.i('二维码已被扫码,等待确认');
|
||||
}
|
||||
break;
|
||||
case 'confirmed':
|
||||
stopPolling();
|
||||
_onConfirmed(token);
|
||||
break;
|
||||
case 'expired':
|
||||
stopPolling();
|
||||
state = state.copyWith(
|
||||
step: QrcodeLoginStep.error,
|
||||
errorMessage: '二维码已过期,请重新生成',
|
||||
);
|
||||
break;
|
||||
case 'cancelled':
|
||||
stopPolling();
|
||||
state = state.copyWith(
|
||||
step: QrcodeLoginStep.error,
|
||||
errorMessage: '二维码已取消',
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// confirmed 后自动登录
|
||||
Future<void> _onConfirmed(String? token) async {
|
||||
if (token == null || token.isEmpty) {
|
||||
state = state.copyWith(
|
||||
step: QrcodeLoginStep.error,
|
||||
errorMessage: '扫码登录Token为空',
|
||||
);
|
||||
Log.e('扫码登录Token为空');
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
step: QrcodeLoginStep.logging,
|
||||
loginToken: token,
|
||||
);
|
||||
Log.i('收到扫码登录Token,开始自动登录');
|
||||
|
||||
try {
|
||||
final user = await AuthService.tokenLogin(
|
||||
token,
|
||||
platform: 'ios',
|
||||
appName: 'xianyan',
|
||||
);
|
||||
state = state.copyWith(step: QrcodeLoginStep.success);
|
||||
Log.i('扫码自动登录成功: ${user.displayName}');
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
step: QrcodeLoginStep.error,
|
||||
errorMessage: '扫码登录失败: $e',
|
||||
);
|
||||
Log.e('扫码自动登录失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 通用操作
|
||||
// ============================================================
|
||||
|
||||
/// 取消/重置
|
||||
void cancel() {
|
||||
stopPolling();
|
||||
state = const QrcodeLoginState();
|
||||
}
|
||||
|
||||
/// 重置到扫描状态
|
||||
void resetToScan() {
|
||||
stopPolling();
|
||||
state = const QrcodeLoginState(step: QrcodeLoginStep.scanning);
|
||||
}
|
||||
|
||||
@@ -135,6 +297,11 @@ class QrcodeLoginNotifier extends Notifier<QrcodeLoginState> {
|
||||
clearError: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 资源清理
|
||||
void _cleanup() {
|
||||
stopPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -142,4 +309,6 @@ class QrcodeLoginNotifier extends Notifier<QrcodeLoginState> {
|
||||
// ============================================================
|
||||
|
||||
final qrcodeLoginProvider =
|
||||
NotifierProvider<QrcodeLoginNotifier, QrcodeLoginState>(QrcodeLoginNotifier.new);
|
||||
NotifierProvider<QrcodeLoginNotifier, QrcodeLoginState>(
|
||||
QrcodeLoginNotifier.new,
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// 创建时间: 2026-06-02
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 通过WebSocket长连接接收二维码状态变更推送,替代HTTP轮询
|
||||
/// 上次更新: 初始创建,支持信令服务器订阅+HTTP轮询降级
|
||||
/// 上次更新: 处理confirmed状态携带token的推送,收到终态后自动取消订阅
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:async';
|
||||
@@ -144,8 +144,14 @@ class QrcodeWsService {
|
||||
final type = json['type'] as String? ?? '';
|
||||
|
||||
if (type == 'qrcode_status_update') {
|
||||
Log.i('QrcodeWsService: 收到状态推送 ${json['status']}');
|
||||
final status = json['status'] as String? ?? '';
|
||||
Log.i('QrcodeWsService: 收到状态推送 $status');
|
||||
_onStatusUpdate?.call(json);
|
||||
if (status == 'confirmed' || status == 'expired' ||
|
||||
status == 'cancelled') {
|
||||
Log.i('QrcodeWsService: 终态 $status,自动取消订阅');
|
||||
unsubscribe();
|
||||
}
|
||||
} else if (type == 'pong') {
|
||||
// heartbeat ack
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import '../../../core/theme/app_radius.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../models/check_models.dart';
|
||||
import '../providers/check_provider.dart';
|
||||
@@ -734,23 +735,25 @@ class _SimilarityGauge extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 160,
|
||||
child: SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
series: [
|
||||
DoughnutSeries<_SimPie, String>(
|
||||
animationDuration: 0,
|
||||
dataSource: [
|
||||
_SimPie('相似', simPercent, riskColor),
|
||||
_SimPie('差异', 100 - simPercent, ext.bgSecondary),
|
||||
],
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
innerRadius: '68%',
|
||||
radius: '82%',
|
||||
strokeWidth: 0,
|
||||
),
|
||||
],
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
series: [
|
||||
DoughnutSeries<_SimPie, String>(
|
||||
animationDuration: 0,
|
||||
dataSource: [
|
||||
_SimPie('相似', simPercent, riskColor),
|
||||
_SimPie('差异', 100 - simPercent, ext.bgSecondary),
|
||||
],
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
innerRadius: '68%',
|
||||
radius: '82%',
|
||||
strokeWidth: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
import 'package:xianyan/core/theme/app_theme.dart';
|
||||
import 'package:xianyan/core/theme/app_spacing.dart';
|
||||
import 'package:xianyan/core/theme/app_typography.dart';
|
||||
import 'package:xianyan/shared/widgets/containers/deferred_builder.dart';
|
||||
import 'package:xianyan/shared/widgets/containers/glass_container.dart';
|
||||
import 'package:xianyan/features/discover/models/chat_message.dart';
|
||||
import 'package:xianyan/shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
@@ -180,7 +181,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 160,
|
||||
child: SfCircularChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCircularChart(
|
||||
series: <CircularSeries<_TypeData, String>>[
|
||||
DoughnutSeries<_TypeData, String>(
|
||||
dataSource: chartData,
|
||||
@@ -205,7 +206,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -303,7 +304,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: SfCartesianChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
@@ -354,7 +355,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -407,7 +408,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: SfCartesianChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
@@ -451,7 +452,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -524,7 +525,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 160,
|
||||
child: SfCircularChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCircularChart(
|
||||
annotations: <CircularChartAnnotation>[
|
||||
CircularChartAnnotation(
|
||||
widget: Text(
|
||||
@@ -562,7 +563,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -691,7 +692,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: SfCartesianChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
@@ -730,7 +731,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -902,7 +903,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 160,
|
||||
child: SfCircularChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCircularChart(
|
||||
annotations: <CircularChartAnnotation>[
|
||||
CircularChartAnnotation(
|
||||
widget: Text(
|
||||
@@ -937,7 +938,7 @@ class ReadlaterStatsPage extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:xianyan/core/theme/app_theme.dart';
|
||||
import 'package:xianyan/core/theme/app_spacing.dart';
|
||||
import 'package:xianyan/core/theme/app_typography.dart';
|
||||
import 'package:xianyan/core/theme/app_radius.dart';
|
||||
import 'package:xianyan/shared/widgets/containers/deferred_builder.dart';
|
||||
import 'package:xianyan/features/file_transfer/providers/transfer_stats_provider.dart';
|
||||
import 'package:xianyan/features/file_transfer/services/transfer_stats_service.dart';
|
||||
import '../../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
@@ -232,7 +233,7 @@ class _TransferStatsPageState extends ConsumerState<TransferStatsPage> {
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return SfCartesianChart(
|
||||
return DeferredBuilder(builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
@@ -268,6 +269,7 @@ class _TransferStatsPageState extends ConsumerState<TransferStatsPage> {
|
||||
name: '接收',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,7 +401,7 @@ class _TransferStatsPageState extends ConsumerState<TransferStatsPage> {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SfCircularChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
series: [
|
||||
DoughnutSeries<_FileTypePie, String>(
|
||||
@@ -439,6 +441,7 @@ class _TransferStatsPageState extends ConsumerState<TransferStatsPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:xianyan/core/theme/app_theme.dart';
|
||||
import 'package:xianyan/core/theme/app_spacing.dart';
|
||||
import 'package:xianyan/core/theme/app_typography.dart';
|
||||
import 'package:xianyan/shared/widgets/containers/deferred_builder.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class TransferSpeedChart extends StatelessWidget {
|
||||
@@ -62,7 +63,8 @@ class TransferSpeedChart extends StatelessWidget {
|
||||
final data = speedHistory.isEmpty ? [0.0] : speedHistory;
|
||||
final chartData = data.asMap().entries.map((e) => _SpeedPt(e.key, e.value)).toList();
|
||||
|
||||
return SfCartesianChart(
|
||||
return DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: const NumericAxis(
|
||||
@@ -99,6 +101,7 @@ class TransferSpeedChart extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import '../../../core/theme/app_typography.dart';
|
||||
import '../../../core/theme/app_radius.dart';
|
||||
import '../../../core/router/app_routes.dart';
|
||||
import '../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
import '../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../../shared/widgets/input/app_slidable.dart';
|
||||
import '../../../shared/widgets/input/app_popup_menu.dart';
|
||||
@@ -441,7 +442,8 @@ class _FavoritePageState extends ConsumerState<FavoritePage> {
|
||||
);
|
||||
}
|
||||
|
||||
return SfCircularChart(
|
||||
return DeferredBuilder(
|
||||
builder: (context) => SfCircularChart(
|
||||
series: <CircularSeries<_PieData, String>>[
|
||||
DoughnutSeries<_PieData, String>(
|
||||
animationDuration: 0,
|
||||
@@ -452,6 +454,7 @@ class _FavoritePageState extends ConsumerState<FavoritePage> {
|
||||
innerRadius: '35%',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
import '../../../core/network/connectivity_provider.dart';
|
||||
import '../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
import '../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../../shared/widgets/input/app_slidable.dart';
|
||||
import '../../../shared/widgets/feedback/app_toast.dart';
|
||||
@@ -545,7 +546,8 @@ class _HistoryPageState extends ConsumerState<HistoryPage> {
|
||||
.map((e) => _TrendPt(e.key, e.value.value.toDouble()))
|
||||
.toList();
|
||||
|
||||
return SfCartesianChart(
|
||||
return DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: const NumericAxis(
|
||||
@@ -581,6 +583,7 @@ class _HistoryPageState extends ConsumerState<HistoryPage> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import '../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../core/theme/app_typography.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/utils/data/level_utils.dart';
|
||||
import '../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../shared/widgets/display/app_icon.dart';
|
||||
import '../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../../../shared/widgets/feedback/offline_banner.dart';
|
||||
@@ -598,22 +599,24 @@ class _ProfileSummary extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
series: [
|
||||
DoughnutSeries<_AchPie, String>(
|
||||
animationDuration: 0,
|
||||
dataSource: [
|
||||
_AchPie('已达成', achieved.toDouble(), ext.accent),
|
||||
_AchPie('未达成', (total - achieved).toDouble(), ext.bgSecondary),
|
||||
],
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
innerRadius: '70%',
|
||||
radius: '90%',
|
||||
),
|
||||
],
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
series: [
|
||||
DoughnutSeries<_AchPie, String>(
|
||||
animationDuration: 0,
|
||||
dataSource: [
|
||||
_AchPie('已达成', achieved.toDouble(), ext.accent),
|
||||
_AchPie('未达成', (total - achieved).toDouble(), ext.bgSecondary),
|
||||
],
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
innerRadius: '70%',
|
||||
radius: '90%',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
|
||||
@@ -473,51 +473,54 @@ class PlatformSection extends StatelessWidget {
|
||||
runSpacing: AppSpacing.sm,
|
||||
children: platforms.map((p) {
|
||||
final isCurrent = p.isCurrent;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrent
|
||||
? ext.accent.withValues(alpha: 0.15)
|
||||
: p.supported
|
||||
? ext.successColor.withValues(alpha: 0.08)
|
||||
: ext.textHint.withValues(alpha: 0.06),
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
border: Border.all(
|
||||
color: isCurrent
|
||||
? ext.accent.withValues(alpha: 0.5)
|
||||
: p.supported
|
||||
? ext.successColor.withValues(alpha: 0.3)
|
||||
: ext.textHint.withValues(alpha: 0.15),
|
||||
return GestureDetector(
|
||||
onTap: () => _showDistributionDialog(context, p.name),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.xs,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isCurrent) ...[
|
||||
Icon(
|
||||
CupertinoIcons.checkmark_circle_fill,
|
||||
size: 12,
|
||||
color: ext.accent,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
],
|
||||
Text(
|
||||
p.name,
|
||||
style: AppTypography.caption1.copyWith(
|
||||
fontWeight: isCurrent
|
||||
? FontWeight.w700
|
||||
: FontWeight.w600,
|
||||
color: isCurrent
|
||||
? ext.accent
|
||||
: p.supported
|
||||
? ext.successColor
|
||||
: ext.textHint,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrent
|
||||
? ext.accent.withValues(alpha: 0.15)
|
||||
: p.supported
|
||||
? ext.successColor.withValues(alpha: 0.08)
|
||||
: ext.textHint.withValues(alpha: 0.06),
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
border: Border.all(
|
||||
color: isCurrent
|
||||
? ext.accent.withValues(alpha: 0.5)
|
||||
: p.supported
|
||||
? ext.successColor.withValues(alpha: 0.3)
|
||||
: ext.textHint.withValues(alpha: 0.15),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isCurrent) ...[
|
||||
Icon(
|
||||
CupertinoIcons.checkmark_circle_fill,
|
||||
size: 12,
|
||||
color: ext.accent,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
],
|
||||
Text(
|
||||
p.name,
|
||||
style: AppTypography.caption1.copyWith(
|
||||
fontWeight: isCurrent
|
||||
? FontWeight.w700
|
||||
: FontWeight.w600,
|
||||
color: isCurrent
|
||||
? ext.accent
|
||||
: p.supported
|
||||
? ext.successColor
|
||||
: ext.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -527,6 +530,55 @@ class PlatformSection extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDistributionDialog(BuildContext context, String platformName) {
|
||||
final channel = _getDistributionChannel(platformName);
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => CupertinoAlertDialog(
|
||||
title: Text(platformName),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: AppSpacing.sm),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
t.about.distributionChannel,
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
channel,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text(t.about.okButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getDistributionChannel(String platformName) {
|
||||
if (platformName.contains('Android')) return t.about.distAndroid;
|
||||
if (platformName.contains('iOS')) return t.about.distIOS;
|
||||
if (platformName.contains('macOS')) return t.about.distMacOS;
|
||||
if (platformName.contains('HarmonyOS')) return t.about.distHarmony;
|
||||
if (platformName.contains('Web')) return t.about.distWeb;
|
||||
if (platformName.contains('Windows')) return t.about.distWindows;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 了解我们页面(分区组件)
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-06-01
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 官方网站、开发者、团队、QQ群、贡献者与特别鸣谢等分区
|
||||
/// 上次更新: 贡献者头像改为首字占位,移除emoji字段;功能分级标准
|
||||
/// 上次更新: 团队新增原生栈成员,简化贡献者和特别鸣谢列表
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -364,6 +364,13 @@ class TeamSection extends StatelessWidget {
|
||||
t.about.member4Sig,
|
||||
'',
|
||||
),
|
||||
TeamMemberData(
|
||||
'📱',
|
||||
t.about.roleNative,
|
||||
'秋叶qy',
|
||||
'春风若有怜花意,可否许我再少年',
|
||||
'',
|
||||
),
|
||||
];
|
||||
|
||||
return GlassContainer(
|
||||
@@ -742,11 +749,7 @@ class ContributorsSection extends StatelessWidget {
|
||||
|
||||
void _showContributorsSheet(BuildContext context) {
|
||||
final contributors = [
|
||||
ContributorData('AI Coder', t.about.contributorRole1),
|
||||
ContributorData('AI Designer', t.about.contributorRole2),
|
||||
ContributorData('AI Tester', t.about.contributorRole3),
|
||||
ContributorData('AI Doc Writer', t.about.contributorRole4),
|
||||
ContributorData(t.about.contributorRole5Name, t.about.contributorRole5),
|
||||
ContributorData('佳佳', t.about.contributorRole5),
|
||||
];
|
||||
|
||||
showCupertinoModalPopup<void>(
|
||||
@@ -831,10 +834,6 @@ class ContributorsSection extends StatelessWidget {
|
||||
|
||||
void _showSpecialThanksSheet(BuildContext context) {
|
||||
final thanks = [
|
||||
ContributorData('Flutter Team', t.about.thanksFlutter),
|
||||
ContributorData('Open Source Community', t.about.thanksOpenSource),
|
||||
ContributorData('QQ Group Members', t.about.thanksQQGroup),
|
||||
ContributorData('All Users', t.about.thanksUsers),
|
||||
ContributorData('Tools/Plugins', t.about.specialThanksTools),
|
||||
];
|
||||
|
||||
|
||||
@@ -508,7 +508,7 @@ class WeChatTile extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
const Text(
|
||||
'微风暴',
|
||||
'微风暴(微信搜索)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
@@ -22,6 +22,7 @@ import '../../../../core/router/app_nav_extension.dart';
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import '../../../../l10n/translations.dart';
|
||||
import '../../../../shared/widgets/feedback/app_toast.dart';
|
||||
import '../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../../../shared/widgets/adaptive/responsive_layout.dart';
|
||||
import '../providers/general_settings_provider.dart';
|
||||
@@ -236,7 +237,7 @@ class _DataManagementPageState extends ConsumerState<DataManagementPage>
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: SfCircularChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
series: [
|
||||
DoughnutSeries<_StoragePie, String>(
|
||||
@@ -258,7 +259,7 @@ class _DataManagementPageState extends ConsumerState<DataManagementPage>
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Wrap(
|
||||
spacing: AppSpacing.md,
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:xianyan/core/storage/database/app_database.dart';
|
||||
import 'package:xianyan/features/mine/settings/providers/plugin_provider.dart';
|
||||
import 'package:xianyan/features/mine/settings/providers/translate_record_provider.dart';
|
||||
import 'package:xianyan/features/discover/models/translate_language.dart';
|
||||
import 'package:xianyan/shared/widgets/containers/deferred_builder.dart';
|
||||
import 'package:xianyan/shared/widgets/containers/glass_container.dart';
|
||||
import 'package:xianyan/shared/widgets/feedback/app_toast.dart';
|
||||
import 'package:xianyan/shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
@@ -819,7 +820,7 @@ class _TranslatePluginPageState extends ConsumerState<TranslatePluginPage> {
|
||||
),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: SfCartesianChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
@@ -848,7 +849,7 @@ class _TranslatePluginPageState extends ConsumerState<TranslatePluginPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ import '../../../../../../core/theme/app_theme.dart';
|
||||
import '../../../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../../../core/theme/app_typography.dart';
|
||||
import '../../../../../../core/theme/app_radius.dart';
|
||||
import '../../../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
import '../../../../../l10n/translation_resolver.dart';
|
||||
@@ -353,7 +354,7 @@ class _PermissionManagementPageState
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 160,
|
||||
child: SfCircularChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCircularChart(
|
||||
series: <CircularSeries<_PermissionChartEntry, String>>[
|
||||
DoughnutSeries<_PermissionChartEntry, String>(
|
||||
animationDuration: 0,
|
||||
@@ -381,7 +382,7 @@ class _PermissionManagementPageState
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
||||
@@ -16,6 +16,7 @@ import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../core/theme/app_typography.dart';
|
||||
import '../../../../core/theme/app_radius.dart';
|
||||
import '../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../providers/coin_provider.dart';
|
||||
import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
@@ -500,7 +501,8 @@ class _CoinTrendChart extends StatelessWidget {
|
||||
);
|
||||
final adjustedMax = maxVal == 0 ? 10.0 : maxVal * 1.2;
|
||||
|
||||
return SfCartesianChart(
|
||||
return DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
@@ -543,6 +545,7 @@ class _CoinTrendChart extends StatelessWidget {
|
||||
name: '支出',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/theme/app_typography.dart';
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import '../../../../core/utils/ui/interaction_animations.dart';
|
||||
import '../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../../../shared/widgets/feedback/offline_banner.dart';
|
||||
import '../../../../shared/widgets/adaptive/responsive_layout.dart';
|
||||
@@ -407,25 +408,28 @@ class _LearningCenterPageState extends ConsumerState<LearningCenterPage> {
|
||||
SizedBox(
|
||||
width: 180,
|
||||
height: 180,
|
||||
child: SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
series: [
|
||||
DoughnutSeries<_RingData, String>(
|
||||
dataSource: [
|
||||
_RingData('已完成', rate, ext.accent),
|
||||
_RingData(
|
||||
'剩余',
|
||||
1.0 - rate,
|
||||
ext.accent.withValues(alpha: 0.12),
|
||||
),
|
||||
],
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
innerRadius: '66%',
|
||||
strokeWidth: 0,
|
||||
),
|
||||
],
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
series: [
|
||||
DoughnutSeries<_RingData, String>(
|
||||
dataSource: [
|
||||
_RingData('已完成', rate, ext.accent),
|
||||
_RingData(
|
||||
'剩余',
|
||||
1.0 - rate,
|
||||
ext.accent.withValues(alpha: 0.12),
|
||||
),
|
||||
],
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
innerRadius: '66%',
|
||||
strokeWidth: 0,
|
||||
animationDuration: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
@@ -590,60 +594,63 @@ class _LearningCenterPageState extends ConsumerState<LearningCenterPage> {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 0.5,
|
||||
color: ext.textHint.withValues(alpha: 0.1),
|
||||
),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 0,
|
||||
),
|
||||
minimum: 0,
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.y',
|
||||
),
|
||||
series: [
|
||||
AreaSeries<_TrendPoint, String>(
|
||||
dataSource: points,
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
color: ext.accent,
|
||||
borderColor: ext.accent,
|
||||
borderWidth: 2.5,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
ext.accent.withValues(alpha: 0.25),
|
||||
ext.accent.withValues(alpha: 0.02),
|
||||
],
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
markerSettings: MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 6,
|
||||
width: 6,
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 0.5,
|
||||
color: ext.textHint.withValues(alpha: 0.1),
|
||||
),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 0,
|
||||
),
|
||||
minimum: 0,
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.y',
|
||||
),
|
||||
series: [
|
||||
AreaSeries<_TrendPoint, String>(
|
||||
dataSource: points,
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
color: ext.accent,
|
||||
borderColor: ext.bgPrimary,
|
||||
borderWidth: 1.5,
|
||||
borderColor: ext.accent,
|
||||
borderWidth: 2.5,
|
||||
animationDuration: 0,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
ext.accent.withValues(alpha: 0.25),
|
||||
ext.accent.withValues(alpha: 0.02),
|
||||
],
|
||||
),
|
||||
markerSettings: MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 6,
|
||||
width: 6,
|
||||
color: ext.accent,
|
||||
borderColor: ext.bgPrimary,
|
||||
borderWidth: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -724,37 +731,40 @@ class _LearningCenterPageState extends ConsumerState<LearningCenterPage> {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
primaryYAxis: const NumericAxis(
|
||||
majorGridLines: MajorGridLines(width: 0),
|
||||
majorTickLines: MajorTickLines(size: 0),
|
||||
axisLine: AxisLine(width: 0),
|
||||
labelStyle: TextStyle(fontSize: 0),
|
||||
minimum: 0,
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: [
|
||||
ColumnSeries<_CategoryItem, String>(
|
||||
dataSource: categories,
|
||||
xValueMapper: (d, _) => d.name,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
width: 0.5,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppRadius.md),
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
primaryYAxis: const NumericAxis(
|
||||
majorGridLines: MajorGridLines(width: 0),
|
||||
majorTickLines: MajorTickLines(size: 0),
|
||||
axisLine: AxisLine(width: 0),
|
||||
labelStyle: TextStyle(fontSize: 0),
|
||||
minimum: 0,
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: [
|
||||
ColumnSeries<_CategoryItem, String>(
|
||||
dataSource: categories,
|
||||
xValueMapper: (d, _) => d.name,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
width: 0.5,
|
||||
animationDuration: 0,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppRadius.md),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/theme/app_typography.dart';
|
||||
import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
import '../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../providers/learning_progress_provider.dart';
|
||||
|
||||
@@ -209,21 +210,24 @@ class _LearningProgressPageState extends ConsumerState<LearningProgressPage> {
|
||||
SizedBox(
|
||||
width: 180,
|
||||
height: 180,
|
||||
child: SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
series: [
|
||||
DoughnutSeries<_RingData, String>(
|
||||
dataSource: [
|
||||
_RingData('已完成', rate, ext.accent),
|
||||
_RingData('剩余', 1.0 - rate, ext.accent.withValues(alpha: 0.12)),
|
||||
],
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
innerRadius: '66%',
|
||||
strokeWidth: 0,
|
||||
),
|
||||
],
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCircularChart(
|
||||
margin: EdgeInsets.zero,
|
||||
series: [
|
||||
DoughnutSeries<_RingData, String>(
|
||||
dataSource: [
|
||||
_RingData('已完成', rate, ext.accent),
|
||||
_RingData('剩余', 1.0 - rate, ext.accent.withValues(alpha: 0.12)),
|
||||
],
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
innerRadius: '66%',
|
||||
strokeWidth: 0,
|
||||
animationDuration: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
@@ -362,61 +366,63 @@ class _LearningProgressPageState extends ConsumerState<LearningProgressPage> {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 0.5,
|
||||
color: ext.textHint.withValues(alpha: 0.1),
|
||||
),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 0,
|
||||
),
|
||||
minimum: 0,
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.y',
|
||||
),
|
||||
series: [
|
||||
AreaSeries<_TrendPoint, String>(
|
||||
dataSource: points,
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
color: ext.accent,
|
||||
borderColor: ext.accent,
|
||||
borderWidth: 2.5,
|
||||
animationDuration: 0,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
ext.accent.withValues(alpha: 0.25),
|
||||
ext.accent.withValues(alpha: 0.02),
|
||||
],
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
markerSettings: MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 6,
|
||||
width: 6,
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 0.5,
|
||||
color: ext.textHint.withValues(alpha: 0.1),
|
||||
),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 0,
|
||||
),
|
||||
minimum: 0,
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.y',
|
||||
),
|
||||
series: [
|
||||
AreaSeries<_TrendPoint, String>(
|
||||
dataSource: points,
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
color: ext.accent,
|
||||
borderColor: ext.bgPrimary,
|
||||
borderWidth: 1.5,
|
||||
borderColor: ext.accent,
|
||||
borderWidth: 2.5,
|
||||
animationDuration: 0,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
ext.accent.withValues(alpha: 0.25),
|
||||
ext.accent.withValues(alpha: 0.02),
|
||||
],
|
||||
),
|
||||
markerSettings: MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 6,
|
||||
width: 6,
|
||||
color: ext.accent,
|
||||
borderColor: ext.bgPrimary,
|
||||
borderWidth: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -493,40 +499,42 @@ class _LearningProgressPageState extends ConsumerState<LearningProgressPage> {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
primaryYAxis: const NumericAxis(
|
||||
majorGridLines: MajorGridLines(width: 0),
|
||||
majorTickLines: MajorTickLines(size: 0),
|
||||
axisLine: AxisLine(width: 0),
|
||||
labelStyle: TextStyle(fontSize: 0),
|
||||
minimum: 0,
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
),
|
||||
series: [
|
||||
ColumnSeries<_CategoryItem, String>(
|
||||
dataSource: categories,
|
||||
xValueMapper: (d, _) => d.name,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
width: 0.5,
|
||||
animationDuration: 0,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppRadius.md),
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
primaryYAxis: const NumericAxis(
|
||||
majorGridLines: MajorGridLines(width: 0),
|
||||
majorTickLines: MajorTickLines(size: 0),
|
||||
axisLine: AxisLine(width: 0),
|
||||
labelStyle: TextStyle(fontSize: 0),
|
||||
minimum: 0,
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
),
|
||||
series: [
|
||||
ColumnSeries<_CategoryItem, String>(
|
||||
dataSource: categories,
|
||||
xValueMapper: (d, _) => d.name,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
pointColorMapper: (d, _) => d.color,
|
||||
width: 0.5,
|
||||
animationDuration: 0,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppRadius.md),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -22,6 +22,7 @@ import '../../../../../shared/widgets/adaptive/adaptive_back_button.dart';
|
||||
import '../../../../shared/widgets/feedback/offline_banner.dart';
|
||||
import '../../../../shared/widgets/feedback/app_toast.dart';
|
||||
import '../../../../shared/widgets/adaptive/responsive_layout.dart';
|
||||
import '../../../../l10n/translation_resolver.dart';
|
||||
import '../../../auth/providers/auth_provider.dart';
|
||||
import '../services/user_center_service.dart';
|
||||
import '../providers/account_insights_provider.dart';
|
||||
@@ -126,11 +127,12 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
|
||||
void _showAvatarPicker() {
|
||||
final ext = AppTheme.ext(context);
|
||||
final t = ref.read(translationsProvider);
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (ctx) => CupertinoActionSheet(
|
||||
title: Text(
|
||||
'修改头像',
|
||||
t.profile.changeAvatar,
|
||||
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
|
||||
),
|
||||
actions: [
|
||||
@@ -144,7 +146,7 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
children: [
|
||||
Icon(CupertinoIcons.link, size: 18, color: ext.accent),
|
||||
const SizedBox(width: 8),
|
||||
const Text('输入头像URL'),
|
||||
Text(t.profile.inputAvatarUrl),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -156,7 +158,7 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
children: [
|
||||
Icon(CupertinoIcons.lock, size: 18, color: ext.textHint),
|
||||
const SizedBox(width: 8),
|
||||
Text('从相册选择(暂未开放)', style: TextStyle(color: ext.textHint)),
|
||||
Text(t.profile.selectFromAlbum, style: TextStyle(color: ext.textHint)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -164,7 +166,7 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
isDestructiveAction: true,
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('取消'),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -173,10 +175,11 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
void _showAvatarUrlInput() {
|
||||
final controller = TextEditingController();
|
||||
final dialogExt = AppTheme.ext(context);
|
||||
final t = ref.read(translationsProvider);
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => CupertinoAlertDialog(
|
||||
title: const Text('🔗 输入头像URL'),
|
||||
title: Text(t.profile.avatarUnderReview.replaceAll('🔍 ', '🔗 ')),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Column(
|
||||
@@ -196,7 +199,7 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'URL长度不能超过2048个字符,仅支持 http/https 开头的图片链接',
|
||||
t.profile.avatarUrlHint,
|
||||
style: AppTypography.caption2.copyWith(
|
||||
color: dialogExt.textHint,
|
||||
),
|
||||
@@ -206,7 +209,7 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: const Text('取消'),
|
||||
child: Text(t.common.cancel),
|
||||
onPressed: () {
|
||||
controller.dispose();
|
||||
ctx.dismissDialog();
|
||||
@@ -214,27 +217,27 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
child: const Text('确认'),
|
||||
child: Text(t.common.confirm),
|
||||
onPressed: () async {
|
||||
final url = controller.text.trim();
|
||||
ctx.dismissDialog();
|
||||
controller.dispose();
|
||||
if (url.isEmpty) {
|
||||
AppToast.showWarning('请输入URL地址');
|
||||
AppToast.showWarning(t.profile.pleaseInputUrl);
|
||||
return;
|
||||
}
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
AppToast.showWarning('请输入以 http:// 或 https:// 开头的URL');
|
||||
AppToast.showWarning(t.profile.urlMustStartWithHttp);
|
||||
return;
|
||||
}
|
||||
if (url.length > 2048) {
|
||||
AppToast.showWarning('URL长度超过2048字符限制,请使用更短的链接');
|
||||
AppToast.showWarning(t.profile.urlTooLong);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Uri.parse(url);
|
||||
} catch (_) {
|
||||
AppToast.showWarning('URL格式不正确,请检查后重试');
|
||||
AppToast.showWarning(t.profile.invalidUrlFormat);
|
||||
return;
|
||||
}
|
||||
final ext = AppTheme.ext(context);
|
||||
@@ -246,13 +249,13 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
children: [
|
||||
CupertinoActivityIndicator(color: ext.accent, radius: 12),
|
||||
const SizedBox(width: 10),
|
||||
const Text('🔍 图像审核中'),
|
||||
Text(t.profile.avatarUnderReview),
|
||||
],
|
||||
),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'正在审核头像图片,请稍候...',
|
||||
t.profile.avatarReviewing,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
@@ -266,12 +269,12 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
await ref.read(authProvider.notifier).refreshUser();
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
_showAvatarResult(true, '头像修改成功');
|
||||
_showAvatarResult(true, t.profile.avatarChangeSuccess);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
_showAvatarResult(false, '头像修改失败: $e');
|
||||
_showAvatarResult(false, '${t.profile.avatarChangeFailed}: $e');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -282,6 +285,7 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
}
|
||||
|
||||
void _showAvatarResult(bool success, String msg) {
|
||||
final t = ref.read(translationsProvider);
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => CupertinoAlertDialog(
|
||||
@@ -298,13 +302,13 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
: CupertinoColors.systemRed,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(success ? '成功' : '失败'),
|
||||
Text(success ? t.profile.success : t.profile.failed),
|
||||
],
|
||||
),
|
||||
content: Text(msg),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: const Text('好的'),
|
||||
child: Text(t.profile.ok),
|
||||
onPressed: () => ctx.dismissDialog(),
|
||||
),
|
||||
],
|
||||
@@ -317,12 +321,13 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
final ext = AppTheme.ext(context);
|
||||
final authState = ref.watch(authProvider);
|
||||
final user = authState.user;
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
return CupertinoPageScaffold(
|
||||
backgroundColor: ext.bgPrimary,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(
|
||||
'个人中心',
|
||||
t.profile.title,
|
||||
style: AppTypography.title3.copyWith(color: ext.textPrimary),
|
||||
),
|
||||
backgroundColor: ext.bgPrimary.withValues(alpha: 0.85),
|
||||
@@ -354,7 +359,7 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
CupertinoActivityIndicator(color: ext.accent),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
'加载中...',
|
||||
t.profile.loading,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
@@ -443,6 +448,7 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
}
|
||||
|
||||
Widget _buildLoggedOutContent(BuildContext context, AppThemeExtension ext) {
|
||||
final t = ref.read(translationsProvider);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
@@ -458,12 +464,12 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
'请先登录',
|
||||
t.profile.tapToLogin,
|
||||
style: AppTypography.title3.copyWith(color: ext.textPrimary),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'登录后查看个人中心',
|
||||
t.profile.loginToViewProfile,
|
||||
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
@@ -472,7 +478,7 @@ class _UserCenterPageState extends ConsumerState<UserCenterPage> {
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
onPressed: () => context.appPush(AppRoutes.login),
|
||||
child: Text(
|
||||
'去登录',
|
||||
t.profile.goLogin,
|
||||
style: AppTypography.body.copyWith(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
@@ -15,6 +15,7 @@ import '../../../../../core/theme/app_theme.dart';
|
||||
import '../../../../../core/theme/app_typography.dart';
|
||||
import '../../../../../shared/widgets/feedback/app_toast.dart';
|
||||
import '../../../../../shared/widgets/containers/bottom_sheet.dart';
|
||||
import '../../../../auth/providers/auth_provider.dart';
|
||||
import '../../models/account_insight_model.dart';
|
||||
import '../../providers/account_insights_provider.dart';
|
||||
|
||||
@@ -53,6 +54,7 @@ class _AccountInsightsSheetContentState
|
||||
children: [
|
||||
_buildHandle(),
|
||||
_buildHeader(ext, insightsState),
|
||||
_buildTestAccountWarning(ext),
|
||||
Expanded(
|
||||
child: insightsState.isLoading
|
||||
? Center(child: CupertinoActivityIndicator(color: ext.accent))
|
||||
@@ -125,6 +127,45 @@ class _AccountInsightsSheetContentState
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTestAccountWarning(AppThemeExtension ext) {
|
||||
final username = ref.watch(authProvider).user?.username;
|
||||
if (username != '123456') return const SizedBox.shrink();
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.systemOrange.withValues(alpha: 0.12),
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
border: Border.all(
|
||||
color: CupertinoColors.systemOrange.withValues(alpha: 0.25),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.exclamationmark_triangle_fill,
|
||||
size: 18,
|
||||
color: CupertinoColors.systemOrange.darkColor,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'⚠️ 当前账号可能是闲言官方提供的测试账号,多人在使用,请勿修改密码,请勿使用当前账号创建笔记,如有需要请自行注册账户',
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: CupertinoColors.systemOrange.darkColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInsightList(
|
||||
AppThemeExtension ext,
|
||||
List<AccountInsight> insights,
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../../../../../../core/theme/app_radius.dart';
|
||||
import '../../../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../../../core/theme/app_theme.dart';
|
||||
import '../../../../../../core/theme/app_typography.dart';
|
||||
import '../../../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../../../shared/widgets/containers/glass_container.dart';
|
||||
|
||||
// ============================================================
|
||||
@@ -75,61 +76,63 @@ class WeeklyTrendChart extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 1,
|
||||
color: ext.textHint.withValues(alpha: 0.08),
|
||||
),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
minimum: 0,
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.y 次',
|
||||
),
|
||||
series: [
|
||||
AreaSeries<_TrendPoint, String>(
|
||||
dataSource: trendData,
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
color: ext.accent,
|
||||
borderColor: ext.accent,
|
||||
borderWidth: 2.5,
|
||||
animationDuration: 0,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
ext.accent.withValues(alpha: 0.2),
|
||||
ext.accent.withValues(alpha: 0.02),
|
||||
],
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
markerSettings: MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 7,
|
||||
width: 7,
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 1,
|
||||
color: ext.textHint.withValues(alpha: 0.08),
|
||||
),
|
||||
majorTickLines: const MajorTickLines(size: 0),
|
||||
axisLine: const AxisLine(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
minimum: 0,
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(
|
||||
enable: true,
|
||||
format: 'point.y 次',
|
||||
),
|
||||
series: [
|
||||
AreaSeries<_TrendPoint, String>(
|
||||
dataSource: trendData,
|
||||
xValueMapper: (d, _) => d.label,
|
||||
yValueMapper: (d, _) => d.value,
|
||||
color: ext.accent,
|
||||
borderColor: ext.bgCard,
|
||||
borderColor: ext.accent,
|
||||
borderWidth: 2.5,
|
||||
animationDuration: 0,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
ext.accent.withValues(alpha: 0.2),
|
||||
ext.accent.withValues(alpha: 0.02),
|
||||
],
|
||||
),
|
||||
markerSettings: MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 7,
|
||||
width: 7,
|
||||
color: ext.accent,
|
||||
borderColor: ext.bgCard,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 软件协议页(引导页第2页)
|
||||
// 创建时间: 2026-05-21
|
||||
// 更新时间: 2026-05-30
|
||||
// 更新时间: 2026-06-02
|
||||
// 作用: 隐私政策/用户协议/权限说明,勾选同意后继续
|
||||
// 上次更新: 协议勾选文字《》内容使用强调色+可点击跳转对应Tab
|
||||
// 上次更新: 协议内容/标题/更新日期支持多语言,章节解析兼容英文罗马数字编号
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -15,6 +15,7 @@ import '../../../../core/constants/character_expression.dart';
|
||||
import '../../../../l10n/translations.dart';
|
||||
import '../../../../core/router/app_nav_extension.dart';
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import '../../../../l10n/app_locale.dart';
|
||||
import '../../../../core/services/auth/permission_service.dart';
|
||||
import '../../../../core/services/device/haptic_service.dart';
|
||||
import '../../../../core/theme/app_radius.dart';
|
||||
@@ -25,6 +26,7 @@ import '../../../../shared/widgets/animation/appbar_character_sprite.dart';
|
||||
import '../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../../agreements/data/agreement_data.dart';
|
||||
import '../../../agreements/data/agreement_types.dart';
|
||||
import '../../../../shared/widgets/media/watermarked_copyright_image.dart';
|
||||
import '../../providers/onboarding_provider.dart';
|
||||
import '../onboarding_page.dart';
|
||||
import '../widgets/page_nav_header.dart';
|
||||
@@ -91,20 +93,28 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
|
||||
);
|
||||
}
|
||||
|
||||
static List<_ChapterInfo> _parseChapters(String content) {
|
||||
final regex = RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、.+');
|
||||
static List<_ChapterInfo> _parseChapters(String content, String languageId) {
|
||||
final zhRegex = RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、.+');
|
||||
final enRegex = RegExp(r'^(Zero|[IVXLCDM]+)\.\s+.+');
|
||||
final lines = content.split('\n');
|
||||
final chapters = <_ChapterInfo>[];
|
||||
for (final line in lines) {
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.isEmpty) continue;
|
||||
if (regex.hasMatch(trimmed)) {
|
||||
if (zhRegex.hasMatch(trimmed)) {
|
||||
final title = trimmed
|
||||
.replaceAll(RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、'), '')
|
||||
.trim();
|
||||
if (title.isNotEmpty) {
|
||||
chapters.add(_ChapterInfo(fullTitle: trimmed, shortTitle: title));
|
||||
}
|
||||
} else if (enRegex.hasMatch(trimmed)) {
|
||||
final title = trimmed
|
||||
.replaceFirst(RegExp(r'^(Zero|[IVXLCDM]+)\.\s+'), '')
|
||||
.trim();
|
||||
if (title.isNotEmpty) {
|
||||
chapters.add(_ChapterInfo(fullTitle: trimmed, shortTitle: title));
|
||||
}
|
||||
}
|
||||
}
|
||||
return chapters;
|
||||
@@ -116,6 +126,7 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
|
||||
final state = ref.watch(onboardingProvider);
|
||||
final notifier = ref.read(onboardingProvider.notifier);
|
||||
final ob = ref.watch(translationsProvider).onboarding;
|
||||
final languageId = _localeToLanguageId(ref.watch(appLocaleProvider));
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
@@ -149,7 +160,7 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
_buildTabBar(ext, state, notifier, ob),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Expanded(child: _buildContent(ext, state, ob)),
|
||||
Expanded(child: _buildContent(ext, state, ob, languageId)),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
_buildCheckboxes(ext, state, notifier, ob),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
@@ -268,15 +279,16 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
|
||||
AppThemeExtension ext,
|
||||
OnboardingState state,
|
||||
TOnboarding ob,
|
||||
String languageId,
|
||||
) {
|
||||
if (state.agreementTabIndex == 2) {
|
||||
return _buildPermissionList(ext, ob);
|
||||
}
|
||||
|
||||
final agreementType = _agreementTypeForIndex(state.agreementTabIndex);
|
||||
final content = AgreementData.getContent(agreementType);
|
||||
final updateDate = AgreementData.getUpdateDate(agreementType);
|
||||
final chapters = _parseChapters(content);
|
||||
final content = AgreementData.getContent(agreementType, languageId: languageId);
|
||||
final updateDate = AgreementData.getUpdateDate(agreementType, languageId: languageId);
|
||||
final chapters = _parseChapters(content, languageId);
|
||||
|
||||
if (_chapterKeys.isEmpty && chapters.isNotEmpty) {
|
||||
for (int i = 0; i < chapters.length; i++) {
|
||||
@@ -302,6 +314,7 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
|
||||
updateDate,
|
||||
chapters,
|
||||
ob,
|
||||
languageId,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -359,6 +372,7 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
|
||||
String updateDate,
|
||||
List<_ChapterInfo> chapters,
|
||||
TOnboarding ob,
|
||||
String languageId,
|
||||
) {
|
||||
final lines = content.split('\n');
|
||||
final chapterIndexMap = <String, int>{};
|
||||
@@ -366,13 +380,16 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
|
||||
chapterIndexMap[chapters[i].fullTitle] = i;
|
||||
}
|
||||
|
||||
final zhChapterRegex = RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、.+');
|
||||
final enChapterRegex = RegExp(r'^(Zero|[IVXLCDM]+)\.\s+.+');
|
||||
|
||||
final widgets = <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
Icon(agreementType.icon, size: 18, color: ext.accent),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
agreementType.title,
|
||||
agreementType.titleFor(languageId),
|
||||
style: AppTypography.headline.copyWith(color: ext.textPrimary),
|
||||
),
|
||||
],
|
||||
@@ -387,13 +404,14 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
];
|
||||
|
||||
final regex = RegExp(r'^[零一二三四五六七八九十百]+[点]*[零一二三四五六七八九十]*、.+');
|
||||
|
||||
for (final line in lines) {
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.isEmpty) continue;
|
||||
|
||||
if (regex.hasMatch(trimmed)) {
|
||||
final isZhChapter = zhChapterRegex.hasMatch(trimmed);
|
||||
final isEnChapter = enChapterRegex.hasMatch(trimmed);
|
||||
|
||||
if (isZhChapter || isEnChapter) {
|
||||
final chIdx = chapterIndexMap[trimmed];
|
||||
final key = chIdx != null && _chapterKeys.containsKey('ch_$chIdx')
|
||||
? _chapterKeys['ch_$chIdx']
|
||||
@@ -430,6 +448,16 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (trimmed.contains('软件著作权') ||
|
||||
trimmed.contains('Software Copyright')) {
|
||||
widgets.add(
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: AppSpacing.sm),
|
||||
child: WatermarkedCopyrightImage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (trimmed.startsWith('•') || trimmed.startsWith('-')) {
|
||||
widgets.add(
|
||||
Padding(
|
||||
@@ -948,6 +976,16 @@ class _AgreementPageState extends ConsumerState<AgreementPage> {
|
||||
};
|
||||
}
|
||||
|
||||
String _localeToLanguageId(Locale locale) {
|
||||
if (locale.languageCode == 'zh') {
|
||||
if (locale.countryCode == 'TW' || locale.scriptCode == 'Hant') {
|
||||
return 'zh_tw';
|
||||
}
|
||||
return 'zh';
|
||||
}
|
||||
return locale.languageCode;
|
||||
}
|
||||
|
||||
Widget _buildSkipButton(
|
||||
AppThemeExtension ext,
|
||||
OnboardingState state,
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../models/reading_report_models.dart';
|
||||
|
||||
class _TrendData {
|
||||
@@ -87,7 +88,8 @@ class TrendChart extends StatelessWidget {
|
||||
|
||||
return SizedBox(
|
||||
height: 220,
|
||||
child: SfCartesianChart(
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: NumericAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
@@ -123,6 +125,7 @@ class TrendChart extends StatelessWidget {
|
||||
),
|
||||
series: series,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
import 'package:xianyan/core/theme/app_theme.dart';
|
||||
import 'package:xianyan/shared/widgets/containers/deferred_builder.dart';
|
||||
import 'package:xianyan/core/theme/app_spacing.dart';
|
||||
import 'package:xianyan/core/theme/app_typography.dart';
|
||||
import 'package:xianyan/core/theme/app_radius.dart';
|
||||
@@ -93,7 +94,7 @@ Widget buildBookmarkTrend(
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
),
|
||||
height: 160,
|
||||
child: SfCartesianChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: const NumericAxis(isVisible: false),
|
||||
primaryYAxis: const NumericAxis(isVisible: false),
|
||||
@@ -117,7 +118,7 @@ Widget buildBookmarkTrend(
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
Widget buildMonthDistribution(
|
||||
@@ -151,7 +152,7 @@ Widget buildMonthDistribution(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SfCircularChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCircularChart(
|
||||
series: <CircularSeries<_SeasonData, String>>[
|
||||
DoughnutSeries<_SeasonData, String>(
|
||||
dataSource: [
|
||||
@@ -185,7 +186,7 @@ Widget buildMonthDistribution(
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
@@ -21,6 +21,7 @@ import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/theme/app_typography.dart';
|
||||
import '../../../../shared/widgets/animation/animated_widgets.dart';
|
||||
import '../../../../shared/widgets/display/app_icon.dart';
|
||||
import '../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../../../shared/widgets/feedback/offline_banner.dart';
|
||||
import '../../../../shared/widgets/adaptive/responsive_layout.dart';
|
||||
@@ -1086,7 +1087,7 @@ class _SigninTrendChart extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: SfCartesianChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
@@ -1124,7 +1125,7 @@ class _SigninTrendChart extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1276,7 +1277,7 @@ class _CoinBarChart extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: SfCartesianChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
@@ -1309,7 +1310,7 @@ class _CoinBarChart extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1388,7 +1389,7 @@ class _ContentPieChart extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: SfCircularChart(
|
||||
child: DeferredBuilder(builder: (context) => SfCircularChart(
|
||||
series: <CircularSeries<_NotePie, String>>[
|
||||
DoughnutSeries<_NotePie, String>(
|
||||
dataSource: pieData,
|
||||
@@ -1408,7 +1409,7 @@ class _ContentPieChart extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,6 +16,7 @@ import '../../../../../core/theme/app_radius.dart';
|
||||
import '../../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../../core/theme/app_theme.dart';
|
||||
import '../../../../../core/theme/app_typography.dart';
|
||||
import '../../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../providers/user_stats_provider.dart';
|
||||
import 'stats_shared.dart';
|
||||
@@ -151,37 +152,40 @@ class CoinTrendChart extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
interval: 5,
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
interval: 5,
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 1,
|
||||
color: ext.textHint.withValues(alpha: 0.08),
|
||||
),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CartesianSeries<_CoinTrend, String>>[
|
||||
LineSeries<_CoinTrend, String>(
|
||||
dataSource: trendData,
|
||||
xValueMapper: (_CoinTrend d, _) => d.label,
|
||||
yValueMapper: (_CoinTrend d, _) => d.value,
|
||||
color: const Color(0xFFF39C12),
|
||||
width: 2.5,
|
||||
animationDuration: 0,
|
||||
name: '积分',
|
||||
),
|
||||
],
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 1,
|
||||
color: ext.textHint.withValues(alpha: 0.08),
|
||||
),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CartesianSeries<_CoinTrend, String>>[
|
||||
LineSeries<_CoinTrend, String>(
|
||||
dataSource: trendData,
|
||||
xValueMapper: (_CoinTrend d, _) => d.label,
|
||||
yValueMapper: (_CoinTrend d, _) => d.value,
|
||||
color: const Color(0xFFF39C12),
|
||||
width: 2.5,
|
||||
name: '积分',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -257,39 +261,41 @@ class CoinSourceBarChart extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
maximum: maxVal,
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CartesianSeries<_SourceBar, String>>[
|
||||
ColumnSeries<_SourceBar, String>(
|
||||
dataSource: barData,
|
||||
xValueMapper: (_SourceBar d, _) => d.label,
|
||||
yValueMapper: (_SourceBar d, _) => d.value,
|
||||
pointColorMapper: (_SourceBar d, _) => d.color,
|
||||
width: 0.5,
|
||||
animationDuration: 0,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(6),
|
||||
topRight: Radius.circular(6),
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
name: '数量',
|
||||
),
|
||||
],
|
||||
primaryYAxis: NumericAxis(
|
||||
maximum: maxVal,
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CartesianSeries<_SourceBar, String>>[
|
||||
ColumnSeries<_SourceBar, String>(
|
||||
dataSource: barData,
|
||||
xValueMapper: (_SourceBar d, _) => d.label,
|
||||
yValueMapper: (_SourceBar d, _) => d.value,
|
||||
pointColorMapper: (_SourceBar d, _) => d.color,
|
||||
width: 0.5,
|
||||
animationDuration: 0,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(6),
|
||||
topRight: Radius.circular(6),
|
||||
),
|
||||
name: '数量',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -353,30 +359,32 @@ class CoinRingChart extends StatelessWidget {
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 180,
|
||||
child: SfCircularChart(
|
||||
series: <CircularSeries<_RingData, String>>[
|
||||
DoughnutSeries<_RingData, String>(
|
||||
dataSource: ringData,
|
||||
xValueMapper: (_RingData d, _) => d.label,
|
||||
yValueMapper: (_RingData d, _) => d.value,
|
||||
pointColorMapper: (_RingData d, _) => d.color,
|
||||
innerRadius: '45%',
|
||||
animationDuration: 0,
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true,
|
||||
textStyle: AppTypography.caption2.copyWith(
|
||||
color: hasData
|
||||
? CupertinoColors.white
|
||||
: ext.textHint,
|
||||
fontWeight: FontWeight.w600,
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCircularChart(
|
||||
series: <CircularSeries<_RingData, String>>[
|
||||
DoughnutSeries<_RingData, String>(
|
||||
dataSource: ringData,
|
||||
xValueMapper: (_RingData d, _) => d.label,
|
||||
yValueMapper: (_RingData d, _) => d.value,
|
||||
pointColorMapper: (_RingData d, _) => d.color,
|
||||
innerRadius: '45%',
|
||||
animationDuration: 0,
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true,
|
||||
textStyle: AppTypography.caption2.copyWith(
|
||||
color: hasData
|
||||
? CupertinoColors.white
|
||||
: ext.textHint,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
dataLabelMapper: (_RingData d, _) {
|
||||
if (!hasData) return d.label;
|
||||
return d.label;
|
||||
},
|
||||
),
|
||||
dataLabelMapper: (_RingData d, _) {
|
||||
if (!hasData) return d.label;
|
||||
return d.label;
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,6 +16,7 @@ import '../../../../../core/theme/app_radius.dart';
|
||||
import '../../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../../core/theme/app_theme.dart';
|
||||
import '../../../../../core/theme/app_typography.dart';
|
||||
import '../../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../providers/user_stats_provider.dart';
|
||||
import 'stats_shared.dart';
|
||||
@@ -144,27 +145,29 @@ class FavoriteCategoryPie extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SfCircularChart(
|
||||
series: <CircularSeries<_PieItem, String>>[
|
||||
DoughnutSeries<_PieItem, String>(
|
||||
dataSource: pieData,
|
||||
xValueMapper: (_PieItem d, _) => d.label,
|
||||
yValueMapper: (_PieItem d, _) => d.value,
|
||||
pointColorMapper: (_PieItem d, _) => d.color,
|
||||
innerRadius: '38%',
|
||||
animationDuration: 0,
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true,
|
||||
textStyle: AppTypography.caption2.copyWith(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCircularChart(
|
||||
series: <CircularSeries<_PieItem, String>>[
|
||||
DoughnutSeries<_PieItem, String>(
|
||||
dataSource: pieData,
|
||||
xValueMapper: (_PieItem d, _) => d.label,
|
||||
yValueMapper: (_PieItem d, _) => d.value,
|
||||
pointColorMapper: (_PieItem d, _) => d.color,
|
||||
innerRadius: '38%',
|
||||
animationDuration: 0,
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true,
|
||||
textStyle: AppTypography.caption2.copyWith(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
dataLabelMapper: (_PieItem d, _) => total > 0
|
||||
? '${(d.value / total * 100).toStringAsFixed(0)}%'
|
||||
: '',
|
||||
),
|
||||
dataLabelMapper: (_PieItem d, _) => total > 0
|
||||
? '${(d.value / total * 100).toStringAsFixed(0)}%'
|
||||
: '',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
@@ -277,44 +280,46 @@ class FavoriteTrendChart extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 1,
|
||||
color: ext.textHint.withValues(alpha: 0.08),
|
||||
),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CartesianSeries<_LinePoint, String>>[
|
||||
LineSeries<_LinePoint, String>(
|
||||
dataSource: trendData,
|
||||
xValueMapper: (_LinePoint d, _) => d.label,
|
||||
yValueMapper: (_LinePoint d, _) => d.value,
|
||||
color: const Color(0xFFFF6B6B),
|
||||
width: 2.5,
|
||||
animationDuration: 0,
|
||||
markerSettings: const MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 7,
|
||||
width: 7,
|
||||
color: Color(0xFFFF6B6B),
|
||||
borderColor: CupertinoColors.white,
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
name: '收藏',
|
||||
),
|
||||
],
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 1,
|
||||
color: ext.textHint.withValues(alpha: 0.08),
|
||||
),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CartesianSeries<_LinePoint, String>>[
|
||||
LineSeries<_LinePoint, String>(
|
||||
dataSource: trendData,
|
||||
xValueMapper: (_LinePoint d, _) => d.label,
|
||||
yValueMapper: (_LinePoint d, _) => d.value,
|
||||
color: const Color(0xFFFF6B6B),
|
||||
width: 2.5,
|
||||
animationDuration: 0,
|
||||
markerSettings: const MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 7,
|
||||
width: 7,
|
||||
color: Color(0xFFFF6B6B),
|
||||
borderColor: CupertinoColors.white,
|
||||
),
|
||||
name: '收藏',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -370,44 +375,46 @@ class FavoriteGroupBarChart extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
maximum: maxVal,
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CartesianSeries<_BarItem, String>>[
|
||||
ColumnSeries<_BarItem, String>(
|
||||
dataSource: items
|
||||
.map(
|
||||
(e) =>
|
||||
_BarItem(e.group, e.count.toDouble(), ext.accent),
|
||||
)
|
||||
.toList(),
|
||||
xValueMapper: (_BarItem d, _) => d.label,
|
||||
yValueMapper: (_BarItem d, _) => d.value,
|
||||
color: ext.accent,
|
||||
width: 0.5,
|
||||
animationDuration: 0,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(6),
|
||||
topRight: Radius.circular(6),
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
name: '数量',
|
||||
),
|
||||
],
|
||||
primaryYAxis: NumericAxis(
|
||||
maximum: maxVal,
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CartesianSeries<_BarItem, String>>[
|
||||
ColumnSeries<_BarItem, String>(
|
||||
dataSource: items
|
||||
.map(
|
||||
(e) =>
|
||||
_BarItem(e.group, e.count.toDouble(), ext.accent),
|
||||
)
|
||||
.toList(),
|
||||
xValueMapper: (_BarItem d, _) => d.label,
|
||||
yValueMapper: (_BarItem d, _) => d.value,
|
||||
color: ext.accent,
|
||||
width: 0.5,
|
||||
animationDuration: 0,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(6),
|
||||
topRight: Radius.circular(6),
|
||||
),
|
||||
name: '数量',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../../../../../core/theme/app_radius.dart';
|
||||
import '../../../../../core/theme/app_spacing.dart';
|
||||
import '../../../../../core/theme/app_theme.dart';
|
||||
import '../../../../../core/theme/app_typography.dart';
|
||||
import '../../../../../shared/widgets/containers/deferred_builder.dart';
|
||||
import '../../../../../shared/widgets/containers/glass_container.dart';
|
||||
import '../../providers/user_stats_provider.dart';
|
||||
import 'stats_shared.dart';
|
||||
@@ -190,74 +191,78 @@ class LearningTrendChart extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCartesianChart(
|
||||
plotAreaBorderWidth: 0,
|
||||
primaryXAxis: CategoryAxis(
|
||||
majorGridLines: const MajorGridLines(width: 0),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 1,
|
||||
color: ext.textHint.withValues(alpha: 0.08),
|
||||
),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CartesianSeries<_TrendPoint, String>>[
|
||||
LineSeries<_TrendPoint, String>(
|
||||
dataSource: viewData,
|
||||
xValueMapper: (_TrendPoint d, _) => d.label,
|
||||
yValueMapper: (_TrendPoint d, _) => d.value,
|
||||
color: const Color(0xFF6C63FF),
|
||||
width: 2.5,
|
||||
animationDuration: 0,
|
||||
markerSettings: const MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 6,
|
||||
width: 6,
|
||||
color: Color(0xFF6C63FF),
|
||||
borderColor: CupertinoColors.white,
|
||||
),
|
||||
name: '浏览',
|
||||
),
|
||||
LineSeries<_TrendPoint, String>(
|
||||
dataSource: interactionData,
|
||||
xValueMapper: (_TrendPoint d, _) => d.label,
|
||||
yValueMapper: (_TrendPoint d, _) => d.value,
|
||||
color: const Color(0xFF4ECDC4),
|
||||
width: 2.5,
|
||||
animationDuration: 0,
|
||||
markerSettings: const MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 6,
|
||||
width: 6,
|
||||
color: Color(0xFF4ECDC4),
|
||||
borderColor: CupertinoColors.white,
|
||||
),
|
||||
name: '互动',
|
||||
),
|
||||
LineSeries<_TrendPoint, String>(
|
||||
dataSource: favData,
|
||||
xValueMapper: (_TrendPoint d, _) => d.label,
|
||||
yValueMapper: (_TrendPoint d, _) => d.value,
|
||||
color: const Color(0xFFFF6B6B),
|
||||
width: 2.5,
|
||||
animationDuration: 0,
|
||||
markerSettings: const MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 6,
|
||||
width: 6,
|
||||
color: Color(0xFFFF6B6B),
|
||||
borderColor: CupertinoColors.white,
|
||||
),
|
||||
name: '收藏',
|
||||
),
|
||||
],
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
majorGridLines: MajorGridLines(
|
||||
width: 1,
|
||||
color: ext.textHint.withValues(alpha: 0.08),
|
||||
),
|
||||
labelStyle: AppTypography.caption2.copyWith(
|
||||
color: ext.textHint,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
tooltipBehavior: TooltipBehavior(enable: true),
|
||||
series: <CartesianSeries<_TrendPoint, String>>[
|
||||
LineSeries<_TrendPoint, String>(
|
||||
dataSource: viewData,
|
||||
xValueMapper: (_TrendPoint d, _) => d.label,
|
||||
yValueMapper: (_TrendPoint d, _) => d.value,
|
||||
color: const Color(0xFF6C63FF),
|
||||
width: 2.5,
|
||||
animationDuration: 0,
|
||||
markerSettings: const MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 6,
|
||||
width: 6,
|
||||
color: Color(0xFF6C63FF),
|
||||
borderColor: CupertinoColors.white,
|
||||
),
|
||||
name: '浏览',
|
||||
),
|
||||
LineSeries<_TrendPoint, String>(
|
||||
dataSource: interactionData,
|
||||
xValueMapper: (_TrendPoint d, _) => d.label,
|
||||
yValueMapper: (_TrendPoint d, _) => d.value,
|
||||
color: const Color(0xFF4ECDC4),
|
||||
width: 2.5,
|
||||
markerSettings: const MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 6,
|
||||
width: 6,
|
||||
color: Color(0xFF4ECDC4),
|
||||
borderColor: CupertinoColors.white,
|
||||
),
|
||||
name: '互动',
|
||||
),
|
||||
LineSeries<_TrendPoint, String>(
|
||||
dataSource: favData,
|
||||
xValueMapper: (_TrendPoint d, _) => d.label,
|
||||
yValueMapper: (_TrendPoint d, _) => d.value,
|
||||
color: const Color(0xFFFF6B6B),
|
||||
width: 2.5,
|
||||
markerSettings: const MarkerSettings(
|
||||
isVisible: true,
|
||||
height: 6,
|
||||
width: 6,
|
||||
color: Color(0xFFFF6B6B),
|
||||
borderColor: CupertinoColors.white,
|
||||
),
|
||||
name: '收藏',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -335,27 +340,29 @@ class CategoryPieChart extends StatelessWidget {
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 180,
|
||||
child: SfCircularChart(
|
||||
series: <CircularSeries<_CatData, String>>[
|
||||
DoughnutSeries<_CatData, String>(
|
||||
dataSource: pieData,
|
||||
xValueMapper: (_CatData d, _) => d.label,
|
||||
yValueMapper: (_CatData d, _) => d.value,
|
||||
pointColorMapper: (_CatData d, _) => d.color,
|
||||
innerRadius: '38%',
|
||||
animationDuration: 0,
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true,
|
||||
textStyle: AppTypography.caption2.copyWith(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
child: DeferredBuilder(
|
||||
builder: (context) => SfCircularChart(
|
||||
series: <CircularSeries<_CatData, String>>[
|
||||
DoughnutSeries<_CatData, String>(
|
||||
dataSource: pieData,
|
||||
xValueMapper: (_CatData d, _) => d.label,
|
||||
yValueMapper: (_CatData d, _) => d.value,
|
||||
pointColorMapper: (_CatData d, _) => d.color,
|
||||
innerRadius: '38%',
|
||||
animationDuration: 0,
|
||||
dataLabelSettings: DataLabelSettings(
|
||||
isVisible: true,
|
||||
textStyle: AppTypography.caption2.copyWith(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
dataLabelMapper: (_CatData d, _) => total > 0
|
||||
? '${(d.value / total * 100).toStringAsFixed(0)}%'
|
||||
: '',
|
||||
),
|
||||
dataLabelMapper: (_CatData d, _) => total > 0
|
||||
? '${(d.value / total * 100).toStringAsFixed(0)}%'
|
||||
: '',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -483,81 +483,122 @@ class _PriorityDot extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PlatformCompatibilityCard extends StatelessWidget {
|
||||
class _PlatformCompatibilityCard extends StatefulWidget {
|
||||
const _PlatformCompatibilityCard({required this.ext});
|
||||
final AppThemeExtension ext;
|
||||
|
||||
@override
|
||||
State<_PlatformCompatibilityCard> createState() =>
|
||||
_PlatformCompatibilityCardState();
|
||||
}
|
||||
|
||||
class _PlatformCompatibilityCardState
|
||||
extends State<_PlatformCompatibilityCard> {
|
||||
bool _isExpanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ext = widget.ext;
|
||||
return GlassContainer(
|
||||
depth: GlassDepth.elevated,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.info_circle_fill,
|
||||
size: 18,
|
||||
color: ext.accent,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
'平台兼容说明',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_PlatformRow(
|
||||
ext: ext,
|
||||
icon: '🤖',
|
||||
name: 'Android',
|
||||
desc: '功能完整,支持交互式按钮和快捷固定',
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_PlatformRow(
|
||||
ext: ext,
|
||||
icon: '🍎',
|
||||
name: 'iOS',
|
||||
desc: 'WidgetKit + SwiftUI,交互需 iOS 17+',
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_PlatformRow(
|
||||
ext: ext,
|
||||
icon: '🔴',
|
||||
name: '鸿蒙',
|
||||
desc: 'FormExtension + ArkUI,无快捷固定,无实时刷新',
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.accent.withValues(alpha: 0.06),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _isExpanded = !_isExpanded),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.lightbulb_fill,
|
||||
size: 14,
|
||||
CupertinoIcons.info_circle_fill,
|
||||
size: 18,
|
||||
color: ext.accent,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'点击「同步主题」可将当前深色/浅色模式推送到所有已安装小部件',
|
||||
style: AppTypography.footnote.copyWith(
|
||||
color: ext.textSecondary,
|
||||
'平台兼容说明',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedRotation(
|
||||
turns: _isExpanded ? 0.0 : -0.25,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
child: Icon(
|
||||
CupertinoIcons.chevron_down,
|
||||
size: 16,
|
||||
color: ext.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: Padding(
|
||||
padding: const EdgeInsets.only(top: AppSpacing.sm),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PlatformRow(
|
||||
ext: ext,
|
||||
icon: '🤖',
|
||||
name: 'Android',
|
||||
desc: '功能不完整,原生侧存在通信问题',
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_PlatformRow(
|
||||
ext: ext,
|
||||
icon: '🍎',
|
||||
name: 'iOS',
|
||||
desc: 'WidgetKit + SwiftUI,交互需 iOS 17+',
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_PlatformRow(
|
||||
ext: ext,
|
||||
icon: '🔴',
|
||||
name: '鸿蒙',
|
||||
desc: 'FormExtension + ArkUI,能力受限,系统限制刷新频率',
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.accent.withValues(alpha: 0.06),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.lightbulb_fill,
|
||||
size: 14,
|
||||
color: ext.accent,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'点击「同步主题」可将当前深色/浅色模式推送到所有已安装小部件',
|
||||
style: AppTypography.footnote.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
crossFadeState: _isExpanded
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
sizeCurve: Curves.easeInOut,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -530,6 +530,24 @@ const ar = T(
|
||||
appStoreNotFound: 'لم يتم العثور على متجر التطبيقات',
|
||||
experimentalFeature: 'ميزات تجريبية',
|
||||
underReview: 'قيد المراجعة',
|
||||
changeAvatar: 'تغيير الصورة الرمزية',
|
||||
inputAvatarUrl: 'أدخل رابط الصورة الرمزية',
|
||||
selectFromAlbum: 'اختر من الألبوم (قريباً)',
|
||||
avatarUrlHint: 'يجب ألا يتجاوز الرابط 2048 حرفاً، روابط http/https فقط',
|
||||
pleaseInputUrl: 'الرجاء إدخال رابط',
|
||||
urlMustStartWithHttp: 'يجب أن يبدأ الرابط بـ http:// أو https://',
|
||||
urlTooLong: 'الرابط يتجاوز حد 2048 حرفاً',
|
||||
invalidUrlFormat: 'صيغة الرابط غير صحيحة',
|
||||
avatarUnderReview: '🔍 الصورة قيد المراجعة',
|
||||
avatarReviewing: 'جاري مراجعة الصورة الرمزية، يرجى الانتظار...',
|
||||
avatarChangeSuccess: 'تم تغيير الصورة الرمزية بنجاح',
|
||||
avatarChangeFailed: 'فشل تغيير الصورة الرمزية',
|
||||
success: 'نجاح',
|
||||
failed: 'فشل',
|
||||
ok: 'حسناً',
|
||||
loading: 'جاري التحميل...',
|
||||
loginToViewProfile: 'سجل الدخول لعرض ملفك الشخصي',
|
||||
goLogin: 'تسجيل الدخول',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: 'اللغة',
|
||||
@@ -945,6 +963,7 @@ const ar = T(
|
||||
roleDesign: 'تطوير وتصميم',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'الخلفية',
|
||||
roleNative: 'المكدس الأصلي',
|
||||
roleSupport: 'دعم i18n',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'دائماً تقريباً',
|
||||
@@ -1024,6 +1043,13 @@ const ar = T(
|
||||
alreadyLatestDesc: 'لديك بالفعل أحدث إصدار',
|
||||
okButton: 'حسناً',
|
||||
comingSoon: 'قريباً',
|
||||
distributionChannel: 'قناة التوزيع',
|
||||
distAndroid: 'تم التنزيل من الموقع الرسمي لـ Xianyan',
|
||||
distIOS: 'التوزيع عبر App Store',
|
||||
distMacOS: 'التوزيع عبر App Store',
|
||||
distHarmony: 'التوزيع عبر AppGallery',
|
||||
distWeb: 'يتطلب تقديم طلب',
|
||||
distWindows: 'تم التنزيل من الموقع الرسمي لـ Xianyan',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'مرحباً بعودتك',
|
||||
|
||||
@@ -528,6 +528,24 @@ const bn = T(
|
||||
appStoreNotFound: 'অ্যাপ স্টোর পাওয়া যায়নি',
|
||||
experimentalFeature: 'পরীক্ষামূলক বৈশিষ্ট্য',
|
||||
underReview: 'পর্যালোচনাধীন',
|
||||
changeAvatar: 'অবতার পরিবর্তন করুন',
|
||||
inputAvatarUrl: 'অবতার URL লিখুন',
|
||||
selectFromAlbum: 'অ্যালবাম থেকে নির্বাচন (শীঘ্রই)',
|
||||
avatarUrlHint: 'URL ২০৪৮ অক্ষরের বেশি হতে পারবে না, শুধুমাত্র http/https লিংক',
|
||||
pleaseInputUrl: 'অনুগ্রহ করে URL লিখুন',
|
||||
urlMustStartWithHttp: 'URL http:// বা https:// দিয়ে শুরু হতে হবে',
|
||||
urlTooLong: 'URL ২০৪৮ অক্ষরের সীমা ছাড়িয়ে গেছে',
|
||||
invalidUrlFormat: 'অবৈধ URL ফরম্যাট',
|
||||
avatarUnderReview: '🔍 ছবি পর্যালোচনাধীন',
|
||||
avatarReviewing: 'অবতার ছবি পর্যালোচনা হচ্ছে...',
|
||||
avatarChangeSuccess: 'অবতার সফলভাবে পরিবর্তিত হয়েছে',
|
||||
avatarChangeFailed: 'অবতার পরিবর্তন ব্যর্থ হয়েছে',
|
||||
success: 'সফল',
|
||||
failed: 'ব্যর্থ',
|
||||
ok: 'ঠিক আছে',
|
||||
loading: 'লোড হচ্ছে...',
|
||||
loginToViewProfile: 'প্রোফাইল দেখতে লগইন করুন',
|
||||
goLogin: 'লগইন',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: 'ভাষা',
|
||||
@@ -949,6 +967,7 @@ const bn = T(
|
||||
roleDesign: 'ডেভ ও ডিজাইন',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'ব্যাকএন্ড',
|
||||
roleNative: 'নেটিভ স্ট্যাক',
|
||||
roleSupport: 'i18n সাপোর্ট',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'সবসময় প্রায়',
|
||||
@@ -1029,6 +1048,13 @@ const bn = T(
|
||||
alreadyLatestDesc: 'আপনার কাছে ইতিমধ্যে সর্বশেষ সংস্করণ আছে',
|
||||
okButton: 'ঠিক আছে',
|
||||
comingSoon: 'শীঘ্রই আসছে',
|
||||
distributionChannel: 'বিতরণ চ্যানেল',
|
||||
distAndroid: 'Xianyan অফিসিয়াল ওয়েবসাইট থেকে ডাউনলোড',
|
||||
distIOS: 'App Store এর মাধ্যমে বিতরণ',
|
||||
distMacOS: 'App Store এর মাধ্যমে বিতরণ',
|
||||
distHarmony: 'AppGallery এর মাধ্যমে বিতরণ',
|
||||
distWeb: 'আবেদন প্রয়োজন',
|
||||
distWindows: 'Xianyan অফিসিয়াল ওয়েবসাইট থেকে ডাউনলোড',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'ফিরে আসার জন্য স্বাগতম',
|
||||
|
||||
@@ -528,6 +528,24 @@ const de = T(
|
||||
appStoreNotFound: 'App-Store nicht gefunden',
|
||||
experimentalFeature: 'Experimentelle Funktionen',
|
||||
underReview: 'Überprüfung läuft',
|
||||
changeAvatar: 'Avatar ändern',
|
||||
inputAvatarUrl: 'Avatar-URL eingeben',
|
||||
selectFromAlbum: 'Aus Album wählen (Demnächst)',
|
||||
avatarUrlHint: 'URL darf 2048 Zeichen nicht überschreiten, nur http/https-Bildlinks',
|
||||
pleaseInputUrl: 'Bitte URL eingeben',
|
||||
urlMustStartWithHttp: 'URL muss mit http:// oder https:// beginnen',
|
||||
urlTooLong: 'URL überschreitet 2048-Zeichen-Limit',
|
||||
invalidUrlFormat: 'Ungültiges URL-Format',
|
||||
avatarUnderReview: '🔍 Bild wird überprüft',
|
||||
avatarReviewing: 'Avatar-Bild wird überprüft, bitte warten...',
|
||||
avatarChangeSuccess: 'Avatar erfolgreich geändert',
|
||||
avatarChangeFailed: 'Avatar-Änderung fehlgeschlagen',
|
||||
success: 'Erfolg',
|
||||
failed: 'Fehlgeschlagen',
|
||||
ok: 'OK',
|
||||
loading: 'Laden...',
|
||||
loginToViewProfile: 'Anmelden um Profil zu sehen',
|
||||
goLogin: 'Anmelden',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: 'Sprache',
|
||||
@@ -960,6 +978,7 @@ const de = T(
|
||||
roleDesign: 'Entwickler',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'Backend',
|
||||
roleNative: 'Nativer Stack',
|
||||
roleSupport: 'i18n-Support',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'Immer fast da',
|
||||
@@ -1044,6 +1063,13 @@ const de = T(
|
||||
alreadyLatestDesc: 'Sie verwenden bereits die neueste Version',
|
||||
okButton: 'OK',
|
||||
comingSoon: 'Demnächst',
|
||||
distributionChannel: 'Vertriebskanal',
|
||||
distAndroid: 'Von der Xianyan-Website heruntergeladen',
|
||||
distIOS: 'Über den App Store vertrieben',
|
||||
distMacOS: 'Über den App Store vertrieben',
|
||||
distHarmony: 'Über AppGallery vertrieben',
|
||||
distWeb: 'Antrag erforderlich',
|
||||
distWindows: 'Von der Xianyan-Website heruntergeladen',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'Willkommen zurück',
|
||||
|
||||
@@ -537,6 +537,24 @@ const en = T(
|
||||
appStoreNotFound: 'App store not found',
|
||||
experimentalFeature: 'Experimental Features',
|
||||
underReview: 'Under Review',
|
||||
changeAvatar: 'Change Avatar',
|
||||
inputAvatarUrl: 'Enter Avatar URL',
|
||||
selectFromAlbum: 'Choose from Album (Coming Soon)',
|
||||
avatarUrlHint: 'URL must not exceed 2048 characters, only http/https image links are supported',
|
||||
pleaseInputUrl: 'Please enter a URL',
|
||||
urlMustStartWithHttp: 'URL must start with http:// or https://',
|
||||
urlTooLong: 'URL exceeds 2048 character limit, please use a shorter link',
|
||||
invalidUrlFormat: 'Invalid URL format, please check and try again',
|
||||
avatarUnderReview: '🔍 Image Under Review',
|
||||
avatarReviewing: 'Reviewing avatar image, please wait...',
|
||||
avatarChangeSuccess: 'Avatar changed successfully',
|
||||
avatarChangeFailed: 'Avatar change failed',
|
||||
success: 'Success',
|
||||
failed: 'Failed',
|
||||
ok: 'OK',
|
||||
loading: 'Loading...',
|
||||
loginToViewProfile: 'Login to view your profile',
|
||||
goLogin: 'Login',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: 'Language',
|
||||
@@ -957,6 +975,7 @@ const en = T(
|
||||
roleDesign: 'Developer',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'Backend',
|
||||
roleNative: 'Native Stack',
|
||||
roleSupport: 'i18n Support',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'Always almost there',
|
||||
@@ -1038,6 +1057,13 @@ const en = T(
|
||||
alreadyLatestDesc: 'You are already on the latest version',
|
||||
okButton: 'OK',
|
||||
comingSoon: 'Coming Soon',
|
||||
distributionChannel: 'Distribution Channel',
|
||||
distAndroid: 'Downloaded from Xianyan Official Website',
|
||||
distIOS: 'Distributed via App Store',
|
||||
distMacOS: 'Distributed via App Store',
|
||||
distHarmony: 'Distributed via AppGallery',
|
||||
distWeb: 'Application Required',
|
||||
distWindows: 'Downloaded from Xianyan Official Website',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'Welcome Back',
|
||||
|
||||
@@ -538,6 +538,24 @@ const es = T(
|
||||
appStoreNotFound: 'Tienda de apps no encontrada',
|
||||
experimentalFeature: 'Funciones experimentales',
|
||||
underReview: 'En revisión',
|
||||
changeAvatar: 'Cambiar avatar',
|
||||
inputAvatarUrl: 'Ingresar URL del avatar',
|
||||
selectFromAlbum: 'Elegir del álbum (Próximamente)',
|
||||
avatarUrlHint: 'La URL no debe superar 2048 caracteres, solo enlaces http/https',
|
||||
pleaseInputUrl: 'Por favor ingrese una URL',
|
||||
urlMustStartWithHttp: 'La URL debe comenzar con http:// o https://',
|
||||
urlTooLong: 'La URL supera el límite de 2048 caracteres',
|
||||
invalidUrlFormat: 'Formato de URL inválido',
|
||||
avatarUnderReview: '🔍 Imagen en revisión',
|
||||
avatarReviewing: 'Revisando imagen del avatar, espere...',
|
||||
avatarChangeSuccess: 'Avatar cambiado exitosamente',
|
||||
avatarChangeFailed: 'Error al cambiar el avatar',
|
||||
success: 'Éxito',
|
||||
failed: 'Fallido',
|
||||
ok: 'OK',
|
||||
loading: 'Cargando...',
|
||||
loginToViewProfile: 'Inicia sesión para ver tu perfil',
|
||||
goLogin: 'Iniciar sesión',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: 'Idioma',
|
||||
@@ -969,6 +987,7 @@ const es = T(
|
||||
roleDesign: 'Dev y Diseño',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'Backend',
|
||||
roleNative: 'Stack Nativo',
|
||||
roleSupport: 'Soporte i18n',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'Siempre a punto',
|
||||
@@ -1053,6 +1072,13 @@ const es = T(
|
||||
alreadyLatestDesc: 'Ya tienes la última versión',
|
||||
okButton: 'OK',
|
||||
comingSoon: 'Próximamente',
|
||||
distributionChannel: 'Canal de distribución',
|
||||
distAndroid: 'Descargado desde el sitio web oficial de Xianyan',
|
||||
distIOS: 'Distribuido a través de App Store',
|
||||
distMacOS: 'Distribuido a través de App Store',
|
||||
distHarmony: 'Distribuido a través de AppGallery',
|
||||
distWeb: 'Solicitud requerida',
|
||||
distWindows: 'Descargado desde el sitio web oficial de Xianyan',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'Bienvenido de nuevo',
|
||||
|
||||
@@ -530,6 +530,24 @@ const fr = T(
|
||||
appStoreNotFound: 'Magasin d\'apps introuvable',
|
||||
experimentalFeature: 'Fonctionnalités expérimentales',
|
||||
underReview: 'En cours de révision',
|
||||
changeAvatar: 'Changer l\'avatar',
|
||||
inputAvatarUrl: 'Entrer l\'URL de l\'avatar',
|
||||
selectFromAlbum: 'Choisir dans l\'album (Bientôt)',
|
||||
avatarUrlHint: 'L\'URL ne doit pas dépasser 2048 caractères, seuls les liens http/https sont supportés',
|
||||
pleaseInputUrl: 'Veuillez entrer une URL',
|
||||
urlMustStartWithHttp: 'L\'URL doit commencer par http:// ou https://',
|
||||
urlTooLong: 'L\'URL dépasse la limite de 2048 caractères',
|
||||
invalidUrlFormat: 'Format d\'URL invalide',
|
||||
avatarUnderReview: '🔍 Image en cours de vérification',
|
||||
avatarReviewing: 'Vérification de l\'avatar en cours...',
|
||||
avatarChangeSuccess: 'Avatar modifié avec succès',
|
||||
avatarChangeFailed: 'Échec du changement d\'avatar',
|
||||
success: 'Succès',
|
||||
failed: 'Échoué',
|
||||
ok: 'OK',
|
||||
loading: 'Chargement...',
|
||||
loginToViewProfile: 'Connectez-vous pour voir votre profil',
|
||||
goLogin: 'Connexion',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: 'Langue',
|
||||
@@ -975,6 +993,7 @@ const fr = T(
|
||||
roleDesign: 'Dev et Design',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'Backend',
|
||||
roleNative: 'Stack Natif',
|
||||
roleSupport: 'Support i18n',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'Toujours presque',
|
||||
@@ -1059,6 +1078,13 @@ const fr = T(
|
||||
alreadyLatestDesc: 'Vous avez déjà la dernière version',
|
||||
okButton: 'OK',
|
||||
comingSoon: 'Bientôt',
|
||||
distributionChannel: 'Canal de distribution',
|
||||
distAndroid: 'Téléchargé depuis le site officiel de Xianyan',
|
||||
distIOS: "Distribué via l'App Store",
|
||||
distMacOS: "Distribué via l'App Store",
|
||||
distHarmony: 'Distribué via AppGallery',
|
||||
distWeb: 'Demande requise',
|
||||
distWindows: 'Téléchargé depuis le site officiel de Xianyan',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'Bon retour',
|
||||
|
||||
@@ -526,6 +526,24 @@ const hi = T(
|
||||
appStoreNotFound: 'ऐप स्टोर नहीं मिला',
|
||||
experimentalFeature: 'प्रायोगिक सुविधाएँ',
|
||||
underReview: 'समीक्षा में',
|
||||
changeAvatar: 'अवतार बदलें',
|
||||
inputAvatarUrl: 'अवतार URL दर्ज करें',
|
||||
selectFromAlbum: 'एल्बम से चुनें (जल्दी आ रहा है)',
|
||||
avatarUrlHint: 'URL 2048 अक्षरों से अधिक नहीं होना चाहिए, केवल http/https लिंक',
|
||||
pleaseInputUrl: 'कृपया URL दर्ज करें',
|
||||
urlMustStartWithHttp: 'URL http:// या https:// से शुरू होना चाहिए',
|
||||
urlTooLong: 'URL 2048 अक्षर की सीमा से अधिक है',
|
||||
invalidUrlFormat: 'अमान्य URL प्रारूप',
|
||||
avatarUnderReview: '🔍 छवि समीक्षा में',
|
||||
avatarReviewing: 'अवतार छवि की समीक्षा हो रही है...',
|
||||
avatarChangeSuccess: 'अवतार सफलतापूर्वक बदला गया',
|
||||
avatarChangeFailed: 'अवतार बदलने में विफल',
|
||||
success: 'सफल',
|
||||
failed: 'विफल',
|
||||
ok: 'ठीक है',
|
||||
loading: 'लोड हो रहा है...',
|
||||
loginToViewProfile: 'प्रोफ़ाइल देखने के लिए लॉग इन करें',
|
||||
goLogin: 'लॉग इन',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: 'भाषा',
|
||||
@@ -944,6 +962,7 @@ const hi = T(
|
||||
roleDesign: 'डेव और डिज़ाइन',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'बैकएंड',
|
||||
roleNative: 'नेटिव स्टैक',
|
||||
roleSupport: 'i18n सहायता',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'हमेशा लगभग',
|
||||
@@ -1025,6 +1044,13 @@ const hi = T(
|
||||
alreadyLatestDesc: 'आपके पास पहले से नवीनतम संस्करण है',
|
||||
okButton: 'ठीक है',
|
||||
comingSoon: 'जल्द आ रहा है',
|
||||
distributionChannel: 'वितरण चैनल',
|
||||
distAndroid: 'Xianyan आधिकारिक वेबसाइट से डाउनलोड',
|
||||
distIOS: 'App Store के माध्यम से वितरित',
|
||||
distMacOS: 'App Store के माध्यम से वितरित',
|
||||
distHarmony: 'AppGallery के माध्यम से वितरित',
|
||||
distWeb: 'आवेदन आवश्यक',
|
||||
distWindows: 'Xianyan आधिकारिक वेबसाइट से डाउनलोड',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'वापसी पर स्वागत',
|
||||
|
||||
@@ -536,6 +536,24 @@ const it = T(
|
||||
appStoreNotFound: 'App store non trovato',
|
||||
experimentalFeature: 'Funzionalità sperimentali',
|
||||
underReview: 'In revisione',
|
||||
changeAvatar: 'Cambia avatar',
|
||||
inputAvatarUrl: 'Inserisci URL avatar',
|
||||
selectFromAlbum: 'Scegli dall\'album (Prossimamente)',
|
||||
avatarUrlHint: 'L\'URL non deve superare 2048 caratteri, solo link http/https',
|
||||
pleaseInputUrl: 'Inserisci un URL',
|
||||
urlMustStartWithHttp: 'L\'URL deve iniziare con http:// o https://',
|
||||
urlTooLong: 'L\'URL supera il limite di 2048 caratteri',
|
||||
invalidUrlFormat: 'Formato URL non valido',
|
||||
avatarUnderReview: '🔍 Immagine in revisione',
|
||||
avatarReviewing: 'Revisione avatar in corso...',
|
||||
avatarChangeSuccess: 'Avatar cambiato con successo',
|
||||
avatarChangeFailed: 'Cambio avatar fallito',
|
||||
success: 'Successo',
|
||||
failed: 'Fallito',
|
||||
ok: 'OK',
|
||||
loading: 'Caricamento...',
|
||||
loginToViewProfile: 'Accedi per visualizzare il profilo',
|
||||
goLogin: 'Accedi',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: 'Lingua',
|
||||
@@ -970,6 +988,7 @@ const it = T(
|
||||
roleDesign: 'Sviluppatore',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'Backend',
|
||||
roleNative: 'Stack Nativo',
|
||||
roleSupport: 'Supporto i18n',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'Quasi sempre',
|
||||
@@ -1052,6 +1071,13 @@ const it = T(
|
||||
alreadyLatestDesc: 'Hai già la versione più recente',
|
||||
okButton: 'OK',
|
||||
comingSoon: 'Prossimamente',
|
||||
distributionChannel: 'Canale di distribuzione',
|
||||
distAndroid: 'Scaricato dal sito ufficiale di Xianyan',
|
||||
distIOS: 'Distribuito tramite App Store',
|
||||
distMacOS: 'Distribuito tramite App Store',
|
||||
distHarmony: 'Distribuito tramite AppGallery',
|
||||
distWeb: 'Richiesta necessaria',
|
||||
distWindows: 'Scaricato dal sito ufficiale di Xianyan',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'Bentornato',
|
||||
|
||||
@@ -525,6 +525,24 @@ const ja = T(
|
||||
appStoreNotFound: 'アプリストアが見つかりません',
|
||||
experimentalFeature: '実験的機能',
|
||||
underReview: '審査中',
|
||||
changeAvatar: 'アバターを変更',
|
||||
inputAvatarUrl: 'アバターURLを入力',
|
||||
selectFromAlbum: 'アルバムから選択(未対応)',
|
||||
avatarUrlHint: 'URLは2048文字以内、http/httpsで始まる画像リンクのみ対応',
|
||||
pleaseInputUrl: 'URLを入力してください',
|
||||
urlMustStartWithHttp: 'http:// または https:// で始まるURLを入力してください',
|
||||
urlTooLong: 'URLが2048文字の制限を超えています',
|
||||
invalidUrlFormat: 'URLの形式が正しくありません',
|
||||
avatarUnderReview: '🔍 画像審査中',
|
||||
avatarReviewing: 'アバター画像を審査中です...',
|
||||
avatarChangeSuccess: 'アバターの変更に成功しました',
|
||||
avatarChangeFailed: 'アバターの変更に失敗しました',
|
||||
success: '成功',
|
||||
failed: '失敗',
|
||||
ok: 'OK',
|
||||
loading: '読み込み中...',
|
||||
loginToViewProfile: 'ログインしてプロフィールを表示',
|
||||
goLogin: 'ログイン',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: '言語',
|
||||
@@ -915,6 +933,7 @@ const ja = T(
|
||||
roleDesign: '開発・デザイン',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'バックエンド',
|
||||
roleNative: 'ネイティブスタック',
|
||||
roleSupport: '多言語対応',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'いつもあと少し',
|
||||
@@ -994,6 +1013,13 @@ const ja = T(
|
||||
alreadyLatestDesc: '最新バージョンのため、更新は不要です',
|
||||
okButton: 'OK',
|
||||
comingSoon: '準備中',
|
||||
distributionChannel: '配布チャンネル',
|
||||
distAndroid: '閒言公式サイトからダウンロード',
|
||||
distIOS: 'App Store経由で配布',
|
||||
distMacOS: 'App Store経由で配布',
|
||||
distHarmony: 'AppGallery経由で配布',
|
||||
distWeb: '申請が必要',
|
||||
distWindows: '閒言公式サイトからダウンロード',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'おかえりなさい',
|
||||
|
||||
@@ -525,6 +525,24 @@ const ko = T(
|
||||
appStoreNotFound: '앱 스토어를 찾을 수 없습니다',
|
||||
experimentalFeature: '실험적 기능',
|
||||
underReview: '검토 중',
|
||||
changeAvatar: '아바타 변경',
|
||||
inputAvatarUrl: '아바타 URL 입력',
|
||||
selectFromAlbum: '앨범에서 선택 (준비 중)',
|
||||
avatarUrlHint: 'URL은 2048자를 초과할 수 없으며, http/https로 시작하는 이미지 링크만 지원',
|
||||
pleaseInputUrl: 'URL을 입력하세요',
|
||||
urlMustStartWithHttp: 'http:// 또는 https://로 시작하는 URL을 입력하세요',
|
||||
urlTooLong: 'URL이 2048자 제한을 초과합니다',
|
||||
invalidUrlFormat: 'URL 형식이 올바르지 않습니다',
|
||||
avatarUnderReview: '🔍 이미지 검토 중',
|
||||
avatarReviewing: '아바타 이미지를 검토 중입니다...',
|
||||
avatarChangeSuccess: '아바타가 변경되었습니다',
|
||||
avatarChangeFailed: '아바타 변경에 실패했습니다',
|
||||
success: '성공',
|
||||
failed: '실패',
|
||||
ok: '확인',
|
||||
loading: '로딩 중...',
|
||||
loginToViewProfile: '로그인하여 프로필 보기',
|
||||
goLogin: '로그인',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: '언어',
|
||||
@@ -915,6 +933,7 @@ const ko = T(
|
||||
roleDesign: '개발자',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: '백엔드',
|
||||
roleNative: '네이티브 스택',
|
||||
roleSupport: '다국어 지원',
|
||||
member1: '无书的书',
|
||||
member1Sig: '항상 아쉬움이 남아',
|
||||
@@ -994,6 +1013,13 @@ const ko = T(
|
||||
alreadyLatestDesc: '이미 최신 버전입니다',
|
||||
okButton: '확인',
|
||||
comingSoon: '준비 중',
|
||||
distributionChannel: '배포 채널',
|
||||
distAndroid: '閒言 공식 웹사이트에서 다운로드',
|
||||
distIOS: 'App Store를 통해 배포',
|
||||
distMacOS: 'App Store를 통해 배포',
|
||||
distHarmony: 'AppGallery를 통해 배포',
|
||||
distWeb: '신청 필요',
|
||||
distWindows: '閒言 공식 웹사이트에서 다운로드',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: '돌아오신 것을 환영합니다',
|
||||
|
||||
@@ -536,6 +536,24 @@ const pt = T(
|
||||
appStoreNotFound: 'Loja de apps não encontrada',
|
||||
experimentalFeature: 'Funcionalidades experimentais',
|
||||
underReview: 'Em revisão',
|
||||
changeAvatar: 'Alterar avatar',
|
||||
inputAvatarUrl: 'Inserir URL do avatar',
|
||||
selectFromAlbum: 'Escolher do álbum (Em breve)',
|
||||
avatarUrlHint: 'A URL não deve exceder 2048 caracteres, apenas links http/https',
|
||||
pleaseInputUrl: 'Por favor, insira uma URL',
|
||||
urlMustStartWithHttp: 'A URL deve começar com http:// ou https://',
|
||||
urlTooLong: 'A URL excede o limite de 2048 caracteres',
|
||||
invalidUrlFormat: 'Formato de URL inválido',
|
||||
avatarUnderReview: '🔍 Imagem em revisão',
|
||||
avatarReviewing: 'Revisando imagem do avatar, aguarde...',
|
||||
avatarChangeSuccess: 'Avatar alterado com sucesso',
|
||||
avatarChangeFailed: 'Falha ao alterar o avatar',
|
||||
success: 'Sucesso',
|
||||
failed: 'Falhou',
|
||||
ok: 'OK',
|
||||
loading: 'Carregando...',
|
||||
loginToViewProfile: 'Entre para ver seu perfil',
|
||||
goLogin: 'Entrar',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: 'Idioma',
|
||||
@@ -964,6 +982,7 @@ const pt = T(
|
||||
roleDesign: 'Dev e Design',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'Backend',
|
||||
roleNative: 'Stack Nativo',
|
||||
roleSupport: 'Suporte i18n',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'Sempre quase lá',
|
||||
@@ -1047,6 +1066,13 @@ const pt = T(
|
||||
alreadyLatestDesc: 'Você já tem a versão mais recente',
|
||||
okButton: 'OK',
|
||||
comingSoon: 'Em breve',
|
||||
distributionChannel: 'Canal de distribuição',
|
||||
distAndroid: 'Baixado do site oficial do Xianyan',
|
||||
distIOS: 'Distribuído via App Store',
|
||||
distMacOS: 'Distribuído via App Store',
|
||||
distHarmony: 'Distribuído via AppGallery',
|
||||
distWeb: 'Solicitação necessária',
|
||||
distWindows: 'Baixado do site oficial do Xianyan',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'Bem-vindo de volta',
|
||||
|
||||
@@ -533,6 +533,24 @@ const ru = T(
|
||||
appStoreNotFound: 'Магазин приложений не найден',
|
||||
experimentalFeature: 'Экспериментальные функции',
|
||||
underReview: 'На рассмотрении',
|
||||
changeAvatar: 'Изменить аватар',
|
||||
inputAvatarUrl: 'Введите URL аватара',
|
||||
selectFromAlbum: 'Выбрать из альбома (Скоро)',
|
||||
avatarUrlHint: 'URL не должен превышать 2048 символов, поддерживаются только ссылки http/https',
|
||||
pleaseInputUrl: 'Пожалуйста, введите URL',
|
||||
urlMustStartWithHttp: 'URL должен начинаться с http:// или https://',
|
||||
urlTooLong: 'URL превышает лимит в 2048 символов',
|
||||
invalidUrlFormat: 'Неверный формат URL',
|
||||
avatarUnderReview: '🔍 Изображение на проверке',
|
||||
avatarReviewing: 'Проверка изображения аватара, подождите...',
|
||||
avatarChangeSuccess: 'Аватар успешно изменён',
|
||||
avatarChangeFailed: 'Не удалось изменить аватар',
|
||||
success: 'Успешно',
|
||||
failed: 'Ошибка',
|
||||
ok: 'OK',
|
||||
loading: 'Загрузка...',
|
||||
loginToViewProfile: 'Войдите, чтобы увидеть профиль',
|
||||
goLogin: 'Войти',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: 'Язык',
|
||||
@@ -963,6 +981,7 @@ const ru = T(
|
||||
roleDesign: 'Разработка и дизайн',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: 'Бэкенд',
|
||||
roleNative: 'Нативный стек',
|
||||
roleSupport: 'Поддержка i18n',
|
||||
member1: '无书的书',
|
||||
member1Sig: 'Всегда почти',
|
||||
@@ -1046,6 +1065,13 @@ const ru = T(
|
||||
alreadyLatestDesc: 'У вас уже последняя версия',
|
||||
okButton: 'OK',
|
||||
comingSoon: 'Скоро',
|
||||
distributionChannel: 'Канал распространения',
|
||||
distAndroid: 'Загрузка с официального сайта Xianyan',
|
||||
distIOS: 'Распространение через App Store',
|
||||
distMacOS: 'Распространение через App Store',
|
||||
distHarmony: 'Распространение через AppGallery',
|
||||
distWeb: 'Требуется заявка',
|
||||
distWindows: 'Загрузка с официального сайта Xianyan',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: 'С возвращением',
|
||||
|
||||
@@ -524,6 +524,24 @@ const zhCN = T(
|
||||
appStoreNotFound: '未找到应用商店',
|
||||
experimentalFeature: 'Beta',
|
||||
underReview: '审核中',
|
||||
changeAvatar: '修改头像',
|
||||
inputAvatarUrl: '输入头像URL',
|
||||
selectFromAlbum: '从相册选择(暂未开放)',
|
||||
avatarUrlHint: 'URL长度不能超过2048个字符,仅支持 http/https 开头的图片链接',
|
||||
pleaseInputUrl: '请输入URL地址',
|
||||
urlMustStartWithHttp: '请输入以 http:// 或 https:// 开头的URL',
|
||||
urlTooLong: 'URL长度超过2048字符限制,请使用更短的链接',
|
||||
invalidUrlFormat: 'URL格式不正确,请检查后重试',
|
||||
avatarUnderReview: '🔍 图像审核中',
|
||||
avatarReviewing: '正在审核头像图片,请稍候...',
|
||||
avatarChangeSuccess: '头像修改成功',
|
||||
avatarChangeFailed: '头像修改失败',
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
ok: '好的',
|
||||
loading: '加载中...',
|
||||
loginToViewProfile: '登录后查看个人中心',
|
||||
goLogin: '去登录',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: '语言',
|
||||
@@ -902,13 +920,14 @@ const zhCN = T(
|
||||
roleDesign: '程序设计',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: '后端开发',
|
||||
roleNative: '原生栈',
|
||||
roleSupport: '多语言支持',
|
||||
member1: '无书的书',
|
||||
member1Sig: '总是差点意思',
|
||||
member2: 'ayk',
|
||||
member2Sig: '小作坊小料就是猛',
|
||||
member3: '伯乐不相马',
|
||||
member3Sig: '谁家紫啧这么哇塞~',
|
||||
member3Sig: '小作坊下料就是猛',
|
||||
member4: '泼茶香',
|
||||
member4Sig: '一起养猫 一起看海',
|
||||
member1Social: '酷安@无书的书',
|
||||
@@ -981,6 +1000,13 @@ const zhCN = T(
|
||||
alreadyLatestDesc: '当前已是最新版本,无需更新',
|
||||
okButton: '好的',
|
||||
comingSoon: '待开发',
|
||||
distributionChannel: '分发渠道',
|
||||
distAndroid: '由闲言官网提供下载',
|
||||
distIOS: '由App Store提供分发',
|
||||
distMacOS: '由App Store提供分发',
|
||||
distHarmony: '由AppGallery提供分发',
|
||||
distWeb: '需申请',
|
||||
distWindows: '由闲言官网提供下载',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: '欢迎回来',
|
||||
|
||||
@@ -524,6 +524,24 @@ const zhTW = T(
|
||||
appStoreNotFound: '未找到應用商店',
|
||||
experimentalFeature: '實驗中的功能',
|
||||
underReview: '審核中',
|
||||
changeAvatar: '修改頭像',
|
||||
inputAvatarUrl: '輸入頭像URL',
|
||||
selectFromAlbum: '從相簿選擇(暫未開放)',
|
||||
avatarUrlHint: 'URL長度不能超過2048個字元,僅支援 http/https 開頭的圖片連結',
|
||||
pleaseInputUrl: '請輸入URL地址',
|
||||
urlMustStartWithHttp: '請輸入以 http:// 或 https:// 開頭的URL',
|
||||
urlTooLong: 'URL長度超過2048字元限制,請使用更短的連結',
|
||||
invalidUrlFormat: 'URL格式不正確,請檢查後重試',
|
||||
avatarUnderReview: '🔍 圖像審核中',
|
||||
avatarReviewing: '正在審核頭像圖片,請稍候...',
|
||||
avatarChangeSuccess: '頭像修改成功',
|
||||
avatarChangeFailed: '頭像修改失敗',
|
||||
success: '成功',
|
||||
failed: '失敗',
|
||||
ok: '好的',
|
||||
loading: '載入中...',
|
||||
loginToViewProfile: '登入後查看個人中心',
|
||||
goLogin: '去登入',
|
||||
),
|
||||
settings: TSettings(
|
||||
language: '語言',
|
||||
@@ -901,6 +919,7 @@ const zhTW = T(
|
||||
roleDesign: '開發設計',
|
||||
roleUIUX: 'UI/UX',
|
||||
roleBackend: '後端開發',
|
||||
roleNative: '原生棧',
|
||||
roleSupport: '多語言支援',
|
||||
member1: '無書的書',
|
||||
member1Sig: '總是差點意思',
|
||||
@@ -980,6 +999,13 @@ const zhTW = T(
|
||||
alreadyLatestDesc: '當前已是最新版本,無需更新',
|
||||
okButton: '好的',
|
||||
comingSoon: '待開發',
|
||||
distributionChannel: '分發渠道',
|
||||
distAndroid: '由閒言官網提供下載',
|
||||
distIOS: '由App Store提供分發',
|
||||
distMacOS: '由App Store提供分發',
|
||||
distHarmony: '由AppGallery提供分發',
|
||||
distWeb: '需申請',
|
||||
distWindows: '由閒言官網提供下載',
|
||||
),
|
||||
auth: TAuth(
|
||||
welcomeBack: '歡迎回來',
|
||||
|
||||
@@ -345,6 +345,37 @@ class TranslationIOService {
|
||||
experimentalFeature:
|
||||
map['experimentalFeature'] as String? ?? fallback.experimentalFeature,
|
||||
underReview: map['underReview'] as String? ?? fallback.underReview,
|
||||
changeAvatar: map['changeAvatar'] as String? ?? fallback.changeAvatar,
|
||||
inputAvatarUrl:
|
||||
map['inputAvatarUrl'] as String? ?? fallback.inputAvatarUrl,
|
||||
selectFromAlbum:
|
||||
map['selectFromAlbum'] as String? ?? fallback.selectFromAlbum,
|
||||
avatarUrlHint:
|
||||
map['avatarUrlHint'] as String? ?? fallback.avatarUrlHint,
|
||||
pleaseInputUrl:
|
||||
map['pleaseInputUrl'] as String? ?? fallback.pleaseInputUrl,
|
||||
urlMustStartWithHttp:
|
||||
map['urlMustStartWithHttp'] as String? ??
|
||||
fallback.urlMustStartWithHttp,
|
||||
urlTooLong: map['urlTooLong'] as String? ?? fallback.urlTooLong,
|
||||
invalidUrlFormat:
|
||||
map['invalidUrlFormat'] as String? ?? fallback.invalidUrlFormat,
|
||||
avatarUnderReview:
|
||||
map['avatarUnderReview'] as String? ?? fallback.avatarUnderReview,
|
||||
avatarReviewing:
|
||||
map['avatarReviewing'] as String? ?? fallback.avatarReviewing,
|
||||
avatarChangeSuccess:
|
||||
map['avatarChangeSuccess'] as String? ??
|
||||
fallback.avatarChangeSuccess,
|
||||
avatarChangeFailed:
|
||||
map['avatarChangeFailed'] as String? ?? fallback.avatarChangeFailed,
|
||||
success: map['success'] as String? ?? fallback.success,
|
||||
failed: map['failed'] as String? ?? fallback.failed,
|
||||
ok: map['ok'] as String? ?? fallback.ok,
|
||||
loading: map['loading'] as String? ?? fallback.loading,
|
||||
loginToViewProfile:
|
||||
map['loginToViewProfile'] as String? ?? fallback.loginToViewProfile,
|
||||
goLogin: map['goLogin'] as String? ?? fallback.goLogin,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 关于页翻译类型
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-06-01
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 关于页面翻译键定义(软件信息、了解我们、技术栈、构建信息、设备信息、官网、开发者、团队、QQ群、备案、贡献者等)
|
||||
/// 上次更新: 新增roleUIUX/member1Social~3Social/qqGroupTelegram/qqGroupTelegramDesc/contributorRole5/contributorRole5Name/specialThanksTools字段
|
||||
/// 上次更新: 新增roleNative字段
|
||||
/// ============================================================
|
||||
|
||||
class TAbout {
|
||||
@@ -59,6 +59,7 @@ class TAbout {
|
||||
required this.roleDesign,
|
||||
required this.roleUIUX,
|
||||
required this.roleBackend,
|
||||
required this.roleNative,
|
||||
required this.roleSupport,
|
||||
required this.member1,
|
||||
required this.member1Sig,
|
||||
@@ -137,6 +138,13 @@ class TAbout {
|
||||
required this.alreadyLatestDesc,
|
||||
required this.okButton,
|
||||
required this.comingSoon,
|
||||
required this.distributionChannel,
|
||||
required this.distAndroid,
|
||||
required this.distIOS,
|
||||
required this.distMacOS,
|
||||
required this.distHarmony,
|
||||
required this.distWeb,
|
||||
required this.distWindows,
|
||||
});
|
||||
|
||||
// ===== 应用信息 =====
|
||||
@@ -318,6 +326,9 @@ class TAbout {
|
||||
/// 后端角色
|
||||
final String roleBackend;
|
||||
|
||||
/// 原生栈角色
|
||||
final String roleNative;
|
||||
|
||||
/// 支持角色
|
||||
final String roleSupport;
|
||||
|
||||
@@ -554,6 +565,29 @@ class TAbout {
|
||||
/// 待开发
|
||||
final String comingSoon;
|
||||
|
||||
// ===== 分发渠道 =====
|
||||
|
||||
/// 分发渠道
|
||||
final String distributionChannel;
|
||||
|
||||
/// Android分发渠道
|
||||
final String distAndroid;
|
||||
|
||||
/// iOS分发渠道
|
||||
final String distIOS;
|
||||
|
||||
/// macOS分发渠道
|
||||
final String distMacOS;
|
||||
|
||||
/// 鸿蒙分发渠道
|
||||
final String distHarmony;
|
||||
|
||||
/// Web分发渠道
|
||||
final String distWeb;
|
||||
|
||||
/// Windows分发渠道
|
||||
final String distWindows;
|
||||
|
||||
Map<String, String> toMap() => {
|
||||
'appInfo': appInfo,
|
||||
'learnUs': learnUs,
|
||||
@@ -606,6 +640,7 @@ class TAbout {
|
||||
'roleDesign': roleDesign,
|
||||
'roleUIUX': roleUIUX,
|
||||
'roleBackend': roleBackend,
|
||||
'roleNative': roleNative,
|
||||
'roleSupport': roleSupport,
|
||||
'member1': member1,
|
||||
'member1Sig': member1Sig,
|
||||
@@ -684,6 +719,13 @@ class TAbout {
|
||||
'alreadyLatestDesc': alreadyLatestDesc,
|
||||
'okButton': okButton,
|
||||
'comingSoon': comingSoon,
|
||||
'distributionChannel': distributionChannel,
|
||||
'distAndroid': distAndroid,
|
||||
'distIOS': distIOS,
|
||||
'distMacOS': distMacOS,
|
||||
'distHarmony': distHarmony,
|
||||
'distWeb': distWeb,
|
||||
'distWindows': distWindows,
|
||||
};
|
||||
|
||||
static TAbout fromMap(Map<String, String> map, {TAbout? fallback}) => TAbout(
|
||||
@@ -838,6 +880,9 @@ class TAbout {
|
||||
roleBackend: map['roleBackend']?.isNotEmpty == true
|
||||
? map['roleBackend']!
|
||||
: (fallback?.roleBackend ?? ''),
|
||||
roleNative: map['roleNative']?.isNotEmpty == true
|
||||
? map['roleNative']!
|
||||
: (fallback?.roleNative ?? ''),
|
||||
roleSupport: map['roleSupport']?.isNotEmpty == true
|
||||
? map['roleSupport']!
|
||||
: (fallback?.roleSupport ?? ''),
|
||||
@@ -1072,5 +1117,26 @@ class TAbout {
|
||||
comingSoon: map['comingSoon']?.isNotEmpty == true
|
||||
? map['comingSoon']!
|
||||
: (fallback?.comingSoon ?? ''),
|
||||
distributionChannel: map['distributionChannel']?.isNotEmpty == true
|
||||
? map['distributionChannel']!
|
||||
: (fallback?.distributionChannel ?? ''),
|
||||
distAndroid: map['distAndroid']?.isNotEmpty == true
|
||||
? map['distAndroid']!
|
||||
: (fallback?.distAndroid ?? ''),
|
||||
distIOS: map['distIOS']?.isNotEmpty == true
|
||||
? map['distIOS']!
|
||||
: (fallback?.distIOS ?? ''),
|
||||
distMacOS: map['distMacOS']?.isNotEmpty == true
|
||||
? map['distMacOS']!
|
||||
: (fallback?.distMacOS ?? ''),
|
||||
distHarmony: map['distHarmony']?.isNotEmpty == true
|
||||
? map['distHarmony']!
|
||||
: (fallback?.distHarmony ?? ''),
|
||||
distWeb: map['distWeb']?.isNotEmpty == true
|
||||
? map['distWeb']!
|
||||
: (fallback?.distWeb ?? ''),
|
||||
distWindows: map['distWindows']?.isNotEmpty == true
|
||||
? map['distWindows']!
|
||||
: (fallback?.distWindows ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,24 @@ class TProfile {
|
||||
required this.appStoreNotFound,
|
||||
required this.experimentalFeature,
|
||||
required this.underReview,
|
||||
required this.changeAvatar,
|
||||
required this.inputAvatarUrl,
|
||||
required this.selectFromAlbum,
|
||||
required this.avatarUrlHint,
|
||||
required this.pleaseInputUrl,
|
||||
required this.urlMustStartWithHttp,
|
||||
required this.urlTooLong,
|
||||
required this.invalidUrlFormat,
|
||||
required this.avatarUnderReview,
|
||||
required this.avatarReviewing,
|
||||
required this.avatarChangeSuccess,
|
||||
required this.avatarChangeFailed,
|
||||
required this.success,
|
||||
required this.failed,
|
||||
required this.ok,
|
||||
required this.loading,
|
||||
required this.loginToViewProfile,
|
||||
required this.goLogin,
|
||||
});
|
||||
|
||||
/// 页面标题
|
||||
@@ -110,6 +128,43 @@ class TProfile {
|
||||
/// 审核中
|
||||
final String underReview;
|
||||
|
||||
/// 修改头像
|
||||
final String changeAvatar;
|
||||
/// 输入头像URL
|
||||
final String inputAvatarUrl;
|
||||
/// 从相册选择
|
||||
final String selectFromAlbum;
|
||||
/// URL长度提示
|
||||
final String avatarUrlHint;
|
||||
/// 请输入URL地址
|
||||
final String pleaseInputUrl;
|
||||
/// URL必须以http开头
|
||||
final String urlMustStartWithHttp;
|
||||
/// URL过长
|
||||
final String urlTooLong;
|
||||
/// URL格式不正确
|
||||
final String invalidUrlFormat;
|
||||
/// 图像审核中
|
||||
final String avatarUnderReview;
|
||||
/// 正在审核头像图片
|
||||
final String avatarReviewing;
|
||||
/// 头像修改成功
|
||||
final String avatarChangeSuccess;
|
||||
/// 头像修改失败
|
||||
final String avatarChangeFailed;
|
||||
/// 成功
|
||||
final String success;
|
||||
/// 失败
|
||||
final String failed;
|
||||
/// 好的
|
||||
final String ok;
|
||||
/// 加载中
|
||||
final String loading;
|
||||
/// 登录后查看个人中心
|
||||
final String loginToViewProfile;
|
||||
/// 去登录
|
||||
final String goLogin;
|
||||
|
||||
Map<String, String> toMap() => {
|
||||
'title': title,
|
||||
'myFavorites': myFavorites,
|
||||
@@ -142,6 +197,24 @@ class TProfile {
|
||||
'appStoreNotFound': appStoreNotFound,
|
||||
'experimentalFeature': experimentalFeature,
|
||||
'underReview': underReview,
|
||||
'changeAvatar': changeAvatar,
|
||||
'inputAvatarUrl': inputAvatarUrl,
|
||||
'selectFromAlbum': selectFromAlbum,
|
||||
'avatarUrlHint': avatarUrlHint,
|
||||
'pleaseInputUrl': pleaseInputUrl,
|
||||
'urlMustStartWithHttp': urlMustStartWithHttp,
|
||||
'urlTooLong': urlTooLong,
|
||||
'invalidUrlFormat': invalidUrlFormat,
|
||||
'avatarUnderReview': avatarUnderReview,
|
||||
'avatarReviewing': avatarReviewing,
|
||||
'avatarChangeSuccess': avatarChangeSuccess,
|
||||
'avatarChangeFailed': avatarChangeFailed,
|
||||
'success': success,
|
||||
'failed': failed,
|
||||
'ok': ok,
|
||||
'loading': loading,
|
||||
'loginToViewProfile': loginToViewProfile,
|
||||
'goLogin': goLogin,
|
||||
};
|
||||
|
||||
static TProfile fromMap(Map<String, String> map, {TProfile? fallback}) =>
|
||||
@@ -239,5 +312,59 @@ class TProfile {
|
||||
underReview: map['underReview']?.isNotEmpty == true
|
||||
? map['underReview']!
|
||||
: (fallback?.underReview ?? ''),
|
||||
changeAvatar: map['changeAvatar']?.isNotEmpty == true
|
||||
? map['changeAvatar']!
|
||||
: (fallback?.changeAvatar ?? ''),
|
||||
inputAvatarUrl: map['inputAvatarUrl']?.isNotEmpty == true
|
||||
? map['inputAvatarUrl']!
|
||||
: (fallback?.inputAvatarUrl ?? ''),
|
||||
selectFromAlbum: map['selectFromAlbum']?.isNotEmpty == true
|
||||
? map['selectFromAlbum']!
|
||||
: (fallback?.selectFromAlbum ?? ''),
|
||||
avatarUrlHint: map['avatarUrlHint']?.isNotEmpty == true
|
||||
? map['avatarUrlHint']!
|
||||
: (fallback?.avatarUrlHint ?? ''),
|
||||
pleaseInputUrl: map['pleaseInputUrl']?.isNotEmpty == true
|
||||
? map['pleaseInputUrl']!
|
||||
: (fallback?.pleaseInputUrl ?? ''),
|
||||
urlMustStartWithHttp: map['urlMustStartWithHttp']?.isNotEmpty == true
|
||||
? map['urlMustStartWithHttp']!
|
||||
: (fallback?.urlMustStartWithHttp ?? ''),
|
||||
urlTooLong: map['urlTooLong']?.isNotEmpty == true
|
||||
? map['urlTooLong']!
|
||||
: (fallback?.urlTooLong ?? ''),
|
||||
invalidUrlFormat: map['invalidUrlFormat']?.isNotEmpty == true
|
||||
? map['invalidUrlFormat']!
|
||||
: (fallback?.invalidUrlFormat ?? ''),
|
||||
avatarUnderReview: map['avatarUnderReview']?.isNotEmpty == true
|
||||
? map['avatarUnderReview']!
|
||||
: (fallback?.avatarUnderReview ?? ''),
|
||||
avatarReviewing: map['avatarReviewing']?.isNotEmpty == true
|
||||
? map['avatarReviewing']!
|
||||
: (fallback?.avatarReviewing ?? ''),
|
||||
avatarChangeSuccess: map['avatarChangeSuccess']?.isNotEmpty == true
|
||||
? map['avatarChangeSuccess']!
|
||||
: (fallback?.avatarChangeSuccess ?? ''),
|
||||
avatarChangeFailed: map['avatarChangeFailed']?.isNotEmpty == true
|
||||
? map['avatarChangeFailed']!
|
||||
: (fallback?.avatarChangeFailed ?? ''),
|
||||
success: map['success']?.isNotEmpty == true
|
||||
? map['success']!
|
||||
: (fallback?.success ?? ''),
|
||||
failed: map['failed']?.isNotEmpty == true
|
||||
? map['failed']!
|
||||
: (fallback?.failed ?? ''),
|
||||
ok: map['ok']?.isNotEmpty == true
|
||||
? map['ok']!
|
||||
: (fallback?.ok ?? ''),
|
||||
loading: map['loading']?.isNotEmpty == true
|
||||
? map['loading']!
|
||||
: (fallback?.loading ?? ''),
|
||||
loginToViewProfile: map['loginToViewProfile']?.isNotEmpty == true
|
||||
? map['loginToViewProfile']!
|
||||
: (fallback?.loginToViewProfile ?? ''),
|
||||
goLogin: map['goLogin']?.isNotEmpty == true
|
||||
? map['goLogin']!
|
||||
: (fallback?.goLogin ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,17 +3,26 @@
|
||||
/// 创建时间: 2026-06-02
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 将子组件(如syncfusion chart)延迟到postFrameCallback渲染
|
||||
/// 避免chart在build阶段触发markNeedsLayout导致卡死
|
||||
/// 上次更新: 初始创建
|
||||
/// 避免chart在build阶段触发markNeedsLayout导致卡死/闪退
|
||||
/// 内置RepaintBoundary隔离重绘,避免图表重绘波及父级
|
||||
/// 上次更新: 增加RepaintBoundary隔离 + 可选placeholder + mounted安全检查
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class DeferredBuilder extends StatefulWidget {
|
||||
const DeferredBuilder({super.key, required this.builder});
|
||||
const DeferredBuilder({
|
||||
super.key,
|
||||
required this.builder,
|
||||
this.placeholder,
|
||||
});
|
||||
|
||||
final WidgetBuilder builder;
|
||||
|
||||
/// 首帧占位组件,默认 SizedBox.expand() 填充父级约束
|
||||
/// 对于有固定高度的图表区域,SizedBox.expand 会自动适配父级高度
|
||||
final Widget? placeholder;
|
||||
|
||||
@override
|
||||
State<DeferredBuilder> createState() => _DeferredBuilderState();
|
||||
}
|
||||
@@ -32,8 +41,8 @@ class _DeferredBuilderState extends State<DeferredBuilder> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_ready) {
|
||||
return const SizedBox.expand();
|
||||
return widget.placeholder ?? const SizedBox.expand();
|
||||
}
|
||||
return widget.builder(context);
|
||||
return RepaintBoundary(child: widget.builder(context));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 网络状态横幅组件
|
||||
/// 创建时间: 2026-05-04
|
||||
/// 更新时间: 2026-05-31
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 在页面顶部显示网络状态提示(离线/网络异常),支持关闭和网络恢复自动隐藏
|
||||
/// 上次更新: 增强为NetworkStatusBanner,支持离线+DNS异常双状态检测、可关闭、自动隐藏
|
||||
/// 上次更新: OfflineBanner增加刷新/关闭按钮,改为ConsumerStatefulWidget
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -15,6 +15,7 @@ import '../../../core/services/network/connectivity_provider.dart'
|
||||
show connectivityCheckProvider;
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
import 'app_toast.dart';
|
||||
|
||||
/// 网络状态横幅 — 显示在页面顶部
|
||||
///
|
||||
@@ -113,14 +114,28 @@ class _NetworkStatusBannerState extends ConsumerState<NetworkStatusBanner> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 简洁离线横幅 — 仅显示离线状态,不可关闭
|
||||
class OfflineBanner extends ConsumerWidget {
|
||||
/// 简洁离线横幅 — 显示离线状态,支持刷新和关闭
|
||||
class OfflineBanner extends ConsumerStatefulWidget {
|
||||
const OfflineBanner({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<OfflineBanner> createState() => _OfflineBannerState();
|
||||
}
|
||||
|
||||
class _OfflineBannerState extends ConsumerState<OfflineBanner> {
|
||||
bool _dismissed = false;
|
||||
bool _wasOffline = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isOffline = ref.isOffline;
|
||||
if (!isOffline) return const SizedBox.shrink();
|
||||
|
||||
if (_wasOffline && !isOffline) {
|
||||
_dismissed = false;
|
||||
}
|
||||
_wasOffline = isOffline;
|
||||
|
||||
if (!isOffline || _dismissed) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
@@ -138,7 +153,6 @@ class OfflineBanner extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.wifi_slash,
|
||||
@@ -146,11 +160,48 @@ class OfflineBanner extends ConsumerWidget {
|
||||
color: CupertinoColors.systemOrange.darkColor,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
'离线模式 · 使用缓存数据',
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: CupertinoColors.systemOrange.darkColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
Expanded(
|
||||
child: Text(
|
||||
'离线模式 · 使用缓存数据',
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: CupertinoColors.systemOrange.darkColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final stillOffline = ref.read(connectivityProvider) == false;
|
||||
if (stillOffline) {
|
||||
AppToast.showWarning('网络仍未连接');
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: AppSpacing.sm),
|
||||
child: Icon(
|
||||
CupertinoIcons.refresh,
|
||||
size: 16,
|
||||
color: CupertinoColors.systemOrange.darkColor
|
||||
.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_dismissed = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: AppSpacing.sm),
|
||||
child: Icon(
|
||||
CupertinoIcons.xmark_circle_fill,
|
||||
size: 16,
|
||||
color: CupertinoColors.systemOrange.darkColor
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
271
lib/shared/widgets/media/watermarked_copyright_image.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 带水印的版权证书图片组件
|
||||
/// 创建时间: 2026-06-02
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 展示软件著作权证书图片,带对角线"闲言"水印,点击全屏查看
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../../core/theme/app_radius.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
|
||||
class WatermarkedCopyrightImage extends StatelessWidget {
|
||||
const WatermarkedCopyrightImage({super.key});
|
||||
|
||||
static const _assetPath = 'assets/images/empty/rz.png';
|
||||
static const _watermarkText = '闲言';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _showFullScreen(context),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
border: Border.all(
|
||||
color: ext.textHint.withValues(alpha: 0.15),
|
||||
width: 0.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ext.textPrimary.withValues(alpha: 0.06),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
child: Image.asset(
|
||||
_assetPath,
|
||||
fit: BoxFit.fitWidth,
|
||||
width: double.infinity,
|
||||
errorBuilder: (_, __, ___) => _buildErrorPlaceholder(ext),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: CustomPaint(
|
||||
painter: _DiagonalWatermarkPainter(
|
||||
text: _watermarkText,
|
||||
color: ext.textPrimary.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: AppSpacing.sm,
|
||||
top: AppSpacing.sm,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xs,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgPrimary.withValues(alpha: 0.7),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.fullscreen,
|
||||
size: 12,
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
'🔍',
|
||||
style: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorPlaceholder(AppThemeExtension ext) {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(CupertinoIcons.doc_text_search, size: 36, color: ext.textHint),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'证书图片加载失败',
|
||||
style: AppTypography.caption1.copyWith(color: ext.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFullScreen(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
showCupertinoDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (dialogContext) => CupertinoPageScaffold(
|
||||
backgroundColor: ext.bgPrimary.withValues(alpha: 0.95),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
CupertinoNavigationBar(
|
||||
backgroundColor: ext.bgPrimary.withValues(alpha: 0.8),
|
||||
border: null,
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Icon(
|
||||
CupertinoIcons.xmark_circle_fill,
|
||||
color: ext.textHint,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
middle: Text(
|
||||
'📜 软件著作权证书',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: InteractiveViewer(
|
||||
minScale: 0.5,
|
||||
maxScale: 4.0,
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Image.asset(
|
||||
_assetPath,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.doc_text_search,
|
||||
size: 48,
|
||||
color: ext.textHint,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'图片加载失败',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: CustomPaint(
|
||||
painter: _DiagonalWatermarkPainter(
|
||||
text: _watermarkText,
|
||||
color: ext.textPrimary.withValues(alpha: 0.06),
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DiagonalWatermarkPainter extends CustomPainter {
|
||||
_DiagonalWatermarkPainter({
|
||||
required this.text,
|
||||
required this.color,
|
||||
this.fontSize = 14,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final Color color;
|
||||
final double fontSize;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final textStyle = TextStyle(
|
||||
color: color,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 4,
|
||||
);
|
||||
final textSpan = TextSpan(text: text, style: textStyle);
|
||||
final textPainter = TextPainter(
|
||||
text: textSpan,
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
|
||||
final stepX = textPainter.width + 60;
|
||||
final stepY = textPainter.height + 80;
|
||||
const angle = -pi / 6;
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(size.width / 2, size.height / 2);
|
||||
canvas.rotate(angle);
|
||||
canvas.translate(-size.width, -size.height);
|
||||
|
||||
final cols = (size.width * 3 / stepX).ceil() + 1;
|
||||
final rows = (size.height * 3 / stepY).ceil() + 1;
|
||||
|
||||
for (var row = 0; row < rows; row++) {
|
||||
for (var col = 0; col < cols; col++) {
|
||||
final x = col * stepX.toDouble();
|
||||
final y = row * stepY.toDouble();
|
||||
textPainter.paint(canvas, Offset(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _DiagonalWatermarkPainter oldDelegate) {
|
||||
return oldDelegate.text != text ||
|
||||
oldDelegate.color != color ||
|
||||
oldDelegate.fontSize != fontSize;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export 'input/infinite_paged_list.dart';
|
||||
export 'media/safe_cached_image.dart';
|
||||
export 'media/thumbnail_image.dart';
|
||||
export 'media/tts_player_bar.dart';
|
||||
export 'media/watermarked_copyright_image.dart';
|
||||
|
||||
// cards
|
||||
export 'cards/rank_item_card.dart';
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 4.0 MiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 16 KiB |