From f09de3a11c96fedce1473769452798d5bb8039cf Mon Sep 17 00:00:00 2001 From: zhaoying Date: Mon, 16 Mar 2026 14:53:52 +0800 Subject: [PATCH] feat(web): components update --- web/src/components/Charts/PieChart.tsx | 23 +++-- web/src/components/RadioGroupButton/index.tsx | 96 +++++++++++++++++++ web/src/components/RbSlider/index.tsx | 11 ++- web/src/components/RbStatistic/index.tsx | 43 +++++++++ web/src/components/SearchInput/index.tsx | 2 +- web/src/components/StatusTag/index.tsx | 8 +- 6 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 web/src/components/RadioGroupButton/index.tsx create mode 100644 web/src/components/RbStatistic/index.tsx diff --git a/web/src/components/Charts/PieChart.tsx b/web/src/components/Charts/PieChart.tsx index 221eb4a6..b0c67549 100644 --- a/web/src/components/Charts/PieChart.tsx +++ b/web/src/components/Charts/PieChart.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-10 13:35:45 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-10 13:35:45 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-16 11:34:30 */ /* * PieChart Component @@ -51,6 +51,11 @@ interface PieChartProps { chartData: ChartData[]; height?: number; colors?: string[]; + itemGap?: number; + seriesWidth?: number; + seriesHeight?: number; + seriesLabel?: boolean; + seriesTop?: number; } /** @@ -76,7 +81,12 @@ interface PieChartProps { const PieChart: FC = ({ chartData, height = 260, + seriesWidth = 182, + seriesHeight = 182, colors = Colors, + itemGap = 48, + seriesLabel = true, + seriesTop = 24, }) => { /** Reference to the ECharts instance for programmatic control */ const chartRef = useRef(null); @@ -139,7 +149,7 @@ const PieChart: FC = ({ itemHeight: 12, borderRadius: 2, orient: 'horizontal', - itemGap: 48, + itemGap: itemGap, textStyle: { color: '#5B6167', fontFamily: 'PingFangSC, PingFang SC', @@ -153,10 +163,10 @@ const PieChart: FC = ({ avoidLabelOverlap: false, percentPrecision: 0, padAngle: 1, - width: 182, - height: 182, + width: seriesWidth, + height: seriesHeight, left: 'center', - top: 24, + top: seriesTop, itemStyle: { borderRadius: 2, shadowBlur: 4, @@ -165,6 +175,7 @@ const PieChart: FC = ({ shadowColor: 'rgba(0,0,0,0.25)', }, label: { + show: seriesLabel, fontWeight: 'bold', color: '#171719', formatter: '{d}%', diff --git a/web/src/components/RadioGroupButton/index.tsx b/web/src/components/RadioGroupButton/index.tsx new file mode 100644 index 00000000..25ed6ef2 --- /dev/null +++ b/web/src/components/RadioGroupButton/index.tsx @@ -0,0 +1,96 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-03-16 14:53:33 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-16 14:53:33 + */ +/** + * RadioGroupButton Component + * + * A pill / chip-style radio group that renders each option as a small + * rounded tag. The selected option is highlighted with a dark background + * and white text; unselected options use a light grey background. + * + * Features: + * - Pill-shaped selectable tags laid out horizontally via Ant Design Space + * - Optional "allowClear" mode: clicking the active option deselects it + * - Disabled state per option + * - Two callbacks: onChange (controlled value) and onValueChange (side-effect) + * + * @component + */ + +import { type FC, type Key, type ReactNode, useEffect } from 'react'; +import { type RadioGroupProps, Space } from 'antd'; +import clsx from 'clsx' + +/** Describes a single selectable option within the radio group. */ +interface RadioCardOption { + /** Unique value that identifies this option. */ + value: string | number | boolean | null | undefined | Key; + /** Display content rendered inside the pill tag. */ + label: string | ReactNode; + /** When true the option is visually muted and cannot be selected. */ + disabled?: boolean; +} + +/** Props for the RadioGroupButton component. */ +interface RadioCardProps extends Omit { + /** List of selectable options to render as pill tags. */ + options: RadioCardOption[]; + /** Side-effect callback invoked whenever the value changes (including on mount). */ + onValueChange?: (value: string | null | undefined, option?: RadioCardOption) => void; + /** Controlled callback invoked when the user clicks an option. */ + onChange?: (value: string | null | undefined, option?: RadioCardOption) => void; + /** If true, clicking the already-selected option will deselect it (set value to null). */ + allowClear?: boolean; +} + +/** Renders a horizontal row of pill-shaped radio options. */ +const RadioGroupButton: FC = ({ + options, + value, + onValueChange, + onChange, + allowClear = false, +}) => { + /* Notify parent of value changes (useful for side-effects like analytics). */ + useEffect(() => { + if (onValueChange) { + onValueChange(value); + } + }, [value, onValueChange]); + + /* Toggle selection; supports allowClear and respects disabled state. */ + const handleChange = (option: RadioCardOption) => { + // Ignore clicks on disabled options + if (option.disabled) return + if (onChange) { + // Clear selection if allowClear is true and option is already selected + if (allowClear && value === option.value) { + onChange(null, undefined); + } else { + onChange(String(option.value), option); + } + } + } + + return ( + + {options.map(option => ( +
handleChange(option)} + > + {option.label} +
+ ))} +
+ ); +}; + +export default RadioGroupButton; \ No newline at end of file diff --git a/web/src/components/RbSlider/index.tsx b/web/src/components/RbSlider/index.tsx index f9c10e4c..c37cdc47 100644 --- a/web/src/components/RbSlider/index.tsx +++ b/web/src/components/RbSlider/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:23:39 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-11 11:10:40 + * @Last Modified time: 2026-03-12 16:16:49 */ /** * RbSlider Component @@ -15,7 +15,7 @@ * @component */ -import { type FC, useEffect, useState } from 'react'; +import { type FC, type ReactNode, useEffect, useState } from 'react'; import { Slider, type SliderSingleProps, Flex, InputNumber, type InputNumberProps } from 'antd'; /** Props interface for RbSlider component */ @@ -27,6 +27,8 @@ interface RbSliderProps extends SliderSingleProps { isInput?: boolean; size?: 'small' | 'default'; className?: string; + prefix?: string | ReactNode; + inputClassName?: string; } /** Custom slider component with value display */ @@ -40,6 +42,8 @@ const RbSlider: FC = ({ size = 'default' , isInput = false, className = '', + prefix, + inputClassName, ...rest }) => { const [curValue, setCurValue] = useState(0) @@ -93,7 +97,8 @@ const RbSlider: FC = ({ step={step as number} value={curValue} onChange={handleInputChange} - className="rb:w-20!" + prefix={prefix} + className={`${inputClassName || '' } rb:w-20!`} /> :
{curValue || min}
} diff --git a/web/src/components/RbStatistic/index.tsx b/web/src/components/RbStatistic/index.tsx new file mode 100644 index 00000000..34e23896 --- /dev/null +++ b/web/src/components/RbStatistic/index.tsx @@ -0,0 +1,43 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-03-16 14:52:06 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-16 14:52:06 + */ +import { type FC } from 'react'; + +/** + * Props for the RbStatistic component. + * @property title - The label displayed above the statistic value. + * @property value - The numeric or string value to display. + * @property suffix - Optional unit or suffix appended after the value (e.g. "%", "items"). + */ +interface RbStatistic { + title: string; + value: number | string; + suffix?: string; +} + +/** + * RbStatistic – A lightweight statistic display component. + * + * Renders a title/label on top and a prominent value (with an optional suffix) + * below it. Commonly used in dashboard cards and summary panels. + * + * @example + * + */ +const RbStatistic: FC = ({ + title, + value, + suffix, +}) => { + return ( +
+
{title}
+
{value} {suffix}
+
+ ); +}; + +export default RbStatistic \ No newline at end of file diff --git a/web/src/components/SearchInput/index.tsx b/web/src/components/SearchInput/index.tsx index 1c05d81c..c421b518 100644 --- a/web/src/components/SearchInput/index.tsx +++ b/web/src/components/SearchInput/index.tsx @@ -115,7 +115,7 @@ const SearchInput: FC = ({ value={value} onChange={handleChange} style={{ width: '300px' }} - className={className} + className={`rb:border-none! ${className}`} {...props} /> ); diff --git a/web/src/components/StatusTag/index.tsx b/web/src/components/StatusTag/index.tsx index 7fb4cc54..913a5a99 100644 --- a/web/src/components/StatusTag/index.tsx +++ b/web/src/components/StatusTag/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:29:42 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-11 11:14:29 + * @Last Modified time: 2026-03-16 11:59:09 */ /** * StatusTag Component @@ -41,9 +41,9 @@ const StatusTag: FC = ({ text }) => { return ( - - - + + + { text }