// screens/about/about_screen.dart — 关于我们(照原型 about.html 重建)。 // 居中 col(max760) 四卡:Hero(渐变+链接+反馈) / 产品信息(2×2 cell) / // 授权信息(3 cell, licenseProvider 实数据) / 更新日志(timeline, /public/release // 与官网同源,默认 2 条可展开)。旧版多余卡(帮助文档/扫码防伪/系统信息)删除, // 构建号并入版本 cell 小字(差异记 design/CONTRACT.md)。 import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../core/config/app_config.dart'; import '../../core/config/app_info.dart'; import '../../core/responsive/responsive.dart'; import '../../core/theme/app_dims.g.dart'; import '../../core/theme/context_tokens.dart'; import '../../core/update/app_updater.dart'; import '../../core/utils/dialog_util.dart'; import '../../providers/changelog_provider.dart'; import '../../providers/feedback_provider.dart'; import '../../providers/update_provider.dart'; import '../../core/theme/app_fonts.dart'; import '../../widgets/brand_mark.dart'; import '../../widgets/ds/ds_atoms.dart'; import '../../widgets/ds/ds_toast.dart'; class AboutScreen extends ConsumerStatefulWidget { const AboutScreen({super.key}); @override ConsumerState createState() => _AboutScreenState(); } class _AboutScreenState extends ConsumerState { bool _logExpanded = false; static const _logShown = 2; // 原型 SHOWN=2,其余折叠 @override Widget build(BuildContext context) { final t = context.tokens; return Container( color: t.bg, child: SingleChildScrollView( padding: context.isMobile ? const EdgeInsets.all(AppDims.sp4) : const EdgeInsets.fromLTRB(26, 22, 26, 22), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 760), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _heroCard(t), _productCard(t), _changelogCard(t), // 页脚 Padding( padding: const EdgeInsets.only(top: 4, bottom: 12), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('© 2026 岩美科技 · 保留所有权利 · ', style: TextStyle( fontSize: AppDims.fsXs, color: t.faint)), InkWell( onTap: () => launchUrl(Uri.parse('https://beian.miit.gov.cn')), child: Text('京ICP备2026039814号', style: TextStyle( fontSize: AppDims.fsXs, color: t.faint)), ), ], ), ), ], ), ), ), ), ); } /// 原型 .card:surface / border / r-lg / pad 20 22 / mb18。 Widget _card(dynamic t, Widget child, {Gradient? gradient}) => Container( margin: const EdgeInsets.only(bottom: 18), padding: const EdgeInsets.fromLTRB(22, 20, 22, 20), decoration: BoxDecoration( color: gradient == null ? t.surface : null, gradient: gradient, border: Border.all(color: t.border), borderRadius: BorderRadius.circular(AppDims.rLg), ), child: child, ); /// 卡标题(.card-h 17/700 + primary 图标)。 Widget _cardH(dynamic t, IconData icon, String title) => Padding( padding: const EdgeInsets.only(bottom: 14), child: Row(children: [ Icon(icon, size: 18, color: t.primary), const SizedBox(width: 8), Text(title, style: TextStyle( fontSize: AppDims.fsH2, fontWeight: FontWeight.w700, color: t.heading)), ]), ); // ── 卡1:Hero(渐变 + 链接 2×2 + 意见反馈)────────────────── Widget _heroCard(dynamic t) { final mobile = context.isMobile; return _card( t, gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [t.brand50, t.surface], ), Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // .hero-top:62×62 logo(真相源 BrandMark,镜像原型 hero SVG)+ 名称/by Row(children: [ const BrandMark(size: 62), const SizedBox(width: 18), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('岩美酒库', style: TextStyle( fontSize: AppDims.fsH1, fontWeight: FontWeight.w800, color: t.heading)), const SizedBox(height: 4), Text('酒水进销存 · 财务对账 · 防伪溯源一体化 · 岩美科技出品', style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), ], ), ), ]), // .hero-links:2×2 链接 Container( margin: const EdgeInsets.only(top: 16), padding: const EdgeInsets.only(top: 14), decoration: BoxDecoration( border: Border(top: BorderSide(color: t.borderSubtle))), child: mobile ? Column(children: [ for (final l in _links()) Padding( padding: const EdgeInsets.only(bottom: 10), child: _linkRow(t, l), ), ]) : Column(children: [ for (var row = 0; row < 2; row++) Padding( padding: EdgeInsets.only(bottom: row == 0 ? 10 : 0), child: Row(children: [ for (var col = 0; col < 2; col++) ...[ if (col > 0) const SizedBox(width: 10), Expanded( child: _linkRow(t, _links()[row * 2 + col])), ], ]), ), ]), ), // .hero-acts:意见反馈 Container( margin: const EdgeInsets.only(top: 16), padding: const EdgeInsets.only(top: 14), decoration: BoxDecoration( border: Border(top: BorderSide(color: t.borderSubtle))), child: Row(children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('意见反馈', style: TextStyle( fontSize: AppDims.fsBody, fontWeight: FontWeight.w700, color: t.heading)), const SizedBox(height: 2), Text('遇到 Bug 或有功能建议,欢迎告诉我们', style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), ], ), ), DsFeedbackButtons(onOpen: _openFeedback), ]), ), ], ), ); } List<({IconData icon, String label, VoidCallback onTap})> _links() => [ ( icon: LucideIcons.globe, label: '官网', onTap: () => _open(AppInfo.website), ), ( icon: LucideIcons.messageCircle, label: '微信客服', onTap: () { showDsToast(context, '微信客服:${AppInfo.wechat.isNotEmpty ? AppInfo.wechat : '请见官网'}'); }, ), ( // 原型 i-ic03 = lock icon: LucideIcons.lock, label: '隐私政策', onTap: () => _open(AppInfo.privacyUrl.isNotEmpty ? AppInfo.privacyUrl : '${AppInfo.website}/privacy/'), ), ( icon: LucideIcons.fileText, label: '服务条款', onTap: () => _open(AppInfo.termsUrl.isNotEmpty ? AppInfo.termsUrl : '${AppInfo.website}/terms/'), ), ]; Future _open(String url) async { if (url.isEmpty) return; final uri = Uri.parse(url); if (await canLaunchUrl(uri)) launchUrl(uri); } /// 原型 .link-row:icon + 文字 + ›。 Widget _linkRow( dynamic t, ({IconData icon, String label, VoidCallback onTap}) l) { return InkWell( onTap: l.onTap, borderRadius: BorderRadius.circular(AppDims.rMd), child: Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: t.surface, border: Border.all(color: t.borderSubtle), borderRadius: BorderRadius.circular(AppDims.rMd), ), child: Row(children: [ Icon(l.icon, size: 15, color: t.primary), const SizedBox(width: 9), Expanded( child: Text(l.label, style: TextStyle(fontSize: AppDims.fsBody, color: t.text)), ), Text('›', style: TextStyle(fontSize: AppDims.fsTitle, color: t.faint)), ]), ), ); } void _openFeedback({required bool isBug}) { showAppDialog( context: context, builder: (_) => _FeedbackDialog(type: isBug ? 'bug' : 'suggestion'), ); } // ── 卡2:产品信息(2×2 cell)─────────────────────────────── Widget _productCard(dynamic t) { final appVersion = ref.watch(appVersionProvider).valueOrNull ?? 'v1.0.0'; final mobile = context.isMobile; final cells = [ _infoCell( t, '当前版本', Row(children: [ // 原型 .iv.mono:fs-title 15 / w700 / mono Text(appVersion, style: TextStyle( fontSize: AppDims.fsTitle, fontWeight: FontWeight.w700, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, color: t.heading)), const Spacer(), DsSmallGhostButton( label: '检查更新', icon: LucideIcons.refreshCw, onTap: _checkUpdate), ]), ), _infoCell(t, '出品方', _iv(t, '岩美科技 · YANMEI TECH')), _infoCell(t, '多端覆盖', _iv(t, 'Web · macOS · Windows · iOS · 安卓')), _infoCell(t, '产品定位', _iv(t, '酒水进销存 / 财务 / 防伪溯源')), ]; return _card( t, Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _cardH(t, LucideIcons.box, '产品信息'), if (mobile) Column(children: [ for (final c in cells) Padding(padding: const EdgeInsets.only(bottom: 10), child: c), ]) else Column(children: [ for (var row = 0; row < 2; row++) Padding( padding: EdgeInsets.only(bottom: row == 0 ? 14 : 0), child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (var col = 0; col < 2; col++) ...[ if (col > 0) const SizedBox(width: 14), Expanded(child: cells[row * 2 + col]), ], ], ), ), ), ]), ]), ); } Widget _iv(dynamic t, String v) => Text(v, style: TextStyle( fontSize: AppDims.fsBody, fontWeight: FontWeight.w600, color: t.heading)); /// 原型 .info-cell:bg 底 / border-subtle / r-md / pad 14 16。 Widget _infoCell(dynamic t, String label, Widget value) => Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), decoration: BoxDecoration( color: t.bg, border: Border.all(color: t.borderSubtle), borderRadius: BorderRadius.circular(AppDims.rMd), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)), const SizedBox(height: 6), value, ], ), ); Future _checkUpdate() async { showDsToast(context, '正在检查更新…'); await ref.read(updateProvider.notifier).forceCheck(); if (!mounted) return; final r = ref.read(updateProvider).valueOrNull; if (r == null) { showDsToast(context, '检查更新失败,请检查网络'); } else if (r.hasUpdate) { showDsToast(context, '发现新版本 v${r.latestVersion}'); startInAppUpdate(context, r); } else { showDsToast(context, '已是最新版 v${r.latestVersion} ✓'); } } // ── 卡3:更新日志(timeline,默认 2 条 + 展开)─────────────── // (原授权信息卡已迁至 设置 → 授权管理) Widget _changelogCard(dynamic t) { final appVersion = ref.watch(appVersionProvider).valueOrNull ?? ''; final entries = ref.watch(changelogProvider).valueOrNull ?? const []; final shown = _logExpanded ? entries : entries.take(_logShown).toList(); return _card( t, Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _cardH(t, LucideIcons.history, '更新日志'), if (entries.isEmpty) Padding( padding: const EdgeInsets.only(bottom: 4), child: Text(appVersion.isEmpty ? '暂无更新记录' : '当前版本 $appVersion', style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), ) else ...[ // .timeline:左竖线 + 圆点 for (var i = 0; i < shown.length; i++) _tlItem(t, shown[i], current: i == 0, last: i == shown.length - 1), if (entries.length > _logShown) Padding( padding: const EdgeInsets.only(top: 10), child: Align( alignment: Alignment.centerLeft, child: DsSmallGhostButton( label: _logExpanded ? '收起' : '更多历史更新', onTap: () => setState(() => _logExpanded = !_logExpanded), ), ), ), ], ]), ); } Widget _tlItem(dynamic t, ChangelogEntry e, {required bool current, required bool last}) { return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 竖线 + 圆点 SizedBox( width: 24, child: Column(children: [ Container( width: 9, height: 9, margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( shape: BoxShape.circle, color: current ? t.primary : t.surface, border: Border.all(color: t.primary, width: 1.5), ), ), if (!last) Expanded( child: Container(width: 1.5, color: t.borderSubtle), ), ]), ), Expanded( child: Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Text('v${e.version}', style: TextStyle( fontWeight: FontWeight.w700, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontSize: AppDims.fsBody, color: t.heading)), const SizedBox(width: 8), Text(e.date, style: TextStyle(fontSize: AppDims.fsXs, color: t.faint)), ]), for (final s in e.sections) ...[ const SizedBox(height: 8), _tlTag(t, s.type), for (final item in s.items) Padding( padding: const EdgeInsets.only(top: 4, left: 2), child: Text('· $item', style: TextStyle( fontSize: AppDims.fsSm, color: t.text)), ), ], ], ), ), ), ], ), ); } /// 原型 .tl-tag:feat=success / impr=info(primary) / fix=warn。 Widget _tlTag(dynamic t, String type) { final (label, color) = switch (type) { 'feat' || '新功能' => ('新功能', t.success), 'impr' || 'improve' || '改进' => ('改进', t.primary), 'fix' || '修复' => ('修复', t.warn), _ => (type, t.muted), }; return Text(label, style: TextStyle( fontSize: AppDims.fsXs, fontWeight: FontWeight.w700, color: color)); } } /// hero-acts 的两个反馈按钮(原型 .btn.primary 反馈 Bug + .btn.ghost 功能建议)。 class DsFeedbackButtons extends StatelessWidget { final void Function({required bool isBug}) onOpen; const DsFeedbackButtons({super.key, required this.onOpen}); @override Widget build(BuildContext context) { final t = context.tokens; Widget btn(String label, IconData icon, bool primary) => InkWell( onTap: () => onOpen(isBug: primary), borderRadius: BorderRadius.circular(AppDims.rMd), child: Container( height: 38, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: primary ? t.primary : t.surface, border: Border.all(color: primary ? t.primary : t.border), borderRadius: BorderRadius.circular(AppDims.rMd), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 16, color: primary ? t.onPrimary : t.text), const SizedBox(width: 7), Text(label, style: TextStyle( fontSize: AppDims.fsBody, fontWeight: FontWeight.w600, color: primary ? t.onPrimary : t.text)), ]), ), ); return Row(mainAxisSize: MainAxisSize.min, children: [ btn('反馈 Bug', LucideIcons.bug, true), const SizedBox(width: 10), btn('功能建议', LucideIcons.lightbulb, false), ]); } } /// 小号 ghost 按钮(.btn.ghost.sm,可带前导图标)。 class DsSmallGhostButton extends StatelessWidget { final String label; final IconData? icon; final VoidCallback onTap; const DsSmallGhostButton( {super.key, required this.label, this.icon, required this.onTap}); @override Widget build(BuildContext context) { final t = context.tokens; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(AppDims.rMd), child: Container( height: 32, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: t.surface, border: Border.all(color: t.border), borderRadius: BorderRadius.circular(AppDims.rMd), ), alignment: Alignment.center, child: Row(mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: 14, color: t.text), const SizedBox(width: 6), ], Text(label, style: TextStyle( fontSize: AppDims.fsSm, fontWeight: FontWeight.w600, color: t.text)), ]), ), ); } } /// 反馈表单弹窗:文字 + 附图,直接提交后台(沿用原实现)。 class _FeedbackDialog extends ConsumerStatefulWidget { final String type; // bug / suggestion const _FeedbackDialog({required this.type}); @override ConsumerState<_FeedbackDialog> createState() => _FeedbackDialogState(); } class _FeedbackDialogState extends ConsumerState<_FeedbackDialog> { final _ctrl = TextEditingController(); final List _images = []; // 已上传的图片 URL bool _uploading = false; bool _submitting = false; static const _maxImages = 9; @override void dispose() { _ctrl.dispose(); super.dispose(); } Future _addImages() async { final result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: true, withData: true, ); if (result == null) return; setState(() => _uploading = true); try { final repo = ref.read(feedbackRepositoryProvider); for (final f in result.files) { if (_images.length >= _maxImages) break; if (f.bytes == null) continue; final url = await repo.uploadImage(bytes: f.bytes!, filename: f.name); if (!mounted) return; setState(() => _images.add(url)); } } catch (e) { if (mounted) { showDsToast(context, '图片上传失败:$e'); } } finally { if (mounted) setState(() => _uploading = false); } } Future _submit() async { if (_ctrl.text.trim().isEmpty && _images.isEmpty) { showDsToast(context, '请先填写反馈内容'); return; } setState(() => _submitting = true); try { await ref.read(feedbackRepositoryProvider).submit( type: widget.type, content: _ctrl.text.trim(), images: _images, ); if (!mounted) return; Navigator.pop(context); showDsToast(context, '已提交反馈,感谢你的反馈 ✓'); } catch (e) { if (!mounted) return; setState(() => _submitting = false); showDsToast(context, '提交失败:$e'); } } @override Widget build(BuildContext context) { final t = context.tokens; final isBug = widget.type == 'bug'; return AlertDialog( title: Text(isBug ? '反馈 Bug' : '功能建议'), content: SizedBox( width: context.dialogWidth(480), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: _ctrl, maxLines: 6, decoration: InputDecoration( hintText: isBug ? '请描述你遇到的问题或建议,我们会尽快跟进…' : '请描述您希望增加的功能…', border: const OutlineInputBorder(), ), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ for (int i = 0; i < _images.length; i++) _thumb(t, i), if (_images.length < _maxImages) _addButton(t), ], ), if (_uploading) const Padding( padding: EdgeInsets.only(top: 8), child: Row(children: [ SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2)), SizedBox(width: 8), Text('图片上传中…', style: TextStyle(fontSize: 12)), ]), ), ], ), ), actions: [ DsButton('取消', onPressed: _submitting ? null : () => Navigator.pop(context)), DsButton( _submitting ? '提交中…' : '提交反馈', variant: DsBtnVariant.primary, onPressed: (_submitting || _uploading) ? null : _submit, ), ], ); } Widget _thumb(dynamic t, int i) { return Stack( clipBehavior: Clip.none, children: [ ClipRRect( borderRadius: BorderRadius.circular(AppDims.rMd), child: Image.network( '${AppConfig.baseUrl}${_images[i]}', width: 64, height: 64, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container( width: 64, height: 64, color: t.borderSubtle, child: Icon(LucideIcons.imageOff, size: 20, color: t.muted), ), ), ), Positioned( right: -8, top: -8, child: GestureDetector( onTap: () => setState(() => _images.removeAt(i)), child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration(color: t.danger, shape: BoxShape.circle), child: Icon(LucideIcons.x, size: 12, color: t.onPrimary), ), ), ), ], ); } Widget _addButton(dynamic t) { return InkWell( onTap: _uploading ? null : _addImages, borderRadius: BorderRadius.circular(AppDims.rMd), child: Container( width: 64, height: 64, decoration: BoxDecoration( border: Border.all(color: t.border), borderRadius: BorderRadius.circular(AppDims.rMd), color: t.bg, ), child: Icon(LucideIcons.imagePlus, size: 20, color: t.muted), ), ); } }