be9afcf68d
对齐移动原型体系(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>
179 lines
5.4 KiB
Dart
179 lines
5.4 KiB
Dart
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||
import 'package:flutter/material.dart';
|
||
import '../core/theme/context_tokens.dart';
|
||
import '../core/theme/app_dims.g.dart';
|
||
|
||
/// KPI 卡(还原原型 `.kpi`):左上标题、右上图标芯片、下方大数值、可选同比/说明。
|
||
/// 色调走 token(禁硬编码 hex),尺寸走 AppDims(禁裸 px)。
|
||
/// 原型规格:surface 面 / 1px border / r-lg 圆角 / padding 15·16;
|
||
/// `.ic` 30×30 r-md;`.v` 24/700 mono;`.d` fs-xs;hover(click) 描边 brand400。
|
||
enum KpiTone { info, ok, blue, alert, warn, danger }
|
||
|
||
enum KpiTrend { none, up, down }
|
||
|
||
class KpiCard extends StatelessWidget {
|
||
final String title;
|
||
final String value;
|
||
final IconData icon;
|
||
final KpiTone tone;
|
||
|
||
/// 同比/说明文案(如「▲ 2.3% 较上月」「需补货 6 项」),null 不显示。
|
||
final String? delta;
|
||
final KpiTrend trend;
|
||
|
||
/// 点击(如清筛选 / 跳转);null 时为静态卡,不显示 hover。
|
||
final VoidCallback? onTap;
|
||
|
||
const KpiCard({
|
||
super.key,
|
||
required this.title,
|
||
required this.value,
|
||
required this.icon,
|
||
this.tone = KpiTone.info,
|
||
this.delta,
|
||
this.trend = KpiTrend.none,
|
||
this.onTap,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
final (Color icBg, Color icFg) = switch (tone) {
|
||
KpiTone.info => (t.infoSoft, t.primary),
|
||
KpiTone.ok => (t.okSoft, t.success),
|
||
KpiTone.blue => (t.infoSoft, t.brand400),
|
||
KpiTone.alert => (t.accentSoft, t.accent),
|
||
KpiTone.warn => (t.warnBg, t.warn),
|
||
KpiTone.danger => (t.dangerBg, t.danger),
|
||
};
|
||
final valueColor = switch (tone) {
|
||
KpiTone.alert => t.accent,
|
||
KpiTone.danger => t.danger,
|
||
_ => t.heading,
|
||
};
|
||
final deltaColor = switch (trend) {
|
||
KpiTrend.up => t.success,
|
||
KpiTrend.down => t.danger,
|
||
KpiTrend.none => t.muted,
|
||
};
|
||
|
||
final card = Container(
|
||
padding:
|
||
const EdgeInsets.symmetric(horizontal: AppDims.sp4, vertical: 15),
|
||
decoration: BoxDecoration(
|
||
color: t.surface,
|
||
border: Border.all(color: t.border),
|
||
borderRadius: BorderRadius.circular(AppDims.rLg),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(
|
||
child: Text(title,
|
||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||
),
|
||
Container(
|
||
width: 30,
|
||
height: 30,
|
||
decoration: BoxDecoration(
|
||
color: icBg,
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
),
|
||
child: Icon(icon, size: 17, color: icFg),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 7),
|
||
Text(
|
||
value,
|
||
style: TextStyle(
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.w700,
|
||
color: valueColor,
|
||
letterSpacing: .3,
|
||
fontFeatures: const [FontFeature.tabularFigures()],
|
||
),
|
||
),
|
||
if (delta != null) ...[
|
||
const SizedBox(height: 5),
|
||
Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
if (trend != KpiTrend.none)
|
||
Icon(
|
||
trend == KpiTrend.up
|
||
? LucideIcons.chevronUp
|
||
: LucideIcons.chevronDown,
|
||
size: 16,
|
||
color: deltaColor,
|
||
),
|
||
Text(delta!,
|
||
style:
|
||
TextStyle(fontSize: AppDims.fsXs, color: deltaColor)),
|
||
],
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
|
||
if (onTap == null) return card;
|
||
return InkWell(
|
||
borderRadius: BorderRadius.circular(AppDims.rLg),
|
||
onTap: onTap,
|
||
child: card,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 状态徽章(还原原型 `.badge`:圆点 + 文字同色 + 软底 pill)。
|
||
/// 用于库存「在售/预警/缺货」等派生状态;色与软底全走 token。
|
||
/// [icon] 非空 → 图标徽章变体(原型 .badge.bi),默认圆点(存量 golden 零漂移)。
|
||
class StatusPill extends StatelessWidget {
|
||
final String label;
|
||
final Color color;
|
||
final Color background;
|
||
final IconData? icon;
|
||
const StatusPill(
|
||
{super.key,
|
||
required this.label,
|
||
required this.color,
|
||
required this.background,
|
||
this.icon});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
height: 22,
|
||
padding: const EdgeInsets.symmetric(horizontal: 9),
|
||
decoration: BoxDecoration(
|
||
color: background,
|
||
borderRadius: BorderRadius.circular(AppDims.rPill),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
if (icon != null)
|
||
Icon(icon, size: 12, color: color)
|
||
else
|
||
Container(
|
||
width: 6,
|
||
height: 6,
|
||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||
),
|
||
const SizedBox(width: 5),
|
||
Text(label,
|
||
style: TextStyle(
|
||
color: color,
|
||
fontSize: AppDims.fsSm,
|
||
fontWeight: FontWeight.w600)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|