feat(web): components update

This commit is contained in:
zhaoying
2026-03-16 14:53:52 +08:00
parent e13acdc8a9
commit f09de3a11c
6 changed files with 169 additions and 14 deletions

View File

@@ -1,8 +1,8 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-10 13:35:45 * @Date: 2026-02-10 13:35:45
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-10 13:35:45 * @Last Modified time: 2026-03-16 11:34:30
*/ */
/* /*
* PieChart Component * PieChart Component
@@ -51,6 +51,11 @@ interface PieChartProps {
chartData: ChartData[]; chartData: ChartData[];
height?: number; height?: number;
colors?: string[]; colors?: string[];
itemGap?: number;
seriesWidth?: number;
seriesHeight?: number;
seriesLabel?: boolean;
seriesTop?: number;
} }
/** /**
@@ -76,7 +81,12 @@ interface PieChartProps {
const PieChart: FC<PieChartProps> = ({ const PieChart: FC<PieChartProps> = ({
chartData, chartData,
height = 260, height = 260,
seriesWidth = 182,
seriesHeight = 182,
colors = Colors, colors = Colors,
itemGap = 48,
seriesLabel = true,
seriesTop = 24,
}) => { }) => {
/** Reference to the ECharts instance for programmatic control */ /** Reference to the ECharts instance for programmatic control */
const chartRef = useRef<ReactEcharts>(null); const chartRef = useRef<ReactEcharts>(null);
@@ -139,7 +149,7 @@ const PieChart: FC<PieChartProps> = ({
itemHeight: 12, itemHeight: 12,
borderRadius: 2, borderRadius: 2,
orient: 'horizontal', orient: 'horizontal',
itemGap: 48, itemGap: itemGap,
textStyle: { textStyle: {
color: '#5B6167', color: '#5B6167',
fontFamily: 'PingFangSC, PingFang SC', fontFamily: 'PingFangSC, PingFang SC',
@@ -153,10 +163,10 @@ const PieChart: FC<PieChartProps> = ({
avoidLabelOverlap: false, avoidLabelOverlap: false,
percentPrecision: 0, percentPrecision: 0,
padAngle: 1, padAngle: 1,
width: 182, width: seriesWidth,
height: 182, height: seriesHeight,
left: 'center', left: 'center',
top: 24, top: seriesTop,
itemStyle: { itemStyle: {
borderRadius: 2, borderRadius: 2,
shadowBlur: 4, shadowBlur: 4,
@@ -165,6 +175,7 @@ const PieChart: FC<PieChartProps> = ({
shadowColor: 'rgba(0,0,0,0.25)', shadowColor: 'rgba(0,0,0,0.25)',
}, },
label: { label: {
show: seriesLabel,
fontWeight: 'bold', fontWeight: 'bold',
color: '#171719', color: '#171719',
formatter: '{d}%', formatter: '{d}%',

View File

@@ -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<RadioGroupProps, 'onChange'> {
/** 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<RadioCardProps> = ({
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 (
<Space size={12}>
{options.map(option => (
<div
key={String(option.value)}
className={clsx('rb:rounded-[14px] rb:py-1 rb:px-2 rb:text-[12px] rb:leading-4.5 rb:cursor-pointer', {
'rb:bg-[#171719] rb:font-medium rb:text-white': value === option.value,
'rb:bg-[#F6F6F6]': value !== option.value,
})}
onClick={() => handleChange(option)}
>
{option.label}
</div>
))}
</Space>
);
};
export default RadioGroupButton;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-02 15:23:39 * @Date: 2026-02-02 15:23:39
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-11 11:10:40 * @Last Modified time: 2026-03-12 16:16:49
*/ */
/** /**
* RbSlider Component * RbSlider Component
@@ -15,7 +15,7 @@
* @component * @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'; import { Slider, type SliderSingleProps, Flex, InputNumber, type InputNumberProps } from 'antd';
/** Props interface for RbSlider component */ /** Props interface for RbSlider component */
@@ -27,6 +27,8 @@ interface RbSliderProps extends SliderSingleProps {
isInput?: boolean; isInput?: boolean;
size?: 'small' | 'default'; size?: 'small' | 'default';
className?: string; className?: string;
prefix?: string | ReactNode;
inputClassName?: string;
} }
/** Custom slider component with value display */ /** Custom slider component with value display */
@@ -40,6 +42,8 @@ const RbSlider: FC<RbSliderProps> = ({
size = 'default' , size = 'default' ,
isInput = false, isInput = false,
className = '', className = '',
prefix,
inputClassName,
...rest ...rest
}) => { }) => {
const [curValue, setCurValue] = useState<SliderSingleProps['value']>(0) const [curValue, setCurValue] = useState<SliderSingleProps['value']>(0)
@@ -93,7 +97,8 @@ const RbSlider: FC<RbSliderProps> = ({
step={step as number} step={step as number}
value={curValue} value={curValue}
onChange={handleInputChange} onChange={handleInputChange}
className="rb:w-20!" prefix={prefix}
className={`${inputClassName || '' } rb:w-20!`}
/> />
: <div className="rb:text-[14px] rb:text-[#155EEF] rb:leading-5">{curValue || min}</div> : <div className="rb:text-[14px] rb:text-[#155EEF] rb:leading-5">{curValue || min}</div>
} }

View File

@@ -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
* <RbStatistic title="Total Users" value={1280} suffix="people" />
*/
const RbStatistic: FC<RbStatistic> = ({
title,
value,
suffix,
}) => {
return (
<div className="rb:leading-5">
<div className="rb:text-[#5B6167] rb:mb-1">{title}</div>
<div className="rb:text-[#212332] rb:font-medium">{value} {suffix}</div>
</div>
);
};
export default RbStatistic

View File

@@ -115,7 +115,7 @@ const SearchInput: FC<SearchInputProps> = ({
value={value} value={value}
onChange={handleChange} onChange={handleChange}
style={{ width: '300px' }} style={{ width: '300px' }}
className={className} className={`rb:border-none! ${className}`}
{...props} {...props}
/> />
); );

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-02 15:29:42 * @Date: 2026-02-02 15:29:42
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-11 11:14:29 * @Last Modified time: 2026-03-16 11:59:09
*/ */
/** /**
* StatusTag Component * StatusTag Component
@@ -41,9 +41,9 @@ const StatusTag: FC<StatusTagProps> = ({
text text
}) => { }) => {
return ( return (
<Tag style={{ backgroundColor: '#fff', borderColor: '#DFE4ED' }}> <Tag className="rb:bg-white! rb:border-[#EBEBEB]! rb:rounded-md!">
<Flex align="center" className='rb:text-[#5B6167] rb:pl-px rb:pr-px'> <Flex align="center" gap={4} className='rb:text-[#5B6167] rb:py-px rb:px-2'>
<span className={clsx(' rb:w-1.25 rb:h-1.25 rb:rounded-full rb:mr-1', Colors[status])}></span> <span className={clsx('rb:size-1.25 rb:rounded-full', Colors[status])}></span>
{ text } { text }
</Flex> </Flex>
</Tag> </Tag>