Skip to content

前端架构

技术栈

层级技术版本用途
UI 框架React19组件渲染
构建工具Vite6开发 + 打包
CSSTailwind CDN最新样式(暗色 Bento Grid)
PWAvite-plugin-pwa最新Service Worker
路由React Routerv6SPA 路由
持久化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≥ 110IndexedDB v2, PWA
Safari≥ 16IndexedDB v2
Firefox≥ 110IndexedDB v2

不支持 IE

项目使用 ES2022 + 原生 IndexedDB,不支持 IE 浏览器

下一步

基于 MIT 协议开源 · 中国大学生计算机设计大赛软件应用与开发类作品