Skip to content

Commit 0444ebb

Browse files
committed
feat: 添加统计卡片组件
1 parent 9f2d74f commit 0444ebb

File tree

14 files changed

+360
-3
lines changed

14 files changed

+360
-3
lines changed
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

client/packages/barda-design/src/icons/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ export { ReactComponent as LeftNumberInput } from "./icon-left-comp-numberInput.
251251
export { ReactComponent as LeftPassword } from "./icon-left-comp-password.svg";
252252
export { ReactComponent as LeftProgress } from "./icon-left-comp-progress.svg";
253253
export { ReactComponent as LeftQrCode } from "./icon-left-comp-qrCode.svg";
254+
export { ReactComponent as LeftStatisticCard } from "./icon-left-comp-statistic-card.svg";
254255
export { ReactComponent as LeftRadio } from "./icon-left-comp-radio.svg";
255256
export { ReactComponent as LeftRating } from "./icon-left-comp-rating.svg";
256257
export { ReactComponent as LeftSegmentedControl } from "./icon-left-comp-segmentedControl.svg";
@@ -287,4 +288,5 @@ export { ReactComponent as FileFolderIcon } from "icons/icon-editor-folder.svg";
287288
export { ReactComponent as CTRLIcon } from "icons/icon-ctrl.svg";
288289
export { ReactComponent as TimeLineIcon } from "icons/icon-timeline-comp.svg"
289290
export { ReactComponent as CommentIcon } from "icons/icon-comment-comp.svg";
290-
export { ReactComponent as AutoCompleteCompIcon } from "icons/icon-autocomplete-comp.svg";
291+
export { ReactComponent as AutoCompleteCompIcon } from "icons/icon-autocomplete-comp.svg";
292+
export { ReactComponent as StatisticCardCompIcon } from "icons/icon-comp-statistic-card.svg";

client/packages/barda/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"rc-trigger": "^5.3.1",
6767
"react": "^18.2.0",
6868
"react-colorful": "^5.5.1",
69+
"react-countup": "6.5.3",
6970
"react-documents": "^1.2.1",
7071
"react-dom": "^18.2.0",
7172
"react-draggable": "4.4.6",
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { Statistic } from "antd";
2+
import { RecordConstructorToView } from "barda-core";
3+
import { Section, sectionNames } from "barda-design";
4+
import { BoolControl } from "comps/controls/boolControl";
5+
import { NumberControl, RangeControl, StringControl } from "comps/controls/codeControl";
6+
import { ButtonEventHandlerControl } from "comps/controls/eventHandlerControl";
7+
import { IconControl } from "comps/controls/iconControl";
8+
import { styleControl } from "comps/controls/styleControl";
9+
import { StatisticCardStyle, StatisticCardStyleType } from "comps/controls/styleControlConstants";
10+
import { withDefault } from "comps/generators";
11+
import { UICompBuilder } from "comps/generators/uiCompBuilder";
12+
import { NameConfig, NameConfigHidden, withExposingConfigs } from "comps/generators/withExposing";
13+
import { hiddenPropertyView } from "comps/utils/propertyUtils";
14+
import { trans } from "i18n";
15+
import React, { Suspense } from "react";
16+
import styled from "styled-components";
17+
import { hasIcon } from "../utils";
18+
19+
const StatisticCardWrapper = styled.div<{
20+
$style: StatisticCardStyleType;
21+
$clickable: boolean;
22+
}>`
23+
width: 100%;
24+
height: 100%;
25+
padding: 16px;
26+
background: ${(props) => props.$style.background};
27+
border-radius: ${(props) => props.$style.radius};
28+
border: 1px solid ${(props) => props.$style.border};
29+
cursor: ${(props) => (props.$clickable ? "pointer" : "default")};
30+
transition: all 0.2s;
31+
32+
${(props) =>
33+
props.$clickable &&
34+
`
35+
&:hover {
36+
${props.$style.hoverBackground ? `background: ${props.$style.hoverBackground} !important;` : ""}
37+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
38+
}
39+
`}
40+
41+
.statistic-card-content {
42+
display: flex;
43+
align-items: flex-start;
44+
gap: 16px;
45+
}
46+
47+
.statistic-card-icon {
48+
width: 48px;
49+
height: 48px;
50+
border-radius: 8px;
51+
display: flex;
52+
align-items: center;
53+
justify-content: center;
54+
flex-shrink: 0;
55+
background: ${(props) => props.$style.iconBackground};
56+
57+
svg {
58+
width: 32px !important;
59+
height: 32px !important;
60+
color: #fff;
61+
}
62+
63+
img {
64+
width: 32px;
65+
height: 32px;
66+
object-fit: contain;
67+
}
68+
}
69+
70+
.statistic-card-info {
71+
flex: 1;
72+
min-width: 0;
73+
}
74+
75+
.ant-statistic-title {
76+
font-size: ${(props) => props.$style.titleFontSize_UNIT} !important;
77+
color: ${(props) => props.$style.titleColor} !important;
78+
margin-bottom: 8px;
79+
}
80+
81+
.ant-statistic-content {
82+
font-size: ${(props) => props.$style.valueFontSize_UNIT} !important;
83+
color: ${(props) => props.$style.valueColor} !important;
84+
}
85+
`;
86+
87+
// 懒加载 CountUp 组件,只有在启用动画时才加载
88+
const CountUp = React.lazy(() => import("react-countup"));
89+
90+
const childrenMap = {
91+
title: withDefault(StringControl, trans("statisticCard.title")),
92+
value: withDefault(NumberControl, 0),
93+
prefix: withDefault(StringControl, ""),
94+
suffix: withDefault(StringControl, ""),
95+
precision: RangeControl.closed(0, 20, 0),
96+
icon: IconControl,
97+
enableAnimation: withDefault(BoolControl, false),
98+
onEvent: ButtonEventHandlerControl,
99+
style: styleControl(StatisticCardStyle),
100+
};
101+
102+
// 动画值组件
103+
const AnimatedValue = (props: {
104+
value: number;
105+
enableAnimation: boolean;
106+
precision: number;
107+
valueStyle: React.CSSProperties;
108+
}) => {
109+
const formattedValue = props.precision > 0 ? props.value.toFixed(props.precision) : props.value;
110+
111+
if (!props.enableAnimation) {
112+
return <>{formattedValue}</>;
113+
}
114+
115+
return (
116+
<Suspense fallback={<>{formattedValue}</>}>
117+
<CountUp
118+
start={0}
119+
end={props.value}
120+
duration={2}
121+
decimals={props.precision}
122+
separator=","
123+
style={props.valueStyle}
124+
/>
125+
</Suspense>
126+
);
127+
};
128+
129+
const StatisticCardView = (
130+
props: RecordConstructorToView<typeof childrenMap> & { $hasClickHandler?: boolean }
131+
) => {
132+
return (
133+
<StatisticCardWrapper
134+
$style={props.style}
135+
$clickable={props.$hasClickHandler ?? false}
136+
onClick={() => {
137+
props.onEvent?.("click");
138+
}}
139+
>
140+
<div className="statistic-card-content">
141+
<div className="statistic-card-info">
142+
<Statistic
143+
title={props.title}
144+
value={props.value}
145+
prefix={props.prefix || undefined}
146+
suffix={props.suffix || undefined}
147+
precision={props.precision}
148+
formatter={(value: string | number) => {
149+
const numericValue = typeof value === "number" ? value : Number(value) || 0;
150+
if (props.enableAnimation) {
151+
return (
152+
<AnimatedValue
153+
value={numericValue}
154+
enableAnimation={props.enableAnimation}
155+
precision={props.precision}
156+
valueStyle={{ color: props.style.valueColor }}
157+
/>
158+
);
159+
}
160+
// 应用精度格式化
161+
return props.precision > 0 ? numericValue.toFixed(props.precision) : numericValue;
162+
}}
163+
valueStyle={{ color: props.style.valueColor }}
164+
/>
165+
</div>
166+
{hasIcon(props.icon) && <div className="statistic-card-icon">{props.icon}</div>}
167+
</div>
168+
</StatisticCardWrapper>
169+
);
170+
};
171+
172+
let StatisticCardBasicComp = (function () {
173+
return new UICompBuilder(
174+
childrenMap,
175+
(props: RecordConstructorToView<typeof childrenMap>, dispatch: any, comp?: any) => {
176+
// 通过 comp.children 访问原始的 control 对象,判断是否绑定了 click 事件
177+
const hasClickHandler = (comp?.children?.onEvent as any)?.isBind?.("click") ?? false;
178+
return <StatisticCardView {...props} $hasClickHandler={hasClickHandler} />;
179+
}
180+
)
181+
.setPropertyViewFn((children) => (
182+
<>
183+
<Section name={sectionNames.basic}>
184+
{children.title.propertyView({
185+
label: trans("statisticCard.title"),
186+
tooltip: trans("statisticCard.titleTooltip"),
187+
})}
188+
{children.value.propertyView({
189+
label: trans("statisticCard.value"),
190+
tooltip: trans("statisticCard.valueTooltip"),
191+
})}
192+
{children.prefix.propertyView({
193+
label: trans("statisticCard.prefix"),
194+
tooltip: trans("statisticCard.prefixTooltip"),
195+
})}
196+
{children.suffix.propertyView({
197+
label: trans("statisticCard.suffix"),
198+
tooltip: trans("statisticCard.suffixTooltip"),
199+
})}
200+
{children.precision.propertyView({
201+
label: trans("statisticCard.precision"),
202+
tooltip: trans("statisticCard.precisionTooltip"),
203+
})}
204+
{children.icon.propertyView({
205+
label: trans("statisticCard.icon"),
206+
tooltip: trans("statisticCard.iconTooltip"),
207+
})}
208+
{children.enableAnimation.propertyView({
209+
label: trans("statisticCard.enableAnimation"),
210+
tooltip: trans("statisticCard.enableAnimationTooltip"),
211+
})}
212+
</Section>
213+
<Section name={sectionNames.interaction}>{children.onEvent.propertyView()}</Section>
214+
<Section name={sectionNames.layout}>{hiddenPropertyView(children)}</Section>
215+
<Section name={sectionNames.style}>{children.style.getPropertyView()}</Section>
216+
</>
217+
))
218+
.build();
219+
})();
220+
221+
export const StatisticCardComp = withExposingConfigs(StatisticCardBasicComp, [
222+
NameConfigHidden,
223+
]);

client/packages/barda/src/comps/controls/styleControlConstants.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,43 @@ export const QRCodeStyle = [
760760
getMargin(),
761761
] as const;
762762

763+
export const StatisticCardStyle = [
764+
getBackground(),
765+
getStaticBorder(),
766+
getRadius(),
767+
{
768+
name: "titleColor",
769+
label: trans("statisticCard.titleColor"),
770+
color: "#8B8FA3",
771+
},
772+
{
773+
name: "titleFontSize_UNIT",
774+
label: trans("statisticCard.titleFontSize"),
775+
default: "14px",
776+
},
777+
{
778+
name: "valueColor",
779+
label: trans("statisticCard.valueColor"),
780+
color: "#222222",
781+
},
782+
{
783+
name: "valueFontSize_UNIT",
784+
label: trans("statisticCard.valueFontSize"),
785+
default: "24px",
786+
},
787+
{
788+
name: "iconBackground",
789+
label: trans("statisticCard.iconBackground"),
790+
color: "#3377FF",
791+
},
792+
{
793+
name: "hoverBackground",
794+
label: trans("statisticCard.hoverBackground"),
795+
color: "",
796+
},
797+
getMargin(),
798+
] as const;
799+
763800
export const TimeLineStyle = [
764801
getBackground(),
765802
{
@@ -923,6 +960,7 @@ export type SignatureStyleType = StyleConfigType<typeof SignatureStyle>;
923960
export type CarouselStyleType = StyleConfigType<typeof CarouselStyle>;
924961
export type RichTextEditorStyleType = StyleConfigType<typeof RichTextEditorStyle>;
925962
export type StandardBoxMargin = [number, number, number, number];
963+
export type StatisticCardStyleType = StyleConfigType<typeof StatisticCardStyle>;
926964
export type TimeLineStyleType = StyleConfigType<typeof TimeLineStyle>;
927965
export type CommentStyleType = StyleConfigType<typeof CommentStyle>;
928966

client/packages/barda/src/comps/generators/multi.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { Comp, CompParams, MultiBaseComp, wrapDispatch } from "barda-core";
77

88
type ViewFnType<ViewReturn, ChildrenType> = (
99
childrenType: ChildrenType,
10-
dispatch: (action: CompAction) => void
10+
dispatch: (action: CompAction) => void,
11+
comp?: any
1112
) => ViewReturn;
1213

1314
export type ToViewReturn<T> = {

client/packages/barda/src/comps/generators/uiCompBuilder.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,10 @@ function UIView(props: { comp: any; viewFn: any }) {
211211
childrenProps["disabled"] = disabled || parentDisabled;
212212
}
213213
// 渲染组件,使用HidableView组件包装视图函数的返回值
214+
// 传递 comp 对象作为第三个参数,以便访问原始的 children
214215
return (
215216
<HidableView hidden={childrenProps.hidden as boolean}>
216-
{props.viewFn(childrenProps, comp.dispatch)}
217+
{props.viewFn(childrenProps, comp.dispatch, comp)}
217218
</HidableView>
218219
);
219220
}

client/packages/barda/src/comps/index.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
ProcessCircleCompIcon,
7171
ProgressCompIcon,
7272
QRCodeCompIcon,
73+
StatisticCardCompIcon,
7374
RadioCompIcon,
7475
RangeSliderCompIcon,
7576
RatingCompIcon,
@@ -104,6 +105,7 @@ import { NavComp } from "./comps/navComp/navComp";
104105
import { TableComp } from "./comps/tableComp";
105106
import { registerComp, UICompManifest, UICompType } from "./uiCompRegistry";
106107
import { QRCodeComp } from "./comps/qrCodeComp";
108+
import { StatisticCardComp } from "./comps/statisticCardComp";
107109
import { JsonExplorerComp } from "./comps/jsonComp/jsonExplorerComp";
108110
import { JsonEditorComp } from "./comps/jsonComp/jsonEditorComp";
109111
import { TreeComp } from "./comps/treeComp/treeComp";
@@ -493,6 +495,19 @@ const uiCompMap: Registry = {
493495
h: 19,
494496
},
495497
},
498+
statisticCard: {
499+
name: trans("uiComp.statisticCardCompName"),
500+
enName: "Statistic Card",
501+
description: trans("uiComp.statisticCardCompDesc"),
502+
categories: ["dataDisplay"],
503+
icon: StatisticCardCompIcon,
504+
keywords: trans("uiComp.statisticCardCompKeywords"),
505+
comp: StatisticCardComp,
506+
layoutInfo: {
507+
w: 3,
508+
h: 8,
509+
},
510+
},
496511
form: {
497512
name: trans("uiComp.formCompName"),
498513
enName: "Form",

client/packages/barda/src/comps/uiCompRegistry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export type UICompType =
113113
| "timeline"
114114
| "comment"
115115
| "autocomplete"
116+
| "statisticCard"
116117

117118
export const uiCompRegistry = {} as Record<UICompType | string, UICompManifest>;
118119

0 commit comments

Comments
 (0)