mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-02 10:29:02 +08:00
refactor(ui): update migration status and enhance component implementations
- Increased the refactored component count to 18 and reduced pending migrations to 184 in the migration status files. - Improved the Ellipsis, ListItem, MaxContextCount, and ThinkingEffect components by simplifying their structure and enhancing styling. - Updated the tsconfig.json to adjust the root directory for better project organization. - Removed unnecessary alias configurations in tsdown.config.ts for cleaner setup. - Added new stories for Ellipsis and ListItem components to improve documentation and showcase their usage.
This commit is contained in:
parent
bd7cd22220
commit
59bf94b118
@ -50,8 +50,8 @@ function MyComponent() {
|
||||
|
||||
- **总组件数**: 236
|
||||
- **已迁移**: 34
|
||||
- **已重构**: 14
|
||||
- **待迁移**: 188
|
||||
- **已重构**: 18
|
||||
- **待迁移**: 184
|
||||
|
||||
## 组件状态表
|
||||
|
||||
@ -69,12 +69,12 @@ function MyComponent() {
|
||||
| | TextBadge | ✅ | ✅ | 文本徽标 |
|
||||
| | CustomCollapse | ✅ | ✅ | 自定义折叠面板 |
|
||||
| **display** | | | | 显示组件 |
|
||||
| | Ellipsis | ✅ | ❌ | 文本省略 |
|
||||
| | Ellipsis | ✅ | ✅ | 文本省略 |
|
||||
| | ExpandableText | ✅ | ✅ | 可展开文本 |
|
||||
| | ThinkingEffect | ✅ | ❌ | 思考效果动画 |
|
||||
| | ThinkingEffect | ✅ | ✅ | 思考效果动画 |
|
||||
| | EmojiAvatar | ✅ | ✅ | 表情头像 |
|
||||
| | ListItem | ✅ | ❌ | 列表项 |
|
||||
| | MaxContextCount | ✅ | ❌ | 最大上下文数显示 |
|
||||
| | ListItem | ✅ | ✅ | 列表项 |
|
||||
| | MaxContextCount | ✅ | ✅ | 最大上下文数显示 |
|
||||
| | ProviderAvatar | ✅ | ✅ | 提供者头像 |
|
||||
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
|
||||
| | OGCard | ❌ | ❌ | OG 卡片 |
|
||||
|
||||
@ -49,8 +49,8 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
|
||||
- **Total Components**: 236
|
||||
- **Migrated**: 34
|
||||
- **Refactored**: 14
|
||||
- **Pending Migration**: 188
|
||||
- **Refactored**: 18
|
||||
- **Pending Migration**: 184
|
||||
|
||||
## Component Status Table
|
||||
|
||||
@ -68,12 +68,12 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
| | TextBadge | ✅ | ✅ | Text badge |
|
||||
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
|
||||
| **display** | | | | Display components |
|
||||
| | Ellipsis | ✅ | ❌ | Text ellipsis |
|
||||
| | Ellipsis | ✅ | ✅ | Text ellipsis |
|
||||
| | ExpandableText | ✅ | ✅ | Expandable text |
|
||||
| | ThinkingEffect | ✅ | ❌ | Thinking effect animation |
|
||||
| | ThinkingEffect | ✅ | ✅ | Thinking effect animation |
|
||||
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
|
||||
| | ListItem | ✅ | ❌ | List item |
|
||||
| | MaxContextCount | ✅ | ❌ | Max context count display |
|
||||
| | ListItem | ✅ | ✅ | List item |
|
||||
| | MaxContextCount | ✅ | ✅ | Max context count display |
|
||||
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
|
||||
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
|
||||
| | OGCard | ❌ | ❌ | OG card |
|
||||
|
||||
@ -1,36 +1,28 @@
|
||||
// Original: src/renderer/src/components/Ellipsis/index.tsx
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
import { cn } from '../../../utils'
|
||||
|
||||
type Props = {
|
||||
maxLine?: number
|
||||
className?: string
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
} & HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const Ellipsis = (props: Props) => {
|
||||
const { maxLine = 1, children, ...rest } = props
|
||||
const { maxLine = 1, children, className, ref, ...rest } = props
|
||||
|
||||
const ellipsisClasses = cn(
|
||||
'overflow-hidden text-ellipsis',
|
||||
maxLine > 1 ? `line-clamp-${maxLine} break-words` : 'block whitespace-nowrap',
|
||||
className
|
||||
)
|
||||
|
||||
return (
|
||||
<EllipsisContainer $maxLine={maxLine} {...rest}>
|
||||
<div ref={ref} className={ellipsisClasses} {...rest}>
|
||||
{children}
|
||||
</EllipsisContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const multiLineEllipsis = css<{ $maxLine: number }>`
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: ${({ $maxLine }) => $maxLine};
|
||||
overflow-wrap: break-word;
|
||||
`
|
||||
|
||||
const singleLineEllipsis = css`
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const EllipsisContainer = styled.div<{ $maxLine: number }>`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
${({ $maxLine }) => ($maxLine > 1 ? multiLineEllipsis : singleLineEllipsis)}
|
||||
`
|
||||
|
||||
export default Ellipsis
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
// Original path: src/renderer/src/components/ListItem/index.tsx
|
||||
import { Typography } from 'antd'
|
||||
import { Tooltip } from '@heroui/react'
|
||||
import { ReactNode } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { cn } from '../../../utils'
|
||||
|
||||
interface ListItemProps {
|
||||
active?: boolean
|
||||
@ -12,81 +13,49 @@ interface ListItemProps {
|
||||
onClick?: () => void
|
||||
rightContent?: ReactNode
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
}
|
||||
|
||||
const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick, rightContent, style }: ListItemProps) => {
|
||||
const ListItem = ({
|
||||
active,
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
titleStyle,
|
||||
onClick,
|
||||
rightContent,
|
||||
style,
|
||||
className,
|
||||
ref
|
||||
}: ListItemProps) => {
|
||||
return (
|
||||
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={style}>
|
||||
<ListItemContent>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<TextContainer>
|
||||
<Typography.Text style={titleStyle} ellipsis={{ expanded: false, tooltip: title }}>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
{subtitle && <SubtitleText>{subtitle}</SubtitleText>}
|
||||
</TextContainer>
|
||||
{rightContent && <RightContentWrapper>{rightContent}</RightContentWrapper>}
|
||||
</ListItemContent>
|
||||
</ListItemContainer>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-md text-xs flex flex-col justify-between relative cursor-pointer border border-transparent',
|
||||
'hover:bg-gray-50 dark:hover:bg-gray-800',
|
||||
active && 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700',
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
style={style}>
|
||||
<div className="flex items-center gap-0.5 overflow-hidden text-xs">
|
||||
{icon && <span className="flex items-center justify-center mr-2">{icon}</span>}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<Tooltip content={title} placement="top">
|
||||
<div className="truncate text-gray-900 dark:text-gray-100" style={titleStyle}>
|
||||
{title}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{subtitle && (
|
||||
<div className="text-[10px] text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-1">{subtitle}</div>
|
||||
)}
|
||||
</div>
|
||||
{rightContent && <div className="ml-auto">{rightContent}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ListItemContainer = styled.div`
|
||||
padding: 7px 12px;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-background-soft);
|
||||
border: 1px solid var(--color-border-soft);
|
||||
}
|
||||
`
|
||||
|
||||
const ListItemContent = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
`
|
||||
|
||||
const IconWrapper = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const SubtitleText = styled.div`
|
||||
font-size: 10px;
|
||||
color: var(--color-text-soft);
|
||||
margin-top: 2px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const RightContentWrapper = styled.div`
|
||||
margin-left: auto;
|
||||
`
|
||||
|
||||
export default ListItem
|
||||
|
||||
@ -8,12 +8,16 @@ type Props = {
|
||||
maxContext: number
|
||||
style?: CSSProperties
|
||||
size?: number
|
||||
className?: string
|
||||
ref?: React.Ref<HTMLSpanElement>
|
||||
}
|
||||
|
||||
export default function MaxContextCount({ maxContext, style, size = 14 }: Props) {
|
||||
export default function MaxContextCount({ maxContext, style, size = 14, className, ref }: Props) {
|
||||
return maxContext === MAX_CONTEXT_COUNT ? (
|
||||
<InfinityIcon size={size} style={style} aria-label="infinity" />
|
||||
<InfinityIcon size={size} style={style} className={className} aria-label="infinity" />
|
||||
) : (
|
||||
<span style={style}>{maxContext.toString()}</span>
|
||||
<span ref={ref} style={style} className={className}>
|
||||
{maxContext.toString()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ import { isEqual } from 'lodash'
|
||||
import { ChevronRight, Lightbulb } from 'lucide-react'
|
||||
import { motion } from 'motion/react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { cn } from '../../../utils'
|
||||
import { lightbulbVariants } from './defaultVariants'
|
||||
|
||||
interface ThinkingEffectProps {
|
||||
@ -12,11 +12,19 @@ interface ThinkingEffectProps {
|
||||
thinkingTimeText: React.ReactNode
|
||||
content: string
|
||||
expanded: boolean
|
||||
className?: string
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
}
|
||||
|
||||
const ThinkingEffect: React.FC<ThinkingEffectProps> = ({ isThinking, thinkingTimeText, content, expanded }) => {
|
||||
const ThinkingEffect: React.FC<ThinkingEffectProps> = ({
|
||||
isThinking,
|
||||
thinkingTimeText,
|
||||
content,
|
||||
expanded,
|
||||
className,
|
||||
ref
|
||||
}) => {
|
||||
const [messages, setMessages] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const allLines = (content || '').split('\n')
|
||||
const newMessages = isThinking ? allLines.slice(0, -1) : allLines
|
||||
@ -39,22 +47,44 @@ const ThinkingEffect: React.FC<ThinkingEffectProps> = ({ isThinking, thinkingTim
|
||||
}, [showThinking, messages.length])
|
||||
|
||||
return (
|
||||
<ThinkingContainer style={{ height: containerHeight }} className={expanded ? 'expanded' : ''}>
|
||||
<LoadingContainer>
|
||||
<motion.div variants={lightbulbVariants} animate={isThinking ? 'active' : 'idle'} initial="idle">
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ height: containerHeight }}
|
||||
className={cn(
|
||||
'w-full rounded-xl overflow-hidden relative flex items-center border-0.5 border-gray-200 dark:border-gray-700 transition-all duration-150 pointer-events-none select-none',
|
||||
expanded && 'rounded-b-none',
|
||||
className
|
||||
)}>
|
||||
<div className="w-12 flex justify-center items-center h-full flex-shrink-0 relative pl-1.5 transition-all duration-150">
|
||||
<motion.div
|
||||
variants={lightbulbVariants}
|
||||
animate={isThinking ? 'active' : 'idle'}
|
||||
initial="idle"
|
||||
className="flex justify-center items-center">
|
||||
<Lightbulb
|
||||
size={!showThinking || messages.length < 2 ? 20 : 30}
|
||||
style={{ transition: 'width,height, 150ms' }}
|
||||
/>
|
||||
</motion.div>
|
||||
</LoadingContainer>
|
||||
</div>
|
||||
|
||||
<TextContainer>
|
||||
<Title className={!showThinking || !messages.length ? 'showThinking' : ''}>{thinkingTimeText}</Title>
|
||||
<div className="flex-1 h-full py-1.5 overflow-hidden relative">
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-x-0 top-0 text-sm leading-3.5 font-medium py-2.5 z-50 transition-all duration-150',
|
||||
(!showThinking || !messages.length) && 'pt-3'
|
||||
)}>
|
||||
{thinkingTimeText}
|
||||
</div>
|
||||
|
||||
{showThinking && (
|
||||
<Content>
|
||||
<Messages
|
||||
<div
|
||||
className="w-full h-full relative"
|
||||
style={{
|
||||
mask: 'linear-gradient(to bottom, rgb(0 0 0 / 0%) 0%, rgb(0 0 0 / 0%) 35%, rgb(0 0 0 / 25%) 40%, rgb(0 0 0 / 100%) 90%, rgb(0 0 0 / 100%) 100%)'
|
||||
}}>
|
||||
<motion.div
|
||||
className="w-full absolute top-full flex flex-col justify-end"
|
||||
style={{
|
||||
height: messages.length * LINE_HEIGHT
|
||||
}}
|
||||
@ -71,120 +101,28 @@ const ThinkingEffect: React.FC<ThinkingEffectProps> = ({ isThinking, thinkingTim
|
||||
{messages.map((message, index) => {
|
||||
if (index < messages.length - 5) return null
|
||||
|
||||
return <Message key={index}>{message}</Message>
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full leading-3.5 text-xs text-gray-600 dark:text-gray-300 whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{message}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Messages>
|
||||
</Content>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</TextContainer>
|
||||
<ArrowContainer className={expanded ? 'expanded' : ''}>
|
||||
<ChevronRight size={20} color="var(--color-text-3)" strokeWidth={1} />
|
||||
</ArrowContainer>
|
||||
</ThinkingContainer>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'w-10 flex justify-center items-center h-full flex-shrink-0 relative text-gray-400 dark:text-gray-500 transition-transform duration-150',
|
||||
expanded && 'rotate-90'
|
||||
)}>
|
||||
<ChevronRight size={20} strokeWidth={1} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ThinkingContainer = styled.div`
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0.5px solid var(--color-border);
|
||||
transition: height, border-radius, 150ms;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
&.expanded {
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
position: absolute;
|
||||
inset: 0 0 auto 0;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
font-weight: 500;
|
||||
padding: 10px 0;
|
||||
z-index: 99;
|
||||
transition: padding-top 150ms;
|
||||
&.showThinking {
|
||||
padding-top: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
const LoadingContainer = styled.div`
|
||||
width: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
padding-left: 5px;
|
||||
transition: width 150ms;
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 5px 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const Content = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgb(0 0 0 / 0%) 0%,
|
||||
rgb(0 0 0 / 0%) 35%,
|
||||
rgb(0 0 0 / 25%) 40%,
|
||||
rgb(0 0 0 / 100%) 90%,
|
||||
rgb(0 0 0 / 100%) 100%
|
||||
);
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const Messages = styled(motion.div)`
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
const Message = styled.div`
|
||||
width: 100%;
|
||||
line-height: 14px;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const ArrowContainer = styled.div`
|
||||
width: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
color: var(--color-border);
|
||||
transition: transform 150ms;
|
||||
&.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
`
|
||||
|
||||
export default ThinkingEffect
|
||||
|
||||
167
packages/ui/stories/components/display/Ellipsis.stories.tsx
Normal file
167
packages/ui/stories/components/display/Ellipsis.stories.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import Ellipsis from '../../../src/components/display/Ellipsis'
|
||||
|
||||
const meta = {
|
||||
title: 'Display/Ellipsis',
|
||||
component: Ellipsis,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: '一个用于显示省略文本的组件,支持单行和多行省略功能。'
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
maxLine: {
|
||||
control: { type: 'number' },
|
||||
description: '最大显示行数,默认为1。设置为1时为单行省略,大于1时为多行省略。'
|
||||
},
|
||||
className: {
|
||||
control: { type: 'text' },
|
||||
description: '自定义 CSS 类名'
|
||||
},
|
||||
children: {
|
||||
control: { type: 'text' },
|
||||
description: '要显示的文本内容'
|
||||
}
|
||||
},
|
||||
args: {
|
||||
children: '这是一段很长的文本内容,用于演示省略功能的效果。当文本超出容器宽度或高度时,会自动显示省略号。'
|
||||
}
|
||||
} satisfies Meta<typeof Ellipsis>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// 默认单行省略
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
maxLine: 1
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-60 p-4 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 多行省略
|
||||
export const MultiLine: Story = {
|
||||
args: {
|
||||
maxLine: 3,
|
||||
children:
|
||||
'这是一段很长的文本内容,用于演示多行省略功能的效果。当文本内容超过指定的最大行数时,会在最后一行的末尾显示省略号。这个功能特别适用于显示文章摘要、商品描述等需要限制显示行数的场景。'
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-80 p-4 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 不同的最大行数
|
||||
export const DifferentMaxLines: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4 max-w-lg">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">单行省略 (maxLine = 1)</h3>
|
||||
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis maxLine={1}>这是一段很长的文本内容,用于演示单行省略功能的效果。</Ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">两行省略 (maxLine = 2)</h3>
|
||||
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis maxLine={2}>
|
||||
这是一段很长的文本内容,用于演示两行省略功能的效果。当文本内容超过两行时,会在第二行的末尾显示省略号。
|
||||
</Ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">三行省略 (maxLine = 3)</h3>
|
||||
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis maxLine={3}>
|
||||
这是一段很长的文本内容,用于演示三行省略功能的效果。当文本内容超过三行时,会在第三行的末尾显示省略号。这个功能特别适用于显示文章摘要、商品描述等需要限制显示行数的场景。
|
||||
</Ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 短文本(不需要省略)
|
||||
export const ShortText: Story = {
|
||||
args: {
|
||||
maxLine: 2,
|
||||
children: '这是一段短文本。'
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-80 p-4 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 自定义样式
|
||||
export const CustomStyle: Story = {
|
||||
args: {
|
||||
maxLine: 2,
|
||||
className: 'text-blue-600 font-medium text-lg',
|
||||
children: '这是一段带有自定义样式的长文本内容,用于演示如何自定义省略文本的样式。'
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-80 p-4 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 不同容器宽度的响应式展示
|
||||
export const ResponsiveWidth: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">窄容器 (200px)</h3>
|
||||
<div className="w-50 p-3 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis maxLine={2}>这是一段在窄容器中显示的文本内容,用于演示在不同宽度下的省略效果。</Ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">中等容器 (300px)</h3>
|
||||
<div className="w-75 p-3 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis maxLine={2}>这是一段在中等宽度容器中显示的文本内容,用于演示在不同宽度下的省略效果。</Ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">宽容器 (400px)</h3>
|
||||
<div className="w-100 p-3 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis maxLine={2}>这是一段在宽容器中显示的文本内容,用于演示在不同宽度下的省略效果。</Ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 包含HTML内容
|
||||
export const WithHTMLContent: Story = {
|
||||
args: {
|
||||
maxLine: 2
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-80 p-4 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<Ellipsis {...args}>
|
||||
<span className="text-red-500">这是红色文本</span>和<strong className="font-bold">加粗文本</strong>
|
||||
以及
|
||||
<em className="italic">斜体文本</em>
|
||||
组合的长文本内容,用于演示包含HTML元素的省略效果。
|
||||
</Ellipsis>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
327
packages/ui/stories/components/display/ListItem.stories.tsx
Normal file
327
packages/ui/stories/components/display/ListItem.stories.tsx
Normal file
@ -0,0 +1,327 @@
|
||||
import { Button } from '@heroui/react'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import {
|
||||
ChevronRight,
|
||||
Edit,
|
||||
File,
|
||||
Folder,
|
||||
Heart,
|
||||
Mail,
|
||||
MoreHorizontal,
|
||||
Phone,
|
||||
Settings,
|
||||
Star,
|
||||
Trash2,
|
||||
User
|
||||
} from 'lucide-react'
|
||||
import { action } from 'storybook/actions'
|
||||
|
||||
import ListItem from '../../../src/components/display/ListItem'
|
||||
|
||||
const meta: Meta<typeof ListItem> = {
|
||||
title: 'Display/ListItem',
|
||||
component: ListItem,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: '一个通用的列表项组件,支持图标、标题、副标题、激活状态和右侧内容等功能。'
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
active: {
|
||||
control: { type: 'boolean' },
|
||||
description: '是否处于激活状态,激活时会显示高亮样式'
|
||||
},
|
||||
icon: {
|
||||
control: { type: 'text' },
|
||||
description: '左侧图标,可以是任何 React 节点'
|
||||
},
|
||||
title: {
|
||||
control: { type: 'text' },
|
||||
description: '标题内容,必填字段,可以是文本或 React 节点'
|
||||
},
|
||||
subtitle: {
|
||||
control: { type: 'text' },
|
||||
description: '副标题内容,显示在标题下方'
|
||||
},
|
||||
titleStyle: {
|
||||
control: { type: 'object' },
|
||||
description: '标题的自定义样式对象'
|
||||
},
|
||||
onClick: {
|
||||
action: 'clicked',
|
||||
description: '点击事件处理函数'
|
||||
},
|
||||
rightContent: {
|
||||
control: { type: 'text' },
|
||||
description: '右侧内容,可以是任何 React 节点'
|
||||
},
|
||||
className: {
|
||||
control: { type: 'text' },
|
||||
description: '自定义 CSS 类名'
|
||||
}
|
||||
},
|
||||
args: {
|
||||
title: '列表项标题',
|
||||
onClick: action('clicked')
|
||||
}
|
||||
} satisfies Meta<typeof ListItem>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// 默认样式
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: '默认列表项'
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-80">
|
||||
<ListItem {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 带图标
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
icon: <File size={16} />,
|
||||
title: '带图标的列表项',
|
||||
subtitle: '这是一个副标题'
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-80">
|
||||
<ListItem {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 激活状态
|
||||
export const Active: Story = {
|
||||
args: {
|
||||
icon: <Folder size={16} />,
|
||||
title: '激活状态的列表项',
|
||||
subtitle: '当前选中项',
|
||||
active: true
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-80">
|
||||
<ListItem {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 带右侧内容
|
||||
export const WithRightContent: Story = {
|
||||
args: {
|
||||
icon: <Star size={16} />,
|
||||
title: '带右侧内容的列表项',
|
||||
subtitle: '右侧有附加信息',
|
||||
rightContent: <ChevronRight size={16} className="text-gray-400" />
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-80">
|
||||
<ListItem {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 多种图标类型
|
||||
export const DifferentIcons: Story = {
|
||||
render: () => (
|
||||
<div className="w-80 space-y-2">
|
||||
<ListItem
|
||||
icon={<File size={16} className="text-blue-500" />}
|
||||
title="文件项"
|
||||
subtitle="文档文件"
|
||||
onClick={action('file-clicked')}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<Folder size={16} className="text-yellow-500" />}
|
||||
title="文件夹项"
|
||||
subtitle="目录文件夹"
|
||||
onClick={action('folder-clicked')}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<User size={16} className="text-green-500" />}
|
||||
title="用户项"
|
||||
subtitle="用户信息"
|
||||
onClick={action('user-clicked')}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<Settings size={16} className="text-gray-500" />}
|
||||
title="设置项"
|
||||
subtitle="系统设置"
|
||||
onClick={action('settings-clicked')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 不同长度的标题和副标题
|
||||
export const DifferentContentLength: Story = {
|
||||
render: () => (
|
||||
<div className="w-80 space-y-2">
|
||||
<ListItem icon={<Mail size={16} />} title="短标题" subtitle="短副标题" />
|
||||
<ListItem
|
||||
icon={<Phone size={16} />}
|
||||
title="这是一个比较长的标题,可能会被截断"
|
||||
subtitle="这也是一个比较长的副标题,用于测试文本溢出效果"
|
||||
/>
|
||||
<ListItem
|
||||
icon={<Heart size={16} />}
|
||||
title="超级长的标题内容用于测试文本省略功能,当标题过长时会自动截断并显示省略号"
|
||||
subtitle="超级长的副标题内容用于测试文本省略功能,当副标题过长时也会自动截断"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 不同的右侧内容类型
|
||||
export const DifferentRightContent: Story = {
|
||||
render: () => (
|
||||
<div className="w-80 space-y-2">
|
||||
<ListItem
|
||||
icon={<File size={16} />}
|
||||
title="带箭头"
|
||||
subtitle="导航类型"
|
||||
rightContent={<ChevronRight size={16} className="text-gray-400" />}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<Folder size={16} />}
|
||||
title="带按钮"
|
||||
subtitle="操作类型"
|
||||
rightContent={
|
||||
<Button size="sm" variant="ghost" isIconOnly>
|
||||
<MoreHorizontal size={16} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<User size={16} />}
|
||||
title="带文本"
|
||||
subtitle="信息显示"
|
||||
rightContent={<span className="text-xs text-gray-500">在线</span>}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<Settings size={16} />}
|
||||
title="带多个操作"
|
||||
subtitle="复合操作"
|
||||
rightContent={
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="ghost" isIconOnly>
|
||||
<Edit size={14} />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" isIconOnly color="danger">
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 激活状态对比
|
||||
export const ActiveComparison: Story = {
|
||||
render: () => (
|
||||
<div className="w-80 space-y-2">
|
||||
<h3 className="text-sm font-medium mb-2">普通状态</h3>
|
||||
<ListItem
|
||||
icon={<File size={16} />}
|
||||
title="普通列表项"
|
||||
subtitle="未激活状态"
|
||||
rightContent={<ChevronRight size={16} className="text-gray-400" />}
|
||||
/>
|
||||
|
||||
<h3 className="text-sm font-medium mb-2 mt-4">激活状态</h3>
|
||||
<ListItem
|
||||
icon={<File size={16} />}
|
||||
title="激活列表项"
|
||||
subtitle="当前选中状态"
|
||||
active={true}
|
||||
rightContent={<ChevronRight size={16} className="text-gray-400" />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 自定义标题样式
|
||||
export const CustomTitleStyle: Story = {
|
||||
render: () => (
|
||||
<div className="w-80 space-y-2">
|
||||
<ListItem
|
||||
icon={<Star size={16} />}
|
||||
title="红色标题"
|
||||
subtitle="自定义颜色"
|
||||
titleStyle={{ color: '#ef4444', fontWeight: 'bold' }}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<Heart size={16} />}
|
||||
title="大号标题"
|
||||
subtitle="自定义大小"
|
||||
titleStyle={{ fontSize: '16px', fontWeight: '600' }}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<User size={16} />}
|
||||
title="斜体标题"
|
||||
subtitle="自定义样式"
|
||||
titleStyle={{ fontStyle: 'italic', color: '#6366f1' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 无副标题
|
||||
export const WithoutSubtitle: Story = {
|
||||
render: () => (
|
||||
<div className="w-80 space-y-2">
|
||||
<ListItem icon={<File size={16} />} title="只有标题的列表项" />
|
||||
<ListItem
|
||||
icon={<Folder size={16} />}
|
||||
title="另一个只有标题的项"
|
||||
rightContent={<ChevronRight size={16} className="text-gray-400" />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 无图标
|
||||
export const WithoutIcon: Story = {
|
||||
render: () => (
|
||||
<div className="w-80 space-y-2">
|
||||
<ListItem title="无图标列表项" subtitle="没有左侧图标" />
|
||||
<ListItem
|
||||
title="另一个无图标项"
|
||||
subtitle="简洁样式"
|
||||
rightContent={<span className="text-xs text-gray-500">标签</span>}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 完整功能展示
|
||||
export const FullFeatures: Story = {
|
||||
render: () => (
|
||||
<div className="w-80 space-y-2">
|
||||
<ListItem
|
||||
icon={<File size={16} className="text-blue-500" />}
|
||||
title="完整功能展示"
|
||||
subtitle="包含所有功能的列表项"
|
||||
titleStyle={{ fontWeight: '600' }}
|
||||
active={true}
|
||||
rightContent={
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500">NEW</span>
|
||||
<ChevronRight size={16} className="text-gray-400" />
|
||||
</div>
|
||||
}
|
||||
onClick={action('full-features-clicked')}
|
||||
className="hover:shadow-sm transition-shadow"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,299 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import MaxContextCount from '../../../src/components/display/MaxContextCount'
|
||||
|
||||
const meta: Meta<typeof MaxContextCount> = {
|
||||
title: 'Display/MaxContextCount',
|
||||
component: MaxContextCount,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: '一个用于显示最大上下文数量的组件。当数量达到100时显示无限符号,否则显示具体数字。'
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
maxContext: {
|
||||
control: { type: 'number', min: 0, max: 100, step: 1 },
|
||||
description: '最大上下文数量。当值为100时显示无限符号(∞),其他值显示具体数字。'
|
||||
},
|
||||
size: {
|
||||
control: { type: 'number', min: 8, max: 48, step: 2 },
|
||||
description: '图标大小,默认为14像素'
|
||||
},
|
||||
className: {
|
||||
control: { type: 'text' },
|
||||
description: '自定义 CSS 类名'
|
||||
},
|
||||
style: {
|
||||
control: { type: 'object' },
|
||||
description: '自定义样式对象'
|
||||
}
|
||||
},
|
||||
args: {
|
||||
maxContext: 10,
|
||||
size: 14
|
||||
}
|
||||
} satisfies Meta<typeof MaxContextCount>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// 默认数字显示
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
maxContext: 10
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded inline-flex items-center gap-2">
|
||||
<span className="text-sm">最大上下文:</span>
|
||||
<MaxContextCount {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 无限符号显示
|
||||
export const InfinitySymbol: Story = {
|
||||
args: {
|
||||
maxContext: 100
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded inline-flex items-center gap-2">
|
||||
<span className="text-sm">最大上下文:</span>
|
||||
<MaxContextCount {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 不同的数值范围
|
||||
export const DifferentValues: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<div className="text-xs text-gray-500 mb-2">小数值</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">上下文:</span>
|
||||
<MaxContextCount maxContext={5} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<div className="text-xs text-gray-500 mb-2">中等数值</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">上下文:</span>
|
||||
<MaxContextCount maxContext={25} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<div className="text-xs text-gray-500 mb-2">大数值</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">上下文:</span>
|
||||
<MaxContextCount maxContext={99} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded">
|
||||
<div className="text-xs text-gray-500 mb-2">无限</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">上下文:</span>
|
||||
<MaxContextCount maxContext={100} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 不同大小
|
||||
export const DifferentSizes: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs">小号 (12px):</span>
|
||||
<MaxContextCount maxContext={20} size={12} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">默认 (14px):</span>
|
||||
<MaxContextCount maxContext={50} size={14} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base">中号 (18px):</span>
|
||||
<MaxContextCount maxContext={75} size={18} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">大号 (24px):</span>
|
||||
<MaxContextCount maxContext={100} size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 无限符号不同大小对比
|
||||
export const InfinityDifferentSizes: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium">无限符号不同大小对比</h3>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs">12px:</span>
|
||||
<MaxContextCount maxContext={100} size={12} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">16px:</span>
|
||||
<MaxContextCount maxContext={100} size={16} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base">20px:</span>
|
||||
<MaxContextCount maxContext={100} size={20} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">28px:</span>
|
||||
<MaxContextCount maxContext={100} size={28} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 自定义样式
|
||||
export const CustomStyles: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm">红色数字:</span>
|
||||
<MaxContextCount maxContext={42} style={{ color: '#ef4444', fontWeight: 'bold' }} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm">蓝色无限符号:</span>
|
||||
<MaxContextCount maxContext={100} size={18} style={{ color: '#3b82f6' }} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm">带背景:</span>
|
||||
<MaxContextCount
|
||||
maxContext={88}
|
||||
className="px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded-full text-xs font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm">带边框:</span>
|
||||
<MaxContextCount
|
||||
maxContext={100}
|
||||
size={16}
|
||||
className="p-1 border-2 border-purple-300 rounded-full text-purple-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 在实际使用场景中的展示
|
||||
export const InRealScenarios: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4 max-w-md">
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium text-sm">AI 模型配置</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">模型:</span>
|
||||
<span>GPT-4</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">温度:</span>
|
||||
<span>0.7</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400">最大上下文:</span>
|
||||
<MaxContextCount maxContext={100} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium text-sm">对话设置</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400">记忆长度:</span>
|
||||
<MaxContextCount maxContext={50} size={13} />
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400">历史消息:</span>
|
||||
<MaxContextCount maxContext={20} size={13} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 边界值测试
|
||||
export const EdgeCases: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium">边界值测试</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded text-center">
|
||||
<div className="text-xs text-gray-500 mb-2">零值</div>
|
||||
<MaxContextCount maxContext={0} />
|
||||
</div>
|
||||
|
||||
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded text-center">
|
||||
<div className="text-xs text-gray-500 mb-2">临界值 99</div>
|
||||
<MaxContextCount maxContext={99} />
|
||||
</div>
|
||||
|
||||
<div className="p-3 border border-gray-200 dark:border-gray-700 rounded text-center">
|
||||
<div className="text-xs text-gray-500 mb-2">临界值 100</div>
|
||||
<MaxContextCount maxContext={100} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 深色主题下的表现
|
||||
export const DarkTheme: Story = {
|
||||
parameters: {
|
||||
backgrounds: { default: 'dark' }
|
||||
},
|
||||
render: () => (
|
||||
<div className="space-y-4 p-4 bg-gray-900 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-white">深色主题下的表现</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-4 text-gray-300">
|
||||
<span className="text-sm">普通数字:</span>
|
||||
<MaxContextCount maxContext={30} />
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-gray-300">
|
||||
<span className="text-sm">无限符号:</span>
|
||||
<MaxContextCount maxContext={100} size={16} />
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-gray-300">
|
||||
<span className="text-sm">自定义颜色:</span>
|
||||
<MaxContextCount maxContext={100} size={18} style={{ color: '#60a5fa' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,399 @@
|
||||
import { Button } from '@heroui/react'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import ThinkingEffect from '../../../src/components/display/ThinkingEffect'
|
||||
|
||||
const meta: Meta<typeof ThinkingEffect> = {
|
||||
title: 'Display/ThinkingEffect',
|
||||
component: ThinkingEffect,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: '一个用于显示AI思考过程的动画组件,包含灯泡动画、思考内容滚动展示和展开收缩功能。'
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
isThinking: {
|
||||
control: { type: 'boolean' },
|
||||
description: '是否正在思考,控制动画状态和内容显示'
|
||||
},
|
||||
thinkingTimeText: {
|
||||
control: { type: 'text' },
|
||||
description: '思考时间文本,显示在组件顶部'
|
||||
},
|
||||
content: {
|
||||
control: { type: 'text' },
|
||||
description: '思考内容,多行文本用换行符分隔,最后一行在思考时会被过滤'
|
||||
},
|
||||
expanded: {
|
||||
control: { type: 'boolean' },
|
||||
description: '是否展开状态,影响组件的显示样式'
|
||||
},
|
||||
className: {
|
||||
control: { type: 'text' },
|
||||
description: '自定义 CSS 类名'
|
||||
}
|
||||
},
|
||||
args: {
|
||||
isThinking: true,
|
||||
thinkingTimeText: '思考中...',
|
||||
content: `正在分析问题\n寻找最佳解决方案\n整理思路和逻辑\n准备回答`,
|
||||
expanded: false
|
||||
}
|
||||
} satisfies Meta<typeof ThinkingEffect>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// 默认思考状态
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
isThinking: true,
|
||||
thinkingTimeText: '思考中 2s',
|
||||
content: `正在分析用户的问题\n查找相关信息\n整理回答思路`,
|
||||
expanded: false
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-96">
|
||||
<ThinkingEffect {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 非思考状态(静止)
|
||||
export const NotThinking: Story = {
|
||||
args: {
|
||||
isThinking: false,
|
||||
thinkingTimeText: '思考完成',
|
||||
content: `已完成思考\n找到最佳答案\n准备响应`,
|
||||
expanded: false
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-96">
|
||||
<ThinkingEffect {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 展开状态
|
||||
export const Expanded: Story = {
|
||||
args: {
|
||||
isThinking: false,
|
||||
thinkingTimeText: '思考用时 5s',
|
||||
content: `第一步:理解问题本质\n第二步:分析可能的解决方案\n第三步:评估各方案的优缺点\n第四步:选择最优方案\n第五步:构建详细回答`,
|
||||
expanded: true
|
||||
},
|
||||
render: (args) => (
|
||||
<div className="w-96">
|
||||
<ThinkingEffect {...args} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 交互式演示
|
||||
export const Interactive: Story = {
|
||||
render: function Render() {
|
||||
const [isThinking, setIsThinking] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [thinkingTime, setThinkingTime] = useState(0)
|
||||
|
||||
const thinkingSteps = useMemo(() => {
|
||||
return [
|
||||
'开始分析问题...',
|
||||
'查找相关资料和信息',
|
||||
'对比不同的解决方案',
|
||||
'评估方案的可行性',
|
||||
'选择最佳解决路径',
|
||||
'构建完整的回答框架',
|
||||
'检查逻辑的连贯性',
|
||||
'优化回答的表达方式'
|
||||
]
|
||||
}, [])
|
||||
|
||||
const [content, setContent] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout
|
||||
if (isThinking) {
|
||||
setThinkingTime(0)
|
||||
setContent(thinkingSteps[0])
|
||||
|
||||
interval = setInterval(() => {
|
||||
setThinkingTime((prev) => {
|
||||
const newTime = prev + 1
|
||||
const stepIndex = Math.min(Math.floor(newTime / 2), thinkingSteps.length - 1)
|
||||
const currentSteps = thinkingSteps.slice(0, stepIndex + 1)
|
||||
setContent(currentSteps.join('\n'))
|
||||
return newTime
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval)
|
||||
}
|
||||
}, [isThinking, thinkingSteps])
|
||||
|
||||
const handleStartThinking = () => {
|
||||
setIsThinking(true)
|
||||
setExpanded(false)
|
||||
}
|
||||
|
||||
const handleStopThinking = () => {
|
||||
setIsThinking(false)
|
||||
}
|
||||
|
||||
const handleToggleExpanded = () => {
|
||||
setExpanded(!expanded)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-96 space-y-4">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button size="sm" color="primary" onClick={handleStartThinking} disabled={isThinking}>
|
||||
开始思考
|
||||
</Button>
|
||||
<Button size="sm" color="secondary" onClick={handleStopThinking} disabled={!isThinking}>
|
||||
停止思考
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={handleToggleExpanded}>
|
||||
{expanded ? '收起' : '展开'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ThinkingEffect
|
||||
isThinking={isThinking}
|
||||
thinkingTimeText={isThinking ? `思考中 ${thinkingTime}s` : `思考完成 ${thinkingTime}s`}
|
||||
content={content}
|
||||
expanded={expanded}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 不同内容长度
|
||||
export const DifferentContentLength: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-6 w-96">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">短内容</h3>
|
||||
<ThinkingEffect isThinking thinkingTimeText="思考中 1s" content={`分析问题\n寻找答案`} expanded={false} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">中等长度内容</h3>
|
||||
<ThinkingEffect
|
||||
isThinking
|
||||
thinkingTimeText="思考中 3s"
|
||||
content={`第一步:理解问题\n第二步:分析背景\n第三步:寻找解决方案\n第四步:验证方案可行性\n第五步:准备详细回答`}
|
||||
expanded={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">长内容</h3>
|
||||
<ThinkingEffect
|
||||
isThinking
|
||||
thinkingTimeText="思考中 8s"
|
||||
content={`开始分析用户提出的复杂问题\n识别问题的核心要素和关键词\n搜索相关的知识领域和概念\n整理可能的解决思路和方法\n评估不同方案的优缺点\n考虑实际应用中的限制条件\n构建逻辑清晰的回答框架\n检查答案的完整性和准确性\n优化语言表达的清晰度`}
|
||||
expanded={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 不同的思考时间文本
|
||||
export const DifferentThinkingTime: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4 w-96">
|
||||
<ThinkingEffect
|
||||
isThinking
|
||||
thinkingTimeText="思考中..."
|
||||
content={`正在处理问题\n分析可能的答案`}
|
||||
expanded={false}
|
||||
/>
|
||||
|
||||
<ThinkingEffect
|
||||
isThinking
|
||||
thinkingTimeText="深度思考中 10s"
|
||||
content={`进行复杂分析\n考虑多种可能性`}
|
||||
expanded={false}
|
||||
/>
|
||||
|
||||
<ThinkingEffect
|
||||
isThinking={false}
|
||||
thinkingTimeText="🎯 思考完成 (用时 15s)"
|
||||
content={`问题分析完毕\n答案已准备就绪`}
|
||||
expanded={false}
|
||||
/>
|
||||
|
||||
<ThinkingEffect
|
||||
isThinking={false}
|
||||
thinkingTimeText={
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-600 text-xs">✓</span>
|
||||
<span>思考完成</span>
|
||||
</div>
|
||||
}
|
||||
content={`成功找到解决方案\n可以开始回答`}
|
||||
expanded={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 空内容状态
|
||||
export const EmptyContent: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4 w-96">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">无内容 - 思考中</h3>
|
||||
<ThinkingEffect isThinking thinkingTimeText="准备开始思考..." content="" expanded={false} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">无内容 - 停止思考</h3>
|
||||
<ThinkingEffect isThinking={false} thinkingTimeText="等待输入" content="" expanded={false} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 实时内容更新演示
|
||||
export const RealTimeUpdate: Story = {
|
||||
render: function Render() {
|
||||
const [content, setContent] = useState('')
|
||||
const [isThinking, setIsThinking] = useState(false)
|
||||
const [step, setStep] = useState(0)
|
||||
|
||||
const steps = useMemo(() => {
|
||||
return [
|
||||
'开始分析问题的复杂性...',
|
||||
'识别关键信息和要求',
|
||||
'搜索相关的知识点',
|
||||
'整理可能的解决思路',
|
||||
'评估不同方案的优缺点',
|
||||
'选择最优的解决方案',
|
||||
'构建详细的回答框架',
|
||||
'检查逻辑的连贯性',
|
||||
'优化表达的清晰度',
|
||||
'完成最终答案的准备'
|
||||
]
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isThinking && step < steps.length) {
|
||||
const timer = setTimeout(() => {
|
||||
const newContent = steps.slice(0, step + 1).join('\n')
|
||||
setContent(newContent)
|
||||
setStep((prev) => prev + 1)
|
||||
}, 1500)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
} else if (step >= steps.length) {
|
||||
setIsThinking(false)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}, [isThinking, step, steps])
|
||||
|
||||
const handleStart = () => {
|
||||
setIsThinking(true)
|
||||
setStep(0)
|
||||
setContent('')
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setIsThinking(false)
|
||||
setStep(0)
|
||||
setContent('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-96 space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" color="primary" onClick={handleStart} disabled={isThinking}>
|
||||
开始实时思考
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ThinkingEffect
|
||||
isThinking={isThinking}
|
||||
thinkingTimeText={isThinking ? `思考中... 步骤 ${step}/${steps.length}` : '思考完成'}
|
||||
content={content}
|
||||
expanded={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义样式
|
||||
export const CustomStyles: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4 w-96">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">自定义边框和背景</h3>
|
||||
<ThinkingEffect
|
||||
isThinking={true}
|
||||
thinkingTimeText="自定义样式思考中..."
|
||||
content="应用自定义样式\n测试视觉效果"
|
||||
expanded={false}
|
||||
className="border-blue-300 bg-blue-50 dark:bg-blue-950"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">圆角和阴影</h3>
|
||||
<ThinkingEffect
|
||||
isThinking={false}
|
||||
thinkingTimeText="思考完成"
|
||||
content="圆角和阴影效果\n增强视觉体验"
|
||||
expanded={false}
|
||||
className="rounded-2xl shadow-lg border-purple-300 bg-purple-50 dark:bg-purple-950"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 错误和边界情况
|
||||
export const EdgeCases: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4 w-96">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">单行内容</h3>
|
||||
<ThinkingEffect isThinking thinkingTimeText="思考中..." content="只有一行内容" expanded={false} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">超长单行</h3>
|
||||
<ThinkingEffect
|
||||
isThinking
|
||||
thinkingTimeText="处理长文本..."
|
||||
content={`这是一行非常长的文本内容,用于测试组件在处理超长单行文本时的表现,看看是否能正确处理文本溢出和省略`}
|
||||
expanded={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">特殊字符</h3>
|
||||
<ThinkingEffect
|
||||
isThinking
|
||||
thinkingTimeText="特殊字符测试"
|
||||
content={`包含特殊字符: @#$%^&*()_+\n中文、English、数字123\n换行\t制表符测试`}
|
||||
expanded={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -13,7 +13,7 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"outDir": "./dist",
|
||||
"resolveJsonModule": true,
|
||||
"rootDir": "./src",
|
||||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "ES2020"
|
||||
|
||||
@ -12,9 +12,6 @@ export default defineConfig({
|
||||
clean: true,
|
||||
dts: true,
|
||||
tsconfig: 'tsconfig.json',
|
||||
alias: {
|
||||
'@shared': '../shared'
|
||||
},
|
||||
// 将 HeroUI、Tailwind 和其他 peer dependencies 标记为外部依赖
|
||||
external: [
|
||||
'react',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user