feat: update migration status and add new UI components

- Updated migration status documentation to reflect the migration of 36 components, with 200 pending.
- Enhanced the component status table with new entries for CustomCollapse, EmojiAvatar, ResetIcon, OcrIcon, ToolIcon, WrapIcon, UnWrapIcon, HelpTooltip, Selector, and WarnTooltip.
- Updated index.ts to export the newly added components for improved accessibility.
This commit is contained in:
MyPrototypeWhat 2025-09-15 15:12:28 +08:00
parent 1c27481813
commit 6eb9ab30b0
13 changed files with 503 additions and 13 deletions

View File

@ -49,9 +49,9 @@ function MyComponent() {
## 迁移概览
- **总组件数**: 236
- **已迁移**: 26
- **已迁移**: 36
- **已重构**: 0
- **待迁移**: 210
- **待迁移**: 200
## 组件状态表
@ -68,11 +68,12 @@ function MyComponent() {
| | SuccessTag | ✅ | ❌ | 成功标签 |
| | TextBadge | ✅ | ❌ | 文本徽标 |
| | WarnTag | ✅ | ❌ | 警告标签 |
| | CustomCollapse | | ❌ | 自定义折叠面板 |
| | CustomCollapse | | ❌ | 自定义折叠面板 |
| **display** | | | | 显示组件 |
| | Ellipsis | ✅ | ❌ | 文本省略 |
| | ExpandableText | ✅ | ❌ | 可展开文本 |
| | ThinkingEffect | ✅ | ❌ | 思考效果动画 |
| | EmojiAvatar | ✅ | ❌ | 表情头像 |
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
| | OGCard | ❌ | ❌ | OG 卡片 |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
@ -90,19 +91,24 @@ function MyComponent() {
| | FileIcons | ✅ | ❌ | 文件图标 (包含 FileSvgIcon、FilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | 推理图标 |
| | RefreshIcon | ✅ | ❌ | 刷新图标 |
| | ResetIcon | ✅ | ❌ | 重置图标 |
| | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 |
| | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 |
| | VisionIcon | ✅ | ❌ | 视觉图标 |
| | WebSearchIcon | ✅ | ❌ | 网页搜索图标 |
| | WrapIcon | ✅ | ❌ | 换行图标 |
| | UnWrapIcon | ✅ | ❌ | 不换行图标 |
| | OcrIcon | ✅ | ❌ | OCR 图标 |
| | ToolIcon | ✅ | ❌ | 工具图标 |
| | Other icons | ❌ | ❌ | 其他图标文件 |
| **interactive** | | | | 交互组件 |
| | InfoTooltip | ✅ | ❌ | 信息提示 |
| | HelpTooltip | ❌ | ❌ | 帮助提示 |
| | WarnTooltip | | ❌ | 警告提示 |
| | HelpTooltip | | ❌ | 帮助提示 |
| | WarnTooltip | | ❌ | 警告提示 |
| | DraggableList | ❌ | ❌ | 可拖拽列表 |
| | EditableNumber | ❌ | ❌ | 可编辑数字 |
| | EmojiPicker | ❌ | ❌ | 表情选择器 |
| | Selector | ❌ | ❌ | 选择器 |
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
| | LanguageSelect | ❌ | ❌ | 语言选择 |
| | TranslateButton | ❌ | ❌ | 翻译按钮 (window.api 依赖) |

View File

@ -48,9 +48,9 @@ When submitting PRs, please place components in the correct directory based on t
## Migration Overview
- **Total Components**: 236
- **Migrated**: 26
- **Migrated**: 36
- **Refactored**: 0
- **Pending Migration**: 210
- **Pending Migration**: 200
## Component Status Table
@ -67,11 +67,12 @@ When submitting PRs, please place components in the correct directory based on t
| | SuccessTag | ✅ | ❌ | Success tag |
| | TextBadge | ✅ | ❌ | Text badge |
| | WarnTag | ✅ | ❌ | Warning tag |
| | CustomCollapse | | ❌ | Custom collapse panel |
| | CustomCollapse | | ❌ | Custom collapse panel |
| **display** | | | | Display components |
| | Ellipsis | ✅ | ❌ | Text ellipsis |
| | ExpandableText | ✅ | ❌ | Expandable text |
| | ThinkingEffect | ✅ | ❌ | Thinking effect animation |
| | EmojiAvatar | ✅ | ❌ | Emoji avatar |
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
| | OGCard | ❌ | ❌ | OG card |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
@ -89,19 +90,24 @@ When submitting PRs, please place components in the correct directory based on t
| | FileIcons | ✅ | ❌ | File icons (includes FileSvgIcon, FilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
| | RefreshIcon | ✅ | ❌ | Refresh icon |
| | ResetIcon | ✅ | ❌ | Reset icon |
| | SvgSpinners180Ring | ✅ | ❌ | Spinners icon |
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
| | VisionIcon | ✅ | ❌ | Vision icon |
| | WebSearchIcon | ✅ | ❌ | Web search icon |
| | WrapIcon | ✅ | ❌ | Wrap icon |
| | UnWrapIcon | ✅ | ❌ | Unwrap icon |
| | OcrIcon | ✅ | ❌ | OCR icon |
| | ToolIcon | ✅ | ❌ | Tool icon |
| | Other icons | ❌ | ❌ | Other icon files |
| **interactive** | | | | Interactive components |
| | InfoTooltip | ✅ | ❌ | Info tooltip |
| | HelpTooltip | ❌ | ❌ | Help tooltip |
| | WarnTooltip | | ❌ | Warning tooltip |
| | HelpTooltip | | ❌ | Help tooltip |
| | WarnTooltip | | ❌ | Warning tooltip |
| | DraggableList | ❌ | ❌ | Draggable list |
| | EditableNumber | ❌ | ❌ | Editable number |
| | EmojiPicker | ❌ | ❌ | Emoji picker |
| | Selector | ❌ | ❌ | Selector |
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
| | LanguageSelect | ❌ | ❌ | Language select |
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
@ -148,4 +154,4 @@ When submitting PRs, please place components in the correct directory based on t
3. **Submission Guidelines**:
- Each PR should focus on one category of components
- Ensure all migrated components are exported
- Update migration status in this document
- Update migration status in this document

View File

@ -0,0 +1,109 @@
// Original path: src/renderer/src/components/CustomCollapse.tsx
import { Collapse } from 'antd'
import { merge } from 'lodash'
import { ChevronRight } from 'lucide-react'
import { FC, memo, useMemo, useState } from 'react'
interface CustomCollapseProps {
label: React.ReactNode
extra: React.ReactNode
children: React.ReactNode
destroyInactivePanel?: boolean
defaultActiveKey?: string[]
activeKey?: string[]
collapsible?: 'header' | 'icon' | 'disabled'
onChange?: (activeKeys: string | string[]) => void
style?: React.CSSProperties
styles?: {
header?: React.CSSProperties
body?: React.CSSProperties
}
}
const CustomCollapse: FC<CustomCollapseProps> = ({
label,
extra,
children,
destroyInactivePanel = false,
defaultActiveKey = ['1'],
activeKey,
collapsible = undefined,
onChange,
style,
styles
}) => {
const [activeKeys, setActiveKeys] = useState(activeKey || defaultActiveKey)
const defaultCollapseStyle = {
width: '100%',
background: 'transparent',
border: '0.5px solid var(--color-border)'
}
const defaultCollpaseHeaderStyle = {
padding: '3px 16px',
alignItems: 'center',
justifyContent: 'space-between',
background: 'var(--color-background-soft)'
}
const getHeaderStyle = () => {
return activeKeys && activeKeys.length > 0
? {
...defaultCollpaseHeaderStyle,
borderTopLeftRadius: '8px',
borderTopRightRadius: '8px'
}
: {
...defaultCollpaseHeaderStyle,
borderRadius: '8px'
}
}
const defaultCollapseItemStyles = {
header: getHeaderStyle(),
body: {
borderTop: 'none'
}
}
const collapseStyle = merge({}, defaultCollapseStyle, style)
const collapseItemStyles = useMemo(() => {
return merge({}, defaultCollapseItemStyles, styles)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeKeys])
return (
<Collapse
bordered={false}
style={collapseStyle}
defaultActiveKey={defaultActiveKey}
activeKey={activeKey}
destroyOnHidden={destroyInactivePanel}
collapsible={collapsible}
onChange={(keys) => {
setActiveKeys(keys)
onChange?.(keys)
}}
expandIcon={({ isActive }) => (
<ChevronRight
size={16}
color="var(--color-text-3)"
strokeWidth={1.5}
style={{ transform: isActive ? 'rotate(90deg)' : 'rotate(0deg)' }}
/>
)}
items={[
{
styles: collapseItemStyles,
key: '1',
label,
extra,
children
}
]}
/>
)
}
export default memo(CustomCollapse)

View File

@ -0,0 +1,54 @@
// Original path: src/renderer/src/components/Avatar/EmojiAvatar.tsx
import React, { memo } from 'react'
import styled from 'styled-components'
interface EmojiAvatarProps {
children: string
size?: number
fontSize?: number
onClick?: React.MouseEventHandler<HTMLDivElement>
className?: string
style?: React.CSSProperties
}
const EmojiAvatar = ({
ref,
children,
size = 31,
fontSize,
onClick,
className,
style
}: EmojiAvatarProps & { ref?: React.RefObject<HTMLDivElement | null> }) => (
<StyledEmojiAvatar
ref={ref}
$size={size}
$fontSize={fontSize ?? size * 0.5}
onClick={onClick}
className={className}
style={style}>
{children}
</StyledEmojiAvatar>
)
EmojiAvatar.displayName = 'EmojiAvatar'
const StyledEmojiAvatar = styled.div<{ $size: number; $fontSize: number }>`
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
border-radius: 20%;
cursor: pointer;
width: ${(props) => props.$size}px;
height: ${(props) => props.$size}px;
font-size: ${(props) => props.$fontSize}px;
transition: opacity 0.3s ease;
&:hover {
opacity: 0.8;
}
`
export default memo(EmojiAvatar)

View File

@ -0,0 +1,8 @@
// Original path: src/renderer/src/components/Icons/OcrIcon.tsx
import { FC } from 'react'
const OcrIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
return <i {...props} className={`iconfont icon-OCRshibie ${props.className}`} />
}
export default OcrIcon

View File

@ -0,0 +1,6 @@
// Original path: src/renderer/src/components/Icons/ResetIcon.tsx
import { RotateCcw } from 'lucide-react'
const ResetIcon = (props: React.ComponentProps<typeof RotateCcw>) => <RotateCcw size="1rem" {...props} />
export default ResetIcon

View File

@ -0,0 +1,8 @@
// Original path: src/renderer/src/components/Icons/ToolIcon.tsx
import { FC } from 'react'
const ToolIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
return <i {...props} className={`iconfont icon-plugin ${props.className}`} />
}
export default ToolIcon

View File

@ -0,0 +1,18 @@
// Original path: src/renderer/src/components/Icons/UnWrapIcon.tsx
const UnWrapIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
className="unwrap_svg__lucide unwrap_svg__lucide-text unwrap_svg__size-4"
viewBox="0 0 24 24"
{...props}>
<path d="M17 6.1H3M21 12.1H3M15.1 18H3" />
</svg>
)
export default UnWrapIcon

View File

@ -0,0 +1,21 @@
// Original path: src/renderer/src/components/Icons/WrapIcon.tsx
import React from 'react'
const WrapIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
className="wrap_svg__lucide wrap_svg__lucide-wrap-text wrap_svg__size-4"
viewBox="0 0 24 24"
{...props}>
<path d="M3 6h18M3 12h15a3 3 0 1 1 0 6h-4" />
<path d="m16 16-2 2 2 2M3 18h7" />
</svg>
)
export default WrapIcon

View File

@ -1,5 +1,6 @@
// Base Components
export { default as CopyButton } from './base/CopyButton'
export { default as CustomCollapse } from './base/CustomCollapse'
export { default as CustomTag } from './base/CustomTag'
export { default as DividerWithText } from './base/DividerWithText'
export { default as EmojiIcon } from './base/EmojiIcon'
@ -12,6 +13,7 @@ export { WarnTag } from './base/WarnTag'
// Display Components
export { default as Ellipsis } from './display/Ellipsis'
export { default as EmojiAvatar } from './display/EmojiAvatar'
export { default as ExpandableText } from './display/ExpandableText'
export { default as ThinkingEffect } from './display/ThinkingEffect'
@ -24,15 +26,23 @@ export { default as CopyIcon } from './icons/CopyIcon'
export { default as DeleteIcon } from './icons/DeleteIcon'
export { default as EditIcon } from './icons/EditIcon'
export { FilePngIcon, FileSvgIcon } from './icons/FileIcons'
export { default as OcrIcon } from './icons/OcrIcon'
export { default as ReasoningIcon } from './icons/ReasoningIcon'
export { default as RefreshIcon } from './icons/RefreshIcon'
export { default as ResetIcon } from './icons/ResetIcon'
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
export { default as ToolIcon } from './icons/ToolIcon'
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'
export { default as UnWrapIcon } from './icons/UnWrapIcon'
export { default as VisionIcon } from './icons/VisionIcon'
export { default as WebSearchIcon } from './icons/WebSearchIcon'
export { default as WrapIcon } from './icons/WrapIcon'
// Interactive Components
export { default as HelpTooltip } from './interactive/HelpTooltip'
export { default as InfoTooltip } from './interactive/InfoTooltip'
export { default as Selector } from './interactive/Selector'
export { default as WarnTooltip } from './interactive/WarnTooltip'
// Composite Components (复合组件)
// 暂无复合组件

View File

@ -0,0 +1,21 @@
// Original path: src/renderer/src/components/TooltipIcons/HelpTooltip.tsx
import { Tooltip, TooltipProps } from 'antd'
import { HelpCircle } from 'lucide-react'
type InheritedTooltipProps = Omit<TooltipProps, 'children'>
interface HelpTooltipProps extends InheritedTooltipProps {
iconColor?: string
iconSize?: string | number
iconStyle?: React.CSSProperties
}
const HelpTooltip = ({ iconColor = 'var(--color-text-2)', iconSize = 14, iconStyle, ...rest }: HelpTooltipProps) => {
return (
<Tooltip {...rest}>
<HelpCircle size={iconSize} color={iconColor} style={{ ...iconStyle }} role="img" aria-label="Help" />
</Tooltip>
)
}
export default HelpTooltip

View File

@ -0,0 +1,197 @@
// Original path: src/renderer/src/components/Selector.tsx
import { Dropdown, DropdownProps } from 'antd'
import { Check, ChevronsUpDown } from 'lucide-react'
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'
interface SelectorOption<V = string | number> {
label: string | ReactNode
value: V
type?: 'group'
options?: SelectorOption<V>[]
disabled?: boolean
}
interface BaseSelectorProps<V = string | number> {
options: SelectorOption<V>[]
placeholder?: string
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
/** 字体大小 */
size?: number
/** 是否禁用 */
disabled?: boolean
}
interface SingleSelectorProps<V> extends BaseSelectorProps<V> {
multiple?: false
value?: V
onChange: (value: V) => void
}
interface MultipleSelectorProps<V> extends BaseSelectorProps<V> {
multiple: true
value?: V[]
onChange: (value: V[]) => void
}
type SelectorProps<V> = SingleSelectorProps<V> | MultipleSelectorProps<V>
const Selector = <V extends string | number>({
options,
value,
onChange = () => {},
placement = 'bottomRight',
size = 13,
placeholder,
disabled = false,
multiple = false
}: SelectorProps<V>) => {
const [open, setOpen] = useState(false)
const { t } = useTranslation()
const inputRef = useRef<any>(null)
useEffect(() => {
let timer: NodeJS.Timeout
if (open) {
timer = setTimeout(() => {
inputRef.current?.focus()
}, 1)
}
return () => {
clearTimeout(timer)
}
}, [open])
const selectedValues = useMemo(() => {
if (multiple) {
return (value as V[]) || []
}
return value !== undefined ? [value as V] : []
}, [value, multiple])
const label = useMemo(() => {
if (selectedValues.length > 0) {
const findLabels = (opts: SelectorOption<V>[]): (string | ReactNode)[] => {
const labels: (string | ReactNode)[] = []
for (const opt of opts) {
if (selectedValues.some((v) => v == opt.value)) {
labels.push(opt.label)
}
if (opt.options) {
labels.push(...findLabels(opt.options))
}
}
return labels
}
const labels = findLabels(options)
if (labels.length === 0) return placeholder
if (labels.length === 1) return labels[0]
return t('common.selectedItems', { count: labels.length })
}
return placeholder
}, [selectedValues, placeholder, options, t])
const items = useMemo(() => {
const mapOption = (option: SelectorOption<V>) => ({
key: option.value,
label: option.label,
extra: <CheckIcon>{selectedValues.some((v) => v == option.value) && <Check size={14} />}</CheckIcon>,
disabled: option.disabled,
type: option.type || (option.options ? 'group' : undefined),
children: option.options?.map(mapOption)
})
return options.map(mapOption)
}, [options, selectedValues])
function onClick(e: { key: string }) {
if (disabled) return
const newValue = e.key as V
if (multiple) {
const newValues = selectedValues.includes(newValue)
? selectedValues.filter((v) => v !== newValue)
: [...selectedValues, newValue]
;(onChange as MultipleSelectorProps<V>['onChange'])(newValues)
} else {
;(onChange as SingleSelectorProps<V>['onChange'])(newValue)
setOpen(false)
}
}
const handleOpenChange: DropdownProps['onOpenChange'] = (nextOpen, info) => {
if (disabled) return
if (info.source === 'trigger' || nextOpen) {
setOpen(nextOpen)
}
}
return (
<Dropdown
overlayClassName="selector-dropdown"
menu={{ items, onClick }}
trigger={['click']}
placement={placement}
open={open && !disabled}
onOpenChange={handleOpenChange}>
<Label $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
{label}
<LabelIcon size={size + 3} />
</Label>
</Dropdown>
)
}
const LabelIcon = styled(ChevronsUpDown)`
border-radius: 4px;
padding: 2px 0;
background-color: var(--color-background-soft);
transition: background-color 0.2s;
`
const Label = styled.div<{ $size: number; $open: boolean; $disabled: boolean; $isPlaceholder: boolean }>`
display: flex;
align-items: center;
gap: 4px;
border-radius: 99px;
padding: 3px 2px 3px 10px;
font-size: ${({ $size }) => $size}px;
line-height: 1;
cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
opacity: ${({ $disabled }) => ($disabled ? 0.6 : 1)};
color: ${({ $isPlaceholder }) => ($isPlaceholder ? 'var(--color-text-2)' : 'inherit')};
transition:
background-color 0.2s,
opacity 0.2s;
&:hover {
${({ $disabled }) =>
!$disabled &&
css`
background-color: var(--color-background-mute);
${LabelIcon} {
background-color: var(--color-background-mute);
}
`}
}
${({ $open, $disabled }) =>
$open &&
!$disabled &&
css`
background-color: var(--color-background-mute);
${LabelIcon} {
background-color: var(--color-background-mute);
}
`}
`
const CheckIcon = styled.div`
width: 20px;
display: flex;
align-items: center;
justify-content: end;
`
export default Selector

View File

@ -0,0 +1,26 @@
// Original path: src/renderer/src/components/TooltipIcons/WarnTooltip.tsx
import { Tooltip, TooltipProps } from 'antd'
import { AlertTriangle } from 'lucide-react'
type InheritedTooltipProps = Omit<TooltipProps, 'children'>
interface WarnTooltipProps extends InheritedTooltipProps {
iconColor?: string
iconSize?: string | number
iconStyle?: React.CSSProperties
}
const WarnTooltip = ({
iconColor = 'var(--color-status-warning)',
iconSize = 14,
iconStyle,
...rest
}: WarnTooltipProps) => {
return (
<Tooltip {...rest}>
<AlertTriangle size={iconSize} color={iconColor} style={{ ...iconStyle }} role="img" aria-label="Information" />
</Tooltip>
)
}
export default WarnTooltip