前端架构
技术栈
| 层级 | 技术 | 版本 | 用途 |
|---|---|---|---|
| UI 框架 | React | 19 | 组件渲染 |
| 构建工具 | Vite | 6 | 开发 + 打包 |
| CSS | Tailwind CDN | 最新 | 样式(暗色 Bento Grid) |
| PWA | vite-plugin-pwa | 最新 | Service Worker |
| 路由 | React Router | v6 | SPA 路由 |
| 持久化 | IndexedDB | 原生 API | 大文件存储 |
目录结构
text
frontend/
├── index.html # HTML 入口(含 Tailwind CDN)
├── index.tsx # React 入口
├── App.tsx # 路由配置
├── MainApp.tsx # 主应用容器
├── pages/
│ ├── LoginPage.tsx
│ ├── ImageWorkspace.tsx
│ ├── ListingPage.tsx
│ ├── AllInOnePage.tsx
│ ├── ProductAnalysisPage.tsx
│ └── admin/
│ ├── UsersPage.tsx
│ ├── ApiPage.tsx
│ └── ActivationKeysPage.tsx
├── components/
│ ├── ImageGallery.tsx
│ ├── PromptEditor.tsx
│ ├── ModelSelector.tsx
│ ├── HistorySidebar.tsx
│ └── KnowledgeGraphViz.tsx
├── lib/
│ ├── api.ts # 后端 API 封装
│ ├── db.ts # IndexedDB 封装
│ ├── providers/ # 直连供应商客户端
│ │ ├── gemini.ts
│ │ └── openai.ts
│ └── graphrag.ts # GraphRAG 客户端逻辑
├── public/
│ ├── manifest.json # PWA 清单
│ └── icons/
└── vite.config.ts路由设计
typescript
// App.tsx
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<RequireAuth><MainApp /></RequireAuth>}>
<Route index element={<ImageWorkspace />} />
<Route path="listing" element={<ListingPage />} />
<Route path="all-in-one" element={<AllInOnePage />} />
<Route path="product-analysis" element={<ProductAnalysisPage />} />
</Route>
<Route path="/admin" element={<RequireAdmin><AdminLayout /></RequireAdmin>}>
<Route index element={<UsersPage />} />
<Route path="api" element={<ApiPage />} />
<Route path="model-configs" element={<ModelConfigsPage />} />
<Route path="activation-keys" element={<ActivationKeysPage />} />
</Route>
</Routes>PWA 配置
typescript
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'icons/*.png'],
manifest: {
name: '海域智舱',
short_name: 'Haiyu',
theme_color: '#0ea5e9',
background_color: '#0f172a',
display: 'standalone',
icons: [
{ src: 'icons/192.png', sizes: '192x192', type: 'image/png' },
{ src: 'icons/512.png', sizes: '512x512', type: 'image/png' }
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/cdn\.tailwindcss\.com/,
handler: 'CacheFirst',
options: { cacheName: 'tailwind-cache' }
}
]
}
})
],
server: {
port: 3000,
proxy: {
'/api': 'http://localhost:8788'
}
}
})API 封装层
typescript
// lib/api.ts
const BASE = '/api'
export async function api<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
credentials: 'include', // 携带 HttpOnly Cookie
headers: {
'Content-Type': 'application/json',
...(options.headers || {})
},
...options
})
if (!res.ok) {
throw new ApiError(res.status, await res.text())
}
return res.json()
}
// 业务封装
export const auth = {
login: (username: string, password: string) =>
api('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }),
me: () => api<User>('/me'),
logout: () => api('/auth/logout', { method: 'POST' })
}
export const generate = {
prepareImage: (prompt: string) =>
api<PrepareResult>('/generate/image/prepare', { method: 'POST', body: JSON.stringify({ prompt }) }),
completeImage: (ticket: string, status: 'success' | 'failed') =>
api('/generate/image/complete', { method: 'POST', body: JSON.stringify({ ticket, status }) }),
copy: (params: CopyParams) =>
api<CopyResult>('/generate/copy', { method: 'POST', body: JSON.stringify(params) })
}图片直连客户端
typescript
// lib/providers/gemini.ts
export async function generateImageGemini(
config: ProviderConfig,
prompt: string
): Promise<Blob> {
const url = `${config.base_url}/models/${config.model}:generateContent`
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': config.api_key
},
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { responseMimeType: 'image/png' }
})
})
if (!res.ok) throw new Error(`Gemini API error: ${res.status}`)
const data = await res.json()
const base64 = data.candidates[0].content.parts[0].inlineData.data
return base64ToBlob(base64, 'image/png')
}IndexedDB 封装
typescript
// lib/db.ts
import { openDB, DBSchema } from 'idb'
interface HaiyuDB extends DBSchema {
images: {
key: string
value: ImageRecord
indexes: { 'session_id': string; 'created_at': string }
}
sessions: {
key: string
value: SessionRecord
indexes: { 'type': string; 'created_at': string }
}
}
export const db = await openDB<HaiyuDB>('haiyu_history', 1, {
upgrade(db) {
const imgStore = db.createObjectStore('images', { keyPath: 'id' })
imgStore.createIndex('session_id', 'session_id')
imgStore.createIndex('created_at', 'created_at')
const sessStore = db.createObjectStore('sessions', { keyPath: 'id' })
sessStore.createIndex('type', 'type')
sessStore.createIndex('created_at', 'created_at')
}
})
// 高层 API
export async function saveImage(record: ImageRecord) {
await db.add('images', record)
// 同时更新 localStorage 元数据索引
updateImageIndex(record)
}状态管理
选型理由
项目规模适中,不引入 Redux / Zustand,采用:
- React State:组件内状态
- React Context:跨组件共享(用户登录态、配额)
- localStorage:跨页面持久化(最近选择的模型、风格)
- IndexedDB:大文件持久化(图片、对话)
简单可控,无额外学习成本。
暗色 Bento Grid 设计
html
<!-- 主题色:深海蓝 + 科技青 -->
<div class="grid grid-cols-12 gap-4 p-6 bg-slate-950 text-slate-100">
<!-- 左侧历史侧栏 -->
<aside class="col-span-2 bg-slate-900 rounded-2xl p-4">
<SessionList />
</aside>
<!-- 中间画廊 -->
<main class="col-span-7 bg-slate-900 rounded-2xl p-4">
<ImageGallery />
</main>
<!-- 右侧控制面板 -->
<aside class="col-span-3 bg-slate-900 rounded-2xl p-4">
<ModelSelector />
<PromptEditor />
</aside>
</div>性能优化
| 技术 | 收益 |
|---|---|
| Vite HMR | < 100ms 热更新 |
| Code Splitting | 首屏 < 1MB(gzip 612KB) |
| 图片懒加载 | IntersectionObserver |
| IndexedDB 异步读 | 不阻塞 UI |
| Service Worker 缓存 | 二次访问秒开 |
| Tailwind JIT | 仅打包用到的样式 |
浏览器兼容
| 浏览器 | 版本要求 | 关键 API |
|---|---|---|
| Chrome / Edge | ≥ 110 | IndexedDB v2, PWA |
| Safari | ≥ 16 | IndexedDB v2 |
| Firefox | ≥ 110 | IndexedDB v2 |
不支持 IE
项目使用 ES2022 + 原生 IndexedDB,不支持 IE 浏览器。