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

@@ -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>
);
};