refactor(json-tools): 优化 JsonDiffPanel 交互体验并增强 TextInputArea 灵活性
This commit is contained in:
@@ -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,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 () => {
|
||||
|
||||
Reference in New Issue
Block a user