From a191e32f715d84aa0e52b760e82913567dcff2fa Mon Sep 17 00:00:00 2001 From: zhaoying Date: Mon, 2 Feb 2026 16:14:39 +0800 Subject: [PATCH] docs: add comments to the src/components directory --- web/src/components/ButtonCheckbox/index.tsx | 42 ++++++++-- web/src/components/CustomSelect/index.tsx | 35 ++++++++ web/src/components/Empty/BodyWrapper.tsx | 22 +++++ web/src/components/Empty/Loading.tsx | 23 ++++- web/src/components/Empty/PageEmpty.tsx | 23 ++++- web/src/components/Empty/PageLoading.tsx | 23 ++++- web/src/components/Empty/index.tsx | 27 ++++++ web/src/components/FormItem/DescWrapper.tsx | 19 +++++ web/src/components/FormItem/LabelWrapper.tsx | 21 +++++ .../components/FormItem/SwitchFormItem.tsx | 30 +++++-- web/src/components/Header/SettingModal.tsx | 37 ++++++-- web/src/components/Header/UserInfoModal.tsx | 54 +++++++++--- web/src/components/Header/index.tsx | 82 +++++++++++------- web/src/components/Layout/AuthLayout.tsx | 35 +++++++- web/src/components/Layout/AuthSpaceLayout.tsx | 38 ++++++++- web/src/components/Layout/BasicLayout.tsx | 28 ++++++- web/src/components/Layout/LayoutBg.tsx | 26 +++++- web/src/components/Layout/LoginLayout.tsx | 21 ++++- web/src/components/Layout/NoAuthLayout.tsx | 21 ++++- web/src/components/Markdown/AudioBlock.tsx | 22 ++++- web/src/components/Markdown/Code.tsx | 32 ++++++- web/src/components/Markdown/CodeBlock.tsx | 27 ++++-- web/src/components/Markdown/CopyBtn.tsx | 19 ++++- web/src/components/Markdown/Link.tsx | 20 ++++- web/src/components/Markdown/MermaidChart.tsx | 23 +++++ web/src/components/Markdown/Paragraph.tsx | 19 ++++- web/src/components/Markdown/RbButton.tsx | 19 ++++- web/src/components/Markdown/Svg.tsx | 21 ++++- web/src/components/Markdown/VideoBlock.tsx | 21 ++++- web/src/components/Markdown/index.tsx | 67 +++++++++++---- web/src/components/PageScrollList/index.tsx | 39 ++++++++- web/src/components/PageTabs/index.tsx | 20 +++++ web/src/components/RadioGroupCard/index.tsx | 42 +++++++++- web/src/components/RbAlert/index.tsx | 22 +++++ web/src/components/RbCard/Card.tsx | 45 ++++++++++ web/src/components/RbCard/index.tsx | 73 ---------------- web/src/components/RbDrawer/index.tsx | 31 +++++-- web/src/components/RbModal/Confirm.tsx | 63 -------------- web/src/components/RbModal/index.tsx | 27 ++++-- web/src/components/RbSlider/index.tsx | 34 ++++++-- web/src/components/SearchInput/index.tsx | 38 ++++++++- web/src/components/SiderMenu/index.tsx | 74 +++++++++++----- web/src/components/SliderInput/index.tsx | 42 +++++++++- .../components/SortableList/DragHandle.tsx | 17 ++++ .../SortableList/SortableListItem.tsx | 19 ++++- .../SortableList/SortableListItemContext.tsx | 18 +++- web/src/components/SortableList/index.tsx | 38 ++++++++- web/src/components/SortableList/types.ts | 17 ++++ web/src/components/StatusTag/index.tsx | 21 +++++ web/src/components/Table/index.module.css | 3 - web/src/components/Table/index.tsx | 84 ++++++++++++++----- web/src/components/Tag/index.tsx | 21 +++++ web/src/components/Upload/UploadImages.tsx | 74 +++++++++++----- web/src/views/KnowledgeBase/index.tsx | 2 +- .../views/MemoryExtractionEngine/index.tsx | 31 +++---- 55 files changed, 1417 insertions(+), 375 deletions(-) delete mode 100644 web/src/components/RbCard/index.tsx delete mode 100644 web/src/components/RbModal/Confirm.tsx delete mode 100644 web/src/components/Table/index.module.css diff --git a/web/src/components/ButtonCheckbox/index.tsx b/web/src/components/ButtonCheckbox/index.tsx index 65813809..4b43f18a 100644 --- a/web/src/components/ButtonCheckbox/index.tsx +++ b/web/src/components/ButtonCheckbox/index.tsx @@ -1,14 +1,36 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:01:59 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:46:05 + */ + +/** + * ButtonCheckbox - A custom checkbox component styled as a button + * + * This component provides a button-like interface for checkbox functionality, + * with support for custom icons and visual states (checked/unchecked). + * + * @component + */ + import { type FC, type ReactNode, useEffect } from 'react'; import { type RadioGroupProps } from 'antd'; import clsx from 'clsx' - +// Button checkbox component props interface ButtonCheckboxProps extends Omit { + /** Whether the checkbox is checked */ checked?: boolean; + /** Callback fired when value changes (for side effects) */ onValueChange?: (checked: boolean) => void; + /** Callback fired when checkbox state changes */ onChange?: (checked: boolean) => void; + /** Icon path for unchecked state */ icon?: string; + /** Icon path for checked state */ checkedIcon?: string; + /** Button content */ children?: ReactNode } @@ -20,13 +42,14 @@ const ButtonCheckbox: FC = ({ checkedIcon, children, }) => { - // 监听value变化 + // Listen to value changes and trigger side effects via onValueChange callback useEffect(() => { if (onValueChange) { onValueChange(checked); } }, [checked, onValueChange]); + // Toggle checked state when button is clicked const handleChange = () => { if (onChange) { onChange(!checked); @@ -34,11 +57,18 @@ const ButtonCheckbox: FC = ({ } return ( -
+
+ {/* Display unchecked icon when not checked */} {icon && !checked && } + {/* Display checked icon when checked */} {checkedIcon && checked && } {children}
diff --git a/web/src/components/CustomSelect/index.tsx b/web/src/components/CustomSelect/index.tsx index f93014c9..ea5ca718 100644 --- a/web/src/components/CustomSelect/index.tsx +++ b/web/src/components/CustomSelect/index.tsx @@ -1,31 +1,61 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:02:17 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:46:29 + */ +/** + * CustomSelect - A select component that fetches options from an API + * + * This component extends Ant Design's Select with automatic data fetching, + * search functionality, and customizable option formatting. + * + * @component + */ + import { useEffect, useState, useMemo, type FC, type Key } from 'react'; import { Select } from 'antd'; import type { SelectProps, DefaultOptionType } from 'antd/es/select'; import { useTranslation } from 'react-i18next'; + import { request } from '@/utils/request'; +// Generic option type for API response data interface OptionType { [key: string]: Key | string | number; } +// API response structure interface ApiResponse { items?: T[]; } interface CustomSelectProps extends Omit { + /** API endpoint URL to fetch options */ url: string; + /** Query parameters for the API request */ params?: Record; + /** Key name for option value in response data */ valueKey?: string; + /** Key name for option label in response data */ labelKey?: string; + /** Placeholder text for the select */ placeholder?: string; + /** Whether to show "All" option */ hasAll?: boolean; + /** Custom text for "All" option */ allTitle?: string; + /** Function to format/transform the options data */ format?: (items: OptionType[]) => OptionType[]; + /** Whether to enable search functionality */ showSearch?: boolean; + /** Property name to filter options by */ optionFilterProp?: string; + /** Custom filter function for search */ filterOption?: (inputValue: string, option?: DefaultOptionType) => boolean; } +// Default filter function for search - performs case-insensitive substring matching const defaultFilterOption = (inputValue: string, option?: DefaultOptionType): boolean => { if (!option || !inputValue) return true; const label = String(option.children || option.label || ''); @@ -47,8 +77,10 @@ const CustomSelect: FC = ({ }) => { const { t } = useTranslation(); const [options, setOptions] = useState([]); + // Memoize params to prevent unnecessary re-fetches const memoizedParams = useMemo(() => params, [JSON.stringify(params)]); + // Fetch options from API when url or params change useEffect(() => { request.get>(url, memoizedParams).then((res) => { const data = Array.isArray(res) ? res : res?.items || []; @@ -56,6 +88,7 @@ const CustomSelect: FC = ({ }); }, [url, memoizedParams]); + // Apply custom format function if provided const displayOptions = format ? format(options) : options; return ( @@ -66,7 +99,9 @@ const CustomSelect: FC = ({ filterOption={filterOption || defaultFilterOption} {...props} > + {/* Optional "All" option for selecting all items */} {hasAll && {allTitle || t('common.all')}} + {/* Render options from API data */} {displayOptions.map((option) => ( {String(option[labelKey])} diff --git a/web/src/components/Empty/BodyWrapper.tsx b/web/src/components/Empty/BodyWrapper.tsx index 9cdeb0e8..22bd226a 100644 --- a/web/src/components/Empty/BodyWrapper.tsx +++ b/web/src/components/Empty/BodyWrapper.tsx @@ -1,19 +1,41 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:02:47 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:47:24 + */ +/** + * BodyWrapper Component + * + * A wrapper component that conditionally renders loading, empty, or content states. + * Simplifies state management for data-driven components. + * + * @component + */ + import type { FC, ReactNode } from 'react' + import PageEmpty from './PageEmpty' import PageLoading from './PageLoading' interface BodyWrapperProps { + /** Content to render when not loading or empty */ children: ReactNode + /** Whether to show loading state */ loading?: boolean + /** Whether the content is empty */ empty: boolean } const BodyWrapper: FC = ({ children, loading = false, empty }) => { + // Show loading spinner while data is being fetched if (loading) { return } + // Show empty state when no data is available if (!loading && empty) { return } + // Render actual content when data is loaded and available return children } export default BodyWrapper diff --git a/web/src/components/Empty/Loading.tsx b/web/src/components/Empty/Loading.tsx index c34b5d29..87bbe5dc 100644 --- a/web/src/components/Empty/Loading.tsx +++ b/web/src/components/Empty/Loading.tsx @@ -1,7 +1,28 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:03:52 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:48:41 + */ +/** + * Loading Component + * + * A specialized empty state component that displays a loading animation. + * Uses the Empty component with a loading icon and localized loading messages. + * + * @component + */ + +import { type FC } from 'react'; import { useTranslation } from 'react-i18next' + import LoadingIcon from '@/assets/images/loading.svg' import Empty from './index' -const Loading = ({ size = 200 }: { size?: number }) => { + +/** + * @param size - Icon size in pixels (default: 200) + */ +const Loading: FC<{ size?: number }> = ({ size = 200 }) => { const { t } = useTranslation() return ( { + +/** + * @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 { t } = useTranslation() return ( { + +/** + * @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 { t } = useTranslation() return ( = ({ @@ -19,14 +41,19 @@ const Empty: FC = ({ className = '', }) => { const { t } = useTranslation(); + // Calculate width and height from size prop (supports single value or [width, height] array) const width = Array.isArray(size) ? size[0] : size ? size : url ? 200 : 88; const height = Array.isArray(size) ? size[1] : size ? size : url ? 200 : 88; + // 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}
} + {/* Optional subtitle with conditional styling */} {curSubTitle &&
{curSubTitle}
}
); diff --git a/web/src/components/FormItem/DescWrapper.tsx b/web/src/components/FormItem/DescWrapper.tsx index 300fc2b6..5060b588 100644 --- a/web/src/components/FormItem/DescWrapper.tsx +++ b/web/src/components/FormItem/DescWrapper.tsx @@ -1,6 +1,25 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:05:16 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:05:16 + */ +/** + * DescWrapper Component + * + * A styled wrapper for displaying description text in forms. + * Provides consistent typography and styling for form field descriptions. + * + * @component + */ + import clsx from "clsx"; import type { FC, ReactNode } from "react"; +/** + * @param desc - Description content (string or React node) + * @param className - Additional CSS classes for customization + */ const DescWrapper: FC<{desc: string | ReactNode, className?: string}> = ({desc, className}) => { return (
diff --git a/web/src/components/FormItem/LabelWrapper.tsx b/web/src/components/FormItem/LabelWrapper.tsx index 461250d8..d608462b 100644 --- a/web/src/components/FormItem/LabelWrapper.tsx +++ b/web/src/components/FormItem/LabelWrapper.tsx @@ -1,9 +1,30 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:05:41 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:05:41 + */ +/** + * LabelWrapper Component + * + * A styled wrapper for displaying form field labels with optional child content. + * Provides consistent typography and layout for form labels. + * + * @component + */ + import clsx from "clsx"; import type { FC, ReactNode } from "react"; +/** + * @param title - Label text or React node to display + * @param className - Additional CSS classes for customization + * @param children - Optional child content to render below the label + */ const LabelWrapper: FC<{ title: string | ReactNode, className?: string; children?: ReactNode}> = ({title, className, children}) => { return (
+ {/* Label title with consistent styling */}
{title}
{children}
diff --git a/web/src/components/FormItem/SwitchFormItem.tsx b/web/src/components/FormItem/SwitchFormItem.tsx index e17a8728..8ac52f98 100644 --- a/web/src/components/FormItem/SwitchFormItem.tsx +++ b/web/src/components/FormItem/SwitchFormItem.tsx @@ -1,17 +1,36 @@ -import { Switch, Form, ConfigProvider } from "antd"; -import useSize from 'antd/lib/config-provider/hooks/useSize' +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:06:24 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:50:49 + */ +/** + * SwitchFormItem Component + * + * A form item component that combines a switch control with a label and optional description. + * Provides a consistent layout for switch-based form fields. + * + * @component + */ + +import { Switch, Form } from "antd"; import type { FC, ReactNode } from "react"; -import { useContext } from "react"; import LabelWrapper from './LabelWrapper' import DescWrapper from './DescWrapper' interface SwitchFormItemProps { + /** Label text or React node */ title: string | ReactNode; + /** Optional description text or React node */ desc?: string | ReactNode; + /** Form field name (string or nested path array) */ name: string | string[]; + /** Switch size */ size?: 'small' | 'default' + /** Additional CSS classes */ className?: string; + /** Whether the switch is disabled */ disabled?: boolean; } @@ -23,14 +42,13 @@ const SwitchFormItem: FC = ({ className, disabled }) => { - const componentSize = useSize() - console.log('componentSize', componentSize) - return (
+ {/* Label and description section */} {desc && } + {/* Switch control */} void; + /** Close the settings modal */ handleClose: () => void; } +/** Settings modal component for language and timezone configuration */ const SettingModal = forwardRef((_props, ref) => { const { t } = useTranslation(); const { changeLanguage, language, timeZone, changeTimeZone } = useI18n() const [visible, setVisible] = useState(false); const [form] = Form.useForm(); - const values = Form.useWatch([], form); - - // 封装取消方法,添加关闭弹窗逻辑 + /** Close modal and reset form to initial state */ const handleClose = () => { setVisible(false); form.resetFields(); }; + /** Open modal and populate form with current settings */ const handleOpen = () => { form.setFieldsValue({ language, timeZone }) setVisible(true); }; - // 封装保存方法,添加提交逻辑 + + /** Validate and save settings, update language and timezone if changed */ const handleSave = () => { form .validateFields() - .then(() => { + .then((values) => { const { language: newLanguage, timeZone: newTimeZone } = values if (newLanguage !== language) { changeLanguage(newLanguage); @@ -47,11 +67,12 @@ const SettingModal = forwardRef((_props, ref) => { }); } - // 暴露给父组件的方法 + /** Expose handleOpen and handleClose methods to parent component via ref */ useImperativeHandle(ref, () => ({ handleOpen, handleClose })); + return ( ((_props, ref) => { form={form} layout="vertical" > - {/* 中英文切换 */} + {/* Language selection dropdown */} ((_props, ref) => { options={['zh', 'en'].map(key => ({ label: t(`header.${key}`), value: key }))} /> - {/* 时区切换 */} + {/* Timezone selection dropdown */} void; + /** Close the user info modal */ handleClose: () => void; } +/** User information modal component displaying user details and security settings */ const UserInfoModal = forwardRef((_props, ref) => { const { t } = useTranslation(); const resetPasswordModalRef = useRef(null) const { user } = useUser(); const [visible, setVisible] = useState(false); - // 封装取消方法,添加关闭弹窗逻辑 + /** Close the modal */ const handleClose = () => { setVisible(false); }; + /** Open the modal */ const handleOpen = () => { setVisible(true); }; - // 暴露给父组件的方法 + /** Expose handleOpen and handleClose methods to parent component via ref */ useImperativeHandle(ref, () => ({ handleOpen, handleClose })); + return ( ((_props, ref) => { onCancel={handleClose} 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.securitySettings')}
+ + {/* Security Settings Section */} +
{t('header.securitySettings')}
-
-
+ {/* Password Change Card */} +
+
-
{t('header.changePassword')}
-
{t('header.changePasswordDesc')}
+
{t('header.changePassword')}
+
{t('header.changePasswordDesc')}
diff --git a/web/src/components/Header/index.tsx b/web/src/components/Header/index.tsx index fac432f5..b58f157b 100644 --- a/web/src/components/Header/index.tsx +++ b/web/src/components/Header/index.tsx @@ -1,16 +1,36 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:07:49 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:07:49 + */ +/** + * AppHeader Component + * + * The main application header that displays breadcrumb navigation and user menu. + * Supports different breadcrumb sources based on the current route. + * + * @component + */ + import { type FC, useRef } from 'react'; -import { Layout, Dropdown, Space, Breadcrumb } from 'antd'; +import { Layout, Dropdown, Breadcrumb } from 'antd'; import type { MenuProps, BreadcrumbProps } from 'antd'; import { UserOutlined, LogoutOutlined, SettingOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; + import { useUser } from '@/store/user'; import { useMenu } from '@/store/menu'; import styles from './index.module.css' import SettingModal, { type SettingModalRef } from './SettingModal' import UserInfoModal, { type UserInfoModalRef } from './UserInfoModal' + const { Header } = Layout; +/** + * @param source - Breadcrumb source type ('space' or 'manage'), defaults to 'manage' + */ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { const { t } = useTranslation(); const location = useLocation(); @@ -20,21 +40,26 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { const { user, logout } = useUser(); const { allBreadcrumbs } = useMenu(); - // 根据当前路由动态选择面包屑源 + /** + * Dynamically select breadcrumb source based on current route + * - Knowledge base list: uses 'space' breadcrumb + * - Knowledge base detail: uses 'space-detail' breadcrumb + * - Other pages: uses the passed source prop + */ const getBreadcrumbSource = () => { const pathname = location.pathname; - // 知识库列表页面使用默认的 space 面包屑 + // Knowledge base list page uses default space breadcrumb if (pathname === '/knowledge-base') { return 'space'; } - // 知识库详情相关页面使用独立的面包屑 + // Knowledge base detail pages use independent breadcrumb if (pathname.includes('/knowledge-base/') && pathname !== '/knowledge-base') { return 'space-detail'; } - // 其他页面使用传入的 source + // Other pages use the passed source return source; }; @@ -42,13 +67,12 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { const breadcrumbs = allBreadcrumbs[breadcrumbSource] || []; - - // 处理退出登录 + /** Handle user logout */ const handleLogout = () => { logout() }; - // 用户下拉菜单配置 + /** User dropdown menu configuration with profile, settings, and logout options */ const userMenuItems: MenuProps['items'] = [ { key: '1', @@ -89,18 +113,25 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { onClick: handleLogout, }, ]; + + /** + * Format breadcrumb items with proper titles, paths, and click handlers + * - Translates i18n keys to display text + * - Handles custom onClick events + * - Disables navigation for the last breadcrumb item + */ const formatBreadcrumbNames = () => { return breadcrumbs.map((menu, index) => { const item: any = { title: menu.i18nKey ? t(menu.i18nKey) : menu.label, }; - // 如果是最后一项,不设置 path + // If it's the last item, don't set path if (index === breadcrumbs.length - 1) { return item; } - // 如果有自定义 onClick,使用 onClick 并设置 href 为 '#' 以显示手型光标 + // If has custom onClick, use onClick and set href to '#' to show pointer cursor if ((menu as any).onClick) { item.onClick = (e: React.MouseEvent) => { e.preventDefault(); @@ -108,35 +139,26 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { }; item.href = '#'; } else if (menu.path && menu.path !== '#') { - // 只有当 path 不是 '#' 时才设置 path + // Only set path when path is not '#' item.path = menu.path; } return item; }); } + return (
+ {/* Breadcrumb navigation */} - {/* 语言切换和主题切换按钮 */} - - {/* */} - - {/* 用户信息下拉菜单 */} - -
{user.username}
-
-
+ {/* User info dropdown menu */} + +
{user.username}
+
diff --git a/web/src/components/Layout/AuthLayout.tsx b/web/src/components/Layout/AuthLayout.tsx index a969298d..356f4678 100644 --- a/web/src/components/Layout/AuthLayout.tsx +++ b/web/src/components/Layout/AuthLayout.tsx @@ -1,6 +1,25 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:11:02 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:11:02 + */ +/** + * AuthLayout Component + * + * The main authenticated layout wrapper that provides: + * - Route authentication and permission checks + * - Automatic breadcrumb navigation updates + * - Sidebar navigation and header + * - Token-based authentication validation + * + * @component + */ + import { Outlet } from 'react-router-dom'; import { useEffect, type FC } from 'react'; import { Layout } from 'antd'; + import useRouteGuard from '@/hooks/useRouteGuard'; import { useNavigationBreadcrumbs } from '@/hooks/useNavigationBreadcrumbs'; import AppHeader from '@/components/Header'; @@ -11,13 +30,20 @@ import { cookieUtils } from '@/utils/request'; const { Content } = Layout; -// 认证布局组件,使用useRouteGuard hook进行路由鉴权 +/** + * Authentication layout component that wraps all authenticated pages. + * Handles route guards, breadcrumb navigation, and user authentication. + */ const AuthLayout: FC = () => { const { getUserInfo } = useUser(); - // 使用路由守卫hook处理认证和权限检查 + + // Use route guard hook to handle authentication and permission checks useRouteGuard('manage'); - // 自动更新面包屑导航 + + // Automatically update breadcrumb navigation based on current route useNavigationBreadcrumbs('manage'); + + // Check authentication token and fetch user info on mount useEffect(() => { const authToken = cookieUtils.get('authToken') if (!authToken && !window.location.hash.includes('#/login')) { @@ -29,9 +55,12 @@ const AuthLayout: FC = () => { return ( + {/* Sidebar navigation */} + {/* 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 17ee0bac..d47f13f2 100644 --- a/web/src/components/Layout/AuthSpaceLayout.tsx +++ b/web/src/components/Layout/AuthSpaceLayout.tsx @@ -1,6 +1,26 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:11:43 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:11:43 + */ +/** + * AuthSpaceLayout Component + * + * The authenticated layout wrapper for knowledge base (space) pages that provides: + * - Route authentication and permission checks for space context + * - Automatic breadcrumb navigation updates + * - Sidebar navigation and header configured for space mode + * - Token-based authentication validation + * - Storage type initialization + * + * @component + */ + import { Outlet } from 'react-router-dom'; import { useEffect, type FC } from 'react'; import { Layout } from 'antd'; + import useRouteGuard from '@/hooks/useRouteGuard'; import { useNavigationBreadcrumbs } from '@/hooks/useNavigationBreadcrumbs'; import AppHeader from '@/components/Header'; @@ -11,28 +31,38 @@ import { cookieUtils } from '@/utils/request'; const { Content } = Layout; -// 认证布局组件,使用useRouteGuard hook进行路由鉴权 +/** + * Authentication layout component for knowledge base (space) pages. + * Similar to AuthLayout but configured for space context with storage type management. + */ const AuthSpaceLayout: FC = () => { const { getUserInfo, getStorageType } = useUser(); - // 使用路由守卫hook处理认证和权限检查 + + // Use route guard hook to handle authentication and permission checks for space context useRouteGuard('space'); - // 自动更新面包屑导航 + + // Automatically update breadcrumb navigation based on current route in space context useNavigationBreadcrumbs('space'); + + // Check authentication token, fetch user info and storage type on mount useEffect(() => { const authToken = cookieUtils.get('authToken') if (!authToken && !window.location.hash.includes('#/login')) { window.location.href = `/#/login`; } else { getUserInfo() - getStorageType() + getStorageType() // Fetch storage type for knowledge base operations } }, []); return ( + {/* Sidebar navigation configured for space mode */} + {/* 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 6b3d2904..cadcafb2 100644 --- a/web/src/components/Layout/BasicLayout.tsx +++ b/web/src/components/Layout/BasicLayout.tsx @@ -1,12 +1,35 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:12:42 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:12:42 + */ +/** + * BasicLayout Component + * + * A minimal layout wrapper that provides: + * - User information initialization + * - Storage type initialization + * - Simple container for child routes without navigation UI + * + * Used for pages that don't require sidebar/header (e.g., login, public pages). + * + * @component + */ + import { Outlet } from 'react-router-dom'; import { useEffect, type FC } from 'react'; + import { useUser } from '@/store/user'; -// 基础布局组件,用于展示内容并保留用户信息获取功能 +/** + * Basic layout component for pages without navigation UI. + * Fetches user info and storage type on mount, then renders child routes. + */ const BasicLayout: FC = () => { const { getUserInfo, getStorageType } = useUser(); - // 获取用户信息 + // Fetch user information and storage type on component mount useEffect(() => { getUserInfo(); getStorageType() @@ -14,6 +37,7 @@ const BasicLayout: FC = () => { return (
+ {/* Render child routes without additional UI */}
) diff --git a/web/src/components/Layout/LayoutBg.tsx b/web/src/components/Layout/LayoutBg.tsx index 2cddfb62..ec76f196 100644 --- a/web/src/components/Layout/LayoutBg.tsx +++ b/web/src/components/Layout/LayoutBg.tsx @@ -1,13 +1,37 @@ +/* + * @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 */}
diff --git a/web/src/components/Layout/LoginLayout.tsx b/web/src/components/Layout/LoginLayout.tsx index ee49e98f..b3c3b8e6 100644 --- a/web/src/components/Layout/LoginLayout.tsx +++ b/web/src/components/Layout/LoginLayout.tsx @@ -1,11 +1,30 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:13:38 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:13:38 + */ +/** + * LoginLayout Component + * + * A minimal layout wrapper for authentication pages (login, register, etc.). + * Provides a simple container without navigation UI or authentication checks. + * + * @component + */ + import { Outlet } from 'react-router-dom'; import { type FC } from 'react'; -// 基础布局组件,用于展示内容并保留用户信息获取功能 +/** + * Login layout component for unauthenticated pages. + * Renders child routes in a simple full-size container. + */ const LoginLayout: FC = () => { return (
+ {/* Render authentication pages (login, register, etc.) */}
) diff --git a/web/src/components/Layout/NoAuthLayout.tsx b/web/src/components/Layout/NoAuthLayout.tsx index a2e6f274..2e6154cf 100644 --- a/web/src/components/Layout/NoAuthLayout.tsx +++ b/web/src/components/Layout/NoAuthLayout.tsx @@ -1,11 +1,30 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:13:55 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:52:17 + */ +/** + * NoAuthLayout Component + * + * A minimal layout wrapper for public pages that don't require authentication. + * Provides a simple container without navigation UI or authentication checks. + * + * @component + */ + import { Outlet } from 'react-router-dom'; import { type FC } from 'react'; -// 基础布局组件,用于展示内容并保留用户信息获取功能 +/** + * No-authentication layout component for public pages. + * Renders child routes in a simple full-size container without any auth requirements. + */ const NoAuthLayout: FC = () => { return (
+ {/* Render public pages without authentication */}
) diff --git a/web/src/components/Markdown/AudioBlock.tsx b/web/src/components/Markdown/AudioBlock.tsx index bd638816..75039d61 100644 --- a/web/src/components/Markdown/AudioBlock.tsx +++ b/web/src/components/Markdown/AudioBlock.tsx @@ -1,15 +1,31 @@ -import { memo } from 'react' +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:14:59 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:14:59 + */ +/** + * AudioBlock Component + * + * Renders audio elements from markdown nodes. + * Extracts audio source URLs and creates HTML audio players with controls. + * + * @component + */ -import type { FC } from 'react' +import { memo, type FC } from 'react' +/** Props interface for AudioBlock component */ interface AudioBlockProps { node: { children: { properties: { src: string } }[] } } + +/** Audio block component that renders audio elements from markdown nodes */ const AudioBlock: FC = (props) => { - // console.log('AudioBlock', props) const { children } = props.node; + /** Extract audio source URLs from node children and filter out empty values */ const srcs = children.map(item => item.properties?.src).filter(item => item) return ( diff --git a/web/src/components/Markdown/Code.tsx b/web/src/components/Markdown/Code.tsx index de60d0de..9aecd977 100644 --- a/web/src/components/Markdown/Code.tsx +++ b/web/src/components/Markdown/Code.tsx @@ -1,22 +1,45 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:15:05 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:15:05 + */ +/** + * Code Component + * + * A versatile code rendering component that supports: + * - Syntax-highlighted code blocks + * - ECharts visualizations + * - SVG rendering + * - Mermaid diagrams + * - Inline code snippets + * + * @component + */ + import { type FC, useMemo } from 'react' import SyntaxHighlighter from 'react-syntax-highlighter'; import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'; -import CopyBtn from './CopyBtn'; import ReactEcharts from 'echarts-for-react'; + +import CopyBtn from './CopyBtn'; import Svg from './Svg' import MermaidChart from './MermaidChart' - +/** Props interface for Code component */ type ICodeProps = { children: string; className: string; } +/** Code block component that renders syntax-highlighted code or special visualizations */ const Code: FC = (props) => { const { children, className } = props; + /** Extract language from className (e.g., 'language-javascript' -> 'javascript') */ const language = className?.split('-')[1] console.log('Code', props) + // Parse ECharts configuration from code content const charData = useMemo(() => { if (language !== 'echarts') return null; try { @@ -27,6 +50,7 @@ const Code: FC = (props) => { } }, [language, children]) + // Render ECharts visualization if (language === 'echarts') { return ( = (props) => { ) } + // Render SVG content if (language === 'svg') { return ( = (props) => { /> ) } + // Render Mermaid diagram if (language === 'mermaid') { return ( = (props) => { ) } + // Render syntax-highlighted code block with copy button if (className) { return (
@@ -81,6 +108,7 @@ const Code: FC = (props) => {
) } + // Render inline code return {children} } diff --git a/web/src/components/Markdown/CodeBlock.tsx b/web/src/components/Markdown/CodeBlock.tsx index a125a997..a830827d 100644 --- a/web/src/components/Markdown/CodeBlock.tsx +++ b/web/src/components/Markdown/CodeBlock.tsx @@ -1,9 +1,27 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:15:11 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:15:11 + */ +/** + * CodeBlock Component + * + * A standalone code block component for displaying formatted code with: + * - Syntax highlighting + * - Optional copy functionality + * - Configurable size and line numbers + * + * @component + */ + import { type FC } from 'react' import SyntaxHighlighter from 'react-syntax-highlighter'; import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'; + import CopyBtn from './CopyBtn'; - +/** Props interface for CodeBlock component */ type ICodeBlockProps = { value: string; needCopy?: boolean; @@ -11,12 +29,7 @@ type ICodeBlockProps = { showLineNumbers?: boolean; } -// enum languageType { -// echarts = 'echarts', -// mermaid = 'mermaid', -// svg = 'svg', -// } - +/** Code block component for displaying formatted code with optional copy functionality */ const CodeBlock: FC = ({ value, needCopy = true, diff --git a/web/src/components/Markdown/CopyBtn.tsx b/web/src/components/Markdown/CopyBtn.tsx index 7a371656..f877f7f8 100644 --- a/web/src/components/Markdown/CopyBtn.tsx +++ b/web/src/components/Markdown/CopyBtn.tsx @@ -1,15 +1,31 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:15:21 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:15:21 + */ +/** + * CopyBtn Component + * + * A button component that copies text to clipboard and displays a success message. + * Uses the copy-to-clipboard library for cross-browser compatibility. + * + * @component + */ + import { type FC } from 'react' import { useTranslation } from 'react-i18next' import copy from 'copy-to-clipboard' import { Button, App } from 'antd' - +/** Props interface for CopyBtn component */ type ICopyBtnProps = { value: string; className?: string; style?: React.CSSProperties; } +/** Copy button component that copies text to clipboard and shows success message */ const CopyBtn: FC = ({ value, className, @@ -18,6 +34,7 @@ const CopyBtn: FC = ({ const { t } = useTranslation() const { message } = App.useApp() + /** Copy value to clipboard and show success message */ const handleCopy = () => { copy(value) message.success(t('common.copySuccess')) diff --git a/web/src/components/Markdown/Link.tsx b/web/src/components/Markdown/Link.tsx index f75476ea..9fe6197f 100644 --- a/web/src/components/Markdown/Link.tsx +++ b/web/src/components/Markdown/Link.tsx @@ -1,13 +1,29 @@ -import { memo } from 'react' +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:15:55 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:15:55 + */ +/** + * Link Component + * + * A secure link component that opens URLs in a new tab. + * Includes security attributes (noopener, noreferrer) to prevent security vulnerabilities. + * + * @component + */ +import { memo } from 'react' import type { FC, ReactNode } from 'react' +/** Props interface for Link component */ interface LinkProps { href: string; children: ReactNode; } + +/** Link component that opens in a new tab with security attributes */ const Link: FC = (props) => { - // console.log('Link', props) const { children, href } = props; return {children} } diff --git a/web/src/components/Markdown/MermaidChart.tsx b/web/src/components/Markdown/MermaidChart.tsx index f92bf27e..3d955dd2 100644 --- a/web/src/components/Markdown/MermaidChart.tsx +++ b/web/src/components/Markdown/MermaidChart.tsx @@ -1,8 +1,26 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:16:01 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:16:01 + */ +/** + * MermaidChart Component + * + * Renders Mermaid diagrams as images. + * - Converts Mermaid syntax to SVG + * - Converts SVG to base64 data URL for display + * - Generates unique IDs based on content hash + * + * @component + */ + import { useRef, useEffect, useState, type FC } from 'react' import mermaid from 'mermaid' import CryptoJS from 'crypto-js' import { Image } from 'antd' +/** Initialize Mermaid with default configuration */ mermaid.initialize({ startOnLoad: true, theme: 'default', @@ -12,6 +30,7 @@ mermaid.initialize({ }, }) +/** Convert SVG string to base64 data URL for image display */ const svgToBase64 = (svgGraph: string) => { const svgBytes = new TextEncoder().encode(svgGraph) const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' }) @@ -22,8 +41,11 @@ const svgToBase64 = (svgGraph: string) => { reader.readAsDataURL(blob) }) } + +/** Mermaid chart component that renders Mermaid diagrams as images */ const MermaidChart: FC<{ content: string }> = ({ content }) => { const [chartSvg, setChartSvg] = useState('') + /** Generate unique ID based on content hash to avoid conflicts */ const id = useRef(`mermaidchart_${CryptoJS.MD5(content).toString()}`) useEffect(() => { @@ -33,6 +55,7 @@ const MermaidChart: FC<{ content: string }> = ({ content }) => { drawDiagram() }, [content]) + /** Render Mermaid diagram and convert to base64 image */ const drawDiagram = async function () { const { svg } = await mermaid.render(id.current, content); diff --git a/web/src/components/Markdown/Paragraph.tsx b/web/src/components/Markdown/Paragraph.tsx index 3e5dd73a..04f155dd 100644 --- a/web/src/components/Markdown/Paragraph.tsx +++ b/web/src/components/Markdown/Paragraph.tsx @@ -1,15 +1,30 @@ -import { memo } from 'react' +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:16:06 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:16:06 + */ +/** + * Paragraph Component + * + * A simple paragraph component for rendering markdown paragraphs. + * + * @component + */ +import { memo } from 'react' import type { FC, ReactNode } from 'react' +/** Props interface for Paragraph component */ interface ParagraphProps { node: { children: ReactNode; }; children: string[] } + +/** Paragraph component for rendering markdown paragraphs */ const Paragraph: FC = (props) => { - // console.log('Paragraph', props) const { children } = props return

{children}

diff --git a/web/src/components/Markdown/RbButton.tsx b/web/src/components/Markdown/RbButton.tsx index b165ce27..9af94de8 100644 --- a/web/src/components/Markdown/RbButton.tsx +++ b/web/src/components/Markdown/RbButton.tsx @@ -1,15 +1,32 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:16:10 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:16:10 + */ +/** + * RbButton Component + * + * A button component for rendering buttons in markdown content. + * Wraps Ant Design Button component. + * + * @component + */ + import { memo } from 'react' import type { FC, ReactNode } from 'react' import { Button } 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) => { - console.log('RbButton', props) const { children } = props; return ( diff --git a/web/src/components/Markdown/Svg.tsx b/web/src/components/Markdown/Svg.tsx index 7618571f..a63a9271 100644 --- a/web/src/components/Markdown/Svg.tsx +++ b/web/src/components/Markdown/Svg.tsx @@ -1,15 +1,28 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:16:14 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:16:14 + */ +/** + * Svg Component + * + * Renders SVG content from string using dangerouslySetInnerHTML. + * Used for displaying SVG code blocks in markdown. + * + * @component + */ + import * as React from 'react'; +/** Props interface for Svg component */ interface SvgProps { content: string; } -/** - * 渲染SVG内容的组件 - */ +/** Component for rendering SVG content from string */ function Svg(props: SvgProps): JSX.Element { const { content } = props; - // console.log('Svg', props) return React.createElement( 'div', diff --git a/web/src/components/Markdown/VideoBlock.tsx b/web/src/components/Markdown/VideoBlock.tsx index 914ddf2d..6127c81d 100644 --- a/web/src/components/Markdown/VideoBlock.tsx +++ b/web/src/components/Markdown/VideoBlock.tsx @@ -1,15 +1,32 @@ -import { memo } from 'react' +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:16:18 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:54:55 + */ +/** + * VideoBlock Component + * + * Renders video elements from markdown nodes. + * Extracts video source URLs and creates HTML video players with controls. + * + * @component + */ +import { memo } from 'react' import type { FC } from 'react' +/** Props interface for VideoBlock component */ interface VideoBlockProps { node: { children: { properties: { src: string } }[] } } + +/** Video block component that renders video elements from markdown nodes */ const VideoBlock: FC = (props) => { - // console.log('VideoBlock', props) const { children } = props.node; + /** Extract video source URLs from node children and filter out empty values */ const srcs = children.map(item => item.properties?.src).filter(item => item) return ( diff --git a/web/src/components/Markdown/index.tsx b/web/src/components/Markdown/index.tsx index 6737f15a..a2fac5ba 100644 --- a/web/src/components/Markdown/index.tsx +++ b/web/src/components/Markdown/index.tsx @@ -1,3 +1,28 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-02 15:17:31 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-02 15:17:31 + */ +/** + * RbMarkdown Component + * + * A comprehensive markdown renderer with support for: + * - Standard markdown syntax (headings, lists, tables, etc.) + * - Code syntax highlighting + * - Math equations (KaTeX) + * - Mermaid diagrams + * - ECharts visualizations + * - SVG rendering + * - Audio/video embedding + * - Interactive form elements + * - HTML comments visibility toggle + * - Editable mode with live preview + * + * @component + */ + +import { useState, useRef, useEffect, type FC } from 'react' import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider } from 'antd' import ReactMarkdown from 'react-markdown' import RemarkGfm from 'remark-gfm' @@ -5,8 +30,6 @@ import RemarkMath from 'remark-math' import RemarkBreaks from 'remark-breaks' import RehypeKatex from 'rehype-katex' import RehypeRaw from 'rehype-raw' -import type { FC } from 'react' -import { useState, useRef, useEffect } from 'react' import Code from './Code' import VideoBlock from './VideoBlock' @@ -14,14 +37,21 @@ import AudioBlock from './AudioBlock' import Link from './Link' import RbButton from './RbButton' +/** Props interface for RbMarkdown component */ interface RbMarkdownProps { + /** Markdown content to render */ content: string; - showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false(隐藏) - editable?: boolean; // 是否可编辑,默认为 false - onContentChange?: (content: string) => void; // 内容变化回调 + /** Whether to display HTML comments (default: false) */ + showHtmlComments?: boolean; + /** Whether the content is editable (default: false) */ + editable?: boolean; + /** Callback fired when content changes in edit mode */ + onContentChange?: (content: string) => void; + /** Additional CSS classes */ className?: string; } +/** Custom component mappings for markdown elements */ const components = { h1: ({ children, ...props }: any) =>

{children}

, h2: ({ children, ...props }: any) =>

{children}

, @@ -38,7 +68,7 @@ const components = { em: ({ children, ...props }: any) => {children}, del: ({ children, ...props }: any) => {children}, span: ({ children, style, ...restProps }: any) => { - // 如果是 HTML 注释的 span,应用特殊样式 + // Apply special styling for HTML comment spans if (style?.color === '#999') { return {children} } @@ -104,30 +134,33 @@ const RbMarkdown: FC = ({ const [editContent, setEditContent] = useState(content) const textareaRef = useRef(null) - // 当外部 content 变化时,同步更新编辑内容 + /** Sync edit content when external content changes */ useEffect(() => { setEditContent(content) }, [content]) - // 处理 textarea 内容变化 + /** Handle textarea content changes and trigger callback */ const handleTextareaChange = (e: React.ChangeEvent) => { const newContent = e.target.value setEditContent(newContent) - // 实时回调内容变化 + /** Trigger real-time content change callback */ onContentChange?.(newContent) } - // 根据参数决定是否将 HTML 注释转换为可见文本 - // 使用特殊的 markdown 语法来显示注释,避免被 rehype-raw 过滤 + /** + * Process content based on showHtmlComments flag + * Converts HTML comments to visible text when showHtmlComments is true + * Uses special span markup to display comments with styling + */ const processedContent = showHtmlComments ? (editable ? editContent : content).replace(//g, (_match, commentContent) => { - // 转换为带样式的文本,使用 标记 + /** Convert to styled text using span with html-comment class */ const escaped = commentContent.trim().replace(//g, '>') return `<!-- ${escaped} -->` }) : (editable ? editContent : content) - // 如果是编辑模式,显示 textarea + /** Render textarea in edit mode */ if (editable) { return (
@@ -138,21 +171,21 @@ const RbMarkdown: FC = ({ } `} - {/* 编辑区域 */} + {/* Edit area with textarea */}
) } - // 处理键盘快捷键 + /** Handle keyboard shortcuts (e.g., Ctrl+C for copy) */ const handleKeyDown = (e: React.KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'c') { const selection = window.getSelection() @@ -162,7 +195,7 @@ const RbMarkdown: FC = ({ } } - // 预览模式 + /** Render markdown preview mode */ return (