@
chore: delete old layout/common/tabs components before redesign @
This commit is contained in:
5658
frontend/package-lock.json
generated
5658
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TLogoProps {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const TLogo: React.FC<TLogoProps> = ({ size = 40 }) => (
|
||||
<img
|
||||
src="/logo/t_mobile_logo_transparent.png"
|
||||
alt="T-Systems"
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ objectFit: 'contain' }}
|
||||
/>
|
||||
);
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '../../contexts';
|
||||
|
||||
export const TPattern: React.FC = () => {
|
||||
const { theme, isDark } = useTheme();
|
||||
const patternOpacity = isDark ? 0.03 : 0.04;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: 300,
|
||||
height: 300,
|
||||
opacity: patternOpacity,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<svg width="300" height="300" viewBox="0 0 300 300">
|
||||
<defs>
|
||||
<pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
|
||||
<path d="M 30 0 L 0 0 0 30" fill="none" stroke={theme.accent} strokeWidth="1"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="300" height="300" fill="url(#grid)"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Moon, Sun, SunMedium } from 'lucide-react';
|
||||
|
||||
import { useTheme } from '../../contexts';
|
||||
import { Button } from '../shadcn/ui/button';
|
||||
|
||||
const NEXT_LABELS: Record<string, string> = {
|
||||
dark: '过渡色模式',
|
||||
dim: '亮色模式',
|
||||
light: '暗色模式',
|
||||
};
|
||||
|
||||
export const ThemeToggle: React.FC = () => {
|
||||
const { themeMode, toggleTheme } = useTheme();
|
||||
|
||||
// Shows the NEXT state's icon: dark→SunMedium(dim next), dim→Sun(light next), light→Moon(dark next)
|
||||
const Icon =
|
||||
themeMode === 'dark' ? SunMedium :
|
||||
themeMode === 'dim' ? Sun :
|
||||
Moon;
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={toggleTheme}
|
||||
variant="outline"
|
||||
size="icon-lg"
|
||||
className="rounded-xl border-border bg-card text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
aria-label={`切换到${NEXT_LABELS[themeMode]}`}
|
||||
title={`切换到${NEXT_LABELS[themeMode]}`}
|
||||
>
|
||||
<Icon />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import type { AppTabConfig } from '../../router/tabs';
|
||||
import { shellFrameClassName } from './shell-config';
|
||||
|
||||
interface ContentLayoutProps {
|
||||
children: ReactNode;
|
||||
tab: AppTabConfig;
|
||||
}
|
||||
|
||||
const widthClassMap = {
|
||||
default: 'mx-auto w-full max-w-[1120px]',
|
||||
wide: 'mx-auto w-full max-w-[1440px]',
|
||||
full: 'w-full',
|
||||
} as const;
|
||||
|
||||
export function ContentLayout({ children, tab }: ContentLayoutProps) {
|
||||
const widthClass = widthClassMap[tab.contentWidth];
|
||||
|
||||
return (
|
||||
<main className="flex min-h-0 flex-1 bg-t-bg">
|
||||
<div
|
||||
className={[
|
||||
shellFrameClassName,
|
||||
'relative flex min-h-0 flex-1 justify-center py-8',
|
||||
].join(' ')}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
'relative flex min-h-0 w-full',
|
||||
widthClass,
|
||||
tab.fillHeight ? 'overflow-hidden' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Badge } from '../shadcn/ui/badge';
|
||||
import { Separator } from '../shadcn/ui/separator';
|
||||
|
||||
import { shellFrameClassName, shellMeta } from './shell-config';
|
||||
|
||||
export function FooterLayout() {
|
||||
return (
|
||||
<footer className="border-t border-t-border bg-t-bg">
|
||||
<div
|
||||
className={[
|
||||
shellFrameClassName,
|
||||
'flex items-center justify-between gap-6 py-4 text-xs text-t-text3',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="min-w-0 max-w-[360px]">
|
||||
<div className="mono mb-1 tracking-[0.18em] text-t-text2">
|
||||
{shellMeta.productLabel}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-3 whitespace-nowrap rounded-xl border border-border bg-card px-3 py-2 shadow-sm">
|
||||
<Badge variant="secondary" className="mono border-0 bg-transparent px-0 py-0 text-[11px] tracking-[0.24em] text-muted-foreground">
|
||||
{shellMeta.version}
|
||||
</Badge>
|
||||
<Separator orientation="vertical" className="h-4 bg-border" />
|
||||
<Badge variant="outline" className="mono gap-2 border-0 bg-transparent px-0 py-0 text-[11px] tracking-[0.24em] text-[var(--t-green)]">
|
||||
<span className="size-2 rounded-full bg-[var(--t-green)]" />
|
||||
{shellMeta.status}
|
||||
</Badge>
|
||||
<Separator orientation="vertical" className="h-4 bg-border" />
|
||||
<span className="mono text-[11px] tracking-[0.18em] text-muted-foreground">
|
||||
{shellMeta.surface}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { TLogo } from '../common/TLogo';
|
||||
|
||||
export function HeaderBrand() {
|
||||
return (
|
||||
<div className="flex min-w-[280px] shrink-0 items-center gap-4 whitespace-nowrap">
|
||||
<div className="shrink-0">
|
||||
<TLogo size={46} />
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2 whitespace-nowrap">
|
||||
<span className="text-[1.18rem] font-semibold tracking-[-0.04em] text-foreground">
|
||||
T-Systems
|
||||
</span>
|
||||
<span className="text-[1.02rem] font-light text-muted-foreground">
|
||||
Regulation
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { AppTabConfig } from '../../router/tabs';
|
||||
import { ThemeToggle } from '../common/ThemeToggle';
|
||||
import { Badge } from '../shadcn/ui/badge';
|
||||
import { Separator } from '../shadcn/ui/separator';
|
||||
|
||||
import { HeaderBrand } from './HeaderBrand';
|
||||
import { shellFrameClassName, shellMeta } from './shell-config';
|
||||
import { TabNav } from './TabNav';
|
||||
|
||||
interface HeaderLayoutProps {
|
||||
activeTab: AppTabConfig;
|
||||
}
|
||||
|
||||
export function HeaderLayout({ activeTab }: HeaderLayoutProps) {
|
||||
return (
|
||||
<header className="sticky top-0 z-[100] border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80">
|
||||
<div className={[shellFrameClassName, 'flex h-20 items-center gap-8'].join(' ')}>
|
||||
<HeaderBrand />
|
||||
<div className="min-w-0 flex-1 self-stretch overflow-hidden">
|
||||
<TabNav activeTab={activeTab} />
|
||||
</div>
|
||||
<div className="ml-auto flex shrink-0 items-center gap-3 self-center">
|
||||
<ThemeToggle />
|
||||
<div className="flex h-11 shrink-0 items-center gap-3 whitespace-nowrap rounded-xl border border-border bg-card px-3 shadow-sm">
|
||||
<Badge variant="secondary" className="mono border-0 bg-transparent px-0 py-0 text-[11px] tracking-[0.24em] text-muted-foreground">
|
||||
{shellMeta.version}
|
||||
</Badge>
|
||||
<Separator orientation="vertical" className="h-4 bg-border" />
|
||||
<Badge variant="outline" className="mono gap-2 border-0 bg-transparent px-0 py-0 text-[11px] tracking-[0.24em] text-[var(--t-green)]">
|
||||
<span className="size-2 rounded-full bg-[var(--t-green)]" />
|
||||
{shellMeta.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { appTabs, type AppTabConfig } from '../../router/tabs';
|
||||
|
||||
interface KeepAliveViewportProps {
|
||||
activeTab: AppTabConfig;
|
||||
}
|
||||
|
||||
export function KeepAliveViewport({ activeTab }: KeepAliveViewportProps) {
|
||||
const [mountedTabIds, setMountedTabIds] = useState<string[]>([activeTab.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const timerId = window.setTimeout(() => {
|
||||
setMountedTabIds((prev) => (prev.includes(activeTab.id) ? prev : [...prev, activeTab.id]));
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timerId);
|
||||
}, [activeTab.id]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1">
|
||||
{appTabs.map((tab) => {
|
||||
const shouldRender = tab.keepAlive ? mountedTabIds.includes(tab.id) : tab.id === activeTab.id;
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const TabComponent = tab.component;
|
||||
const isActive = tab.id === activeTab.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tab.id}
|
||||
aria-hidden={!isActive}
|
||||
className={[
|
||||
'min-h-0 flex-1',
|
||||
isActive ? 'flex' : 'hidden',
|
||||
].join(' ')}
|
||||
>
|
||||
<TabComponent />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import type { AppTabConfig, TabId } from '../../router/tabs';
|
||||
import { appTabs } from '../../router/tabs';
|
||||
|
||||
interface TabNavProps {
|
||||
activeTab: AppTabConfig;
|
||||
}
|
||||
|
||||
interface IndicatorStyle {
|
||||
opacity: number;
|
||||
transform: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const reducedMotionQuery = '(prefers-reduced-motion: reduce)';
|
||||
|
||||
export function TabNav({ activeTab }: TabNavProps) {
|
||||
const navigate = useNavigate();
|
||||
const trackRef = useRef<HTMLDivElement | null>(null);
|
||||
const buttonRefs = useRef<Record<TabId, HTMLButtonElement | null>>({
|
||||
perception: null,
|
||||
docs: null,
|
||||
compliance: null,
|
||||
status: null,
|
||||
rag: null,
|
||||
});
|
||||
const [indicatorStyle, setIndicatorStyle] = useState<IndicatorStyle>({
|
||||
opacity: 0,
|
||||
transform: 'translateX(0px)',
|
||||
width: 0,
|
||||
});
|
||||
const [reducedMotion, setReducedMotion] = useState(false);
|
||||
|
||||
const handleValueChange = (value: string) => {
|
||||
const nextTab = appTabs.find((tab) => tab.id === value);
|
||||
if (nextTab && nextTab.path !== activeTab.path) {
|
||||
navigate(nextTab.path);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia(reducedMotionQuery);
|
||||
const updateMotionPreference = () => {
|
||||
setReducedMotion(mediaQuery.matches);
|
||||
};
|
||||
|
||||
updateMotionPreference();
|
||||
mediaQuery.addEventListener('change', updateMotionPreference);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', updateMotionPreference);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const updateIndicator = () => {
|
||||
const trackNode = trackRef.current;
|
||||
const activeNode = buttonRefs.current[activeTab.id];
|
||||
if (!trackNode || !activeNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trackRect = trackNode.getBoundingClientRect();
|
||||
const activeRect = activeNode.getBoundingClientRect();
|
||||
|
||||
setIndicatorStyle({
|
||||
opacity: 1,
|
||||
transform: `translateX(${activeRect.left - trackRect.left}px)`,
|
||||
width: activeRect.width,
|
||||
});
|
||||
};
|
||||
|
||||
updateIndicator();
|
||||
window.addEventListener('resize', updateIndicator);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateIndicator);
|
||||
};
|
||||
}, [activeTab.id]);
|
||||
|
||||
return (
|
||||
<nav className="flex h-full min-w-0 items-stretch overflow-x-auto overflow-y-hidden">
|
||||
<div
|
||||
ref={trackRef}
|
||||
className="relative flex h-full min-w-max flex-nowrap items-stretch gap-3 pr-6"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={[
|
||||
'pointer-events-none absolute bottom-0 left-0 h-0.5 rounded-full bg-primary',
|
||||
reducedMotion
|
||||
? 'transition-none'
|
||||
: 'transition-[transform,width,opacity] duration-220 ease-[cubic-bezier(0.22,1,0.36,1)]',
|
||||
].join(' ')}
|
||||
style={indicatorStyle}
|
||||
/>
|
||||
{appTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
ref={(node) => {
|
||||
buttonRefs.current[tab.id] = node;
|
||||
}}
|
||||
data-shell-tab="true"
|
||||
type="button"
|
||||
onClick={() => handleValueChange(tab.id satisfies TabId)}
|
||||
aria-current={tab.id === activeTab.id ? 'page' : undefined}
|
||||
className={[
|
||||
'inline-flex h-full shrink-0 appearance-none items-center justify-center border-0 border-b-2 border-transparent bg-transparent px-5 pt-1 text-[0.95rem] font-medium tracking-[0.02em] outline-none',
|
||||
reducedMotion
|
||||
? 'transition-none'
|
||||
: 'transition-[color,opacity] duration-200 ease-out',
|
||||
tab.id === activeTab.id
|
||||
? 'text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
].join(' ')}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
import { CompliancePage } from '../pages/Compliance';
|
||||
import { DocsPage } from '../pages/Docs';
|
||||
import { PerceptionPage } from '../pages/Perception';
|
||||
import { RagChatPage } from '../pages/RagChat';
|
||||
import { StatusPage } from '../pages/Status';
|
||||
|
||||
export type TabId = 'perception' | 'docs' | 'compliance' | 'status' | 'rag';
|
||||
|
||||
export type ContentWidth = 'default' | 'wide' | 'full';
|
||||
|
||||
export interface AppTabConfig {
|
||||
id: TabId;
|
||||
path: string;
|
||||
label: string;
|
||||
component: ComponentType;
|
||||
keepAlive: boolean;
|
||||
contentWidth: ContentWidth;
|
||||
fillHeight?: boolean;
|
||||
}
|
||||
|
||||
export const appTabs: AppTabConfig[] = [
|
||||
{
|
||||
id: 'perception',
|
||||
path: '/perception',
|
||||
label: '智能感知',
|
||||
component: PerceptionPage,
|
||||
keepAlive: true,
|
||||
contentWidth: 'wide',
|
||||
fillHeight: true,
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
path: '/docs',
|
||||
label: '文档管理',
|
||||
component: DocsPage,
|
||||
keepAlive: true,
|
||||
contentWidth: 'default',
|
||||
},
|
||||
{
|
||||
id: 'compliance',
|
||||
path: '/compliance',
|
||||
label: '合规分析',
|
||||
component: CompliancePage,
|
||||
keepAlive: true,
|
||||
contentWidth: 'wide',
|
||||
fillHeight: true,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
path: '/status',
|
||||
label: '系统状态',
|
||||
component: StatusPage,
|
||||
keepAlive: true,
|
||||
contentWidth: 'default',
|
||||
},
|
||||
{
|
||||
id: 'rag',
|
||||
path: '/rag',
|
||||
label: '法规对话',
|
||||
component: RagChatPage,
|
||||
keepAlive: true,
|
||||
contentWidth: 'wide',
|
||||
fillHeight: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultTab = appTabs.find((tab) => tab.id === 'compliance') ?? appTabs[0];
|
||||
|
||||
export function getTabByPath(pathname: string): AppTabConfig {
|
||||
return appTabs.find((tab) => tab.path === pathname) ?? defaultTab;
|
||||
}
|
||||
@@ -411,7 +411,6 @@ body {
|
||||
.docs-card { display: flex; flex-direction: column; gap: 0; }
|
||||
.doc-row { display: flex; gap: 10px; padding: 10px 0; border-top: 1px solid var(--border); }
|
||||
.doc-score { font-size: 11px; font-weight: 700; font-family: var(--font-mono); color: var(--success); width: 34px; flex-shrink: 0; padding-top: 1px; }
|
||||
.doc-info {}
|
||||
.doc-name { font-size: 12px; font-weight: 600; margin-bottom: 3px; }
|
||||
.doc-clause { font-size: 10px; font-family: var(--font-mono); color: var(--muted); margin-left: 5px; }
|
||||
.doc-snippet { font-size: 11px; color: var(--muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
|
||||
Reference in New Issue
Block a user