refactor: 将 TextStatistics 页面和 TextInputArea 组件全面迁移至 shadcn 样式并优化布局
This commit is contained in:
@@ -220,7 +220,6 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('w-full flex flex-col gap-1.5', className)}>
|
<div className={cn('w-full flex flex-col gap-1.5', className)}>
|
||||||
{/* 顶部工具栏 */}
|
|
||||||
{hasTopBar && (
|
{hasTopBar && (
|
||||||
<div className="flex items-center justify-between px-0.5">
|
<div className="flex items-center justify-between px-0.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -247,10 +246,9 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 核心卡片容器:完美适配 shadcn 风格的多行包裹框 */}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md border border-input bg-background shadow-sm transition-all focus-within:ring-1 focus-within:ring-ring focus-within:border-input',
|
'rounded-md border border-input bg-background shadow-sm transition-all focus-within:ring-1 focus-within:ring-ring focus-within:border-input overflow-hidden',
|
||||||
displayError &&
|
displayError &&
|
||||||
'border-destructive focus-within:ring-destructive focus-within:border-destructive',
|
'border-destructive focus-within:ring-destructive focus-within:border-destructive',
|
||||||
)}
|
)}
|
||||||
@@ -264,14 +262,14 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
|||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className="w-full bg-transparent px-3 py-2.5 font-mono text-sm leading-relaxed text-foreground placeholder:text-muted-foreground focus:outline-none resize-none border-0 block"
|
className="w-full bg-transparent px-4 py-3 font-mono text-sm leading-relaxed text-foreground placeholder:text-muted-foreground/50 focus:outline-none resize-none border-0 block"
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 底部隔离式功能区:杜绝重叠和塌陷 */}
|
|
||||||
{hasBottomBar && (
|
{hasBottomBar && (
|
||||||
<div className="flex items-center justify-between px-2 py-1.5 bg-muted/20 border-t border-border/60">
|
<div className="flex h-10 items-center justify-between px-4 bg-muted/30 border-t border-border/50">
|
||||||
<div className="flex items-center gap-1.5">
|
{/* 左侧自定义动作 */}
|
||||||
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
{bottomActions.map((action) => (
|
{bottomActions.map((action) => (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
key={action.key}
|
key={action.key}
|
||||||
@@ -283,14 +281,13 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 ml-auto">
|
{/* 右侧系统按钮组 */}
|
||||||
|
<div className="flex items-center gap-1.5 ml-auto shrink-0">
|
||||||
{allowCopy && value && (
|
{allowCopy && value && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
aria-label={t('textInputArea.copyContent')}
|
className="p-1 h-7 w-7 flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-background/80 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
title={t('textInputArea.copyContent')}
|
|
||||||
className="p-1 h-7 w-7 flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -299,9 +296,7 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
aria-label={t('textInputArea.clear')}
|
className="p-1 h-7 w-7 flex items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
title={t('textInputArea.clear')}
|
|
||||||
className="p-1 h-7 w-7 flex items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
|
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -311,7 +306,7 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 错误提示区域 */}
|
{/* 错误提示 */}
|
||||||
{displayError && (
|
{displayError && (
|
||||||
<p className="text-xs font-medium text-destructive px-0.5 animate-in fade-in slide-in-from-top-1 duration-150">
|
<p className="text-xs font-medium text-destructive px-0.5 animate-in fade-in slide-in-from-top-1 duration-150">
|
||||||
{displayError}
|
{displayError}
|
||||||
|
|||||||
@@ -3,15 +3,10 @@ import { FileText } from 'lucide-react';
|
|||||||
import PageHeader from '@/components/PageHeader';
|
import PageHeader from '@/components/PageHeader';
|
||||||
import TextInputArea from '@/components/TextInputArea';
|
import TextInputArea from '@/components/TextInputArea';
|
||||||
import { formatByteSize, getTextStats } from '@/utils/textStatistics';
|
import { formatByteSize, getTextStats } from '@/utils/textStatistics';
|
||||||
import { textStatisticsPageStyles } from '@/config/pageTheme';
|
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
import { useContextMenuData } from '@/utils/useContextMenuData';
|
import { useContextMenuData } from '@/utils/useContextMenuData';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
/**
|
|
||||||
* 文本统计页面组件
|
|
||||||
*
|
|
||||||
* 提供实时的文本分析功能,包括字符数、单词数、行数和字节大小。
|
|
||||||
*/
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { t } = useLazyTranslation('textStatistics');
|
const { t } = useLazyTranslation('textStatistics');
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
@@ -22,8 +17,7 @@ export default function Index() {
|
|||||||
|
|
||||||
useContextMenuData({ featureKey: 'textStatistics', onData: handleContextMenuData });
|
useContextMenuData({ featureKey: 'textStatistics', onData: handleContextMenuData });
|
||||||
|
|
||||||
// 实时计算统计信息,使用 useMemo 优化性能
|
// 实时计算统计信息,由 useMemo 拦截非必要计算
|
||||||
// 对于 10,000 字符以上的文本,Intl.Segmenter 也能保持良好的性能
|
|
||||||
const stats = useMemo(() => getTextStats(text), [text]);
|
const stats = useMemo(() => getTextStats(text), [text]);
|
||||||
|
|
||||||
const statItems = [
|
const statItems = [
|
||||||
@@ -34,14 +28,13 @@ export default function Index() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="p-4 w-full space-y-4 animate-in fade-in duration-300">
|
||||||
<div className="p-2">
|
|
||||||
{/* 头部区域 */}
|
{/* 头部区域 */}
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t('textStatistics:pageTitle')}
|
title={t('textStatistics:pageTitle')}
|
||||||
subtitle={t('textStatistics:pageSubtitle')}
|
subtitle={t('textStatistics:pageSubtitle')}
|
||||||
icon={<FileText />}
|
icon={<FileText />}
|
||||||
iconColor={textStatisticsPageStyles.primaryColor}
|
iconColor="text-purple-500"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 文本输入区域 */}
|
{/* 文本输入区域 */}
|
||||||
@@ -49,35 +42,32 @@ export default function Index() {
|
|||||||
value={text}
|
value={text}
|
||||||
onChange={setText}
|
onChange={setText}
|
||||||
placeholder={t('textStatistics:placeholder')}
|
placeholder={t('textStatistics:placeholder')}
|
||||||
minRows={8}
|
minRows={10}
|
||||||
maxRows={15}
|
maxRows={18}
|
||||||
showClear={false}
|
showClear={true}
|
||||||
|
allowCopy={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 统计结果展示区域 */}
|
{/* 统计结果展示区域 */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
{statItems.map((item) => (
|
{statItems.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.label}
|
key={item.label}
|
||||||
className="p-2 text-center rounded-2xl border transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] min-h-[64px] md:min-h-[90px] flex flex-row md:flex-col items-center justify-between md:justify-center px-3 md:px-2 hover:-translate-y-0.5 hover:shadow-[0_4px_12px_rgba(156,39,176,0.15)] hover:border-purple-600"
|
className={cn(
|
||||||
style={{
|
'flex flex-col justify-center items-center p-4 text-center rounded-xl border border-border bg-card shadow-sm text-card-foreground',
|
||||||
backgroundColor: `${textStatisticsPageStyles.primaryColor}0a`,
|
'transition-all duration-200 ease-out',
|
||||||
borderColor: `${textStatisticsPageStyles.primaryColor}1a`,
|
'hover:-translate-y-0.5 hover:shadow-md hover:border-primary/50 focus-within:ring-1 focus-within:ring-ring',
|
||||||
}}
|
)}
|
||||||
>
|
>
|
||||||
<span className="text-muted-foreground whitespace-nowrap mb-0 md:mb-1">
|
<span className="text-xs font-medium text-muted-foreground tracking-wider mb-1 select-none">
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span className="font-mono text-lg md:text-2xl font-extrabold text-primary break-all tracking-tight leading-none tabular-nums select-all">
|
||||||
className="break-all font-semibold"
|
|
||||||
style={{ color: textStatisticsPageStyles.primaryColor }}
|
|
||||||
>
|
|
||||||
{item.value}
|
{item.value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user