refactor(json-tools): 优化 JsonDiffPanel 交互体验并增强 TextInputArea 灵活性
This commit is contained in:
@@ -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,8 +245,11 @@ 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',
|
||||||
|
!borderless &&
|
||||||
displayError &&
|
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,33 +32,48 @@ 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="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">
|
<span className="text-[11px] font-semibold text-foreground/90 shrink-0">{title}</span>
|
||||||
{title}
|
<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>
|
||||||
<span className="flex-1 truncate font-mono text-xs text-muted-foreground">{preview}</span>
|
<ChevronRight
|
||||||
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
|
className={cn(
|
||||||
|
'h-4 w-4 shrink-0 text-muted-foreground hover:text-foreground transition-transform duration-300',
|
||||||
|
!collapsed && 'rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div
|
<div
|
||||||
onClick={onToggleCollapse}
|
className={cn(
|
||||||
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"
|
'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
|
<TextInputArea
|
||||||
fill
|
fill
|
||||||
value={value}
|
value={value}
|
||||||
@@ -67,10 +82,10 @@ export default function JsonDiffPanel({
|
|||||||
externalError={error ?? undefined}
|
externalError={error ?? undefined}
|
||||||
showClear={true}
|
showClear={true}
|
||||||
allowCopy={true}
|
allowCopy={true}
|
||||||
|
borderless
|
||||||
className="min-h-0 flex-1"
|
className="min-h-0 flex-1"
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
</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 { 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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user