feat(client): 库存列表 KPI 卡 + 状态徽章按原型重建(阶段3 首切)
新增可复用 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
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user