From 0b3b241436d678cf84b7c4c5c5b456a2d39d65dd Mon Sep 17 00:00:00 2001 From: zhaoying Date: Sat, 7 Mar 2026 12:18:11 +0800 Subject: [PATCH] feat(web): components update --- web/src/App.tsx | 2 - web/src/components/ButtonCheckbox/index.tsx | 15 +- web/src/components/Charts/AreaLineChart.tsx | 306 ++++++++++++++++++ web/src/components/Charts/BarChart.tsx | 295 +++++++++++++++++ .../components/Charts/GraphNetworkChart.tsx | 200 ++++++++++++ web/src/components/Charts/LineChart.tsx | 260 +++++++++++++++ web/src/components/Charts/PieChart.tsx | 193 +++++++++++ web/src/components/Chat/ChatContent.tsx | 17 +- web/src/components/Chat/ChatInput.tsx | 41 ++- web/src/components/CustomSelect/index.tsx | 8 +- web/src/components/Empty/PageEmpty.tsx | 6 +- web/src/components/Empty/PageLoading.tsx | 4 +- web/src/components/Empty/index.tsx | 16 +- .../components/FormItem/SwitchFormItem.tsx | 14 +- web/src/components/Header/UserInfoModal.tsx | 86 +++-- web/src/components/Header/index.module.css | 16 +- web/src/components/Header/index.tsx | 10 +- web/src/components/Layout/AuthLayout.tsx | 6 +- web/src/components/Layout/AuthSpaceLayout.tsx | 6 +- web/src/components/Layout/BasicLayout.tsx | 9 +- web/src/components/Layout/LayoutBg.tsx | 41 --- web/src/components/Layout/PageHeader.tsx | 70 ++++ web/src/components/Layout/layout.module.css | 37 --- web/src/components/Markdown/RbButton.tsx | 17 +- web/src/components/PageScrollList/index.tsx | 28 +- web/src/components/RadioGroupCard/index.tsx | 21 +- web/src/components/RbAlert/index.tsx | 9 +- web/src/components/RbButton/index.module.css | 27 ++ web/src/components/RbButton/index.tsx | 22 ++ web/src/components/RbCard/Card.tsx | 43 +-- web/src/components/RbCard/index.tsx | 76 +++++ .../RbDescriptions/index.module.css | 21 ++ web/src/components/RbDescriptions/index.tsx | 14 + web/src/components/RbDrawer/index.tsx | 6 +- web/src/components/RbSlider/index.module.css | 3 + web/src/components/RbSlider/index.tsx | 66 +++- web/src/components/SearchInput/index.tsx | 8 +- web/src/components/SiderMenu/index.module.css | 18 +- web/src/components/SiderMenu/index.tsx | 165 ++++++---- web/src/components/SliderInput/index.tsx | 2 +- web/src/components/StatusTag/index.tsx | 11 +- web/src/components/Table/index.tsx | 5 +- web/src/components/Upload/UploadFiles.tsx | 2 +- web/src/components/Upload/UploadImages.tsx | 4 +- 44 files changed, 1881 insertions(+), 345 deletions(-) create mode 100644 web/src/components/Charts/AreaLineChart.tsx create mode 100644 web/src/components/Charts/BarChart.tsx create mode 100644 web/src/components/Charts/GraphNetworkChart.tsx create mode 100644 web/src/components/Charts/LineChart.tsx create mode 100644 web/src/components/Charts/PieChart.tsx delete mode 100644 web/src/components/Layout/LayoutBg.tsx create mode 100644 web/src/components/Layout/PageHeader.tsx delete mode 100644 web/src/components/Layout/layout.module.css create mode 100644 web/src/components/RbButton/index.module.css create mode 100644 web/src/components/RbButton/index.tsx create mode 100644 web/src/components/RbCard/index.tsx create mode 100644 web/src/components/RbDescriptions/index.module.css create mode 100644 web/src/components/RbDescriptions/index.tsx create mode 100644 web/src/components/RbSlider/index.module.css diff --git a/web/src/App.tsx b/web/src/App.tsx index 1d298358..a10f9409 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -21,7 +21,6 @@ import { useTranslation } from 'react-i18next'; import { lightTheme } from './styles/antdThemeConfig.ts' import router from './routes'; import { useI18n } from '@/store/locale' -import LayoutBg from '@/components/Layout/LayoutBg' import dayjs from 'dayjs' import 'dayjs/locale/en' import 'dayjs/locale/zh-cn' @@ -61,7 +60,6 @@ function App() { theme={lightTheme} > - }> = ({ } return ( -
@@ -71,7 +72,7 @@ const ButtonCheckbox: FC = ({ {/* Display checked icon when checked */} {checkedIcon && checked && } {children} -
+ ); }; diff --git a/web/src/components/Charts/AreaLineChart.tsx b/web/src/components/Charts/AreaLineChart.tsx new file mode 100644 index 00000000..40bfabb1 --- /dev/null +++ b/web/src/components/Charts/AreaLineChart.tsx @@ -0,0 +1,306 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-10 13:36:03 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-25 13:51:52 + */ +/* + * AreaLineChart Component + * + * A reusable area line chart component built with ECharts that displays time-series data + * with gradient-filled areas under the lines. Supports multiple data series with + * customizable colors and responsive behavior. + * + * Features: + * - Multiple line series with gradient area fills + * - Gradient line colors (white to color to white) + * - Customizable x-axis key for flexible data structures + * - Date-based x-axis with formatted labels (DD/MM) + * - Responsive resizing using ResizeObserver + * - Interactive tooltips on hover + * - Customizable grid layout and colors + * - Legend at the bottom for series identification + * - Empty state when no data is available + * - Smooth rendering with requestAnimationFrame + */ +import { type FC, useEffect, useRef, useMemo } from 'react' +import ReactEcharts from 'echarts-for-react'; +import * as echarts from 'echarts'; + +import { formatDateTime } from '@/utils/format'; +import Empty from '@/components/Empty' + +/** Base configuration for all line series */ +const SeriesConfig = { + type: 'line', + stack: 'Total', + symbol: 'circle', + symbolSize: 5, + showSymbol: true, + label: { + show: false, + position: 'top' + }, + emphasis: { + focus: 'series' + }, +} + +/** Default color palette for area line series */ +const Colors = ['#155EEF', '#FFB048', '#4DA8FF'] + +/** + * Data structure for chart data points + * Flexible structure allowing any string key with string or number values + * + * @interface ChartData + * @property {string | number} [key: string] - Dynamic properties for x-axis and data series + */ +export interface ChartData { + [key: string]: string | number; +} + +/** + * Props for the AreaLineChart component + * + * @interface AreaLineChartProps + * @property {string} xAxisKey - Key name in chartData to use for x-axis values + * @property {ChartData[]} chartData - Array of data points with dynamic properties + * @property {Record} seriesList - Map of data keys to display names + * @property {string} [className] - Additional CSS classes for the container + * @property {number} [height] - Height of the chart in pixels + * @property {string[]} [colors] - Custom color array for line series and gradients + * @property {any} [grid] - ECharts grid configuration for chart positioning + */ +interface AreaLineChartProps { + xAxisKey: string; + chartData: ChartData[]; + seriesList: Record; + className?: string; + height?: number; + colors?: string[]; + grid?: any; + lineStyle?: any; + showLegend?: boolean; + smooth?: boolean; +} + +/** + * AreaLineChart Component + * + * Renders a multi-series area line chart with gradient fills. + * The area gradient goes from the series color at the top to white at the bottom. + * The line gradient goes from white to the series color and back to white. + * Automatically resizes when container dimensions change. + * + * @param {AreaLineChartProps} props - Component props + * @returns {JSX.Element} Rendered area line chart or empty state + * + * @example + * ```tsx + * + * ``` + */ +const AreaLineChart: FC = ({ + xAxisKey, + chartData, + seriesList, + height, + colors = Colors, + grid = { + top: 7, + left: 4, + right: 16, + bottom: 32, + containLabel: true + }, + lineStyle, + showLegend = true, + smooth = true +}) => { + /** Reference to the ECharts instance for programmatic control */ + const chartRef = useRef(null); + /** Flag to prevent multiple simultaneous resize operations */ + const resizeScheduledRef = useRef(false) + + /** + * Generate series configuration for each data series with gradient effects + * Creates area fills with vertical gradients (color to white) + * and line colors with horizontal gradients (white to color to white) + * + * @returns {Array} Array of ECharts series configurations with gradient styles + */ + const getSeries = () => { + return Object.entries(seriesList).map(([key, name], index) => ({ + ...SeriesConfig, + name: name, + data: chartData.map(vo => vo[key as keyof ChartData]), + areaStyle: { + opacity: 0.8, + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: colors[index] + }, + { + offset: 1, + color: '#FFFFFF' + } + ]) + }, + lineStyle: lineStyle || { + width: 3, + color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [ + { + offset: 0, + color: '#FFFFFF' + }, + { + offset: 0.8, + color: colors[index] + }, + { + offset: 1, + color: '#FFFFFF' + } + ]) + }, + smooth + })) + } + /** + * Memoized legend data to prevent unnecessary recalculations + * Formats series list for display in chart legend + */ + const formatSeriesList = useMemo(() => { + return Object.entries(seriesList).map(([_key, name]) => ({ + ...SeriesConfig, + name: name, + })) + }, [seriesList]) + + /** + * Set up responsive behavior using ResizeObserver + * Resizes chart when parent container dimensions change + */ + useEffect(() => { + const handleResize = () => { + if (chartRef.current && !resizeScheduledRef.current) { + resizeScheduledRef.current = true + requestAnimationFrame(() => { + chartRef.current?.getEchartsInstance().resize(); + resizeScheduledRef.current = false + }); + } + } + + const resizeObserver = new ResizeObserver(handleResize) + const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement + if (chartElement) { + resizeObserver.observe(chartElement) + } + + return () => { + resizeObserver.disconnect() + } + }, [chartData]) + + return ( +
+ {chartData && chartData.length > 0 + ? formatDateTime(item[xAxisKey], 'DD/MM')), + boundaryGap: false, + axisLabel: { + color: '#5B6167', + fontFamily: 'PingFangSC, PingFang SC', + lineHeight: 17, + }, + axisLine: { + show: false, + lineStyle: { + color: '#EBEBEB', + } + }, + splitLine: { + show: false, + }, + axisTick: { + show: false + } + }, + yAxis: { + type: 'value', + axisLabel: { + color: '#A8A9AA', + fontFamily: 'PingFangSC, PingFang SC', + align: 'right', + lineHeight: 17, + }, + axisLine: { + lineStyle: { + color: '#EBEBEB', + } + }, + }, + series: getSeries() + }} + style={{ height: `${height}px`, width: '100%', minWidth: '100%', boxSizing: 'border-box' }} + opts={{ renderer: 'canvas' }} + notMerge={true} + lazyUpdate={true} + /> + : + } +
+ ) +} + +export default AreaLineChart diff --git a/web/src/components/Charts/BarChart.tsx b/web/src/components/Charts/BarChart.tsx new file mode 100644 index 00000000..e476a2cb --- /dev/null +++ b/web/src/components/Charts/BarChart.tsx @@ -0,0 +1,295 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-10 13:36:03 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-25 13:49:04 + */ +/* + * BarChart Component + * + * A reusable area line chart component built with ECharts that displays time-series data + * with gradient-filled areas under the lines. Supports multiple data series with + * customizable colors and responsive behavior. + * + * Features: + * - Multiple line series with gradient area fills + * - Gradient line colors (white to color to white) + * - Customizable x-axis key for flexible data structures + * - Date-based x-axis with formatted labels (DD/MM) + * - Responsive resizing using ResizeObserver + * - Interactive tooltips on hover + * - Customizable grid layout and colors + * - Legend at the bottom for series identification + * - Empty state when no data is available + * - Smooth rendering with requestAnimationFrame + */ +import { type FC, useEffect, useRef, useMemo } from 'react' +import ReactEcharts from 'echarts-for-react'; +import * as echarts from 'echarts'; + +import { formatDateTime } from '@/utils/format'; +import Empty from '@/components/Empty' + +/** Base configuration for all line series */ +const SeriesConfig = { + type: 'bar', + stack: 'Total', + symbol: 'circle', + symbolSize: 5, + showSymbol: true, + label: { + show: false, + position: 'top' + }, + emphasis: { + focus: 'series' + }, + showBackground: true, +} + +/** Default color palette for area line series */ +const Colors = ['#155EEF', '#FFB048', '#4DA8FF'] + +/** + * Data structure for chart data points + * Flexible structure allowing any string key with string or number values + * + * @interface ChartData + * @property {string | number} [key: string] - Dynamic properties for x-axis and data series + */ +export interface ChartData { + [key: string]: string | number; +} + +/** + * Props for the BarChart component + * + * @interface BarChartProps + * @property {string} xAxisKey - Key name in chartData to use for x-axis values + * @property {ChartData[]} chartData - Array of data points with dynamic properties + * @property {Record} seriesList - Map of data keys to display names + * @property {string} [className] - Additional CSS classes for the container + * @property {number} [height] - Height of the chart in pixels + * @property {string[]} [colors] - Custom color array for line series and gradients + * @property {any} [grid] - ECharts grid configuration for chart positioning + */ +interface BarChartProps { + xAxisKey: string; + chartData: ChartData[]; + seriesList: Record; + className?: string; + height?: number; + colors?: string[]; + grid?: any; + itemStyle?: any; + showLegend?: boolean; + showBackground?: boolean; +} + +/** + * BarChart Component + * + * Renders a multi-series area line chart with gradient fills. + * The area gradient goes from the series color at the top to white at the bottom. + * The line gradient goes from white to the series color and back to white. + * Automatically resizes when container dimensions change. + * + * @param {BarChartProps} props - Component props + * @returns {JSX.Element} Rendered area line chart or empty state + * + * @example + * ```tsx + * + * ``` + */ +const BarChart: FC = ({ + xAxisKey, + chartData, + seriesList, + height, + colors = Colors, + grid = { + top: 7, + left: 4, + right: 16, + bottom: 32, + containLabel: true + }, + itemStyle, + showLegend = true, + showBackground = true, +}) => { + /** Reference to the ECharts instance for programmatic control */ + const chartRef = useRef(null); + /** Flag to prevent multiple simultaneous resize operations */ + const resizeScheduledRef = useRef(false) + + /** + * Generate series configuration for each data series with gradient effects + * Creates area fills with vertical gradients (color to white) + * and line colors with horizontal gradients (white to color to white) + * + * @returns {Array} Array of ECharts series configurations with gradient styles + */ + const getSeries = () => { + return Object.entries(seriesList).map(([key, name], index) => ({ + ...SeriesConfig, + name: name, + data: chartData.map(vo => vo[key as keyof ChartData]), + barWidth: 16, + itemStyle: itemStyle || { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: colors[index] + }, + { + offset: 1, + color: '#FFFFFF' + } + ]), + }, + emphasis: { + itemStyle: { + } + }, + barGap: '-100%', + showBackground: showBackground, + })) + } + /** + * Memoized legend data to prevent unnecessary recalculations + * Formats series list for display in chart legend + */ + const formatSeriesList = useMemo(() => { + return Object.entries(seriesList).map(([_key, name]) => ({ + ...SeriesConfig, + name: name, + })) + }, [seriesList]) + + /** + * Set up responsive behavior using ResizeObserver + * Resizes chart when parent container dimensions change + */ + useEffect(() => { + const handleResize = () => { + if (chartRef.current && !resizeScheduledRef.current) { + resizeScheduledRef.current = true + requestAnimationFrame(() => { + chartRef.current?.getEchartsInstance().resize(); + resizeScheduledRef.current = false + }); + } + } + + const resizeObserver = new ResizeObserver(handleResize) + const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement + if (chartElement) { + resizeObserver.observe(chartElement) + } + + return () => { + resizeObserver.disconnect() + } + }, [chartData]) + + return ( +
+ {chartData && chartData.length > 0 + ? formatDateTime(item[xAxisKey], 'DD/MM')), + boundaryGap: false, + axisLabel: { + color: '#5B6167', + fontFamily: 'PingFangSC, PingFang SC', + lineHeight: 17, + }, + axisLine: { + show: false, + itemStyle: { + color: '#EBEBEB', + } + }, + splitLine: { + show: false, + }, + axisTick: { + show: false + } + }, + yAxis: { + type: 'value', + axisLabel: { + color: '#A8A9AA', + fontFamily: 'PingFangSC, PingFang SC', + align: 'right', + lineHeight: 17, + }, + axisLine: { + itemStyle: { + color: '#EBEBEB', + } + }, + }, + series: getSeries() + }} + style={{ height: `${height}px`, width: '100%', minWidth: '100%', boxSizing: 'border-box' }} + opts={{ renderer: 'canvas' }} + notMerge={true} + lazyUpdate={true} + /> + : + } +
+ ) +} + +export default BarChart diff --git a/web/src/components/Charts/GraphNetworkChart.tsx b/web/src/components/Charts/GraphNetworkChart.tsx new file mode 100644 index 00000000..8f4ec796 --- /dev/null +++ b/web/src/components/Charts/GraphNetworkChart.tsx @@ -0,0 +1,200 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-10 14:06:09 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-10 14:06:09 + */ +/** + * GraphNetworkChart Component + * + * A force-directed graph visualization component built with ECharts. + * Displays nodes and edges in an interactive network diagram with physics-based layout. + * Supports zooming, panning, dragging nodes, and click interactions. + */ +import { type FC, useEffect, useRef, type SetStateAction, type Dispatch } from 'react' +import ReactEcharts from 'echarts-for-react'; + +import PageEmpty from '@/components/Empty/PageEmpty' + +// Default color palette for node categories +const Colors = ['#171719', '#155EEF', '#9C6FFF', '#FF8A4C'] + +/** + * Node interface representing a graph node/vertex + */ +export interface Node { + id: string; // Unique identifier for the node + label: string; // Display label for the node + category: number; // Category index for grouping and coloring + symbolSize: number; // Size of the node symbol in pixels + name: string; // Node name (used in ECharts) + itemStyle: { + color: string; // Custom color for this node + } + caption: string; // Additional description or caption + [key: string]: any; // Allow additional custom properties +} + +/** + * Edge interface representing a connection between two nodes + */ +export interface Edge { + id: string; // Unique identifier for the edge + source: string; // Source node ID + target: string; // Target node ID + type: string; // Type/category of the relationship + caption: string; // Description of the relationship + value: number; // Numeric value associated with the edge + weight: number; // Weight/strength of the connection +} + +/** + * Props for the GraphNetworkChart component + */ +interface GraphNetworkChartProps { + nodes: Node[]; // Array of nodes to display in the graph + links: Edge[]; // Array of edges connecting the nodes + categories: { name: string }[]; // Category definitions for node grouping + colors?: string[]; // Optional custom color palette (defaults to Colors) + onNodeClick: Dispatch>; // Callback when a node is clicked +} + +const GraphNetworkChart: FC = ({ + nodes, + links, + categories, + colors = Colors, + onNodeClick, +}) => { + // Reference to the ECharts instance for programmatic control + const chartRef = useRef(null); + + // Flag to prevent multiple simultaneous resize operations (debouncing) + const resizeScheduledRef = useRef(false) + + /** + * Effect: Handle responsive chart resizing + * + * Uses ResizeObserver to detect container size changes and resize the chart accordingly. + * Implements requestAnimationFrame for smooth, debounced resize operations. + * Re-runs when nodes change to ensure proper sizing with new data. + */ + useEffect(() => { + const handleResize = () => { + if (chartRef.current && !resizeScheduledRef.current) { + resizeScheduledRef.current = true + // Use requestAnimationFrame for smooth, optimized resize + requestAnimationFrame(() => { + chartRef.current?.getEchartsInstance().resize(); + resizeScheduledRef.current = false + }); + } + } + + // Observe the chart container for size changes + const resizeObserver = new ResizeObserver(handleResize) + const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement + if (chartElement) { + resizeObserver.observe(chartElement) + } + + // Cleanup: disconnect observer when component unmounts + return () => { + resizeObserver.disconnect() + } + }, [nodes]) + + return ( +
+ {/* Render chart only if nodes exist, otherwise show empty state */} + {nodes && nodes.length > 0 + ? { + // Only trigger callback for node clicks (not edges or background) + if (params.dataType === 'node') { + onNodeClick(params.data) + } + } + }} + /> + : + } +
+ ) +} + +export default GraphNetworkChart diff --git a/web/src/components/Charts/LineChart.tsx b/web/src/components/Charts/LineChart.tsx new file mode 100644 index 00000000..e5217336 --- /dev/null +++ b/web/src/components/Charts/LineChart.tsx @@ -0,0 +1,260 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-10 13:35:55 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-10 13:35:55 + */ +/* + * LineChart Component + * + * A reusable line chart component built with ECharts for displaying time-series data + * with multiple data series. Supports customizable colors, responsive behavior, + * and interactive tooltips. + * + * Features: + * - Multiple line series with different colors + * - Date-based x-axis with formatted labels (DD/MM) + * - Responsive resizing using ResizeObserver + * - Interactive tooltips on hover + * - Customizable grid layout and colors + * - Legend at the bottom for series identification + * - Empty state when no data is available + * - Smooth rendering with requestAnimationFrame + */ +import { type FC, useEffect, useRef, useMemo } from 'react' +import ReactEcharts from 'echarts-for-react'; + +import { formatDateTime } from '@/utils/format'; +import Empty from '@/components/Empty' + +/** Base configuration for all line series */ +const SeriesConfig = { + type: 'line', + stack: 'Total', + symbol: 'circle', + symbolSize: 5, + showSymbol: true, + label: { + show: false, + position: 'top' + }, + emphasis: { + focus: 'series' + }, +} + +/** Default color palette for line series */ +const Colors = ['#171719', '#155EEF', '#FF5D34'] + +/** + * Data structure for chart data points + * + * @interface ChartData + * @property {string | number} date - Date value for x-axis (timestamp or date string) + * @property {string | number} [key: string] - Dynamic properties for different data series + */ +export interface ChartData { + date: string | number; + [key: string]: string | number; +} + +/** + * Props for the LineChart component + * + * @interface LineChartProps + * @property {ChartData[]} chartData - Array of data points with date and series values + * @property {Record} seriesList - Map of data keys to display names + * @property {string} [className] - Additional CSS classes for the container + * @property {number} [height] - Height of the chart in pixels + * @property {string[]} [colors] - Custom color array for line series + * @property {any} [grid] - ECharts grid configuration for chart positioning + */ +interface LineChartProps { + chartData: ChartData[]; + seriesList: Record; + className?: string; + height?: number; + colors?: string[]; + grid?: any; +} + +/** + * LineChart Component + * + * Renders a multi-series line chart with date-based x-axis. + * Automatically resizes when container dimensions change. + * + * @param {LineChartProps} props - Component props + * @returns {JSX.Element} Rendered line chart or empty state + * + * @example + * ```tsx + * + * ``` + */ +const LineChart: FC = ({ + chartData, + seriesList, + height, + colors = Colors, + grid = { + top: 7, + right: 16, + } +}) => { + /** Reference to the ECharts instance for programmatic control */ + const chartRef = useRef(null); + /** Flag to prevent multiple simultaneous resize operations */ + const resizeScheduledRef = useRef(false) + + /** + * Generate series configuration for each data series + * Maps seriesList keys to chart series with corresponding data and colors + * + * @returns {Array} Array of ECharts series configurations + */ + const getSeries = () => { + return Object.entries(seriesList).map(([key, name], index) => ({ + ...SeriesConfig, + name: name, + data: chartData.map(vo => vo[key as keyof ChartData]), + lineStyle: { + width: 2, + color: colors[index] + }, + })) + } + /** + * Memoized legend data to prevent unnecessary recalculations + * Formats series list for display in chart legend + */ + const formatSeriesList = useMemo(() => { + return Object.entries(seriesList).map(([_key, name]) => ({ + ...SeriesConfig, + name: name, + })) + }, [seriesList]) + + /** + * Set up responsive behavior using ResizeObserver + * Resizes chart when parent container dimensions change + */ + useEffect(() => { + const handleResize = () => { + if (chartRef.current && !resizeScheduledRef.current) { + resizeScheduledRef.current = true + requestAnimationFrame(() => { + chartRef.current?.getEchartsInstance().resize(); + resizeScheduledRef.current = false + }); + } + } + + const resizeObserver = new ResizeObserver(handleResize) + const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement + if (chartElement) { + resizeObserver.observe(chartElement) + } + + return () => { + resizeObserver.disconnect() + } + }, [chartData]) + + return ( +
+ {chartData && chartData.length > 0 + ? formatDateTime(item.date, 'DD/MM')), + boundaryGap: false, + axisLabel: { + color: '#5B6167', + fontFamily: 'PingFangSC, PingFang SC', + lineHeight: 17, + }, + axisLine: { + show: false, + lineStyle: { + color: '#EBEBEB', + } + }, + splitLine: { + show: false, + }, + axisTick: { + show: false + } + }, + yAxis: { + type: 'value', + axisLabel: { + color: '#A8A9AA', + fontFamily: 'PingFangSC, PingFang SC', + align: 'right', + lineHeight: 17, + }, + axisLine: { + lineStyle: { + color: '#EBEBEB', + } + }, + }, + series: getSeries() + }} + style={{ height: '100%', width: '100%', minWidth: '100%', boxSizing: 'border-box' }} + opts={{ renderer: 'canvas' }} + notMerge={true} + lazyUpdate={true} + /> + : + } +
+ ) +} + +export default LineChart diff --git a/web/src/components/Charts/PieChart.tsx b/web/src/components/Charts/PieChart.tsx new file mode 100644 index 00000000..221eb4a6 --- /dev/null +++ b/web/src/components/Charts/PieChart.tsx @@ -0,0 +1,193 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-10 13:35:45 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-10 13:35:45 + */ +/* + * PieChart Component + * + * A reusable pie chart component built with ECharts that displays data distribution + * in a donut chart format with customizable colors and responsive behavior. + * + * Features: + * - Donut-style pie chart with percentage labels + * - Customizable color palette + * - Responsive resizing using ResizeObserver + * - Hover tooltips showing percentage values + * - Legend at the bottom with horizontal layout + * - Empty state when no data is available + * - Shadow effects for better visual depth + */ +import { type FC, useEffect, useRef } from 'react' +import ReactEcharts from 'echarts-for-react'; + +import Empty from '@/components/Empty' + +/** Default color palette for pie chart segments */ +const Colors = ['#171719', '#155EEF', '#4DA8FF', '#9C6FFF', '#ABEBFF', '#DFE4ED'] + +/** + * Data structure for each pie chart segment + * + * @interface ChartData + * @property {string} name - Label for the segment (displayed in legend) + * @property {number} value - Numeric value for the segment (determines size) + */ +export interface ChartData { + name: string; + value: number; +} + +/** + * Props for the PieChart component + * + * @interface PieChartProps + * @property {ChartData[]} chartData - Array of data points to display in the chart + * @property {number} [height=260] - Height of the chart in pixels + * @property {string[]} [colors] - Custom color array for chart segments (defaults to Colors) + */ +interface PieChartProps { + chartData: ChartData[]; + height?: number; + colors?: string[]; +} + +/** + * PieChart Component + * + * Renders a donut-style pie chart with percentage labels and legend. + * Automatically resizes when container dimensions change. + * + * @param {PieChartProps} props - Component props + * @returns {JSX.Element} Rendered pie chart or empty state + * + * @example + * ```tsx + * + * ``` + */ +const PieChart: FC = ({ + chartData, + height = 260, + colors = Colors, +}) => { + /** Reference to the ECharts instance for programmatic control */ + const chartRef = useRef(null); + /** Flag to prevent multiple simultaneous resize operations */ + const resizeScheduledRef = useRef(false) + + /** + * Set up responsive behavior using ResizeObserver + * Resizes chart when parent container dimensions change + */ + useEffect(() => { + const handleResize = () => { + if (chartRef.current && !resizeScheduledRef.current) { + resizeScheduledRef.current = true + // Use requestAnimationFrame for smooth resize performance + requestAnimationFrame(() => { + chartRef.current?.getEchartsInstance().resize(); + resizeScheduledRef.current = false + }); + } + } + + const resizeObserver = new ResizeObserver(handleResize) + const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement + if (chartElement) { + resizeObserver.observe(chartElement) + } + + // Cleanup: disconnect observer when component unmounts + return () => { + resizeObserver.disconnect() + } + }, [chartData]) + + return ( +
+ {chartData && chartData.length > 0 + ? + : + } +
+ ) +} + +export default PieChart diff --git a/web/src/components/Chat/ChatContent.tsx b/web/src/components/Chat/ChatContent.tsx index c1f5223c..1dfed604 100644 --- a/web/src/components/Chat/ChatContent.tsx +++ b/web/src/components/Chat/ChatContent.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2025-12-10 16:46:17 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-06 21:05:52 + * @Last Modified time: 2026-02-25 19:04:55 */ import { type FC, useRef, useEffect } from 'react' import clsx from 'clsx' @@ -85,26 +85,29 @@ const ChatContent: FC = ({ : <> {/* Top label (such as timestamp, username, etc.) */} {labelPosition === 'top' && -
+
{labelFormat(item)}
} {/* Message bubble */} -
+ {item.status &&
} {item.subContent && renderRuntime && renderRuntime(item, index)} {/* Render message content using Markdown component */}
{/* Bottom label (such as timestamp, username, etc.) */} {labelPosition === 'bottom' && -
+
{labelFormat(item)}
} diff --git a/web/src/components/Chat/ChatInput.tsx b/web/src/components/Chat/ChatInput.tsx index 508b0d0c..516b9528 100644 --- a/web/src/components/Chat/ChatInput.tsx +++ b/web/src/components/Chat/ChatInput.tsx @@ -4,12 +4,10 @@ * @Last Modified by: ZhaoYing * @Last Modified time: 2026-03-06 13:36:20 */ -import { type FC, useEffect, useMemo } from 'react' +import { type FC, useEffect, useMemo, useState } from 'react' import { Flex, Input, Form } from 'antd' +import clsx from 'clsx' -import SendIcon from '@/assets/images/conversation/send.svg' -import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg' -import LoadingIcon from '@/assets/images/conversation/loading.svg' import type { ChatInputProps } from './types' /** @@ -28,6 +26,7 @@ const ChatInput: FC = ({ }) => { const [form] = Form.useForm() const values = Form.useWatch([], form) + const [isFocus, setIsFocus] = useState(false) // Monitor form value changes to control send button state // Clear form when external message is empty @@ -69,9 +68,18 @@ const ChatInput: FC = ({ onSend(values.message) } + const handleFocus = () => { + setIsFocus(true) + } + const handleBlur = () => { + setIsFocus(false) + } + return (
- + {previewFileList.length > 0 &&
{previewFileList.map((file) => { if (file.type.includes('image')) { @@ -108,7 +116,12 @@ const ChatInput: FC = ({ ) } return ( -
+ {(file.type.includes('doc') || file.type.includes('docx') || file.type.includes('word') || file.type.includes('wordprocessingml.document')) &&
} @@ -126,7 +139,7 @@ const ChatInput: FC = ({ className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]" onClick={() => handleDelete(file)} >
-
+
) })}
} @@ -145,23 +158,25 @@ const ChatInput: FC = ({ handleSend(); } }} + onFocus={handleFocus} + onBlur={handleBlur} /> {/* Bottom action area */} - + {/* Child component content (such as buttons) */}
{children}
-
+ {/* Send button - display different icons based on state */} {loading - ? + ?
: !values || !values?.message || values?.message?.trim() === '' - ? - : + ?
+ :
} -
+
diff --git a/web/src/components/CustomSelect/index.tsx b/web/src/components/CustomSelect/index.tsx index ea5ca718..c2de26ac 100644 --- a/web/src/components/CustomSelect/index.tsx +++ b/web/src/components/CustomSelect/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:02:17 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:46:29 + * @Last Modified time: 2026-03-04 14:43:42 */ /** * CustomSelect - A select component that fetches options from an API @@ -13,7 +13,7 @@ * @component */ -import { useEffect, useState, useMemo, type FC, type Key } from 'react'; +import { useEffect, useState, useMemo, type FC } from 'react'; import { Select } from 'antd'; import type { SelectProps, DefaultOptionType } from 'antd/es/select'; import { useTranslation } from 'react-i18next'; @@ -22,7 +22,7 @@ import { request } from '@/utils/request'; // Generic option type for API response data interface OptionType { - [key: string]: Key | string | number; + [key: string]: any; } // API response structure @@ -104,7 +104,7 @@ const CustomSelect: FC = ({ {/* Render options from API data */} {displayOptions.map((option) => ( - {String(option[labelKey])} + {option[labelKey]} ))} diff --git a/web/src/components/Empty/PageEmpty.tsx b/web/src/components/Empty/PageEmpty.tsx index c54b47d1..d2899f3f 100644 --- a/web/src/components/Empty/PageEmpty.tsx +++ b/web/src/components/Empty/PageEmpty.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:04:18 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:49:01 + * @Last Modified time: 2026-02-09 13:50:04 */ /** * PageEmpty Component @@ -22,7 +22,7 @@ import Empty from './index' /** * @param size - Icon size in pixels - single number or [width, height] array (default: [240, 210]) */ -const PageEmpty: FC<{ size?: number | number[] }> = ({ size = [240, 210] }) => { +const PageEmpty: FC<{ size?: number | number[]; className?: string; }> = ({ size = [240, 210], className = '' }) => { const { t } = useTranslation() return ( = ({ size = [240, 210] }) => { title={t('empty.pageEmpty')} subTitle={t('empty.pageEmptyDesc')} size={size} - className="rb:h-full" + className={`rb:h-full ${className}`} /> ) } diff --git a/web/src/components/Empty/PageLoading.tsx b/web/src/components/Empty/PageLoading.tsx index 5f146bf6..eaa68ff7 100644 --- a/web/src/components/Empty/PageLoading.tsx +++ b/web/src/components/Empty/PageLoading.tsx @@ -22,7 +22,7 @@ import Empty from './index' /** * @param size - Icon size in pixels - single number or [width, height] array (default: [240, 210]) */ -const PageLoading: FC<{ size?: number | number[] }> = ({ size = [240, 210] }) => { +const PageLoading: FC<{ size?: number | number[]; className?: string; }> = ({ size = [240, 210], className = '' }) => { const { t } = useTranslation() return ( = ({ size = [240, 210] }) => title={t('empty.loadingEmpty')} subTitle={t('empty.loadingEmptyDesc')} size={size} - className="rb:h-full" + className={`rb:h-full ${className}`} /> ) } diff --git a/web/src/components/Empty/index.tsx b/web/src/components/Empty/index.tsx index 48bfa33c..ed30ccdb 100644 --- a/web/src/components/Empty/index.tsx +++ b/web/src/components/Empty/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:03:25 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-25 11:14:25 + * @Last Modified time: 2026-03-04 14:02:53 */ /** * Empty Component @@ -15,6 +15,7 @@ import { type FC, type ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; +import { Flex } from 'antd'; import emptyIcon from '@/assets/images/empty/empty.svg'; @@ -48,14 +49,19 @@ const Empty: FC = ({ // Use custom subtitle or default translation if subtitle is needed const curSubTitle = isNeedSubTitle ? (subTitle || t('empty.tableEmpty')) : null; return ( -
+ {/* Empty state icon */} 404 {/* Optional title */} - {title &&
{title}
} + {title &&
{title}
} {/* Optional subtitle with conditional styling */} - {curSubTitle &&
{curSubTitle}
} -
+ {curSubTitle &&
{curSubTitle}
} + ); } export default Empty; \ No newline at end of file diff --git a/web/src/components/FormItem/SwitchFormItem.tsx b/web/src/components/FormItem/SwitchFormItem.tsx index 8ac52f98..44be6089 100644 --- a/web/src/components/FormItem/SwitchFormItem.tsx +++ b/web/src/components/FormItem/SwitchFormItem.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:06:24 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:50:49 + * @Last Modified time: 2026-02-25 14:52:52 */ /** * SwitchFormItem Component @@ -13,7 +13,7 @@ * @component */ -import { Switch, Form } from "antd"; +import { Switch, Form, Flex } from "antd"; import type { FC, ReactNode } from "react"; import LabelWrapper from './LabelWrapper' @@ -43,10 +43,14 @@ const SwitchFormItem: FC = ({ disabled }) => { return ( -
+ {/* Label and description section */} - {desc && } + {desc && } {/* Switch control */} = ({ > -
+ ) } diff --git a/web/src/components/Header/UserInfoModal.tsx b/web/src/components/Header/UserInfoModal.tsx index 94a4db7c..25006060 100644 --- a/web/src/components/Header/UserInfoModal.tsx +++ b/web/src/components/Header/UserInfoModal.tsx @@ -15,7 +15,7 @@ */ import { forwardRef, useImperativeHandle, useState, useRef } from 'react'; -import { Button, Space } from 'antd'; +import { Button, Flex, Space } from 'antd'; import { UnlockOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; @@ -79,49 +79,67 @@ const UserInfoModal = forwardRef((_props, ref) => { footer={null} > {/* Basic Information Section */} -
{t('header.basicInfo')}
- - {/* Username */} -
- {t('user.username')} - {user.username} -
- {/* Email */} -
- {t('user.email')} - - {user.email} -
-
-
- {/* Role */} -
- {t('user.role')} - {user.is_superuser ? t('user.superuser') : t('user.normalUser')} -
- {/* Created Date */} -
- {t('user.createdAt')} - {formatDateTime(user.created_at, 'YYYY-MM-DD HH:mm:ss')} -
+ +
{t('header.basicInfo')}
+ {/* Username */} + + {t('user.username')} + {user.username} + + {/* Email */} + + {t('user.email')} + + {user.email} +
+
+
+ {/* Role */} + + {t('user.role')} + {user.is_superuser ? t('user.superuser') : t('user.normalUser')} + + {/* Created Date */} + + {t('user.createdAt')} + {formatDateTime(user.created_at, 'YYYY-MM-DD HH:mm:ss')} + +
{/* Security Settings Section */} -
{t('header.securitySettings')}
+
{t('header.securitySettings')}
{/* Password Change Card */} -
-
+ +
{t('header.changePassword')}
{t('header.changePasswordDesc')}
-
+ -
+ = ({source = 'manage'}) => { * - Disables navigation for the last breadcrumb item */ const formatBreadcrumbNames = () => { - return breadcrumbs.map((menu, index) => { + return breadcrumbs.filter(item => item.type !== 'group').map((menu, index) => { const item: any = { title: menu.i18nKey ? t(menu.i18nKey) : menu.label, }; @@ -150,14 +150,14 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { return (
{/* Breadcrumb navigation */} - + {/* User info dropdown menu */} -
{user.username}
+
{user.username}
{ {/* Header with breadcrumbs and user menu */} {/* Main content area - renders child routes */} - + diff --git a/web/src/components/Layout/AuthSpaceLayout.tsx b/web/src/components/Layout/AuthSpaceLayout.tsx index d47f13f2..1efa6fd4 100644 --- a/web/src/components/Layout/AuthSpaceLayout.tsx +++ b/web/src/components/Layout/AuthSpaceLayout.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-02 15:11:43 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:11:43 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-05 14:57:08 */ /** * AuthSpaceLayout Component @@ -63,7 +63,7 @@ const AuthSpaceLayout: FC = () => { {/* Header with breadcrumbs and user menu configured for space mode */} {/* Main content area for knowledge base pages - renders child routes */} - + diff --git a/web/src/components/Layout/BasicLayout.tsx b/web/src/components/Layout/BasicLayout.tsx index cadcafb2..3aaa3895 100644 --- a/web/src/components/Layout/BasicLayout.tsx +++ b/web/src/components/Layout/BasicLayout.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-02 15:12:42 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:12:42 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-24 15:07:38 */ /** * BasicLayout Component @@ -19,6 +19,7 @@ import { Outlet } from 'react-router-dom'; import { useEffect, type FC } from 'react'; +import { Layout } from 'antd'; import { useUser } from '@/store/user'; @@ -36,10 +37,10 @@ const BasicLayout: FC = () => { }, [getUserInfo, getStorageType]); return ( -
+ {/* Render child routes without additional UI */} -
+ ) }; diff --git a/web/src/components/Layout/LayoutBg.tsx b/web/src/components/Layout/LayoutBg.tsx deleted file mode 100644 index ec76f196..00000000 --- a/web/src/components/Layout/LayoutBg.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * @Author: ZhaoYing - * @Date: 2026-02-02 15:13:20 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:13:20 - */ -/** - * LayoutBg Component - * - * A decorative background component that displays styled background elements. - * Provides visual aesthetics with positioned decorative shapes. - * - * @component - */ - -import { type FC } from 'react'; -import clsx from 'clsx'; - -import styles from './layout.module.css'; - -/** - * Background layout component with decorative elements. - * Renders a fixed full-screen background with styled shapes. - */ -const LayoutBg: FC = () => { - return ( -
- {/* Top section with decorative background shapes */} -
- {/* Left decorative element 1 */} -
- {/* Left decorative element 2 */} -
- {/* Right decorative element */} -
-
-
- ) -}; - -export default LayoutBg; \ No newline at end of file diff --git a/web/src/components/Layout/PageHeader.tsx b/web/src/components/Layout/PageHeader.tsx new file mode 100644 index 00000000..2070a1b8 --- /dev/null +++ b/web/src/components/Layout/PageHeader.tsx @@ -0,0 +1,70 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-10 11:08:27 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-24 15:25:14 + */ +/* + * PageHeader Component + * + * A reusable page header component that provides a consistent layout for page titles, + * subtitles, and action buttons across the application. + * + * Features: + * - Displays a main title and optional subtitle + * - Supports custom action buttons or extra content on the right side + * - Fixed height (64px) with responsive layout + * - Text overflow handling with ellipsis + * - Consistent spacing and styling using Tailwind CSS + */ +import { type FC, type ReactNode } from 'react'; +import { Layout, Flex } from 'antd'; +import clsx from 'clsx'; + +const { Header } = Layout; +interface ConfigHeaderProps { + avatarUrl?: string | null; + avatarText?: string; + avatarClassName?: string; + title?: ReactNode | string; + operation?: ReactNode; + extra?: ReactNode; + centerContent?: ReactNode; +} + +const PageHeader: FC = ({ + avatarUrl, + avatarText, + avatarClassName, + title, + operation, + extra, + centerContent +}) => { + return ( + // Main header container: full width, 64px height, flex layout with space between +
+ + {avatarUrl + ? {avatarUrl} + : avatarText + ? {avatarText} : null + } + {/* Left section: Title and subtitle */} +
+ {/* Main title: 18px font, semibold, [#212332] color, single line with ellipsis */} +
{title}
+
+ {operation} +
+ + {centerContent} + {/* Right section: Extra content (buttons, filters, etc.) */} + + {extra} + +
+ ); +}; + +export default PageHeader; \ No newline at end of file diff --git a/web/src/components/Layout/layout.module.css b/web/src/components/Layout/layout.module.css deleted file mode 100644 index f6e78f92..00000000 --- a/web/src/components/Layout/layout.module.css +++ /dev/null @@ -1,37 +0,0 @@ -.bg-top { - width: 100%; - height: 240px; - background: linear-gradient(to bottom, rgba(229, 242, 254, 1), rgba(251, 253, 255, 1)); - opacity: 0.8; -} - -.left1 { - width: 748px; - height: 354px; - background: radial-gradient(circle, rgba(138, 239, 255, 1) 100%, rgba(232, 244, 255, 0.71) 100%); - opacity: 0.24; - filter: blur(50px); - position: absolute; - left: -374px; - top: -187px; -} -.left2 { - width: 558px; - height: 504px; - background: radial-gradient(circle, #155EEF 0%, rgba(232, 244, 255, 0) 100%); - opacity: 0.22399999999999998; - filter: blur(50px); - position: absolute; - left: -279px; - top: -252px; -} -.right1 { - width: 470px; - height: 474px; - background: radial-gradient(circle, rgba(21, 94, 239, 1) 0%, rgba(232, 244, 255, 0) 100%); - opacity: 0.32; - filter: blur(50px); - position: absolute; - right: -235px; - top: -237px; -} \ No newline at end of file diff --git a/web/src/components/Markdown/RbButton.tsx b/web/src/components/Markdown/RbButton.tsx index 9af94de8..48ea2ff6 100644 --- a/web/src/components/Markdown/RbButton.tsx +++ b/web/src/components/Markdown/RbButton.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-02 15:16:10 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:16:10 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-25 14:02:17 */ /** * RbButton Component @@ -14,19 +14,12 @@ */ import { memo } from 'react' -import type { FC, ReactNode } from 'react' -import { Button } from 'antd' +import type { FC } from 'react' +import { Button, type ButtonProps } from 'antd' -/** Props interface for RbButton component */ -interface RbButtonProps { - node: { - children: ReactNode; - }; - children: string[] -} /** Button component for rendering buttons in markdown */ -const RbButton: FC = (props) => { +const RbButton: FC = (props) => { const { children } = props; return ( diff --git a/web/src/components/PageScrollList/index.tsx b/web/src/components/PageScrollList/index.tsx index a877a9c7..2de02b16 100644 --- a/web/src/components/PageScrollList/index.tsx +++ b/web/src/components/PageScrollList/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:18:19 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 15:44:42 + * @Last Modified time: 2026-02-09 13:51:01 */ /** * PageScrollList Component @@ -17,7 +17,7 @@ */ import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'; -import { List } from 'antd'; +import { Row, Col } from 'antd'; import InfiniteScroll from 'react-infinite-scroll-component'; import { request } from '@/utils/request'; @@ -58,6 +58,8 @@ interface PageScrollListProps> { needLoading?: boolean; } +const heightClass = 'rb:h-[calc(100vh-124px)]!'; + /** Infinite scroll list component with pagination support */ const PageScrollList = forwardRef(>({ renderItem, @@ -136,29 +138,29 @@ const PageScrollList = forwardRef(>({
: false} + loader={loading && needLoading ? : false} // endMessage={It is all, nothing more 🤐} scrollableTarget="scrollableDiv" className='rb:h-full!' > {/* Render grid list or empty state */} {data.length > 0 ? ( - ( - + + {data.map((item, index) => ( + {renderItem(item)} - - )} - /> - ) : !loading ? : null} + + ))} + + ) : !loading ? : null}
diff --git a/web/src/components/RadioGroupCard/index.tsx b/web/src/components/RadioGroupCard/index.tsx index e09466cd..49020b38 100644 --- a/web/src/components/RadioGroupCard/index.tsx +++ b/web/src/components/RadioGroupCard/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-02 15:19:30 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:19:30 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-04 10:11:37 */ /** * RadioGroupCard Component @@ -95,22 +95,23 @@ const RadioGroupCard: FC = ({ })}> {/* Render each option as a selectable card */} {options.map(option => ( -
handleChange(option)}> - {option.recommend &&
{t('common.recommend')}
} + {option.recommend &&
{t('common.recommend')}
} {/* Use custom render or default card layout */} {itemRender ? itemRender(option) : ( <> - {option.icon && }
-
{option.label}
-
{option.labelDesc}
+
{option.label}
+
{option.labelDesc}
)} diff --git a/web/src/components/RbAlert/index.tsx b/web/src/components/RbAlert/index.tsx index 6099fe0b..1c07b6c8 100644 --- a/web/src/components/RbAlert/index.tsx +++ b/web/src/components/RbAlert/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-02 15:19:59 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:19:59 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-11 15:02:24 */ /** * RbAlert Component @@ -14,6 +14,7 @@ */ import { type FC, type ReactNode } from 'react' +import { Flex } from 'antd'; /** Props interface for RbAlert component */ interface RbAlertProps { @@ -38,10 +39,10 @@ const colors = { /** Custom alert component with color themes and optional icon */ const RbAlert: FC = ({ color = 'blue', icon, className, children }) => { return ( -
+ {icon && {icon}} {children} -
+ ) } export default RbAlert diff --git a/web/src/components/RbButton/index.module.css b/web/src/components/RbButton/index.module.css new file mode 100644 index 00000000..b5b51658 --- /dev/null +++ b/web/src/components/RbButton/index.module.css @@ -0,0 +1,27 @@ +.rbButton { + border-radius: 8px; +} +.rbButton:global(:not(.ant-btn-disabled):hover) { + opacity: 0.7; +} +.rbButton:global(.ant-btn-color-dangerous.ant-btn-variant-outlined), +.rbButton:global(.ant-btn-color-dangerous.ant-btn-variant-dashed) { + border-color: #EBEBEB; + color: #171719; +} +.rbButton:global(.ant-btn-color-dangerous.ant-btn-variant-outlined:not(:disabled):not(.ant-btn-disabled):hover), +.rbButton:global(.ant-btn-color-dangerous.ant-btn-variant-dashed:not(:disabled):not(.ant-btn-disabled):hover) { + border-color: #FF5D34; + background-color: rgba(255, 93, 52, 0.06); + color: #FF5D34; + opacity: 1 !important; +} +.rbButton:global(.ant-btn-color-primary.ant-btn-background-ghost:not(:disabled):not(.ant-btn-disabled):hover) { + color: #171719; + border-color: #171719; +} +.rbButton:global(.ant-btn-color-primary.ant-btn-background-ghost:not(:disabled):not(.ant-btn-disabled):active) { + color: #171719; + border-color: #171719; + background-color: #FAFAFA; +} \ No newline at end of file diff --git a/web/src/components/RbButton/index.tsx b/web/src/components/RbButton/index.tsx new file mode 100644 index 00000000..9ce6c3ff --- /dev/null +++ b/web/src/components/RbButton/index.tsx @@ -0,0 +1,22 @@ +import { type FC } from 'react' +import clsx from 'clsx' +import { Button, type ButtonProps } from 'antd' + +import styles from './index.module.css' + +const RbButton: FC = ({ + children, + className, + ...props +}) => { + return ( + + ) +} + +export default RbButton \ No newline at end of file diff --git a/web/src/components/RbCard/Card.tsx b/web/src/components/RbCard/Card.tsx index 896dc201..db45915d 100644 --- a/web/src/components/RbCard/Card.tsx +++ b/web/src/components/RbCard/Card.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:21:14 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-04 13:49:05 + * @Last Modified time: 2026-02-25 16:20:39 */ /** * RbCard Component @@ -27,6 +27,7 @@ interface RbCardProps { headerClassName?: string; /** Card title (string, ReactNode, or function) */ title?: string | ReactNode | (() => ReactNode); + titleClassName?: string; /** Subtitle text displayed below title */ subTitle?: string | ReactNode; /** Extra content displayed in header (top-right) */ @@ -51,13 +52,14 @@ interface RbCardProps { className?: string; /** Click handler */ onClick?: () => void; - variant?: 'borderL'; + variant?: 'borderL' | 'borderless' | 'outlined'; } /** Custom card component with flexible styling and header options */ const RbCard: FC = ({ headerClassName, title, + titleClassName, subTitle, extra, children, @@ -66,10 +68,10 @@ const RbCard: FC = ({ bodyPadding, bodyClassName: bodyClassNames, headerType = 'border', - bgColor = '#FBFDFF', + bgColor = '#FFFFFF', height = 'auto', className, - variant, + variant = 'borderless', ...props }) => { /** Calculate body padding based on header type and avatar presence */ @@ -78,7 +80,7 @@ const RbCard: FC = ({ : headerType === 'borderL' ? 'rb:p-[0_16px_12px_16px]!' : avatarUrl || avatar - ? 'rb:p-[16px_20px_16px_16px]!' + ? 'rb:p-4!' : (headerType === 'borderless') ? 'rb:p-[0_20px_16px_16px]!' : (headerType === 'border' && !avatarUrl && !avatar) || headerType === 'borderBL' @@ -88,15 +90,15 @@ const RbCard: FC = ({ if (variant === 'borderL') { return (
{typeof title === 'function' ? title() : title ? -
+ {avatarUrl - ? + ? {avatarUrl} : avatar ? avatar : null }
= ({ } ) }> -
{title}
+
{title}
{subTitle &&
{subTitle}
}
-
: null + : null }
{subTitle &&
{subTitle}
} @@ -125,12 +127,13 @@ const RbCard: FC = ({ } return ( + {/* Avatar image or custom avatar component */} {avatarUrl - ? + ? {avatarUrl} : avatar ? avatar : null }
= ({ ) }> {/* Title with tooltip for overflow text */} -
{title}
+
{title}
{/* Optional subtitle */} {subTitle &&
{subTitle}
}
-
: null + : null } extra={extra} classNames={{ @@ -154,11 +157,11 @@ const RbCard: FC = ({ 'rb:font-medium', { /** Borderless header style */ - 'rb:border-[0]! rb:text-[16px] rb:p-[0_16px]!': headerType === 'borderless', + 'rb:border-[0]! rb:text-[16px] rb:p-[0_16px]! rb:min-h-10!': headerType === 'borderless', /** Header with avatar */ 'rb:border-[0]! rb:text-[16px] rb:p-[16px_16px_0_16px]!': avatarUrl || avatar, /** Standard border header */ - 'rb:text-[18px] rb:p-[0]! rb:m-[0_20px]!': headerType === 'border' && !avatarUrl && !avatar, + 'rb:text-[18px] rb:p-[0]! rb:m-[0_20px]! rb:border-b-[0.5px]!': headerType === 'border' && !avatarUrl && !avatar, /** Border bottom-left style */ "rb:m-[0_16px]! rb:p-[0]! rb:relative rb:before:content-[''] rb:before:w-[4px] rb:before:h-[16px] rb:before:bg-[#5B6167] rb:before:absolute rb:before:top-[50%] rb:before:left-[-16px] rb:before:translate-y-[-50%] rb:before:bg-[#5B6167]! rb:before:h-[16px]!": headerType === 'borderBL', /** Border left style */ @@ -170,9 +173,13 @@ const RbCard: FC = ({ }} style={{ background: bgColor, - height: height + height: height, + border: variant === 'outlined' ? '1px solid #EBEBEB' : 'none' }} - className={`rb:hover:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] ${className}`} + className={clsx({ + 'rb:shadow-none!': variant === 'borderless' || variant === 'outlined', + 'rb:hover:shadow-[0px_2px_8px_0px_rgba(23,23,25,0.16)]!': variant !== 'borderless' && variant !== 'outlined' + }, className)} > {children} diff --git a/web/src/components/RbCard/index.tsx b/web/src/components/RbCard/index.tsx new file mode 100644 index 00000000..7b1f0d63 --- /dev/null +++ b/web/src/components/RbCard/index.tsx @@ -0,0 +1,76 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:21:14 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-24 14:59:53 + */ +/** + * RbCard Component + * + * A customizable card component that extends Ant Design's Card with: + * - Multiple header styles (border, borderless, borderBL, borderL) + * - Avatar support with image or custom component + * - Flexible padding and styling options + * - Tooltip support for long titles + * - Hover effects + * + * @component + */ + +import { type FC, type ReactNode } from 'react' +import { Card, Tooltip, Flex, type CardProps } from 'antd'; +import clsx from 'clsx'; + +/** Props interface for RbCard component */ +interface RbCardProps extends CardProps { + children?: ReactNode; + /** Custom avatar component */ + avatarText?: string; + avatarClassName?: string; + /** Avatar image URL */ + avatarUrl?: string | null; + /** Click handler */ + onClick?: () => void; + footer?: ReactNode; +} + +/** Custom card component with flexible styling and header options */ +const RbCard: FC = ({ + title, + children, + avatarText, + avatarClassName, + avatarUrl, + footer, + ...props +}) => { + return ( + + {avatarUrl + ? {avatarUrl} + : avatarText + ? {avatarText} : null + } + +
+ {title} +
+
+ } + classNames={{ + header: 'rb:text-[16px] rb:p-[16px_16px_8px_16px]! rb:border-0!', + body: 'rb:p-4! rb:bg-white!', + }} + className="rb:hover:shadow-[0px_2px_8px_0px_rgba(23,23,25,0.16)]! rb:group" + > + {children} + + {footer ?
{footer}
: null} +
+ ) +} + +export default RbCard \ No newline at end of file diff --git a/web/src/components/RbDescriptions/index.module.css b/web/src/components/RbDescriptions/index.module.css new file mode 100644 index 00000000..1ab051d5 --- /dev/null +++ b/web/src/components/RbDescriptions/index.module.css @@ -0,0 +1,21 @@ +.rbDescriptions:global(.ant-descriptions.ant-descriptions-bordered>.ant-descriptions-view), +.rbDescriptions:global(.ant-descriptions.ant-descriptions-bordered>.ant-descriptions-view .ant-descriptions-row) { + border: none; +} +.rbDescriptions:global(.ant-descriptions.ant-descriptions-bordered>.ant-descriptions-view .ant-descriptions-row>.ant-descriptions-item-label), +.rbDescriptions:global(.ant-descriptions.ant-descriptions-bordered>.ant-descriptions-view .ant-descriptions-row>.ant-descriptions-item-content) { + border-inline-end: none; +} +.rbDescriptions:global(.ant-descriptions.ant-descriptions-bordered>.ant-descriptions-view .ant-descriptions-row>.ant-descriptions-item-label) { + color: #5B6167; + background: transparent; +} +.rbDescriptions:global(.ant-descriptions.ant-descriptions-bordered>.ant-descriptions-view .ant-descriptions-row>.ant-descriptions-item-label), +.rbDescriptions:global(.ant-descriptions.ant-descriptions-bordered>.ant-descriptions-view .ant-descriptions-row>.ant-descriptions-item-content) { + padding: 0; + line-height: 20px; +} +.rbDescriptions:global(.ant-descriptions.ant-descriptions-bordered>.ant-descriptions-view .ant-descriptions-row:not(:last-child)>.ant-descriptions-item-label), +.rbDescriptions:global(.ant-descriptions.ant-descriptions-bordered>.ant-descriptions-view .ant-descriptions-row:not(:last-child)>.ant-descriptions-item-content) { + padding-bottom: 8px; +} \ No newline at end of file diff --git a/web/src/components/RbDescriptions/index.tsx b/web/src/components/RbDescriptions/index.tsx new file mode 100644 index 00000000..97c6663c --- /dev/null +++ b/web/src/components/RbDescriptions/index.tsx @@ -0,0 +1,14 @@ +import { type FC } from 'react' +import { Descriptions, type DescriptionsProps } from 'antd' + +import styles from './index.module.css' + +const RbDescriptions: FC = ({ + items, +}) => { + return ( + + ) +} + +export default RbDescriptions \ No newline at end of file diff --git a/web/src/components/RbDrawer/index.tsx b/web/src/components/RbDrawer/index.tsx index b646bb55..e9101d48 100644 --- a/web/src/components/RbDrawer/index.tsx +++ b/web/src/components/RbDrawer/index.tsx @@ -19,7 +19,7 @@ */ import { type FC, useState, useEffect } from 'react' -import { Button, Drawer, Space } from 'antd'; +import { Button, Drawer, Space, Flex } from 'antd'; import type { DrawerProps } from 'antd'; import { CloseOutlined } from '@ant-design/icons'; @@ -78,9 +78,9 @@ const RbDrawer: FC =({ {...props} > {/* Full-height flex container for content */} -
+ {children} -
+ ) } diff --git a/web/src/components/RbSlider/index.module.css b/web/src/components/RbSlider/index.module.css new file mode 100644 index 00000000..52beb915 --- /dev/null +++ b/web/src/components/RbSlider/index.module.css @@ -0,0 +1,3 @@ +.ant-slider-horizontal .ant-slider-rail { + width: calc(100% - 6px); +} \ No newline at end of file diff --git a/web/src/components/RbSlider/index.tsx b/web/src/components/RbSlider/index.tsx index 544e3600..f9c10e4c 100644 --- a/web/src/components/RbSlider/index.tsx +++ b/web/src/components/RbSlider/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-02 15:23:39 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:23:39 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-11 11:10:40 */ /** * RbSlider Component @@ -15,32 +15,59 @@ * @component */ -import { type FC, useEffect } from 'react'; -import { Slider, type SliderSingleProps } from 'antd'; +import { type FC, useEffect, useState } from 'react'; +import { Slider, type SliderSingleProps, Flex, InputNumber, type InputNumberProps } from 'antd'; /** Props interface for RbSlider component */ interface RbSliderProps extends SliderSingleProps { /** Callback fired when value changes (for side effects) */ onValueChange?: (value: number | null | undefined) => void; + /** Callback fired when value changes (for side effects) */ + onChange?: (value: SliderSingleProps['value']) => void; + isInput?: boolean; + size?: 'small' | 'default'; + className?: string; } /** Custom slider component with value display */ const RbSlider: FC = ({ value, min = 0, + max, onValueChange, - step = 1, + onChange, + step = 0.01, + size = 'default' , + isInput = false, + className = '', ...rest }) => { + const [curValue, setCurValue] = useState(0) + useEffect(() => { + setCurValue(value) + }, [value]) /** Listen to value changes and trigger side effects via onValueChange callback */ useEffect(() => { if (onValueChange) { - onValueChange(value); + onValueChange(curValue); } - }, [value, onValueChange]); + }, [curValue, onValueChange]); + const handleInputChange: InputNumberProps['onChange'] = (newValue) => { + onChange?.(newValue as number | undefined); + setCurValue(newValue as number | undefined) + }; + const handleSliderChange: SliderSingleProps['onChange'] = (newValue) => { + onChange?.(newValue); + setCurValue(newValue) + }; return ( -
+ {/* Slider with fixed width */} = ({ width: '384px' }} {...rest} + min={min} + max={max} step={step} - value={value} + value={curValue} + onChange={handleSliderChange} + classNames={size === 'small' ? { + rail: 'rb:w-[calc(100%-6px)]!' + } : undefined} + className={size === 'small' ? `${size} rb:flex-1` : undefined} /> {/* Display current value or minimum value */} -
{value || min}
-
+ {isInput + ? + :
{curValue || min}
+ } + ); }; diff --git a/web/src/components/SearchInput/index.tsx b/web/src/components/SearchInput/index.tsx index 476c2cbb..1c05d81c 100644 --- a/web/src/components/SearchInput/index.tsx +++ b/web/src/components/SearchInput/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-02 15:24:23 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:24:23 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-10 14:59:39 */ /** * SearchInput Component @@ -21,8 +21,6 @@ import { useState, type FC, useCallback, useRef } from 'react'; import { Input, type InputProps } from 'antd'; import { useTranslation } from 'react-i18next'; -import searchIcon from '@/assets/images/search.svg' - /** Props interface for SearchInput component */ interface SearchInputProps { /** Placeholder text */ @@ -112,7 +110,7 @@ const SearchInput: FC = ({ return ( } + prefix={
} placeholder={placeholder || t('user.searchPlaceholder')} value={value} onChange={handleChange} diff --git a/web/src/components/SiderMenu/index.module.css b/web/src/components/SiderMenu/index.module.css index 4a6feebd..9cc61665 100644 --- a/web/src/components/SiderMenu/index.module.css +++ b/web/src/components/SiderMenu/index.module.css @@ -1,10 +1,10 @@ .sider { - border-right: 1px solid #EAECEE; + /* border-right: 1px solid #EAECEE; */ max-height: 100vh; } .title { height: 64px; - padding: 24px 12px 12px 12px; + padding: 24px 10px 12px 12px; font-family: Gilroy, Gilroy; font-weight: 800; font-size: 16px; @@ -13,7 +13,7 @@ text-align: left; font-style: normal; display: flex; - border-bottom: 1px solid #EAECEE; + /* border-bottom: 1px solid #EAECEE; */ justify-content: space-between; gap: 12px; } @@ -27,8 +27,12 @@ height: 24px; margin-right: 8px; } -.menuIcon { - cursor: pointer; - width: 28px; - height: 28px; +.sider :global(.ant-menu .ant-menu-item-group-title) { + padding-left: 20px; + padding-top: 10px; + padding-bottom: 4px; + font-weight: 500; +} +.sider :global(.ant-menu .ant-menu-item-divider) { + margin: 20px 20px 10px 20px; } \ No newline at end of file diff --git a/web/src/components/SiderMenu/index.tsx b/web/src/components/SiderMenu/index.tsx index 21202aa0..a0e7db1c 100644 --- a/web/src/components/SiderMenu/index.tsx +++ b/web/src/components/SiderMenu/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:25:31 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-04 13:49:16 + * @Last Modified time: 2026-02-24 11:32:15 */ /** * SiderMenu Component @@ -19,7 +19,7 @@ */ import { useState, useEffect, type FC } from 'react'; -import { Menu as AntMenu, Layout } from 'antd'; +import { Menu as AntMenu, Layout, Flex } from 'antd'; import { UserOutlined } from '@ant-design/icons'; import type { MenuProps } from 'antd'; import { useNavigate, useLocation } from 'react-router-dom'; @@ -29,47 +29,49 @@ import clsx from 'clsx'; import { useMenu, type MenuItem } from '@/store/menu'; import styles from './index.module.css' import logo from '@/assets/images/logo.png' -import menuFold from '@/assets/images/menuFold.png' -import menuUnfold from '@/assets/images/menuUnfold.png' import { useUser } from '@/store/user'; -import logout from '@/assets/images/logout.svg' // Import SVG files -import dashboardIcon from '@/assets/images/menu/dashboard.svg'; -import dashboardActiveIcon from '@/assets/images/menu/dashboard_active.svg'; +import dashboardIcon from '@/assets/images/menuNew/dashboard.svg'; +import dashboardActiveIcon from '@/assets/images/menuNew/dashboard_active.svg'; + import modelIcon from '@/assets/images/menu/model.svg'; import modelActiveIcon from '@/assets/images/menu/model_active.svg'; -import memoryIcon from '@/assets/images/menu/memory.svg'; -import memoryActiveIcon from '@/assets/images/menu/memory_active.svg'; import spaceIcon from '@/assets/images/menu/space.svg'; import spaceActiveIcon from '@/assets/images/menu/space_active.svg'; import userIcon from '@/assets/images/menu/user.svg'; import userActiveIcon from '@/assets/images/menu/user_active.svg'; -import userMemoryIcon from '@/assets/images/menu/userMemory.svg'; -import userMemoryActiveIcon from '@/assets/images/menu/userMemory_active.svg'; -import applicationIcon from '@/assets/images/menu/application.svg'; -import applicationActiveIcon from '@/assets/images/menu/application_active.svg'; -import knowledgeIcon from '@/assets/images/menu/knowledge.svg'; -import knowledgeActiveIcon from '@/assets/images/menu/knowledge_active.svg'; -import memoryConversationIcon from '@/assets/images/menu/memoryConversation.svg'; -import memoryConversationActiveIcon from '@/assets/images/menu/memoryConversation_active.svg'; -import memberIcon from '@/assets/images/menu/member.svg'; -import memberActiveIcon from '@/assets/images/menu/member_active.svg'; + +import applicationIcon from '@/assets/images/menuNew/application.svg'; +import applicationActiveIcon from '@/assets/images/menuNew/application_active.svg'; +import knowledgeIcon from '@/assets/images/menuNew/knowledge.svg'; +import knowledgeActiveIcon from '@/assets/images/menuNew/knowledge_active.svg'; +import memoryIcon from '@/assets/images/menuNew/memory.svg'; +import memoryActiveIcon from '@/assets/images/menuNew/memory_active.svg'; +import userMemoryIcon from '@/assets/images/menuNew/userMemory.svg'; +import userMemoryActiveIcon from '@/assets/images/menuNew/userMemory_active.svg'; +import memoryConversationIcon from '@/assets/images/menuNew/memoryConversation.svg'; +import memoryConversationActiveIcon from '@/assets/images/menuNew/memoryConversation_active.svg'; +import apiKeyIcon from '@/assets/images/menuNew/apiKey.svg'; +import apiKeyActiveIcon from '@/assets/images/menuNew/apiKey_active.svg'; +import memberIcon from '@/assets/images/menuNew/member.svg'; +import memberActiveIcon from '@/assets/images/menuNew/member_active.svg'; +import ontologyIcon from '@/assets/images/menuNew/ontology.svg' +import ontologyActiveIcon from '@/assets/images/menuNew/ontology_active.svg' + +import spaceConfigIcon from '@/assets/images/menuNew/spaceConfig.svg' +import spaceConfigActiveIcon from '@/assets/images/menuNew/spaceConfig_active.svg' +import promptIcon from '@/assets/images/menuNew/prompt.svg' +import promptActiveIcon from '@/assets/images/menuNew/prompt_active.svg' + import toolIcon from '@/assets/images/menu/tool.png'; import toolActiveIcon from '@/assets/images/menu/tool_active.png'; -import apiKeyIcon from '@/assets/images/menu/apiKey.png'; -import apiKeyActiveIcon from '@/assets/images/menu/apiKey_active.png'; import pricingIcon from '@/assets/images/menu/pricing.svg' import pricingActiveIcon from '@/assets/images/menu/pricing_active.svg' -import spaceConfigIcon from '@/assets/images/menu/spaceConfig.svg' -import spaceConfigActiveIcon from '@/assets/images/menu/spaceConfig_active.svg' -import ontologyIcon from '@/assets/images/menu/ontology.svg' -import ontologyActiveIcon from '@/assets/images/menu/ontology_active.svg' -import promptIcon from '@/assets/images/menu/prompt.svg' -import promptActiveIcon from '@/assets/images/menu/prompt_active.svg' import skillsIcon from '@/assets/images/menu/skills.svg' import skillsActiveIcon from '@/assets/images/menu/skills_active.svg' + /** Icon path mapping table for menu items (normal and active states) */ const iconPathMap: Record = { 'dashboard': dashboardIcon, @@ -145,43 +147,57 @@ const Menu: FC<{ /** Convert custom menu format to Ant Design Menu items format */ const generateMenuItems = (menuList: MenuItem[]): MenuProps['items'] => { + const items: MenuProps['items'] = []; + const filteredMenus = menuList.filter(menu => menu.display); - return menuList.filter(menu => menu.display).map((menu) => { + filteredMenus.forEach((menu, index) => { const iconKey = selectedKeys.includes(menu.path || '') ? `${menu.code}Active` : menu.code; const iconSrc = iconPathMap[iconKey as keyof typeof iconPathMap]; const subs = (menu.subs || []).filter(sub => sub.display); + /** Leaf node - menu item without children */ if (!subs || subs.length === 0) { - if (!menu.path) return null; - - return { - key: menu.path, - title: menu.i18nKey ? t(menu.i18nKey) : menu.label, - label: ( - - {menu.i18nKey ? t(menu.i18nKey) : menu.label} - - ), + if (menu.path) { + items.push({ + key: menu.path, + title: menu.i18nKey ? t(menu.i18nKey) : menu.label, + label: ( + + {menu.i18nKey ? t(menu.i18nKey) : menu.label} + + ), + icon: iconSrc ? {iconSrc} : null, + }); + } + } else { + /** Node with submenu - menu item with children */ + const menuLabel = collapsed && menu.type === 'group'? '': menu.i18nKey ? t(menu.i18nKey) : menu.label; + const children = generateMenuItems(subs) || []; + items.push({ + key: `submenu-${menu.id}`, + ...(menu.type === 'group' ? { type: 'group' as const } : {}), + title: menuLabel, + label: menuLabel, icon: iconSrc ? : null, - }; + className="rb:w-4 rb:h-4" + alt={iconSrc} + /> : , + children, + }); } - /** Node with submenu - menu item with children */ - const menuLabel = menu.i18nKey ? t(menu.i18nKey) : menu.label; - return { - key: `submenu-${menu.id}`, - title: menuLabel, - label: menuLabel, - icon: iconSrc ? : , - children: generateMenuItems(subs), - }; - }).filter(Boolean); + /** Add divider after group items (except the last one) */ + if (menu.type === 'group' && index < filteredMenus.length - 1) { + items.push({ type: 'divider', key: `divider-${menu.id}` }); + } + }); + + return items; }; /** Generate menu items from configuration */ @@ -252,20 +268,26 @@ const Menu: FC<{ 'rb:flex rb:items-center rb:text-[14px]! rb:py-2!': !collapsed && source === 'space' && user.current_workspace_name, })}> {!collapsed && source === 'space' && user.current_workspace_name - ?
-
{user.current_workspace_name}
- - {t(`space.${storageType}`)} - -
- : !collapsed - ?
- - {t('title')} + ? + {user.current_workspace_name[0]} +
+
{user.current_workspace_name}
+ + {t(`space.${storageType}`)} +
+
+ : !collapsed + ? + {logo} + {t('title')} + : null } - +
{/* Main navigation menu */} {/* Return to space button for superusers */} {user?.is_superuser && source === 'space' && -
- +
{collapsed ? null : t('common.returnToSpace')} -
+ } ); diff --git a/web/src/components/SliderInput/index.tsx b/web/src/components/SliderInput/index.tsx index f862010b..d3200eef 100644 --- a/web/src/components/SliderInput/index.tsx +++ b/web/src/components/SliderInput/index.tsx @@ -107,7 +107,7 @@ const SliderInput: FC = ({
{/* Optional label */} {label && ( -
+
{label}
)} diff --git a/web/src/components/StatusTag/index.tsx b/web/src/components/StatusTag/index.tsx index 47a1b6f8..7fb4cc54 100644 --- a/web/src/components/StatusTag/index.tsx +++ b/web/src/components/StatusTag/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-02 15:29:42 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:29:42 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-11 11:14:29 */ /** * StatusTag Component @@ -14,7 +14,7 @@ */ import { type FC } from 'react' -import { Tag } from 'antd'; +import { Tag, Flex } from 'antd'; import clsx from 'clsx'; /** Props interface for StatusTag component */ @@ -40,13 +40,12 @@ const StatusTag: FC = ({ status, text }) => { - console.log('status', status) return ( - + { text } - + ) } diff --git a/web/src/components/Table/index.tsx b/web/src/components/Table/index.tsx index b045e76d..f5a487ab 100644 --- a/web/src/components/Table/index.tsx +++ b/web/src/components/Table/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:29:46 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:32:11 + * @Last Modified time: 2026-02-25 15:06:40 */ /** * RbTable Component @@ -199,7 +199,7 @@ const RbTable = forwardRef(({ if (scrollY !== undefined) { config.y = scrollY; } else if (isScroll) { - config.y = 'calc(100vh - 280px)'; + config.y = 'calc(100vh - 240px)'; } return Object.keys(config).length > 0 ? config : undefined; @@ -214,7 +214,6 @@ const RbTable = forwardRef(({ dataSource={data} pagination={paginationConfig} rowSelection={rowSelection} - rowClassName="rb:text-[#5B6167]" locale={{ emptyText: }} scroll={getScrollConfig()} tableLayout="auto" diff --git a/web/src/components/Upload/UploadFiles.tsx b/web/src/components/Upload/UploadFiles.tsx index 86864d9a..68377d4d 100644 --- a/web/src/components/Upload/UploadFiles.tsx +++ b/web/src/components/Upload/UploadFiles.tsx @@ -243,7 +243,7 @@ const UploadFiles = forwardRef(({
- +
{(!isAutoUpload || !hasProgress && (!fileList || !fileList.length)) && <>
diff --git a/web/src/components/Upload/UploadImages.tsx b/web/src/components/Upload/UploadImages.tsx index e60a5b81..246d0a7c 100644 --- a/web/src/components/Upload/UploadImages.tsx +++ b/web/src/components/Upload/UploadImages.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:30:52 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:57:03 + * @Last Modified time: 2026-02-10 15:01:42 */ /** * UploadImages Component @@ -238,7 +238,7 @@ const UploadImages = forwardRef(({ {...uploadProps} > {fileList.length < maxCount && ( - +
)} {previewImage && (