feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏

- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服
  pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验;
  登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段,
  已记 CONTRACT,screens.mjs 留存根)
- ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/
  DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态
- 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、
  h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、
  BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast)
- 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后
  失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览
- 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/
  SelectProductDialog/tabStateProvider
- golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打;
  修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-03 09:58:14 +08:00
parent ca7595b113
commit 6238b86dcb
286 changed files with 16831 additions and 11136 deletions
+61 -63
View File
@@ -1,3 +1,4 @@
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../core/utils/dialog_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -21,6 +22,8 @@ import '../../widgets/network_retry_button.dart';
import '../../widgets/theme_picker_pill.dart';
import '../../widgets/notification_bell.dart';
import '../../widgets/app_status_bar.dart';
import '../../widgets/ds/ds_atoms.dart';
import '../../widgets/ds/ds_toast.dart';
/// 侧栏导航项(含其 StatefulShellRoute 分支序号 = router 中的 branch index)。
class _NavItem {
@@ -40,19 +43,19 @@ class _NavGroup {
// 分支序号必须与 router StatefulShellRoute 的 branch 顺序一致(勿改顺序)。
const _navGroups = <_NavGroup>[
_NavGroup('单据', [
_NavItem(Icons.file_download_outlined, '入库管理', 0),
_NavItem(Icons.file_upload_outlined, '出库管理', 1),
_NavItem(LucideIcons.download, '入库管理', 0),
_NavItem(LucideIcons.upload, '出库管理', 1),
]),
_NavGroup('经营', [
_NavItem(Icons.inventory_2_outlined, '库存管理', 2),
_NavItem(Icons.receipt_long_outlined, '财务管理', 3),
_NavItem(Icons.people_alt_outlined, '往来单位', 4),
_NavItem(Icons.grid_view_outlined, '基础数据', 5),
_NavItem(LucideIcons.box, '库存管理', 2),
_NavItem(LucideIcons.receiptText, '财务管理', 3),
_NavItem(LucideIcons.users, '往来单位', 4),
_NavItem(LucideIcons.layoutGrid, '基础数据', 5),
]),
_NavGroup('系统', [
_NavItem(Icons.devices_outlined, '设备管理', 6),
_NavItem(Icons.settings_outlined, '系统设置', 7),
_NavItem(Icons.info_outline, '关于我们', 8),
_NavItem(LucideIcons.monitorSmartphone, '设备管理', 6),
_NavItem(LucideIcons.settings, '系统设置', 7),
_NavItem(LucideIcons.info, '关于我们', 8),
]),
];
@@ -80,8 +83,7 @@ class AppShell extends ConsumerStatefulWidget {
}
class _AppShellState extends ConsumerState<AppShell> {
final String _loginTime =
DateFormat('HH:mm:ss').format(AppStatusBar.clock());
final String _loginTime = DateFormat('HH:mm:ss').format(AppStatusBar.clock());
bool _forceDialogShown = false;
bool _licenseDialogShown = false;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@@ -97,8 +99,8 @@ class _AppShellState extends ConsumerState<AppShell> {
if (rb == null || overlay == null) return;
final topLeft = rb.localToGlobal(Offset.zero, ancestor: overlay);
final items = <PopupMenuEntry<String>>[
_userMenuItem('profile', Icons.manage_accounts_outlined, '个人设置', t),
_userMenuItem('logout', Icons.logout, '退出登录', t),
_userMenuItem('profile', LucideIcons.userCog, '个人设置', t),
_userMenuItem('logout', LucideIcons.logOut, '退出登录', t),
];
// 菜单弹到按钮上方:top = 按钮顶 − 菜单估高 − 间隙(菜单项 40 + 上下 padding 8*2
const itemH = 40.0;
@@ -143,9 +145,7 @@ class _AppShellState extends ConsumerState<AppShell> {
// 全局轻提示(写请求被后端 403 拒绝等):统一在此弹出并清空。
ref.listen<String?>(apiMessageProvider, (prev, next) {
if (next == null || next.isEmpty) return;
ScaffoldMessenger.of(context)
..clearSnackBars()
..showSnackBar(SnackBar(content: Text(next)));
showDsToast(context, next);
ref.read(apiMessageProvider.notifier).state = null;
});
final isOnline = ref.watch(connectivityProvider);
@@ -170,8 +170,8 @@ class _AppShellState extends ConsumerState<AppShell> {
Expanded(
child: Column(
children: [
..._buildBanners(
context, isOnline, updateInfo, updateNotifier, licenseInfo),
..._buildBanners(context, isOnline, updateInfo,
updateNotifier, licenseInfo),
Expanded(child: widget.navigationShell),
if (!isMobile) AppStatusBar(loginTime: _loginTime),
],
@@ -196,7 +196,8 @@ class _AppShellState extends ConsumerState<AppShell> {
color: t.topBg,
border: Border(bottom: BorderSide(color: t.topBorder)),
),
padding: EdgeInsets.only(top: topInset, left: isMobile ? 6 : 18, right: 14),
padding:
EdgeInsets.only(top: topInset, left: isMobile ? 6 : 18, right: 14),
child: Row(
children: [
// 左组(占满剩余空间,靠左):把右组(主题器+铃)挤到最右
@@ -205,7 +206,7 @@ class _AppShellState extends ConsumerState<AppShell> {
children: [
if (isMobile) ...[
IconButton(
icon: Icon(Icons.menu, color: t.topFg),
icon: Icon(LucideIcons.menu, color: t.topFg),
tooltip: '菜单',
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
),
@@ -250,7 +251,7 @@ class _AppShellState extends ConsumerState<AppShell> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.visibility_outlined, size: 13, color: AppChrome.onAccent),
Icon(LucideIcons.eye, size: 13, color: AppChrome.onAccent),
SizedBox(width: 4),
Text('只读',
style: TextStyle(
@@ -311,7 +312,8 @@ class _AppShellState extends ConsumerState<AppShell> {
Widget _buildSideUser(BuildContext context, AuthUser user) {
final t = context.tokens;
final shopName = ref.watch(shopInfoProvider).valueOrNull?.name ?? user.shopNo;
final shopName =
ref.watch(shopInfoProvider).valueOrNull?.name ?? user.shopNo;
return Column(
children: [
Container(
@@ -399,11 +401,10 @@ class _AppShellState extends ConsumerState<AppShell> {
color: t.sideBorder,
margin: const EdgeInsets.symmetric(horizontal: 16)),
ListTile(
leading: Icon(Icons.logout, color: t.sideFg),
leading: Icon(LucideIcons.logOut, color: t.sideFg),
title: Text('退出登录',
style: TextStyle(
color: t.sideActiveFg,
fontWeight: FontWeight.w500)),
color: t.sideActiveFg, fontWeight: FontWeight.w500)),
onTap: () {
Navigator.pop(context);
_logout();
@@ -437,7 +438,7 @@ class _AppShellState extends ConsumerState<AppShell> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Row(
children: [
Icon(Icons.system_update, size: 16, color: t.warn),
Icon(LucideIcons.hardDriveDownload, size: 16, color: t.warn),
const SizedBox(width: 8),
Expanded(
child: Text(
@@ -487,7 +488,7 @@ class _AppShellState extends ConsumerState<AppShell> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: const Row(
children: [
Icon(Icons.wifi_off, size: 16, color: AppChrome.onAccent),
Icon(LucideIcons.wifiOff, size: 16, color: AppChrome.onAccent),
SizedBox(width: 8),
Expanded(
child: Text(
@@ -516,7 +517,8 @@ class _AppShellState extends ConsumerState<AppShell> {
child: AlertDialog(
title: Row(
children: [
Icon(Icons.system_update, color: context.tokens.primary),
Icon(LucideIcons.hardDriveDownload,
color: context.tokens.primary),
const SizedBox(width: 8),
const Text('发现新版本'),
],
@@ -524,10 +526,9 @@ class _AppShellState extends ConsumerState<AppShell> {
content: Text('发现新版本 v${info.latestVersion},需更新后才能继续使用,'
'更新以获得最新功能与修复。'),
actions: [
ElevatedButton(
onPressed: () => startInAppUpdate(context, info),
child: const Text('立即更新'),
),
DsButton('立即更新',
variant: DsBtnVariant.primary,
onPressed: () => startInAppUpdate(context, info)),
],
),
),
@@ -543,7 +544,8 @@ class _AppShellState extends ConsumerState<AppShell> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Row(
children: [
const Icon(Icons.warning_amber_rounded, size: 16, color: AppChrome.onAccent),
const Icon(LucideIcons.triangleAlert,
size: 16, color: AppChrome.onAccent),
const SizedBox(width: 8),
Expanded(
child: Text(msg,
@@ -552,7 +554,8 @@ class _AppShellState extends ConsumerState<AppShell> {
),
TextButton(
onPressed: () => context.go('/settings?tab=license'),
style: TextButton.styleFrom(foregroundColor: AppChrome.onAccentFaint),
style:
TextButton.styleFrom(foregroundColor: AppChrome.onAccentFaint),
child: const Text('去激活'),
),
],
@@ -569,24 +572,20 @@ class _AppShellState extends ConsumerState<AppShell> {
barrierDismissible: true,
builder: (_) => AlertDialog(
title: Row(children: [
Icon(Icons.warning_amber_rounded, color: titleColor, size: 20),
Icon(LucideIcons.triangleAlert, color: titleColor, size: 20),
const SizedBox(width: 8),
Text(title, style: TextStyle(color: titleColor, fontSize: 16)),
]),
content: Text(body, style: const TextStyle(fontSize: 14)),
actions: [
TextButton(
onPressed: () => Navigator.pop(_),
child: const Text('稍后处理'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: titleColor),
onPressed: () {
Navigator.pop(_);
ctx.go('/settings?tab=license');
},
child: const Text('立即前往', style: TextStyle(color: AppChrome.onAccent)),
),
DsButton('稍后处理', onPressed: () => Navigator.pop(_)),
DsButton('立即前往',
variant: lic.phase == 'grace'
? DsBtnVariant.primary
: DsBtnVariant.danger, onPressed: () {
Navigator.pop(_);
ctx.go('/settings?tab=license');
}),
],
),
);
@@ -649,8 +648,8 @@ class _BrandMarkPainter extends CustomPainter {
..lineTo(50, 33);
canvas.drawPath(peak, p);
canvas.drawLine(const Offset(15, 41), const Offset(49, 41), p);
canvas.drawCircle(const Offset(32, 48), 3.4,
Paint()..color = AppChrome.brandDot);
canvas.drawCircle(
const Offset(32, 48), 3.4, Paint()..color = AppChrome.brandDot);
}
@override
@@ -741,8 +740,8 @@ class _ShopMark extends StatelessWidget {
decoration: BoxDecoration(color: bg, borderRadius: radius),
alignment: Alignment.center,
child: Text(initial,
style: TextStyle(
color: fg, fontSize: 13, fontWeight: FontWeight.w800)),
style:
TextStyle(color: fg, fontSize: 13, fontWeight: FontWeight.w800)),
);
}
}
@@ -824,7 +823,7 @@ class _SideUserTileState extends State<_SideUserTile> {
],
),
),
Icon(Icons.keyboard_arrow_down, size: 14, color: t.sideMuted),
Icon(LucideIcons.chevronDown, size: 14, color: t.sideMuted),
],
),
),
@@ -852,9 +851,8 @@ class _SidebarNavState extends State<_SidebarNav> {
Widget build(BuildContext context) {
final t = context.tokens;
final active = widget.active;
final Color bg = active
? t.sideActiveBg
: (_hover ? t.sideHover : Colors.transparent);
final Color bg =
active ? t.sideActiveBg : (_hover ? t.sideHover : Colors.transparent);
final Color fg = active || _hover ? t.sideActiveFg : t.sideFg;
return MouseRegion(
cursor: SystemMouseCursors.click,
@@ -912,7 +910,8 @@ void _showShopPanel(BuildContext context, AuthUser u,
),
child: Row(
children: [
const Icon(Icons.store, color: AppChrome.onAccent, size: 20),
const Icon(LucideIcons.store,
color: AppChrome.onAccent, size: 20),
const SizedBox(width: 10),
const Text('门店信息',
style: TextStyle(
@@ -922,7 +921,7 @@ void _showShopPanel(BuildContext context, AuthUser u,
const Spacer(),
IconButton(
onPressed: () => Navigator.pop(ctx),
icon: const Icon(Icons.close,
icon: const Icon(LucideIcons.x,
color: AppChrome.onAccentFaint, size: 18),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
@@ -934,18 +933,17 @@ void _showShopPanel(BuildContext context, AuthUser u,
padding: const EdgeInsets.all(20),
child: Column(
children: [
_InfoRow(icon: Icons.tag, label: '门店编号', value: u.shopNo),
_InfoRow(
icon: LucideIcons.tag, label: '门店编号', value: u.shopNo),
const SizedBox(height: 14),
_InfoRow(
icon: Icons.person, label: '登录账号', value: u.username),
icon: LucideIcons.user, label: '登录账号', value: u.username),
const SizedBox(height: 14),
_InfoRow(
icon: Icons.badge_outlined,
label: '姓名',
value: u.realName),
icon: LucideIcons.idCard, label: '姓名', value: u.realName),
const SizedBox(height: 14),
_InfoRow(
icon: Icons.info_outline, label: '系统版本', value: version),
icon: LucideIcons.info, label: '系统版本', value: version),
],
),
),