Files
jiu/client/lib/widgets/ds/m_card.dart
T
wangjia 980e38e020 feat(client): 库存/出入库移动卡片与详情 sheet 逐一对齐原型结构
- 新增 MCard(原型 .m-card 双栏:mc-main 标题+副行 mono | mc-right 徽章行+
  金额 mono | › | mc-foot ⓘ脚注)+ DsIconBadge(.badge.ico 22px 纯图标)/
  DsTextIconBadge(.badge.bi 图标+文字) + statusToneOf 状态词配色表——
  替换原 MobileListCard「标签:值」布局与各屏本地 _icoBadge/_statusColors
- 出入库卡:单号 + 供应商/客户·N 项 + 状态/退单/待定价图标徽章 + 合计金额 +
  日期·经手人·审核 脚注;库存卡:品名 + 编码·规格 + 状态徽章 + 数量(缺货红/
  预警黄);移动卡不再有行内操作(对齐原型,操作统一在详情 sheet;打标签=
  打印,移动端裁剪)
- DsTable 窄屏卡片流去外层大卡容器(卡直接铺页面底色,对齐原型 m-scroll)——
  往来/基础数据等全部卡片流屏同步受益
- 出入库详情窄屏改 sheet 专属布局(原型 openOrder):drow 字段行 + 明细
  pe-card(头行 图标+「明细 · N 项」+ 行=品名+系列·×数量 | 金额/待定价徽章)+
  合计 drow + 等分按钮行(无组标签);桌面右滑抽屉布局不变
- 相关移动 golden 全量重生成(出入库/库存/往来/基础数据 ×3 主题),
  桌面 golden 零漂移;283 测试全绿

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

231 lines
7.8 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_card.dart — 移动列表卡(镜像原型 mobile-atoms .m-card / .m-row 结构)
// 与 .badge.ico22px 纯图标徽章)/.badge.bi(图标+文字徽章)。
//
// 卡结构:mc-main(mc-nm 标题 + mc-sub 副行 mono) | mc-right(徽章行 + mc-amt 金额)
// | mc-chev ›;可选 mc-foot 脚注行(ⓘ 前缀,上分隔线)。
// 徽章色调按状态词查表(statusToneOf),图标按 status_icon_map(原型 BADGE_ICON)。
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../core/theme/app_dims.g.dart';
import '../../core/theme/app_fonts.dart';
import '../../core/theme/context_tokens.dart';
import 'status_icon_map.dart';
/// 状态词 → (前景, 软底)。对齐原型 .badge.b-* 的语义配色。
(Color fg, Color bg) statusToneOf(BuildContext context, String label) {
final t = context.tokens;
switch (label) {
case '待审核':
case '待定价':
case '部分退单':
case '预警':
case '进行中':
case '未结清':
return (t.warn, t.warnBg);
case '已审核':
case '已通过':
case '在售':
case '启用':
case '已完成':
case '已结清':
case '已兑换':
case '在线':
return (t.success, t.successBg);
case '已拒绝':
case '已驳回':
case '已退单':
case '缺货':
case '停用':
case '作废':
case '离线':
return (t.danger, t.dangerBg);
case '草稿':
return (t.muted, t.infoSoft);
default:
return (t.muted, t.borderSubtle);
}
}
/// 纯图标徽章(原型 .badge.ico):22×22 圆 pill13px 状态图标,长按出状态词。
class DsIconBadge extends StatelessWidget {
final String label;
const DsIconBadge(this.label, {super.key});
@override
Widget build(BuildContext context) {
final (fg, bg) = statusToneOf(context, label);
return Tooltip(
message: label,
child: Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: bg, borderRadius: BorderRadius.circular(AppDims.rPill)),
alignment: Alignment.center,
child:
Icon(statusIcon(label) ?? LucideIcons.circle, size: 13, color: fg),
),
);
}
}
/// 图标 + 文字徽章(原型 .badge.bi):h22 pill12px 图标替圆点。
class DsTextIconBadge extends StatelessWidget {
final String label;
const DsTextIconBadge(this.label, {super.key});
@override
Widget build(BuildContext context) {
final (fg, bg) = statusToneOf(context, label);
return Container(
height: 22,
padding: const EdgeInsets.symmetric(horizontal: 9),
decoration: BoxDecoration(
color: bg, borderRadius: BorderRadius.circular(AppDims.rPill)),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(statusIcon(label) ?? LucideIcons.circle, size: 12, color: fg),
const SizedBox(width: 5),
Text(label,
style: TextStyle(
color: fg,
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600)),
]),
);
}
}
/// 移动列表卡(原型 .m-card 双栏结构)。
class MCard extends StatelessWidget {
/// mc-nm 标题(600 heading)。
final String nm;
/// mc-sub 副行(xs muted mono);null 不渲染。
final String? sub;
/// mc-right 上行徽章(DsIconBadge / DsBadge 等),横排 gap5。
final List<Widget> badges;
/// mc-amt 金额/数量(700 heading mono);amtWidget 优先。
final String? amt;
final Widget? amtWidget;
/// mc-foot 脚注(ⓘ 前缀 + xs muted,上分隔线);null 不渲染。
final String? foot;
final VoidCallback? onTap;
const MCard({
super.key,
required this.nm,
this.sub,
this.badges = const [],
this.amt,
this.amtWidget,
this.foot,
this.onTap,
});
@override
Widget build(BuildContext context) {
final t = context.tokens;
return Material(
color: t.surface,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
side: BorderSide(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg),
),
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 13, 14, 13),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
// mc-main
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(nm,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w600,
color: t.heading)),
if (sub != null) ...[
const SizedBox(height: 3),
Text(sub!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: AppDims.fsXs,
color: t.muted,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback)),
],
],
),
),
const SizedBox(width: 12),
// mc-right
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (badges.isNotEmpty)
Row(mainAxisSize: MainAxisSize.min, children: [
for (var i = 0; i < badges.length; i++) ...[
if (i > 0) const SizedBox(width: 5),
badges[i],
],
]),
if (amtWidget != null || amt != null) ...[
if (badges.isNotEmpty) const SizedBox(height: 5),
amtWidget ??
Text(amt!,
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w700,
color: t.heading,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback)),
],
],
),
const SizedBox(width: 8),
// mc-chev
Icon(LucideIcons.chevronRight, size: 18, color: t.faint),
],
),
if (foot != null)
Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.only(top: 10),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: t.borderSubtle)),
),
child: Row(children: [
Icon(LucideIcons.info, size: 13, color: t.muted),
const SizedBox(width: 10),
Expanded(
child: Text(foot!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: AppDims.fsXs, color: t.muted)),
),
]),
),
],
),
),
),
);
}
}