feat(web): components update
This commit is contained in:
@@ -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}%',
|
||||||
|
|||||||
96
web/src/components/RadioGroupButton/index.tsx
Normal file
96
web/src/components/RadioGroupButton/index.tsx
Normal 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;
|
||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
43
web/src/components/RbStatistic/index.tsx
Normal file
43
web/src/components/RbStatistic/index.tsx
Normal 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
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user