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:
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user