dd09f1a526
- 新增 screens/about/about_screen.dart(版本信息/授权信息/关于我们/意见反馈) - 左侧导航新增「关于我们」(/about),从系统设置移除原「关于」Tab(7→6) - 迁移并清理 settings 中仅关于页使用的 widget/弹窗/import Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
357 lines
13 KiB
Dart
357 lines
13 KiB
Dart
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<AboutScreen> createState() => _AboutScreenState();
|
||
}
|
||
|
||
class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||
@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<Widget> 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!,
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|