refactor(json-tools): 优化 JsonDiffPanel 交互体验并增强 TextInputArea 灵活性

This commit is contained in:
2026-07-13 23:31:06 +08:00
parent 72e3f9012f
commit 31e28b136f
4 changed files with 63 additions and 40 deletions
+8 -1
View File
@@ -48,6 +48,9 @@ export interface TextInputAreaProps extends Omit<
* 用于需要固定高度 + 内部滚动的卡片面板。
*/
fill?: boolean;
/** 是否无边框、无阴影、无圆角(适用于嵌套在其他有边框的容器中) */
borderless?: boolean;
}
function ActionButton({
@@ -113,6 +116,7 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
externalError,
onClear,
fill = false,
borderless = false,
...restProps
} = props;
@@ -241,8 +245,11 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
<div
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 overflow-hidden',
borderless
? 'bg-transparent overflow-hidden'
: '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',
fill && 'flex-1 min-h-0 flex flex-col',
!borderless &&
displayError &&
'border-destructive focus-within:ring-destructive focus-within:border-destructive',
)}
@@ -1,5 +1,5 @@
import React from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { ChevronRight } from 'lucide-react';
import TextInputArea from '@/components/TextInputArea';
import { cn } from '@/lib/utils';
@@ -10,7 +10,7 @@ export interface JsonDiffPanelProps extends Omit<React.HTMLAttributes<HTMLDivEle
value: string;
onChange: (value: string) => void;
error?: string | null;
/** 是否处于折叠态Accordion 收起) */
/** 是否处于折叠态 */
collapsed: boolean;
onToggleCollapse: () => void;
/** 折叠态展示的单行缩略预览文本 */
@@ -32,33 +32,48 @@ export default function JsonDiffPanel({
return (
<div
className={cn(
'flex flex-col overflow-hidden rounded-md border border-border bg-card',
collapsed ? 'h-[38px] shrink-0' : 'flex-1 min-h-[120px]',
'flex flex-col overflow-hidden rounded-md border bg-card transition-all duration-300 ease-in-out',
error
? 'border-destructive focus-within:ring-1 focus-within:ring-destructive focus-within:border-destructive'
: 'border-border focus-within:ring-1 focus-within:ring-ring focus-within:border-border/80',
className,
)}
style={{
flex: collapsed ? '0 0 36px' : '1 1 120px',
}}
{...props}
>
{collapsed ? (
<button
type="button"
onClick={onToggleCollapse}
className="flex h-[36px] w-full items-center gap-2 px-3 text-left select-none bg-muted/40 hover:bg-muted/70 transition-colors"
className={cn(
'flex h-[36px] w-full cursor-pointer select-none items-center justify-between bg-muted/20 hover:bg-muted/30 transition-colors px-3 shrink-0 text-left focus:outline-none focus:bg-muted/40',
!collapsed && 'border-b border-border/60',
)}
>
<span className="shrink-0 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/80">
{title}
<span className="text-[11px] font-semibold text-foreground/90 shrink-0">{title}</span>
<span
className={cn(
'flex-1 truncate font-mono text-xs text-muted-foreground/70 ml-3 transition-all duration-300',
collapsed ? 'opacity-100 max-w-full' : 'opacity-0 max-w-0 pointer-events-none',
)}
>
{preview}
</span>
<span className="flex-1 truncate font-mono text-xs text-muted-foreground">{preview}</span>
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
<ChevronRight
className={cn(
'h-4 w-4 shrink-0 text-muted-foreground hover:text-foreground transition-transform duration-300',
!collapsed && 'rotate-90',
)}
/>
</button>
) : (
<>
<div
onClick={onToggleCollapse}
className="flex h-[36px] cursor-pointer select-none items-center justify-between border-b border-border/60 bg-muted/20 hover:bg-muted/40 transition-colors px-3"
className={cn(
'flex-1 min-h-0 flex flex-col transition-opacity duration-300',
collapsed ? 'opacity-0 pointer-events-none' : 'opacity-100',
)}
>
<span className="text-[11px] font-semibold text-foreground/90">{title}</span>
<ChevronDown className="h-4 w-4 text-muted-foreground hover:text-foreground" />
</div>
<TextInputArea
fill
value={value}
@@ -67,10 +82,10 @@ export default function JsonDiffPanel({
externalError={error ?? undefined}
showClear={true}
allowCopy={true}
borderless
className="min-h-0 flex-1"
/>
</>
)}
</div>
</div>
);
}
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import type { DiffNode, DiffType } from '../types';
import { cn } from '@/lib/utils';
@@ -33,7 +33,8 @@ describe('JsonDiffPanel', () => {
/>,
);
expect(screen.getByText('abc(点击展开)')).toBeInTheDocument();
expect(screen.queryByPlaceholderText('x')).not.toBeInTheDocument();
const input = screen.getByPlaceholderText('x');
expect(input.closest('.opacity-0')).toBeInTheDocument();
});
it('点击标题可切换折叠', async () => {