Files
jiu/client/lib/widgets/ds/m_card.dart
T

258 lines
9.0 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 '在线':
case '已支付':
return (t.success, t.successBg);
case '已拒绝':
case '已驳回':
case '已退单':
case '缺货':
case '停用':
case '作废':
case '离线':
return (t.danger, t.dangerBg);
case '待支付':
return (t.warn, t.warnBg);
case '草稿':
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-date 标题后缀(原型 .mc-nm .mc-datexs muted mono,左距 8);null 不渲染。
final String? nmSuffix;
/// 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.nmSuffix,
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: [
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Flexible(
child: Text(nm,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w600,
color: t.heading)),
),
if (nmSuffix != null) ...[
const SizedBox(width: 8),
Text(nmSuffix!,
style: TextStyle(
fontSize: AppDims.fsXs,
color: t.muted,
fontFamily: AppFonts.mono,
fontFamilyFallback:
AppFonts.monoFallback)),
],
],
),
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)),
),
]),
),
],
),
),
),
);
}
}