From dd09f1a526e856ea6a4ad1e8b40a249a5d862511 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 5 Jun 2026 20:05:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E5=85=B3=E4=BA=8E=E2=86=92?= =?UTF-8?q?=E5=85=B3=E4=BA=8E=E6=88=91=E4=BB=AC=EF=BC=8C=E7=A7=BB=E5=88=B0?= =?UTF-8?q?=E5=B7=A6=E4=BE=A7=E8=8F=9C=E5=8D=95=E7=8B=AC=E7=AB=8B=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 screens/about/about_screen.dart(版本信息/授权信息/关于我们/意见反馈) - 左侧导航新增「关于我们」(/about),从系统设置移除原「关于」Tab(7→6) - 迁移并清理 settings 中仅关于页使用的 widget/弹窗/import Co-Authored-By: Claude Opus 4.8 --- client/lib/core/router/app_router.dart | 4 + client/lib/screens/about/about_screen.dart | 356 ++++++++++++++++++ .../lib/screens/settings/settings_screen.dart | 348 +---------------- client/lib/screens/shell/app_shell.dart | 1 + 4 files changed, 362 insertions(+), 347 deletions(-) create mode 100644 client/lib/screens/about/about_screen.dart diff --git a/client/lib/core/router/app_router.dart b/client/lib/core/router/app_router.dart index 55ae949..a5d50f8 100644 --- a/client/lib/core/router/app_router.dart +++ b/client/lib/core/router/app_router.dart @@ -15,6 +15,7 @@ import '../../screens/products/products_screen.dart'; import '../../screens/products/product_detail_screen.dart'; import '../../screens/public/public_product_screen.dart'; import '../../screens/settings/settings_screen.dart'; +import '../../screens/about/about_screen.dart'; import '../auth/auth_state.dart'; Page _noTransition(Widget child) => @@ -131,6 +132,9 @@ final appRouterProvider = Provider((ref) { GoRoute( path: '/settings', pageBuilder: (_, __) => _noTransition(const SettingsScreen())), + GoRoute( + path: '/about', + pageBuilder: (_, __) => _noTransition(const AboutScreen())), ], ), ], diff --git a/client/lib/screens/about/about_screen.dart b/client/lib/screens/about/about_screen.dart new file mode 100644 index 0000000..b5f3504 --- /dev/null +++ b/client/lib/screens/about/about_screen.dart @@ -0,0 +1,356 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../core/config/app_info.dart'; +import '../../core/theme/app_theme.dart'; +import '../../core/update/app_updater.dart'; +import '../../core/utils/dialog_util.dart'; +import '../../providers/license_provider.dart'; +import '../../providers/update_provider.dart'; + +/// 「关于我们」独立页面(左侧菜单项)。原为系统设置里的「关于」Tab。 +class AboutScreen extends ConsumerStatefulWidget { + const AboutScreen({super.key}); + + @override + ConsumerState createState() => _AboutScreenState(); +} + +class _AboutScreenState extends ConsumerState { + @override + Widget build(BuildContext context) { + final appVersion = ref.watch(appVersionProvider).valueOrNull ?? 'v1.0.0'; + final updateInfo = ref.watch(updateProvider).valueOrNull; + final licenseAsync = ref.watch(licenseProvider); + + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── 版本信息 ── + _AboutSection( + title: '版本信息', + children: [ + _AboutRow(label: '当前版本', value: appVersion), + if (updateInfo != null && updateInfo.hasUpdate) + _AboutRow( + label: '最新版本', + value: 'v${updateInfo.latestVersion}', + valueColor: AppTheme.success, + trailing: TextButton( + onPressed: () => startInAppUpdate(context, updateInfo), + child: const Text('立即更新'), + ), + ) + else + _AboutRow( + label: '最新版本', + value: updateInfo != null ? '已是最新' : '检查中…', + valueColor: AppTheme.textSecondary, + trailing: TextButton( + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + messenger.showSnackBar( + const SnackBar(content: Text('正在检查更新…'))); + await ref.read(updateProvider.notifier).forceCheck(); + if (!context.mounted) return; + final r = ref.read(updateProvider).valueOrNull; + messenger.hideCurrentSnackBar(); + final String msg; + if (r == null) { + msg = '检查更新失败,请检查网络'; + } else if (r.hasUpdate) { + msg = '发现新版本 v${r.latestVersion}'; + } else { + msg = '已是最新版本(v${r.latestVersion})'; + } + messenger.showSnackBar(SnackBar(content: Text(msg))); + }, + child: const Text('检查更新'), + ), + ), + ], + ), + const SizedBox(height: 20), + + // ── 授权信息 ── + _AboutSection( + title: '授权信息', + children: [ + licenseAsync.when( + loading: () => const _AboutRow(label: '授权状态', value: '加载中…'), + error: (_, __) => + const _AboutRow(label: '授权状态', value: '暂无授权信息'), + data: (lic) { + if (lic == null) { + return const _AboutRow(label: '授权状态', value: '未激活'); + } + return Column( + children: [ + _AboutRow(label: '授权类型', value: lic.typeLabel), + _AboutRow( + label: '授权状态', + value: lic.isExpired + ? '已过期' + : lic.isActive + ? '正常' + : '已停用', + valueColor: lic.isExpired + ? AppTheme.danger + : lic.isActive + ? AppTheme.success + : AppTheme.textSecondary, + ), + if (lic.expiresAt != null) + _AboutRow( + label: '到期时间', + value: DateFormat('yyyy-MM-dd').format(lic.expiresAt!), + trailing: lic.daysRemaining != null && + lic.daysRemaining! <= 30 + ? Chip( + label: Text( + lic.isExpired + ? '已过期' + : '剩余 ${lic.daysRemaining} 天', + style: const TextStyle( + fontSize: 11, color: Colors.white), + ), + backgroundColor: lic.isExpired + ? AppTheme.danger + : Colors.orange, + padding: EdgeInsets.zero, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ) + : null, + ) + else + const _AboutRow(label: '到期时间', value: '永久有效'), + if (lic.activatedAt != null) + _AboutRow( + label: '激活时间', + value: DateFormat('yyyy-MM-dd') + .format(lic.activatedAt!), + ), + ], + ); + }, + ), + const SizedBox(height: 8), + Row( + children: [ + OutlinedButton.icon( + onPressed: () => _showRenewDialog(), + icon: const Icon(Icons.card_membership, size: 16), + label: const Text('续费 / 升级授权'), + ), + ], + ), + ], + ), + const SizedBox(height: 20), + + // ── 关于我们 ── + _AboutSection( + title: '关于我们', + children: [ + _AboutRow(label: '服务商', value: AppInfo.provider), + _AboutRow(label: '官方网站', value: AppInfo.website), + _AboutRow(label: '联系邮箱', value: AppInfo.email), + const _AboutRow(label: '技术支持', value: '周一至周五 9:00 - 18:00'), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + OutlinedButton.icon( + onPressed: () async { + final uri = Uri.parse( + 'mailto:${AppInfo.email}?subject=酒库管理系统咨询'); + if (await canLaunchUrl(uri)) launchUrl(uri); + }, + icon: const Icon(Icons.email_outlined, size: 16), + label: const Text('发送邮件'), + ), + ], + ), + ], + ), + const SizedBox(height: 20), + + // ── 意见反馈 ── + _AboutSection( + title: '意见反馈', + children: [ + const _AboutRow( + label: '问题反馈', + value: '遇到 Bug 或有功能建议,欢迎告知我们', + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + ElevatedButton.icon( + onPressed: () => _showFeedbackDialog(isBug: true), + icon: const Icon(Icons.bug_report_outlined, size: 16), + label: const Text('反馈 Bug'), + ), + OutlinedButton.icon( + onPressed: () => _showFeedbackDialog(isBug: false), + icon: const Icon(Icons.lightbulb_outline, size: 16), + label: const Text('功能建议'), + ), + ], + ), + ], + ), + ], + ), + ); + } + + void _showRenewDialog() { + showAppDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('续费 / 升级授权'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('请联系我们获取续费报价:'), + const SizedBox(height: 12), + SelectableText('📧 ${AppInfo.email}', + style: const TextStyle(fontSize: 13)), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('关闭'), + ), + ElevatedButton( + onPressed: () async { + await Clipboard.setData(ClipboardData(text: AppInfo.email)); + if (ctx.mounted) { + Navigator.pop(ctx); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('邮箱已复制到剪贴板')), + ); + } + }, + child: const Text('复制邮箱'), + ), + ], + ), + ); + } + + void _showFeedbackDialog({required bool isBug}) { + final ctrl = TextEditingController(); + showAppDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(isBug ? '反馈 Bug' : '功能建议'), + content: SizedBox( + width: 400, + child: TextField( + controller: ctrl, + maxLines: 6, + decoration: InputDecoration( + hintText: isBug ? '请描述问题的复现步骤和预期行为…' : '请描述您希望增加的功能…', + border: const OutlineInputBorder(), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () async { + final subject = Uri.encodeComponent(isBug ? 'Bug反馈' : '功能建议'); + final body = Uri.encodeComponent(ctrl.text); + final uri = Uri.parse( + 'mailto:${AppInfo.email}?subject=$subject&body=$body'); + if (await canLaunchUrl(uri)) launchUrl(uri); + if (ctx.mounted) Navigator.pop(ctx); + }, + child: const Text('通过邮件发送'), + ), + ], + ), + ); + } +} + +// ── 关于页辅助 widgets ────────────────────────────────────── + +class _AboutSection extends StatelessWidget { + final String title; + final List children; + const _AboutSection({required this.title, required this.children}); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppTheme.primaryDark)), + const Divider(height: 24), + ...children, + ], + ), + ), + ); + } +} + +class _AboutRow extends StatelessWidget { + final String label; + final String value; + final Color? valueColor; + final Widget? trailing; + + const _AboutRow({ + required this.label, + required this.value, + this.valueColor, + this.trailing, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + SizedBox( + width: 88, + child: Text(label, + style: const TextStyle( + fontSize: 13, color: AppTheme.textSecondary)), + ), + Expanded( + child: Text(value, + style: TextStyle( + fontSize: 13, + color: valueColor ?? AppTheme.textPrimary, + fontWeight: FontWeight.w500)), + ), + if (trailing != null) trailing!, + ], + ), + ); + } +} diff --git a/client/lib/screens/settings/settings_screen.dart b/client/lib/screens/settings/settings_screen.dart index c3d8e91..b62937b 100644 --- a/client/lib/screens/settings/settings_screen.dart +++ b/client/lib/screens/settings/settings_screen.dart @@ -7,19 +7,13 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../../core/auth/auth_state.dart'; import '../../core/config/app_config.dart'; -import '../../core/config/app_info.dart'; -import '../../core/update/app_updater.dart'; import '../../core/theme/app_theme.dart'; import '../../models/number_rule.dart'; import '../../models/user.dart'; import '../../models/warehouse.dart'; -import '../../providers/license_provider.dart'; import '../../providers/number_rule_provider.dart'; -import '../../providers/update_provider.dart'; import '../../providers/user_provider.dart'; import '../../providers/warehouse_provider.dart'; import '../../providers/shop_provider.dart'; @@ -45,7 +39,7 @@ class _SettingsScreenState extends ConsumerState { @override Widget build(BuildContext context) { return DefaultTabController( - length: 7, + length: 6, child: Column( children: [ Container( @@ -65,7 +59,6 @@ class _SettingsScreenState extends ConsumerState { Tab(text: '编号规则'), Tab(text: '系统参数'), Tab(text: '数据导入'), - Tab(text: '关于'), ], ), ), @@ -79,7 +72,6 @@ class _SettingsScreenState extends ConsumerState { _buildNumberRulesTab(), _buildSystemParamsTab(), _buildImportTab(), - _buildAboutTab(), ], ), ), @@ -708,276 +700,6 @@ class _SettingsScreenState extends ConsumerState { return _BatchImportWidget(isSuperAdmin: isSuperAdmin); } - // ── 关于 Tab ───────────────────────────────────────────── - Widget _buildAboutTab() { - final appVersion = ref.watch(appVersionProvider).valueOrNull ?? 'v1.0.0'; - final updateInfo = ref.watch(updateProvider).valueOrNull; - final licenseAsync = ref.watch(licenseProvider); - - return SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ── 版本信息 ── - _AboutSection( - title: '版本信息', - children: [ - _AboutRow(label: '当前版本', value: appVersion), - if (updateInfo != null && updateInfo.hasUpdate) - _AboutRow( - label: '最新版本', - value: 'v${updateInfo.latestVersion}', - valueColor: AppTheme.success, - trailing: TextButton( - onPressed: () => startInAppUpdate(context, updateInfo), - child: const Text('立即更新'), - ), - ) - else - _AboutRow( - label: '最新版本', - value: updateInfo != null ? '已是最新' : '检查中…', - valueColor: AppTheme.textSecondary, - trailing: TextButton( - onPressed: () async { - final messenger = ScaffoldMessenger.of(context); - messenger.showSnackBar( - const SnackBar(content: Text('正在检查更新…'))); - await ref.read(updateProvider.notifier).forceCheck(); - if (!context.mounted) return; - final r = ref.read(updateProvider).valueOrNull; - messenger.hideCurrentSnackBar(); - final String msg; - if (r == null) { - msg = '检查更新失败,请检查网络'; - } else if (r.hasUpdate) { - msg = '发现新版本 v${r.latestVersion}'; - } else { - msg = '已是最新版本(v${r.latestVersion})'; - } - messenger.showSnackBar(SnackBar(content: Text(msg))); - }, - child: const Text('检查更新'), - ), - ), - ], - ), - const SizedBox(height: 20), - - // ── 授权信息 ── - _AboutSection( - title: '授权信息', - children: [ - licenseAsync.when( - loading: () => const _AboutRow(label: '授权状态', value: '加载中…'), - error: (_, __) => - const _AboutRow(label: '授权状态', value: '暂无授权信息'), - data: (lic) { - if (lic == null) { - return const _AboutRow(label: '授权状态', value: '未激活'); - } - return Column( - children: [ - _AboutRow(label: '授权类型', value: lic.typeLabel), - _AboutRow( - label: '授权状态', - value: lic.isExpired - ? '已过期' - : lic.isActive - ? '正常' - : '已停用', - valueColor: lic.isExpired - ? AppTheme.danger - : lic.isActive - ? AppTheme.success - : AppTheme.textSecondary, - ), - if (lic.expiresAt != null) - _AboutRow( - label: '到期时间', - value: DateFormat('yyyy-MM-dd').format(lic.expiresAt!), - trailing: lic.daysRemaining != null && - lic.daysRemaining! <= 30 - ? Chip( - label: Text( - lic.isExpired - ? '已过期' - : '剩余 ${lic.daysRemaining} 天', - style: const TextStyle( - fontSize: 11, color: Colors.white), - ), - backgroundColor: lic.isExpired - ? AppTheme.danger - : Colors.orange, - padding: EdgeInsets.zero, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - ) - : null, - ) - else - const _AboutRow(label: '到期时间', value: '永久有效'), - if (lic.activatedAt != null) - _AboutRow( - label: '激活时间', - value: DateFormat('yyyy-MM-dd') - .format(lic.activatedAt!), - ), - ], - ); - }, - ), - const SizedBox(height: 8), - Row( - children: [ - OutlinedButton.icon( - onPressed: () => _showRenewDialog(), - icon: const Icon(Icons.card_membership, size: 16), - label: const Text('续费 / 升级授权'), - ), - ], - ), - ], - ), - const SizedBox(height: 20), - - // ── 关于我们 ── - _AboutSection( - title: '关于我们', - children: [ - _AboutRow(label: '服务商', value: AppInfo.provider), - _AboutRow(label: '官方网站', value: AppInfo.website), - _AboutRow(label: '联系邮箱', value: AppInfo.email), - const _AboutRow(label: '技术支持', value: '周一至周五 9:00 - 18:00'), - const SizedBox(height: 8), - Wrap( - spacing: 8, - children: [ - OutlinedButton.icon( - onPressed: () async { - final uri = Uri.parse( - 'mailto:${AppInfo.email}?subject=酒库管理系统咨询'); - if (await canLaunchUrl(uri)) launchUrl(uri); - }, - icon: const Icon(Icons.email_outlined, size: 16), - label: const Text('发送邮件'), - ), - ], - ), - ], - ), - const SizedBox(height: 20), - - // ── 意见反馈 ── - _AboutSection( - title: '意见反馈', - children: [ - const _AboutRow( - label: '问题反馈', - value: '遇到 Bug 或有功能建议,欢迎告知我们', - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - children: [ - ElevatedButton.icon( - onPressed: () => _showFeedbackDialog(isBug: true), - icon: const Icon(Icons.bug_report_outlined, size: 16), - label: const Text('反馈 Bug'), - ), - OutlinedButton.icon( - onPressed: () => _showFeedbackDialog(isBug: false), - icon: const Icon(Icons.lightbulb_outline, size: 16), - label: const Text('功能建议'), - ), - ], - ), - ], - ), - ], - ), - ); - } - - void _showRenewDialog() { - showAppDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text('续费 / 升级授权'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('请联系我们获取续费报价:'), - const SizedBox(height: 12), - SelectableText('📧 ${AppInfo.email}', - style: const TextStyle(fontSize: 13)), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('关闭'), - ), - ElevatedButton( - onPressed: () async { - await Clipboard.setData( - ClipboardData(text: AppInfo.email)); - if (ctx.mounted) { - Navigator.pop(ctx); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('邮箱已复制到剪贴板')), - ); - } - }, - child: const Text('复制邮箱'), - ), - ], - ), - ); - } - - void _showFeedbackDialog({required bool isBug}) { - final ctrl = TextEditingController(); - showAppDialog( - context: context, - builder: (ctx) => AlertDialog( - title: Text(isBug ? '反馈 Bug' : '功能建议'), - content: SizedBox( - width: 400, - child: TextField( - controller: ctrl, - maxLines: 6, - decoration: InputDecoration( - hintText: isBug - ? '请描述问题的复现步骤和预期行为…' - : '请描述您希望增加的功能…', - border: const OutlineInputBorder(), - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('取消'), - ), - ElevatedButton( - onPressed: () async { - final subject = Uri.encodeComponent(isBug ? 'Bug反馈' : '功能建议'); - final body = Uri.encodeComponent(ctrl.text); - final uri = Uri.parse( - 'mailto:${AppInfo.email}?subject=$subject&body=$body'); - if (await canLaunchUrl(uri)) launchUrl(uri); - if (ctx.mounted) Navigator.pop(ctx); - }, - child: const Text('通过邮件发送'), - ), - ], - ), - ); - } - void _showEditParamDialog( String label, String current, ValueChanged onSave) { final ctrl = TextEditingController(text: current); @@ -1469,74 +1191,6 @@ class _ParamRow extends StatelessWidget { } } -// ── 关于页辅助 widgets ────────────────────────────────────── - -class _AboutSection extends StatelessWidget { - final String title; - final List children; - const _AboutSection({required this.title, required this.children}); - - @override - Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: AppTheme.primaryDark)), - const Divider(height: 24), - ...children, - ], - ), - ), - ); - } -} - -class _AboutRow extends StatelessWidget { - final String label; - final String value; - final Color? valueColor; - final Widget? trailing; - - const _AboutRow({ - required this.label, - required this.value, - this.valueColor, - this.trailing, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Row( - children: [ - SizedBox( - width: 88, - child: Text(label, - style: const TextStyle( - fontSize: 13, color: AppTheme.textSecondary)), - ), - Expanded( - child: Text(value, - style: TextStyle( - fontSize: 13, - color: valueColor ?? AppTheme.textPrimary, - fontWeight: FontWeight.w500)), - ), - if (trailing != null) trailing!, - ], - ), - ); - } -} - // ── 批量数据导入 ─────────────────────────────────────────── class _ImportSlot { diff --git a/client/lib/screens/shell/app_shell.dart b/client/lib/screens/shell/app_shell.dart index fa61dad..b75e2e7 100644 --- a/client/lib/screens/shell/app_shell.dart +++ b/client/lib/screens/shell/app_shell.dart @@ -66,6 +66,7 @@ class _AppShellState extends ConsumerState { _NavItem(icon: Icons.people, label: '往来单位', path: '/partners'), _NavItem(icon: Icons.category, label: '基础数据', path: '/products'), _NavItem(icon: Icons.settings, label: '系统设置', path: '/settings'), + _NavItem(icon: Icons.info_outline, label: '关于我们', path: '/about'), ]; @override