feat(client): 设置/用户/设备/关于窄屏对齐移动原型(hub 形态/卡片流/sheet/无打印)
- 系统设置窄屏改 hub 三组(门店/偏好/数据):门店信息/编号规则/默认仓库走 底部 sheet,主题外观接 showThemeSheet,?tab= 深链窄屏忽略;桌面 subnav 不动 - 用户管理窄屏:m-section + 头像卡片流 + 角色图标徽章 + FAB 新增; 点卡开详情 sheet(drow + 重置密码/启停/编辑),表单复用为 sheet 形态 - 设备管理窄屏:会话卡流(本机标注/在线图标徽章)+ 外设卡流(mc-foot 脚注), 「+ 添加设备」在区块标题行右侧(m-pill 选择 sheet),打印模板窄屏不渲染 - 关于我们窄屏:品牌 hero + hub 链接组 + 产品信息 drow 卡 + 时间线 pill 标签 + 意见反馈 sheet(类型 pill/描述/联系方式);授权信息不显示(已迁 /me/license) - golden:四屏窄屏基准独立成 *_mobile_golden_test.dart(390×844 @2x 三主题), 桌面 golden 零漂移 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
// 授权信息(3 cell, licenseProvider 实数据) / 更新日志(timeline, /public/release
|
||||
// 与官网同源,默认 2 条可展开)。旧版多余卡(帮助文档/扫码防伪/系统信息)删除,
|
||||
// 构建号并入版本 cell 小字(差异记 design/CONTRACT.md)。
|
||||
// 窄屏(原型 m-about.html):品牌 hero(图标+名+版本+检查更新+反馈) + hub 链接组 +
|
||||
// 产品信息卡(drow) + 更新日志时间线(pill 标签) + 反馈 sheet + 页脚备案;
|
||||
// 授权信息窄屏不显示(已迁 /me/license)。
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -23,6 +26,8 @@ import '../../core/theme/app_fonts.dart';
|
||||
import '../../widgets/brand_mark.dart';
|
||||
import '../../widgets/ds/ds_atoms.dart';
|
||||
import '../../widgets/ds/ds_toast.dart';
|
||||
import '../../widgets/ds/m_hub.dart';
|
||||
import '../../widgets/ds/m_sheet.dart';
|
||||
|
||||
class AboutScreen extends ConsumerStatefulWidget {
|
||||
const AboutScreen({super.key});
|
||||
@@ -38,12 +43,12 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
// ── 窄屏(原型 m-about.html):hero + hub 链接组 + 产品信息 + 时间线 ──
|
||||
if (context.isMobile) return _mobileBody(t);
|
||||
return Container(
|
||||
color: t.bg,
|
||||
child: SingleChildScrollView(
|
||||
padding: context.isMobile
|
||||
? const EdgeInsets.all(AppDims.sp4)
|
||||
: const EdgeInsets.fromLTRB(26, 22, 26, 22),
|
||||
padding: const EdgeInsets.fromLTRB(26, 22, 26, 22),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 760),
|
||||
@@ -80,6 +85,284 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── 窄屏整体(原型 m-about.html)────────────────────────────
|
||||
Widget _mobileBody(dynamic t) {
|
||||
return Container(
|
||||
color: t.bg,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(14),
|
||||
children: [
|
||||
_mHero(t),
|
||||
// hub 链接组(原型 .m-hub margin-top:10)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: MHubGroup(items: [
|
||||
MHubItem(
|
||||
icon: LucideIcons.globe,
|
||||
label: '官方网站',
|
||||
onTap: () => _open(AppInfo.website)),
|
||||
MHubItem(
|
||||
icon: LucideIcons.messageCircle,
|
||||
label: '微信客服',
|
||||
onTap: () => showDsToast(context,
|
||||
'微信客服:${AppInfo.wechat.isNotEmpty ? AppInfo.wechat : '请见官网'}')),
|
||||
MHubItem(
|
||||
// 原型 m-about 用 i-shield-2
|
||||
icon: LucideIcons.shield,
|
||||
label: '隐私政策',
|
||||
onTap: () => _open(AppInfo.privacyUrl.isNotEmpty
|
||||
? AppInfo.privacyUrl
|
||||
: '${AppInfo.website}/privacy/')),
|
||||
MHubItem(
|
||||
icon: LucideIcons.fileText,
|
||||
label: '服务条款',
|
||||
onTap: () => _open(AppInfo.termsUrl.isNotEmpty
|
||||
? AppInfo.termsUrl
|
||||
: '${AppInfo.website}/terms/')),
|
||||
]),
|
||||
),
|
||||
_mSection(t, '产品信息'),
|
||||
_mProductCard(t),
|
||||
_mSection(t, '更新日志'),
|
||||
_mChangelogCard(t),
|
||||
// 页脚 © + 备案号
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 18, 0, 6),
|
||||
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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// .m-section
|
||||
Widget _mSection(dynamic t, String label) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(2, 16, 2, 8),
|
||||
child: Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: .4,
|
||||
color: t.muted)),
|
||||
);
|
||||
|
||||
/// 原型 .ab-hero:居中品牌图标 + 名称 + 版本 + 检查更新 + 反馈双按钮。
|
||||
Widget _mHero(dynamic t) {
|
||||
final appVersion = ref.watch(appVersionProvider).valueOrNull ?? '';
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: t.surface,
|
||||
border: Border.all(color: t.border),
|
||||
borderRadius: BorderRadius.circular(AppDims.rLg),
|
||||
),
|
||||
child: Column(children: [
|
||||
const BrandMark(size: 72),
|
||||
const SizedBox(height: 14),
|
||||
Text('岩美酒库',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsH1,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: t.heading)),
|
||||
const SizedBox(height: 4),
|
||||
// 版本号 mono、中文说明走默认字体(mono 无 CJK 字形)
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (appVersion.isNotEmpty)
|
||||
Text('$appVersion · ',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
color: t.muted,
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback)),
|
||||
Text('酒水进销存管理系统',
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
DsSmallGhostButton(
|
||||
label: '检查更新', icon: LucideIcons.refreshCw, onTap: _checkUpdate),
|
||||
const SizedBox(height: 18),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: DsButton('反馈 Bug',
|
||||
icon: LucideIcons.bug,
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => _openFeedbackSheet('Bug')),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DsButton('功能建议',
|
||||
icon: LucideIcons.lightbulb,
|
||||
onPressed: () => _openFeedbackSheet('功能建议')),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/// 产品信息卡(原型 .pe-card + drow:出品方 / 多端覆盖 / 产品定位)。
|
||||
Widget _mProductCard(dynamic t) {
|
||||
Widget drow(String label, String value, {bool last = false}) => Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 11),
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
last ? null : Border(bottom: BorderSide(color: t.borderSubtle)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(value,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: t.text)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(14, 2, 14, 2),
|
||||
decoration: BoxDecoration(
|
||||
color: t.surface,
|
||||
border: Border.all(color: t.border),
|
||||
borderRadius: BorderRadius.circular(AppDims.rLg),
|
||||
),
|
||||
child: Column(children: [
|
||||
drow('出品方', '岩美科技 · YANMEI TECH'),
|
||||
drow('多端覆盖', 'Web · macOS · Windows · iOS · 安卓'),
|
||||
drow('产品定位', '酒水进销存 / 财务 / 防伪溯源', last: true),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/// 更新日志卡(原型 .tl 时间线:竖线 + 圆点 + 版本/日期 + pill 分组标签)。
|
||||
Widget _mChangelogCard(dynamic t) {
|
||||
final appVersion = ref.watch(appVersionProvider).valueOrNull ?? '';
|
||||
final entries = ref.watch(changelogProvider).valueOrNull ?? const [];
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: t.surface,
|
||||
border: Border.all(color: t.border),
|
||||
borderRadius: BorderRadius.circular(AppDims.rLg),
|
||||
),
|
||||
child: entries.isEmpty
|
||||
? Text(appVersion.isEmpty ? '暂无更新记录' : '当前版本 $appVersion',
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted))
|
||||
: Column(children: [
|
||||
for (var i = 0; i < entries.length; i++)
|
||||
_mTlItem(t, entries[i], last: i == entries.length - 1),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/// 原型 .tl-item:左 2px 竖线 + 8px primary 圆点 + 版本/日期 + 分组。
|
||||
Widget _mTlItem(dynamic t, ChangelogEntry e, {required bool last}) {
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
child: Column(children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
decoration:
|
||||
BoxDecoration(shape: BoxShape.circle, color: t.primary),
|
||||
),
|
||||
if (!last)
|
||||
Expanded(child: Container(width: 2, 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,
|
||||
fontSize: AppDims.fsBody,
|
||||
color: t.heading)),
|
||||
const SizedBox(width: 8),
|
||||
Text(e.date,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsXs,
|
||||
color: t.faint,
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback)),
|
||||
]),
|
||||
for (final s in e.sections) ...[
|
||||
const SizedBox(height: 8),
|
||||
_mTlTag(t, s.type),
|
||||
for (final item in s.items)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 5, left: 2),
|
||||
child: Text('· $item',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
height: 1.5,
|
||||
color: t.text)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 原型 .tlt:pill 标签(feat=success-bg / impr=info-soft / fix=warn-bg)。
|
||||
Widget _mTlTag(dynamic t, String type) {
|
||||
final (label, bg, fg) = switch (type) {
|
||||
'feat' || '新功能' => ('新功能', t.successBg, t.success),
|
||||
'impr' || 'improve' || '改进' => ('改进', t.infoSoft, t.primary),
|
||||
'fix' || '修复' => ('修复', t.warnBg, t.warn),
|
||||
_ => (type, t.borderSubtle, t.muted),
|
||||
};
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(AppDims.rPill),
|
||||
),
|
||||
child: Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsXs, fontWeight: FontWeight.w600, color: fg)),
|
||||
);
|
||||
}
|
||||
|
||||
/// 意见反馈 sheet(原型 openFb:类型 pill Bug/功能建议/其他 + 描述 + 联系方式)。
|
||||
void _openFeedbackSheet(String initialType) {
|
||||
showMSheet<void>(
|
||||
context,
|
||||
title: '意见反馈',
|
||||
builder: (_) => _FeedbackSheetBody(initialType: initialType),
|
||||
);
|
||||
}
|
||||
|
||||
/// 原型 .card:surface / border / r-lg / pad 20 22 / mb18。
|
||||
Widget _card(dynamic t, Widget child, {Gradient? gradient}) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 18),
|
||||
@@ -560,6 +843,131 @@ class DsSmallGhostButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 窄屏意见反馈 sheet 内容(原型 m-about drawFb:类型 pill + 描述 + 联系方式)。
|
||||
/// 提交沿用 feedbackRepository.submit(联系方式并入 content,接口无独立字段)。
|
||||
class _FeedbackSheetBody extends ConsumerStatefulWidget {
|
||||
final String initialType; // Bug / 功能建议 / 其他
|
||||
const _FeedbackSheetBody({required this.initialType});
|
||||
|
||||
@override
|
||||
ConsumerState<_FeedbackSheetBody> createState() => _FeedbackSheetBodyState();
|
||||
}
|
||||
|
||||
class _FeedbackSheetBodyState extends ConsumerState<_FeedbackSheetBody> {
|
||||
static const _types = ['Bug', '功能建议', '其他'];
|
||||
late String _type =
|
||||
_types.contains(widget.initialType) ? widget.initialType : _types.first;
|
||||
final _descCtrl = TextEditingController();
|
||||
final _contactCtrl = TextEditingController();
|
||||
bool _submitting = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_descCtrl.dispose();
|
||||
_contactCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (_descCtrl.text.trim().isEmpty) {
|
||||
showDsToast(context, '请先填写反馈内容');
|
||||
return;
|
||||
}
|
||||
setState(() => _submitting = true);
|
||||
final apiType = switch (_type) {
|
||||
'Bug' => 'bug',
|
||||
'功能建议' => 'suggestion',
|
||||
_ => 'other',
|
||||
};
|
||||
final contact = _contactCtrl.text.trim();
|
||||
final content = contact.isEmpty
|
||||
? _descCtrl.text.trim()
|
||||
: '${_descCtrl.text.trim()}\n联系方式:$contact';
|
||||
try {
|
||||
await ref.read(feedbackRepositoryProvider).submit(
|
||||
type: apiType,
|
||||
content: content,
|
||||
images: const [],
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showDsToast(context, '感谢反馈,我们会尽快跟进 ✓');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _submitting = false);
|
||||
showDsToast(context, '提交失败:$e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
Widget label(String s) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child:
|
||||
Text(s, style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
label('反馈类型'),
|
||||
Wrap(spacing: 8, children: [
|
||||
for (final v in _types)
|
||||
// 原型 .m-pill:h32 / r-pill / on=brand50+primary
|
||||
InkWell(
|
||||
onTap: () => setState(() => _type = v),
|
||||
borderRadius: BorderRadius.circular(AppDims.rPill),
|
||||
child: Container(
|
||||
height: 32,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 13),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: v == _type ? t.brand50 : t.surface,
|
||||
border: Border.all(color: v == _type ? t.primary : t.border),
|
||||
borderRadius: BorderRadius.circular(AppDims.rPill),
|
||||
),
|
||||
child: Text(v,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
fontWeight:
|
||||
v == _type ? FontWeight.w600 : FontWeight.w400,
|
||||
color: v == _type ? t.primary : t.text)),
|
||||
),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
label('问题描述'),
|
||||
TextField(
|
||||
controller: _descCtrl,
|
||||
maxLines: 4,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '请描述遇到的问题或建议…',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
label('联系方式(选填)'),
|
||||
DsInput(controller: _contactCtrl, hintText: '微信 / 手机号,便于我们回访'),
|
||||
const SizedBox(height: 16),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: DsButton('取消',
|
||||
onPressed:
|
||||
_submitting ? null : () => Navigator.of(context).pop()),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: DsButton(_submitting ? '提交中…' : '提交',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: _submitting ? null : _submit),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 反馈表单弹窗:文字 + 附图,直接提交后台(沿用原实现)。
|
||||
class _FeedbackDialog extends ConsumerStatefulWidget {
|
||||
final String type; // bug / suggestion
|
||||
|
||||
Reference in New Issue
Block a user