Files
jiu/client/lib/widgets/ds/m_sheet.dart
T
wangjia be9afcf68d feat(client): 移动壳基建——底部 5 tab + 我的 hub + 统一底部 sheet + 移动组件四件套
对齐移动原型体系(m-*.html)的 Stage 0 共享基建,桌面形态零改动:

- 导航:router 新增 branch 9(/me 我的 + /me/license 授权屏);窄屏 tab 根
  (库存/入库/出库/财务/我的)出底部 MTabBar,二级屏隐藏 tabbar + 顶栏返回箭头
  (PopScope 拦系统返回键防退 app);窄屏 Drawer/汉堡删除;窄屏顶栏对齐 m-top
  (标题 + 主题/通知铃/头像→我的)
- sheet:showMSheet(grip/标题/86vh/键盘避让)+ showAdaptiveSheet;六个共享
  弹层呈现函数加窄屏 sheet 分支(订单/往来/财务往来/商品编辑抽屉 + 退单/登记收支
  dialog 的 asSheet 形态)
- 徽章:DsBadge/StatusPill/StatusBadge 加图标变体(默认圆点零漂移)+
  status_icon_map 转录原型 BADGE_ICON 28 词
- 组件:MKpiGrid(2×2 可点 KPI)/ MSearchRow(搜索+状态钮+详搜钮)/
  MHubGroup(hub 入口组)/ showThemeSheet(主题外观 sheet)
- 授权:_LicensePanel/_SettingsCard 抽取为公共 LicensePanel/SettingsCard,
  窄屏授权跳转 /me/license
- golden:app_shell_mobile + me 各三主题(390×844@2x);全量 259 测试绿,
  check_ds_code / check-l1-sync 闸过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:56:04 +08:00

181 lines
6.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// widgets/ds/m_sheet.dart — 统一移动底部 sheet(镜像原型 mobile-atoms .m-sheet)。
// 结构:grip(36×4) + 标题行(m-sheet-h:标题 + X) + 滚动体(m-sheet-b) + 可选底部操作条。
// max 高 86vh、顶部 r-xl 圆角、SelectionArea 可选中复制;键盘弹出自动避让。
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_chrome.g.dart';
import '../../core/theme/app_dims.g.dart';
import '../../core/theme/context_tokens.dart';
import '../../core/utils/dialog_util.dart';
/// 打开移动底部 sheet(对齐原型 mobile-shell openSheet)。
/// - [title] 为 null 时只渲染 grip,内容自带头部(详情抽屉复用场景)。
/// - [scrollable] true:内容包 SingleChildScrollViewfalse:内容自管布局
/// (需自带定界高度,如 SizedBox(height:…),供 Column/Expanded 正常工作)。
/// - [actions] 底部固定操作条(原型 .m-actionbar 形态:横排等分按钮)。
Future<T?> showMSheet<T>(
BuildContext context, {
String? title,
required WidgetBuilder builder,
bool scrollable = true,
double maxHeightFactor = 0.86,
List<Widget>? actions,
}) {
return showModalBottomSheet<T>(
context: context,
isScrollControlled: true,
useSafeArea: true,
backgroundColor: Colors.transparent,
barrierColor: AppChrome.scrim, // .m-sheet-mask --scrim
builder: (ctx) {
final t = ctx.tokens;
final maxH = MediaQuery.of(ctx).size.height * maxHeightFactor;
final viewInsets = MediaQuery.of(ctx).viewInsets.bottom;
return Padding(
// 键盘弹出时整体上移(sheet 内含输入框场景)
padding: EdgeInsets.only(bottom: viewInsets),
child: Container(
clipBehavior: Clip.antiAlias,
constraints: BoxConstraints(maxHeight: maxH),
decoration: BoxDecoration(
color: t.surface,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(AppDims.rXl)),
),
// sheet 是独立 overlay(在 app_shell 的 SelectionArea 之外)→ 自带一层
child: SelectionArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// .grip
Center(
child: Container(
width: 36,
height: 4,
margin: const EdgeInsets.only(top: 8, bottom: 2),
decoration: BoxDecoration(
color: t.border,
borderRadius: BorderRadius.circular(AppDims.rPill),
),
),
),
if (title != null)
Container(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 12),
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(color: t.borderSubtle)),
),
child: Row(children: [
Expanded(
child: Text(title,
style: TextStyle(
fontSize: AppDims.fsTitle,
fontWeight: FontWeight.w700,
color: t.heading)),
),
InkWell(
onTap: () => Navigator.of(ctx).pop(),
child: Icon(LucideIcons.x, size: 20, color: t.muted),
),
]),
),
Flexible(
child: scrollable
? SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
child: Builder(builder: builder),
)
: Builder(builder: builder),
),
if (actions != null)
Container(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: t.borderSubtle)),
),
child: Row(children: [
for (var i = 0; i < actions.length; i++) ...[
if (i > 0) const SizedBox(width: 10),
Expanded(child: actions[i]),
],
]),
),
],
),
),
),
);
},
);
}
/// 自适应弹层:窄屏走底部 sheet,宽屏走既有 showAppDialogDialog 卡)。
/// 供各屏的筛选 / 轻表单 / 选项选择统一调用,减少 isMobile 样板分支。
Future<T?> showAdaptiveSheet<T>(
BuildContext context, {
required String title,
required WidgetBuilder builder,
List<Widget>? actions,
double desktopWidth = 480,
}) {
if (context.isMobile) {
return showMSheet<T>(context,
title: title, builder: builder, actions: actions);
}
return showAppDialog<T>(
context: context,
builder: (ctx) {
final t = ctx.tokens;
return Dialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppDims.rLg)),
child: SizedBox(
width: ctx.dialogWidth(desktopWidth),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 0),
child: Row(children: [
Expanded(
child: Text(title,
style: TextStyle(
fontSize: AppDims.fsTitle,
fontWeight: FontWeight.w700,
color: t.heading)),
),
InkWell(
onTap: () => Navigator.of(ctx).pop(),
child: Icon(LucideIcons.x, size: 18, color: t.muted),
),
]),
),
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 14, 20, 18),
child: Builder(builder: builder),
),
),
if (actions != null)
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 18),
child: Row(children: [
for (var i = 0; i < actions.length; i++) ...[
if (i > 0) const SizedBox(width: 10),
Expanded(child: actions[i]),
],
]),
),
],
),
),
);
},
);
}