feat: implement new layout components and routing structure

- Added HeaderLayout component for the application header.
- Introduced KeepAliveViewport for managing tab states and rendering.
- Created TabNav for tab navigation with animated indicator.
- Removed old Tabs component in favor of new layout structure.
- Updated routing with AppRouter and defined appTabs for navigation.
- Enhanced theme context to manage dark mode styles.
- Added new UI components: Badge, Button, Separator, and Tabs.
- Refactored pages to utilize new layout components and improve responsiveness.
- Updated global styles for better theming and layout consistency.
- Introduced TypeScript path aliases for cleaner imports.
This commit is contained in:
ash66
2026-05-25 16:19:18 +08:00
parent 10a034e294
commit 987cc097da
43 changed files with 5099 additions and 265 deletions

View File

@@ -587,7 +587,7 @@ export const CompliancePage: React.FC = () => {
flex: 1,
display: 'flex',
height: '100%',
minHeight: 'calc(100vh - 128px)',
minHeight: 0,
position: 'relative',
}}>
{/* Main Content Area */}

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { useTheme } from '../../contexts';
import { Content } from '../../components/layout/Content';
import { TPattern } from '../../components/common/TPattern';
import { getDocumentList, getDocumentStatus, searchRegulations, uploadDocument, deleteDocument, retryDocument, type RegulationSearchItem } from '../../api/docs';
import type { Doc } from '../../types';
@@ -40,6 +39,7 @@ export const DocsPage: React.FC = () => {
const [searchResults, setSearchResults] = useState<RegulationSearchItem[]>([]);
const [searchLoading, setSearchLoading] = useState(false);
const [searchError, setSearchError] = useState('');
const [batchQueueLength, setBatchQueueLength] = useState(0);
// Upload metadata
const [regulationType, setRegulationType] = useState('');
@@ -48,12 +48,17 @@ export const DocsPage: React.FC = () => {
// Batch queue: files waiting to be uploaded after the current one finishes
const batchQueueRef = useRef<File[]>([]);
const setBatchQueue = (files: File[]) => {
batchQueueRef.current = files;
setBatchQueueLength(files.length);
};
async function loadDocuments() {
setLoading(true);
try {
const response = await getDocumentList();
const apiDocs: Doc[] = response.docs.map((doc) => ({
id: parseInt(String(doc.id).replace('doc-', ''), 10) || Math.floor(Math.random() * 10000),
const apiDocs: Doc[] = response.docs.map((doc, index) => ({
id: Number.parseInt(String(doc.id).replace('doc-', ''), 10) || -(index + 1),
name: doc.name,
chunks: doc.chunks,
size: doc.updated_at ? new Date(doc.updated_at).toLocaleString() : 'Indexed document',
@@ -209,6 +214,7 @@ export const DocsPage: React.FC = () => {
// Process next file in batch queue
const next = batchQueueRef.current.shift();
setBatchQueueLength(batchQueueRef.current.length);
if (next) {
const nextRunId = pipelineRunIdRef.current + 1;
pipelineRunIdRef.current = nextRunId;
@@ -222,7 +228,7 @@ export const DocsPage: React.FC = () => {
if (files.length === 0 || uploading) return;
const [first, ...rest] = files;
batchQueueRef.current = rest;
setBatchQueue(rest);
const runId = pipelineRunIdRef.current + 1;
pipelineRunIdRef.current = runId;
@@ -262,7 +268,7 @@ export const DocsPage: React.FC = () => {
if (files.length === 0 || uploading) return;
const [first, ...rest] = files;
batchQueueRef.current = rest;
setBatchQueue(rest);
const runId = pipelineRunIdRef.current + 1;
pipelineRunIdRef.current = runId;
void uploadSingleFile(first, runId);
@@ -282,8 +288,7 @@ export const DocsPage: React.FC = () => {
const getPipelineHint = () => {
if (pipelineStatus === 'running') {
const queueLen = batchQueueRef.current.length;
const suffix = queueLen > 0 ? ` (+${queueLen} 待上传)` : '';
const suffix = batchQueueLength > 0 ? ` (+${batchQueueLength} 待上传)` : '';
return `${activeStep >= 0 ? PIPELINE_STEPS[activeStep].name : 'LOAD'} · ${uploadFileName}${suffix}`;
}
if (pipelineStatus === 'completed') return 'PIPELINE COMPLETE';
@@ -291,6 +296,11 @@ export const DocsPage: React.FC = () => {
return 'WAITING FOR UPLOAD';
};
const getDocKey = (doc: Doc) => {
// Prefer the backend document identifier because the numeric display id is not guaranteed unique.
return doc.docId ?? `local-${doc.id}-${doc.name}`;
};
const inputStyle: React.CSSProperties = {
padding: '8px 12px',
fontSize: 13,
@@ -302,7 +312,7 @@ export const DocsPage: React.FC = () => {
};
return (
<Content>
<div className="relative w-full">
<TPattern />
<section style={{ marginBottom: 56 }}>
@@ -432,7 +442,7 @@ export const DocsPage: React.FC = () => {
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{docs.map((doc) => (
<div
key={doc.id}
key={getDocKey(doc)}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: 20, background: theme.bgCard, borderRadius: 12, border: `1px solid ${doc.status === 'parsing' ? theme.accent : theme.border}`, transition: 'all 0.2s ease', boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none' }}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 16 }}>
@@ -543,6 +553,6 @@ export const DocsPage: React.FC = () => {
</div>
</div>
</section>
</Content>
</div>
);
};

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTheme } from '../../contexts';
import { Content } from '../../components/layout/Content';
import { TPattern } from '../../components/common/TPattern';
import {
listEvents,
@@ -53,7 +52,10 @@ export const PerceptionPage: React.FC = () => {
}
}, [filterSource, filterImpact]);
useEffect(() => { void loadFeed(); }, [loadFeed]);
useEffect(() => {
const timerId = window.setTimeout(() => { void loadFeed(); }, 0);
return () => window.clearTimeout(timerId);
}, [loadFeed]);
// When selecting a new event, clear previous analysis
const handleSelectEvent = (id: string) => {
@@ -96,7 +98,7 @@ export const PerceptionPage: React.FC = () => {
};
return (
<Content wide>
<div className="relative flex min-h-0 flex-1 flex-col">
<style>{`
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
`}</style>
@@ -113,7 +115,7 @@ export const PerceptionPage: React.FC = () => {
display: 'grid',
gridTemplateColumns: '400px 1fr',
gap: 24,
height: 'calc(100vh - 220px)',
flex: 1,
minHeight: 560,
}}>
{/* Left: Event feed */}
@@ -139,6 +141,6 @@ export const PerceptionPage: React.FC = () => {
onAbort={handleAbort}
/>
</div>
</Content>
</div>
);
};

View File

@@ -133,7 +133,6 @@ export const RagChatPage: React.FC = () => {
sessionId,
abortRef.current.signal,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterRegulationType, sessionId]);
const sendMessage = (text: string) => {
@@ -173,7 +172,7 @@ export const RagChatPage: React.FC = () => {
};
return (
<div style={{ flex: 1, display: 'flex', height: 'calc(100vh - 128px)' }}>
<div style={{ flex: 1, display: 'flex', minHeight: 0, height: '100%' }}>
{/* ── Left: chat panel ─────────────────────────────────── */}
<div style={{
flex: '0 0 60%',

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useTheme } from '../../contexts';
import { Content } from '../../components/layout/Content';
import { TPattern } from '../../components/common/TPattern';
import { getSystemStats, getSystemConfig, getSystemHealth, type SystemStats, type SystemConfig, type SystemHealth } from '../../api/status';
import { getDocumentList, type DocInfo } from '../../api/docs';
@@ -107,7 +106,8 @@ export const StatusPage: React.FC = () => {
// Initial load
useEffect(() => {
void loadData();
const timerId = window.setTimeout(() => { void loadData(); }, 0);
return () => window.clearTimeout(timerId);
}, [loadData]);
// Auto-poll every 5 s while any document is still processing
@@ -119,7 +119,7 @@ export const StatusPage: React.FC = () => {
}, [docs, loadData]);
return (
<Content>
<div className="relative w-full">
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
<TPattern />
@@ -386,6 +386,6 @@ export const StatusPage: React.FC = () => {
</div>
))}
</section>
</Content>
</div>
);
};