Files
jiu/client/lib/widgets/ds/ds_kpi.dart
T
wangjia 2bf5b98ba2 feat(client): 草稿 KPI 卡(暗黄)+ 出库详情总售价列 + 明细去¥缩号 + 建单默认值 + 若干列名
- 出入库列表加草稿 KPI 卡(DsKpiTone.warn 新档,审核卡后,点击筛选草稿)
- 出库详情 7 列:售价后加总售价;明细行内价格去 ¥ 且缩一号(合计保留 ¥);
  5/7 列宽重排(系列/数量左移、进价(单瓶)不折行)
- 出库建单:数量默认=可用数量、售价默认=参考售价(product.sale_price)
- 入库详情列名:进价(单瓶)/总价;库存列表「单价」→「总价」(库存×成本);
  商品抽屉:成本价(单瓶)/总成本价
- 公开商品预览页加返回按钮(仅 App 内 canPop 时显示,扫码直达保持纯净)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 14:31:50 +08:00

120 lines
4.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/ds_kpi.dart — 原型 .kpi 卡(镜像 atoms.css)。
import 'package:flutter/material.dart';
import '../../core/theme/context_tokens.dart';
import '../../core/theme/app_dims.g.dart';
import '../../core/theme/app_fonts.dart';
/// 图标块 .ic 变体(软底 + 前景色)。
enum DsKpiTone { info, ok, blue, alert, warn }
/// 环比方向 .d.up/.down(绿/红),neutral=纯文案(muted)。
enum DsKpiDelta { up, down, neutral }
/// 原型 .kpisurface/border/r-lg/pad 15 16;右上 .ic(30×30 r-md)
/// .t(fs-sm muted) / .v(24 fw700 heading monoalert→accent) / .d(fs-xsup绿/down红)。
class DsKpi extends StatelessWidget {
final String title;
final String value;
// 原型 users.html 的 KPI 无右上图标块 → icon 可空时不渲染 .ic
final IconData? icon;
final DsKpiTone tone;
final String? delta;
final DsKpiDelta deltaTone;
final VoidCallback? onTap;
const DsKpi({
super.key,
required this.title,
required this.value,
this.icon,
this.tone = DsKpiTone.info,
this.delta,
this.deltaTone = DsKpiDelta.neutral,
this.onTap,
});
@override
Widget build(BuildContext context) {
final t = context.tokens;
final (icBg, icFg) = switch (tone) {
DsKpiTone.info => (t.infoSoft, t.primary),
DsKpiTone.ok => (t.okSoft, t.success),
DsKpiTone.blue => (t.infoSoft, t.brand400),
DsKpiTone.alert => (t.accentSoft, t.accent),
// 暗黄(草稿卡):icon 与数字走 warn token
DsKpiTone.warn => (t.warnBg, t.warn),
};
final deltaColor = switch (deltaTone) {
DsKpiDelta.up => t.success,
DsKpiDelta.down => t.danger,
DsKpiDelta.neutral => t.muted,
};
final card = Container(
padding: const EdgeInsets.fromLTRB(16, 15, 16, 15),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(title,
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
),
),
if (icon != null)
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: icBg,
borderRadius: BorderRadius.circular(AppDims.rMd)),
alignment: Alignment.center,
child: Icon(icon, size: 17, color: icFg),
),
],
),
const SizedBox(height: 7),
Text(value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback,
color: switch (tone) {
DsKpiTone.alert => t.accent,
DsKpiTone.warn => t.warn,
_ => t.heading,
})),
if (delta != null) ...[
const SizedBox(height: 5),
// 原型 .d 的箭头是文本字符 ▲/▼(非图标),gap4。
Text(
switch (deltaTone) {
DsKpiDelta.up => '${delta!}',
DsKpiDelta.down => '${delta!}',
DsKpiDelta.neutral => delta!,
},
style: TextStyle(fontSize: AppDims.fsXs, color: deltaColor),
),
],
],
),
);
if (onTap == null) return card;
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(onTap: onTap, child: card),
);
}
}