// widgets/ds/m_card.dart — 移动列表卡(镜像原型 mobile-atoms .m-card / .m-row 结构) // 与 .badge.ico(22px 纯图标徽章)/.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 圆 pill,13px 状态图标,长按出状态词。 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 pill,12px 图标替圆点。 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 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)), ), ]), ), ], ), ), ), ); } }