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

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