60483ae483
新增可复用 widgets/kpi_card.dart:KpiCard(还原原型 .kpi/.ic,6 档色调 info/ok/blue/alert/warn/danger,token+AppDims 驱动)+ StatusPill(.badge 圆点软底)。库存查询页 3 张旧 _SummaryCard → 4 张 KPI 卡(商品总数/在库数量/ 库存预警/缺货,商品总数可点清筛选)。状态徽章 正常/库存不足→在售/预警, 硬编码 FFEBEE/FFF3E0/E8F5E9 → dangerBg/warnBg/okSoft;出入库方向徽章同迁。 该屏 0 硬编码色残留,三主题随 token 换色。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
171 lines
5.1 KiB
Dart
171 lines
5.1 KiB
Dart
import 'dart:ui' show FontFeature;
|
||
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
|
||
? Icons.arrow_drop_up
|
||
: Icons.arrow_drop_down,
|
||
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。
|
||
class StatusPill extends StatelessWidget {
|
||
final String label;
|
||
final Color color;
|
||
final Color background;
|
||
const StatusPill(
|
||
{super.key,
|
||
required this.label,
|
||
required this.color,
|
||
required this.background});
|
||
|
||
@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: [
|
||
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)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|