6238b86dcb
- 登录/注册(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
363 lines
12 KiB
Dart
363 lines
12 KiB
Dart
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||
import 'dart:math' as math;
|
||
|
||
import 'package:flutter/material.dart';
|
||
|
||
import '../core/theme/context_tokens.dart';
|
||
import '../core/theme/app_dims.g.dart';
|
||
import '../core/theme/app_fonts.dart';
|
||
|
||
/// 右侧滑入抽屉呈现(对齐原型 .drawer / .drawer-mask)。
|
||
/// useRootNavigator:false → 只覆盖内容区(分支 Navigator),不盖侧栏/顶栏。
|
||
const _kDrawerMask = Color(0x52000000); // ds-ignore: .drawer-mask 遮罩固定色(黑 32%)
|
||
|
||
Future<T?> showOrderDetailDrawer<T>(
|
||
BuildContext context, {
|
||
required WidgetBuilder builder,
|
||
}) {
|
||
return showGeneralDialog<T>(
|
||
context: context,
|
||
useRootNavigator: false,
|
||
barrierDismissible: true,
|
||
barrierLabel: '关闭',
|
||
// .drawer-mask scrim(抽屉遮罩固定色)
|
||
barrierColor: _kDrawerMask,
|
||
transitionDuration: const Duration(milliseconds: 250),
|
||
pageBuilder: (ctx, _, __) {
|
||
final w = math.min(600.0, MediaQuery.of(ctx).size.width * 0.94);
|
||
return Align(
|
||
alignment: Alignment.centerRight,
|
||
child: Material(
|
||
color: ctx.tokens.surface,
|
||
elevation: 16,
|
||
// 抽屉在 app_shell 的全局 SelectionArea 之外(独立 overlay)→ 自带一层,文字可选中复制。
|
||
child: SelectionArea(
|
||
child: SizedBox(
|
||
width: w, height: double.infinity, child: builder(ctx)),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
transitionBuilder: (ctx, anim, _, child) => SlideTransition(
|
||
position: Tween<Offset>(begin: const Offset(1, 0), end: Offset.zero)
|
||
.animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)),
|
||
child: child,
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 抽屉明细行(对齐原型 .lines .lr,5 列:商品·编码 / 系列·规格 / 数量 / 单价 / 金额)。
|
||
class OrderLine {
|
||
final String name;
|
||
final String code;
|
||
final String series;
|
||
final String spec;
|
||
final String qty;
|
||
final String price; // 已算好文案(或「待定价」)
|
||
final String amount;
|
||
final bool pending; // 单价<=0:单价/金额显示待定价样式
|
||
const OrderLine({
|
||
required this.name,
|
||
required this.code,
|
||
required this.series,
|
||
required this.spec,
|
||
required this.qty,
|
||
required this.price,
|
||
required this.amount,
|
||
this.pending = false,
|
||
});
|
||
}
|
||
|
||
/// 抽屉底部操作分组(对齐原型 .dact-group:标签 + 一排按钮)。
|
||
class DrawerActionGroup {
|
||
final String label;
|
||
final List<Widget> buttons;
|
||
const DrawerActionGroup(this.label, this.buttons);
|
||
}
|
||
|
||
/// 详情抽屉骨架(对齐原型 openDetail 布局):
|
||
/// 头(单号+X) / 信息行(.drow) / 明细标题 / 明细表(.lines) / 合计(.ltotal) / 操作组(.dactions)。
|
||
class OrderDetailDrawer extends StatelessWidget {
|
||
final String title;
|
||
final List<({String label, Widget value})> infoRows;
|
||
final String linesLabel; // 入库明细 / 出库明细
|
||
final List<OrderLine> lines;
|
||
final String totalText; // 合计金额(已格式化)
|
||
final List<DrawerActionGroup> actionGroups;
|
||
|
||
const OrderDetailDrawer({
|
||
super.key,
|
||
required this.title,
|
||
required this.infoRows,
|
||
required this.linesLabel,
|
||
required this.lines,
|
||
required this.totalText,
|
||
required this.actionGroups,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
// ── 头:单号 + 关闭 ──(.drawer-head)
|
||
Container(
|
||
padding: const EdgeInsets.fromLTRB(20, 18, 16, 18),
|
||
decoration: BoxDecoration(
|
||
border: Border(bottom: BorderSide(color: t.border))),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(title,
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsH2,
|
||
fontWeight: FontWeight.w700,
|
||
color: t.heading)),
|
||
),
|
||
IconButton(
|
||
icon: Icon(LucideIcons.x, size: 22, color: t.muted),
|
||
splashRadius: 20,
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
// ── 信息行(.drow)──
|
||
for (final r in infoRows)
|
||
_DrawerRow(label: r.label, value: r.value),
|
||
// ── 明细标题 ──
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 8),
|
||
child: Text(linesLabel,
|
||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||
),
|
||
_LinesTable(lines: lines),
|
||
// ── 合计(.ltotal)──
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(4, 8, 4, 0),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: [
|
||
Text('合计金额 ',
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsBody, color: t.muted)),
|
||
const SizedBox(width: 6),
|
||
Text(totalText,
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback,
|
||
fontWeight: FontWeight.w700,
|
||
color: t.accent)),
|
||
],
|
||
),
|
||
),
|
||
// ── 操作组(.dactions)──
|
||
if (actionGroups.isNotEmpty) const SizedBox(height: 18),
|
||
for (var i = 0; i < actionGroups.length; i++) ...[
|
||
if (i > 0) const SizedBox(height: 14),
|
||
_ActionGroup(group: actionGroups[i]),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _DrawerRow extends StatelessWidget {
|
||
final String label;
|
||
final Widget value;
|
||
const _DrawerRow({required this.label, required this.value});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 11),
|
||
decoration: BoxDecoration(
|
||
border: Border(bottom: BorderSide(color: t.borderSubtle))),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(label,
|
||
style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
|
||
const Spacer(),
|
||
DefaultTextStyle(
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsBody,
|
||
fontWeight: FontWeight.w600,
|
||
color: t.text),
|
||
child: value,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LinesTable extends StatelessWidget {
|
||
final List<OrderLine> lines;
|
||
const _LinesTable({required this.lines});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: t.border),
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// 表头
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||
decoration: BoxDecoration(
|
||
color: t.thBg,
|
||
borderRadius:
|
||
const BorderRadius.vertical(top: Radius.circular(6)),
|
||
),
|
||
child: _lineRow(
|
||
context,
|
||
c1: Text('商品 · 编码',
|
||
style: _hStyle(t), overflow: TextOverflow.ellipsis),
|
||
c2: Text('系列 / 规格', style: _hStyle(t)),
|
||
c3: Text('数量', textAlign: TextAlign.right, style: _hStyle(t)),
|
||
c4: Text('单价', textAlign: TextAlign.right, style: _hStyle(t)),
|
||
c5: Text('金额', textAlign: TextAlign.right, style: _hStyle(t)),
|
||
),
|
||
),
|
||
for (var i = 0; i < lines.length; i++)
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||
decoration: BoxDecoration(
|
||
border: i == 0
|
||
? null
|
||
: Border(top: BorderSide(color: t.borderSubtle)),
|
||
),
|
||
child: _lineRow(
|
||
context,
|
||
c1: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(lines[i].name,
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsBody,
|
||
fontWeight: FontWeight.w600,
|
||
color: t.text)),
|
||
Text(lines[i].code,
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsXs,
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback,
|
||
color: t.faint)),
|
||
],
|
||
),
|
||
c2: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(lines[i].series,
|
||
style:
|
||
TextStyle(fontSize: AppDims.fsSm, color: t.text)),
|
||
Text(lines[i].spec,
|
||
style:
|
||
TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
|
||
],
|
||
),
|
||
c3: Text(lines[i].qty,
|
||
textAlign: TextAlign.right,
|
||
style: const TextStyle(
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback)),
|
||
c4: _priceCell(context, lines[i].price, lines[i].pending),
|
||
c5: _priceCell(context, lines[i].amount, lines[i].pending,
|
||
accent: true),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
TextStyle _hStyle(dynamic t) => TextStyle(
|
||
fontSize: AppDims.fsSm, color: t.muted, fontWeight: FontWeight.w600);
|
||
|
||
Widget _priceCell(BuildContext context, String text, bool pending,
|
||
{bool accent = false}) {
|
||
final t = context.tokens;
|
||
if (pending) {
|
||
return Text(text,
|
||
textAlign: TextAlign.right,
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsXs,
|
||
color: t.warn,
|
||
fontWeight: FontWeight.w600));
|
||
}
|
||
return Text(text,
|
||
textAlign: TextAlign.right,
|
||
style: TextStyle(
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback,
|
||
fontWeight: accent ? FontWeight.w700 : FontWeight.w400,
|
||
color: accent ? t.text : t.text));
|
||
}
|
||
|
||
// 5 列布局(对齐原型 grid 1fr 124 40 64 78)。
|
||
Widget _lineRow(BuildContext context,
|
||
{required Widget c1,
|
||
required Widget c2,
|
||
required Widget c3,
|
||
required Widget c4,
|
||
required Widget c5}) {
|
||
return Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(child: c1),
|
||
const SizedBox(width: 8),
|
||
SizedBox(width: 96, child: c2),
|
||
const SizedBox(width: 8),
|
||
SizedBox(width: 34, child: c3),
|
||
const SizedBox(width: 8),
|
||
SizedBox(width: 60, child: c4),
|
||
const SizedBox(width: 8),
|
||
SizedBox(width: 72, child: c5),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ActionGroup extends StatelessWidget {
|
||
final DrawerActionGroup group;
|
||
const _ActionGroup({required this.group});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(group.label,
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsXs,
|
||
color: t.muted,
|
||
fontWeight: FontWeight.w600,
|
||
letterSpacing: 0.4)),
|
||
const SizedBox(height: 8),
|
||
Wrap(spacing: 10, runSpacing: 10, children: group.buttons),
|
||
],
|
||
);
|
||
}
|
||
}
|