更新前端项目

This commit is contained in:
2025-11-17 16:49:03 +08:00
parent 638e152cff
commit 033890742d
16 changed files with 862 additions and 130 deletions

View File

@@ -0,0 +1,10 @@
# API 基础地址
VITE_API_BASE_URL=http://192.168.124.23:8000
# 应用配置
VITE_APP_TITLE=Hertz Admin
VITE_APP_VERSION=1.0.0
# 开发服务器配置
VITE_DEV_SERVER_HOST=localhost
VITE_DEV_SERVER_PORT=3000

View File

@@ -0,0 +1 @@
VITE_API_BASE_URL=http://192.168.124.40:8000

View File

@@ -0,0 +1 @@
VITE_API_BASE_URL=http://192.168.124.40:8000

View File

@@ -2,7 +2,7 @@
<h1>通用大模型模板 · Hertz Admin + AI</h1>
现代化、可即用的管理后台前端模板。聚焦“工程化 + 体验”,内置账号体系、权限路由、主题美化、知识库、YOLO 模型全流程(管理/类别/告警/历史)等典型模块。
现代化的管理后台前端模板,面向二次开发的前端工程师。内置账号体系、权限路由、主题美化、知识库、YOLO 模型全流程(管理 / 类别 / 告警 / 历史)等典型模块。
<p>
基于 Vite + Vue 3 + TypeScript + Ant Design Vue + Pinia + Vue Router 构建
@@ -12,15 +12,16 @@
---
## ✨ 特性一览
## ✨ 特性(面向前端)
- 设计统一:全局“苹果风格”主题(卡片/弹窗/按钮/输入/分页),开箱即用且风格一致
- 工程规范TypeScript 强类型、请求与错误拦截、模块化 API、权限化菜单/路由
- 典型业务
- 知识库管理:分类树列表搜索编辑/发布,已优化分类切换闪烁
- **工程化完善**TS 强类型、模块化 API、统一请求封装、权限化菜单/路由
- **设计统一**:全局“超现代风格”主题,卡片 / 弹窗 / 按钮 / 输入 / 分页风格一致
- **业务可复用**
- 知识库管理:分类树 + 列表搜索 + 编辑/发布
- YOLO 模型:模型管理、模型类别管理、告警处理中心、检测历史管理
- 认证体系:登录/注册(字段对齐、错误信息透出),可扩展验证码
- 体验友好:延迟 loading 避免闪烁、毛玻璃质感、统一按钮与交互反馈
- AI 助手:多会话列表 + 消息记录 + 多布局对话界面(含错误调试信息)
- 认证体系:登录/注册、验证码
- **可扩展**:清晰的目录划分和命名规范,方便直接加模块或替换现有实现
## 🧩 技术栈
@@ -31,28 +32,158 @@
- 状态Pinia
- 路由Vue Router
## 📦 目录结构(核心)
## 📦 项目结构与职责
```
> 根目录:`通用大模型模板/`
```bash
通用大模型模板/
└─ hertz_server_diango_ui_2/ # 前端工程Vite
├─ public/ # 公共静态资源
├─ public/ # 公共静态资源(不走打包器)
├─ src/
│ ├─ api/ # 接口定义authyoloknowledge、…)
├─ locales/ # 国际化
│ ├─ router/ # 路由与菜单admin_menu.ts 自动化)
│ ├─ stores/ # Pinia
├─ styles/ # 全局样式与变量index.scss、variables.scss
├─ utils/ # 工具请求、权限、URL 等)
└─ views/ # 页面
├─ admin_page/ # 管理端模块
│ ├─ user_pages/ # 用户端模块
register.vue / Login.vue # 登录注册
├─ index.html
├─ package.json
└─ vite.config.ts
│ ├─ api/ # 接口定义auth / yolo / knowledge / captcha / ai ...
│ └─ yolo.ts # YOLO 模型 & 检测 & 类别相关 API
│ ├─ locales/ # 国际化文案
│ ├─ router/ # 路由与菜单配置
│ ├─ admin_menu.ts # 管理端菜单 + 路由映射(权限 key
│ ├─ user_menu_ai.ts # 用户端菜单 + 路由映射(含 AI 助手)
│ └─ index.ts # Vue Router 实例 + 全局路由守卫
├─ stores/ # Pinia Store
├─ hertz_app.ts # 全局应用设置(语言、布局、菜单折叠等)
hertz_user.ts # 用户 / 鉴权状态
│ │ └─ hertz_theme.ts # 主题配置与 CSS 变量
│ ├─ styles/ # 全局样式与变量
│ │ ├─ index.scss # 全局组件风格覆盖Button / Table / Modal ...
│ │ └─ variables.scss # 主题色、阴影、圆角等变量
│ ├─ utils/ # 工具方法 & 基础设施
│ │ ├─ hertz_request.ts # Axios 封装baseURL、拦截器、错误提示
│ │ ├─ hertz_url.ts # 统一 URL 构造API / 媒体 / WebSocket
│ │ ├─ hertz_env.ts # 读取 & 校验 env 变量
│ │ └─ hertz_router_utils.ts # 路由相关工具 & 调试
│ ├─ views/ # 所有页面
│ │ ├─ admin_page/ # 管理端页面
│ │ │ ├─ ModelManagement.vue # YOLO 模型管理
│ │ │ ├─ AlertLevelManagement.vue # 模型类别管理
│ │ │ ├─ DetectionHistoryManagement.vue # 检测历史管理
│ │ │ └─ ... # 其他管理端模块
│ │ ├─ user_pages/ # 用户端页面(检测端 + AI 助手)
│ │ │ ├─ index.vue # 用户端主布局 + 顶部导航
│ │ │ ├─ AiChat.vue # AI 助手对话页面
│ │ │ ├─ YoloDetection.vue # 离线检测页面
│ │ │ ├─ LiveDetection.vue # 实时检测页面WebSocket
│ │ │ └─ ... # 告警中心 / 通知中心 / 知识库等
│ │ ├─ Login.vue # 登录页
│ │ └─ register.vue # 注册页
│ ├─ App.vue # 应用根组件
│ └─ main.ts # 入口文件(挂载 Vue / 路由 / Pinia
├─ .env.development # 开发环境变量(前端专用)
├─ .env.production # 生产构建环境变量
├─ vite.config.ts # Vite 配置(代理、构建、别名等)
└─ package.json
```
## 📁 文件与命名规范(建议)
- **组件 / 页面**
- 页面:`src/views/admin_page/FooBarManagement.vue`,以业务 + Management 命名
- 纯组件:放到 `src/components/`,使用大驼峰命名,如 `UserSelector.vue`
- **接口文件**
- 同一业务一个文件:`src/api/yolo.ts``src/api/auth.ts`
- 内部导出 `xxxApi` 对象 + TS 类型:`type AlertLevel`, `type YoloModel`
- **样式**
- 全局或主题相关:放 `src/styles/`(注意不要在这里写页面私有样式)
- 单页面样式:使用 `<style scoped lang="scss">` 写在对应 `.vue`
- **工具函数**
- 通用工具:`src/utils/` 下按领域拆分,如 `hertz_url.ts``hertz_env.ts`
## 🌐 后端 IP / 域名配置指引(前端视角最重要)
当前工程已经统一了后端地址配置,只需要 **改 2 个地方**
1. **环境变量文件**(推荐只改这个)
- `hertz_server_diango_ui_2/.env.development`
- `hertz_server_diango_ui_2/.env.production`
两个文件里都有一行:
```bash
# 示例:开发环境
VITE_API_BASE_URL=http://192.168.124.40:8022
```
约定:
- **只写协议 + 域名/IP + 端口**,不要包含 `/api`
- ✅ `http://192.168.124.40:8022`
- ❌ `http://192.168.124.40:8022/api`
- 开发与生产可指向不同后端,只要保证同样的接口路径即可。
2. **Vite 代理 & URL 工具**(已接好,通常不用改)
- `vite.config.ts`
- 利用 `loadEnv` 读取 `VITE_API_BASE_URL`,自动去掉末尾 `/`
```ts
const env = loadEnv(mode, process.cwd(), '')
const apiBaseUrl = env.VITE_API_BASE_URL || 'http://localhost:3000'
const backendOrigin = apiBaseUrl.replace(/\/+$/, '')
```
- 开发环境通过代理转发:
```ts
server: {
proxy: {
'/api': { target: backendOrigin, changeOrigin: true },
'/media': { target: backendOrigin, changeOrigin: true }
}
}
define: {
__VITE_API_BASE_URL__: JSON.stringify(`${backendOrigin}/api`)
}
```
- `src/utils/hertz_url.ts`
- 统一获取后端基础地址:
```ts
export function getBackendBaseUrl(): string {
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'
}
```
- 构造 HTTP / WebSocket / 媒体地址:
```ts
export function getApiBaseUrl() {
return import.meta.env.DEV ? '' : getBackendBaseUrl()
}
export function getMediaBaseUrl() {
if (import.meta.env.DEV) return ''
return getBackendBaseUrl().replace('/api', '')
}
export function getFullFileUrl(relativePath: string) {
const baseURL = getBackendBaseUrl()
return `${baseURL}${relativePath}`
}
```
- `src/utils/hertz_request.ts`
- Axios 实例的 `baseURL` 在开发环境为空字符串(走 Vite 代理);生产环境使用 `VITE_API_BASE_URL`
```ts
const isDev = import.meta.env.DEV
const baseURL = isDev ? '' : (import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000')
```
👉 **结论:前端同事只需要改 `.env.development` 和 `.env.production` 里的 `VITE_API_BASE_URL`,其余 URL 都通过工具/代理自动生效,无需到处搜 `localhost`。**
## 🚀 快速开始
```bash
@@ -62,74 +193,66 @@ cd hertz_server_diango_ui_2
# 安装依赖
npm i
# 开发启动
# 开发启动(默认 http://localhost:3001
npm run dev
## ⚙️ 环境与请求
- 默认使用同域代理或反向代理。按需要在环境文件中设置:
```bash
# .env.development示例
VITE_API_BASE=/api
```
- 请求封装:`src/utils/hertz_request.ts`(拦截器、错误提示、统一 header
⚠️注:所有后端IP都更改为“localhost:3000”需根据具体项目与对应后端开发对接
## 🔧 关键模块速览
## 🔧 关键模块说明
- **主题与 Design System**
- 入口:`src/styles/index.scss`、`src/styles/variables.scss`
- 内容:按钮 / 表格 / 弹窗 / 输入框 等统一风格含毛玻璃、hover、active、focus 细节
- 主题美化Design System
- `src/styles/index.scss``src/styles/variables.scss`
- 全局统一Modal/Drawer/Button/Input/Select/Table/Pagination…
- 专门处理闪烁与焦点态的视觉细节
- **菜单与路由**
- `src/router/admin_menu.ts`:单文件维护管理端菜单树 + 路由映射 + 权限标识
- 面包屑逻辑已整理:不再重复展示“首页/”,只保留当前层级链路
- 菜单与路由
- `src/router/admin_menu.ts`:单文件维护菜单与路由,支持权限过滤与自动生成
- 统一面包屑:已移除“首页/”的冗余展示,仅保留当前层
- **YOLO 模块**
- `ModelManagement.vue`:模型上传 / 列表 / 启用、拖拽上传区
- `AlertLevelManagement.vue`:模型类别管理,支持单条 & 批量修改告警等
- `DetectionHistoryManagement.vue`:检测历史列表、图片/视频预览
- 知识库管理
- `src/views/admin_page/KnowledgeBaseManagement.vue`
- 分类树 + 列表搜索 + 编辑/发布
- 已优化分类切换闪烁(分类卡片不 Loading、表格 Loading 延迟)
- YOLO 模块
- 模型管理:上传/列表/启用禁用(苹果风格拖拽区与卡片)
- 模型类别管理:别名编辑、等级切换
- 告警处理中心:统计卡片、筛选、批量处理、详情预览
- 检测历史管理:搜索、筛选、对比查看(图片/视频),已移除“下载结果”按钮(后端未实现)
- 认证模块
- **认证模块**
- API`src/api/auth.ts`
- 注册页:`src/views/register.vue` 已与后端对齐字段
- 提交字段:`username, password, confirm_password, email, phone, real_name`
- 兼容字段:`captcha, captcha_id`(未启用可传空串)
- 统一错误提示透出
- 页面:`src/views/Login.vue`、`src/views/register.vue`
- 注册表单字段已与后端约定一致:
`username, password, confirm_password, email, phone, real_name, captcha, captcha_id`
## 🧪 常见问题FAQ
- 按钮样式与其他页面不一致?
- 已在 `src/styles/index.scss``.ant-btn` 全局统一。若仍不一致,检查局部覆盖或第三方样式。
- **需要改哪些地方才能连上新的后端 IP**
- 只改:`.env.development` 和 `.env.production` 的 `VITE_API_BASE_URL`
- 不需要:修改页面内的 `http://localhost:xxxx`,已统一收敛到工具函数
- 分类切换时闪烁?
- 左侧分类卡片不再受列表 Loading 影响;表格 Loading 使用 `{ spinning, delay: 200 }`。仍抖动可增加骨架屏或请求防抖。
- **接口不走 / 返回字段对不上?**
- 对比:`src/api/*.ts` 里定义的请求路径与 payload
- 打开浏览器 Network 看真实请求 URL、body 与响应
- 接口没有请求或字段不匹配?
- 检查 `src/api/*.ts` 与页面 `payload` 是否一致;打开浏览器 Network 面板查看请求/响应详情。
- **页面样式和设计稿不一致?**
- 先看 `src/styles/index.scss` 是否有全局覆盖
- 再查对应 `.vue` 文件中的 scoped 样式是否有特殊处理
## 🛠️ 二次开发建议
- 新增模块:在 `src/views/admin_page/` 增加页面,并在 `src/router/admin_menu.ts` 添加菜单与路由映射
- 改造主题:在 `styles/variables.scss` 修改色板与圆角/阴影;在 `index.scss` 扩展组件级风格
- 对接后端:在 `src/api/` 创建对应接口文件,使用统一 `request` 封装
- **新增管理模块**
- 在 `src/views/admin_page/` 下新增页面,如 `FooBarManagement.vue`
- 在 `src/router/admin_menu.ts` 中增加菜单配置path + component + permission
## 📜 脚本列表
- **扩展接口**
- 在 `src/api/` 新增 `xxx.ts`,导出 `xxxApi` 对象
- 使用统一的 `request` 封装(`hertz_request.ts`),保持错误处理一致
- **改造主题 / 品牌色**
- 修改 `src/styles/variables.scss` 中的主色、背景色、圆角、阴影
- 如需大改导航栏、卡片风格,优先在全局样式里做统一,而不是每页重新写
## 📜 NPM 脚本
```json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
}
}

View File

@@ -73,7 +73,6 @@ declare module 'vue' {
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
AUpload: typeof import('ant-design-vue/es')['Upload']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}

View File

@@ -26,12 +26,10 @@
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.21",
"daisyui": "^5.1.13",
"eslint": "^9.36.0",
"eslint-plugin-vue": "^10.4.0",
"postcss": "^8.5.6",
"sass-embedded": "^1.93.0",
"tailwindcss": "^4.1.13",
"typescript": "~5.8.3",
"typescript-eslint": "^8.44.0",
"unplugin-vue-components": "^29.1.0",

View File

@@ -9,7 +9,7 @@ export const checkEnvironmentVariables = () => {
// 在Vite中环境变量可能通过define选项直接定义
// 或者通过import.meta.env读取
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://hertzServer:8000/api'
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'
const appTitle = import.meta.env.VITE_APP_TITLE || 'Hertz Admin'
const appVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
@@ -30,7 +30,7 @@ export const checkEnvironmentVariables = () => {
// 检查可选的环境变量
const devServerHost = import.meta.env.VITE_DEV_SERVER_HOST || 'localhost'
const devServerPort = import.meta.env.VITE_DEV_SERVER_PORT || '3001'
const devServerPort = import.meta.env.VITE_DEV_SERVER_PORT || '3000'
const optionalVars = [
{ key: 'VITE_DEV_SERVER_HOST', value: devServerHost },
@@ -72,7 +72,7 @@ export const validateEnvironment = () => {
// 获取API基础地址
export const getApiBaseUrl = (): string => {
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001/api'
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'
}
// 获取应用配置
@@ -82,6 +82,6 @@ export const getAppConfig = () => {
version: import.meta.env.VITE_APP_VERSION || '1.0.0',
apiBaseUrl: getApiBaseUrl(),
devServerHost: import.meta.env.VITE_DEV_SERVER_HOST || 'localhost',
devServerPort: import.meta.env.VITE_DEV_SERVER_PORT || '3001',
devServerPort: import.meta.env.VITE_DEV_SERVER_PORT || '3000',
}
}

View File

@@ -122,16 +122,16 @@ export const showRoutesInfo = () => {
console.log(' 💡 提示: 可以在浏览器中直接访问这些路径')
console.log('\n🌐 可用链接:')
console.log(' http://localhost:3001/ - 首页 (需要登录)')
console.log(' http://localhost:3001/login - 登录页面')
console.log(' http://localhost:3001/dashboard - 仪表板 (需要登录)')
console.log(' http://localhost:3001/user - 用户管理 (需要登录)')
console.log(' http://localhost:3001/profile - 个人资料 (需要登录)')
console.log(' http://localhost:3001/settings - 系统设置 (需要登录)')
console.log(' http://localhost:3001/test - 样式测试 (公开)')
console.log(' http://localhost:3001/websocket-test - WebSocket测试 (公开)')
console.log(' http://localhost:3001/demo - 动态路由演示 (公开)')
console.log(' http://localhost:3001/any-other-path - 404页面 (公开)')
console.log(' http://localhost:3000/ - 首页 (需要登录)')
console.log(' http://localhost:3000/login - 登录页面')
console.log(' http://localhost:3000/dashboard - 仪表板 (需要登录)')
console.log(' http://localhost:3000/user - 用户管理 (需要登录)')
console.log(' http://localhost:3000/profile - 个人资料 (需要登录)')
console.log(' http://localhost:3000/settings - 系统设置 (需要登录)')
console.log(' http://localhost:3000/test - 样式测试 (公开)')
console.log(' http://localhost:3000/websocket-test - WebSocket测试 (公开)')
console.log(' http://localhost:3000/demo - 动态路由演示 (公开)')
console.log(' http://localhost:3000/any-other-path - 404页面 (公开)')
console.log('\n✅ 路由配置加载完成!')
console.log('💡 提示: 启动项目后会在控制台看到真正的动态路由信息')

View File

@@ -24,10 +24,25 @@ export function getFullFileUrl(relativePath: string): string {
}
// 在生产环境中拼接完整的URL
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001'
const baseURL = getBackendBaseUrl()
return `${baseURL}${relativePath}`
}
export function getBackendBaseUrl(): string {
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'
}
export function getWsBaseUrl(): string {
const httpBase = getBackendBaseUrl()
if (httpBase.startsWith('https://')) {
return 'wss://' + httpBase.slice('https://'.length)
}
if (httpBase.startsWith('http://')) {
return 'ws://' + httpBase.slice('http://'.length)
}
return httpBase
}
/**
* 获取API基础URL
* @returns API基础URL
@@ -36,7 +51,7 @@ export function getApiBaseUrl(): string {
if (import.meta.env.DEV) {
return '' // 开发环境使用空字符串通过Vite代理
}
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001'
return getBackendBaseUrl()
}
/**
@@ -47,7 +62,7 @@ export function getMediaBaseUrl(): string {
if (import.meta.env.DEV) {
return '' // 开发环境使用空字符串通过Vite代理
}
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001'
const baseURL = getBackendBaseUrl()
return baseURL.replace('/api', '') // 移除/api后缀
}

View File

@@ -17,6 +17,13 @@
</template>
刷新
</a-button>
<a-button
type="primary"
:disabled="!selectedRowKeys.length"
@click="openBatchEdit"
>
批量修改等级
</a-button>
</div>
<div class="action-right">
<a-input-search
@@ -35,6 +42,7 @@
:loading="loading"
:pagination="pagination"
row-key="id"
:row-selection="rowSelection"
class="levels-table"
>
<template #bodyCell="{ column, record }">
@@ -180,6 +188,41 @@
</a-form>
</div>
</a-modal>
<!-- 批量切换警告等级弹窗 -->
<a-modal
v-model:visible="batchEditModalVisible"
title="批量修改警告等级"
@ok="handleBatchEdit"
@cancel="handleBatchEditCancel"
width="500px"
>
<div class="alert-level-edit">
<p style="margin-bottom: 12px;">
已选择 <strong>{{ selectedRowKeys.length }}</strong> 个类别将统一修改为新的警告等级
</p>
<a-form
ref="batchEditFormRef"
:model="batchEditForm"
:rules="editRules"
layout="vertical"
style="margin-top: 12px"
>
<a-form-item label="新的警告等级" name="alert_level">
<a-select
v-model:value="batchEditForm.alert_level"
placeholder="请选择新的警告等级"
style="width: 100%"
>
<a-select-option value="low"></a-select-option>
<a-select-option value="medium"></a-select-option>
<a-select-option value="high"></a-select-option>
</a-select>
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</template>
@@ -193,7 +236,7 @@ import {
EditOutlined,
PoweroffOutlined
} from '@ant-design/icons-vue'
import { yoloApi, type AlertLevel } from '@/api/yolo'
import { yoloApi, type AlertLevel, type YoloModel } from '@/api/yolo'
import dayjs from 'dayjs'
// 响应式数据
@@ -201,19 +244,36 @@ const loading = ref(false)
const editing = ref(false)
const levels = ref<AlertLevel[]>([])
const searchKeyword = ref('')
const currentModel = ref<YoloModel | null>(null)
const selectedRowKeys = ref<number[]>([])
// 表格多选配置
const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: (string | number)[]) => {
selectedRowKeys.value = keys as number[]
}
}))
// 弹窗状态
const detailModalVisible = ref(false)
const editModalVisible = ref(false)
const currentLevel = ref<AlertLevel | null>(null)
const batchEditModalVisible = ref(false)
// 编辑表单
const editForm = reactive({
alert_level: 'low' as 'low' | 'medium' | 'high'
})
// 批量编辑表单
const batchEditForm = reactive({
alert_level: 'low' as 'low' | 'medium' | 'high'
})
// 表单引用
const editFormRef = ref()
const batchEditFormRef = ref()
// 表单验证规则
const editRules = {
@@ -222,14 +282,22 @@ const editRules = {
]
}
// 分页配置
// 分页配置(受控分页)
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total} 条记录`
showTotal: (total: number) => `${total} 条记录`,
onChange: (page: number, pageSize: number) => {
pagination.current = page
pagination.pageSize = pageSize
},
onShowSizeChange: (page: number, pageSize: number) => {
pagination.current = page
pagination.pageSize = pageSize
}
})
// 表格列配置
@@ -293,18 +361,45 @@ const getAlertLevelColor = (level: string) => {
return colorMap[level] || 'default'
}
// 获取警告等级列表
// 获取警告等级列表(仅显示当前启用模型的类别)
const fetchLevels = async () => {
try {
loading.value = true
console.log('🔍 开始获取警告等级列表...')
// 先获取当前启用的模型
let enabledModelId: string | null = null
try {
const modelResp = await yoloApi.getCurrentEnabledModel()
console.log('📋 当前启用模型API响应:', modelResp)
if (modelResp.success && modelResp.data) {
currentModel.value = modelResp.data
enabledModelId = modelResp.data.id
} else {
currentModel.value = null
}
} catch (e) {
console.error('❌ 获取当前启用模型失败:', e)
currentModel.value = null
}
// 再获取所有类别
const response = await yoloApi.getAlertLevels()
console.log('📋 警告等级API响应:', response)
if (response.success && response.data) {
levels.value = response.data
pagination.total = response.data.length
let data = response.data
// 如果有启用模型,则按模型过滤类别
if (enabledModelId) {
const modelIdNum = Number(enabledModelId)
data = data.filter(level => level.model === modelIdNum)
console.log('✅ 过滤后类别列表:', data)
}
levels.value = data
pagination.total = data.length
pagination.current = 1
console.log('✅ 警告等级获取成功:', levels.value)
} else {
console.error('❌ 获取警告等级失败:', response.message)
@@ -400,28 +495,28 @@ const handleEdit = async () => {
if (!currentLevel.value) return
editing.value = true
console.log('🔍 开始切换警告等级...', currentLevel.value.id, editForm)
console.log('开始切换警告等级...', currentLevel.value.id, editForm)
// 构造更新数据 - 只传递 alert_level
const updateData = {
alert_level: editForm.alert_level
}
console.log('📤 发送更新数据:', updateData)
console.log('发送更新数据:', updateData)
const response = await yoloApi.updateAlertLevel(currentLevel.value.id.toString(), updateData)
console.log('📋 更新警告等级API响应:', response)
console.log('更新警告等级API响应:', response)
if (response.success) {
message.success(`警告等级已切换为: ${editForm.alert_level}`)
editModalVisible.value = false
fetchLevels()
} else {
console.error('切换警告等级失败:', response.message)
console.error('切换警告等级失败:', response.message)
message.error(response.message || '切换失败')
}
} catch (error) {
console.error('切换警告等级异常:', error)
console.error('切换警告等级异常:', error)
message.error('切换失败')
} finally {
editing.value = false
@@ -434,13 +529,78 @@ const handleEditCancel = () => {
currentLevel.value = null
}
// 打开批量编辑弹窗
const openBatchEdit = () => {
if (!selectedRowKeys.value.length) {
message.warning('请先选择要修改的类别')
return
}
batchEditForm.alert_level = 'low'
batchEditModalVisible.value = true
}
// 批量修改警告等级
const handleBatchEdit = async () => {
try {
if (batchEditFormRef.value && batchEditFormRef.value.validate) {
await batchEditFormRef.value.validate()
}
if (!selectedRowKeys.value.length) {
message.warning('请先选择要修改的类别')
return
}
editing.value = true
const targetLevel = batchEditForm.alert_level
let successCount = 0
let failCount = 0
for (const id of selectedRowKeys.value) {
try {
const response = await yoloApi.updateAlertLevel(id.toString(), {
alert_level: targetLevel
})
if (response.success) {
successCount++
} else {
failCount++
}
} catch (error) {
console.error('批量切换警告等级异常:', error)
failCount++
}
}
if (successCount > 0) {
message.success(`成功更新 ${successCount} 条警告等级`)
}
if (failCount > 0) {
message.error(`${failCount} 条更新失败,请检查日志`)
}
batchEditModalVisible.value = false
selectedRowKeys.value = []
fetchLevels()
} catch (error) {
console.error('批量切换警告等级失败:', error)
} finally {
editing.value = false
}
}
// 取消批量编辑
const handleBatchEditCancel = () => {
batchEditModalVisible.value = false
}
// 切换状态
const toggleStatus = async (level: AlertLevel) => {
try {
console.log('🔍 开始切换警告等级状态...', level.id)
console.log('开始切换警告等级状态...', level.id)
const response = await yoloApi.toggleAlertLevelStatus(level.id.toString())
console.log('📋 切换状态API响应:', response)
console.log('切换状态API响应:', response)
if (response.success) {
message.success(level.is_active ? '警告等级已禁用' : '警告等级已启用')
@@ -457,7 +617,9 @@ const toggleStatus = async (level: AlertLevel) => {
// 搜索处理
const handleSearch = () => {
// 搜索逻辑已在computed中处理
// 搜索逻辑已在computed中处理,这里只负责重置分页
pagination.current = 1
pagination.total = filteredLevels.value.length
}
// 组件挂载时获取数据

View File

@@ -332,6 +332,7 @@ import {
} from '@ant-design/icons-vue'
import { yoloApi, type DetectionHistoryRecord, type YoloModel } from '@/api/yolo'
import dayjs from 'dayjs'
import { getFullFileUrl } from '@/utils/hertz_url'
// 响应式数据
const loading = ref(false)
@@ -467,8 +468,7 @@ const formatDate = (dateString: string) => {
// 获取图片URL
const getImageUrl = (filePath: string) => {
if (!filePath) return ''
if (filePath.startsWith('http')) return filePath
return `http://localhost:3001${filePath}`
return getFullFileUrl(filePath)
}
// 处理图片加载错误

View File

@@ -468,7 +468,7 @@ import {
UploadOutlined,
ReloadOutlined
} from '@ant-design/icons-vue'
import { getFullFileUrl } from '@/utils/hertz_url'
import { getFullFileUrl, getWsBaseUrl, getBackendBaseUrl } from '@/utils/hertz_url'
import { yoloDetector, type YOLODetectionResult } from '@/utils/yolo_frontend'
import { yoloApi } from '@/api/yolo'
@@ -477,7 +477,7 @@ const inferenceMode = ref<'pt' | 'onnx'>('pt')
// WebSocket连接PT推理模式使用
let ws: WebSocket | null = null
const wsUrl = 'ws://localhost:8000/ws/yolo/live/'
const wsUrl = `${getWsBaseUrl()}/ws/yolo/live/`
// 响应式数据
const loading = ref(false)
@@ -1342,7 +1342,7 @@ const handleOnnxUpload = async () => {
// 如果URL不是完整路径构建完整URL
if (!labelsDownloadUrl.startsWith('http')) {
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001'
const baseUrl = getBackendBaseUrl()
labelsDownloadUrl = labelsDownloadUrl.startsWith('/')
? `${baseUrl}${labelsDownloadUrl}`
: `${baseUrl}/${labelsDownloadUrl}`
@@ -1417,7 +1417,7 @@ const handleOnnxUpload = async () => {
// 如果URL不是完整路径构建完整URL
if (!downloadUrl.startsWith('http')) {
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001'
const baseUrl = getBackendBaseUrl()
downloadUrl = downloadUrl.startsWith('/')
? `${baseUrl}${downloadUrl}`
: `${baseUrl}/${downloadUrl}`

View File

@@ -918,6 +918,7 @@ import {
} from '@ant-design/icons-vue'
import { yoloApi, detectionHistoryApi, type YoloDetection, type YoloModel } from '@/api/yolo'
import { useUserStore } from '@/stores/hertz_user'
import { getFullFileUrl } from '@/utils/hertz_url'
// 布局模式
const layoutMode = ref<'classic' | 'vertical' | 'grid'>('classic')
@@ -1141,25 +1142,14 @@ const startDetection = async () => {
console.log('检测API响应:', response)
if (response.data) {
// 构建完整的图片URL
const baseUrl = 'http://localhost:3001'
// 构建完整的图片URL(统一通过工具函数处理)
console.log('🔍 原始URL数据:', {
original_file_url: response.data.original_file_url,
result_file_url: response.data.result_file_url
})
// 确保URL路径正确
let originalImageUrl = response.data.original_file_url
let resultImageUrl = response.data.result_file_url
// 如果URL不是以http开头则添加baseUrl
if (!originalImageUrl.startsWith('http')) {
originalImageUrl = `${baseUrl}${originalImageUrl}`
}
if (!resultImageUrl.startsWith('http')) {
resultImageUrl = `${baseUrl}${resultImageUrl}`
}
let originalImageUrl = getFullFileUrl(response.data.original_file_url)
let resultImageUrl = getFullFileUrl(response.data.result_file_url)
console.log('🖼️ 构建后的图片URL:', {
original: originalImageUrl,

View File

@@ -4362,6 +4362,11 @@ const handleLogout = () => {
flex: 1;
border-bottom: none;
line-height: 62px;
background: transparent;
:deep(.ant-menu) {
background: transparent;
}
:deep(.ant-menu-item),
:deep(.ant-menu-submenu) {

View File

@@ -1,4 +1,4 @@
import { defineConfig, type Plugin } from 'vite'
import { defineConfig, type Plugin, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import fs from 'fs'
@@ -47,7 +47,12 @@ function modelsManifestPlugin(): Plugin {
}
}
export default defineConfig({
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const apiBaseUrl = env.VITE_API_BASE_URL || 'http://localhost:3000'
const backendOrigin = apiBaseUrl.replace(/\/+$/, '')
return {
plugins: [
vue(),
modelsManifestPlugin(),
@@ -228,7 +233,7 @@ export default defineConfig({
},
// API代理转发到后端服务器
'/api': {
target: 'http://localhost:8000',
target: backendOrigin,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, '/api'),
@@ -267,7 +272,7 @@ export default defineConfig({
},
// 媒体文件代理转发到后端服务器
'/media': {
target: 'http://localhost:8000',
target: backendOrigin,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/media/, '/media'),
@@ -308,7 +313,7 @@ export default defineConfig({
},
define: {
// 环境变量定义,确保在没有.env文件时也能正常工作
__VITE_API_BASE_URL__: JSON.stringify('http://localhost:8000/api'),
__VITE_API_BASE_URL__: JSON.stringify(`${backendOrigin}/api`),
__VITE_APP_TITLE__: JSON.stringify('Hertz Admin'),
__VITE_APP_VERSION__: JSON.stringify('1.0.0'),
},
@@ -324,4 +329,5 @@ export default defineConfig({
},
},
},
}
})

View File

@@ -0,0 +1,422 @@
# 前端样式 / 布局 / UI 修改操作指南
> 面向二次开发前端,告诉你:**改样式 / 改布局 / 改 UI 具体要动哪些文件、怎么改**。
>
> 项目技术栈Vite + Vue 3 + TypeScript + Ant Design Vue + Pinia + Vue Router + SCSS
---
## 🧭 速查表
- **改全局颜色 / 按钮 / 弹窗风格**:看第 1 章「整体样式体系总览」(`src/styles/index.scss` + `src/styles/variables.scss`
- **改管理端整体布局(侧边栏、头部、内容区排版)**:看 2.1「管理端整体布局」(`src/views/admin_page/index.vue`
- **改用户端整体布局(顶部导航 + 内容容器)」**:看 2.2「用户端整体布局」(`src/views/user_pages/index.vue`
- **改 YOLO 检测排版 / 三种布局 / 卡片样式**:看第 3 章「YOLO 检测页面修改指南」(`src/views/user_pages/YoloDetection.vue`
- **改 AI 助手聊天布局**:看第 4 章「修改 AI 助手页面」(`src/views/user_pages/AiChat.vue`
---
## 1. 整体样式体系总览
### 1.1 全局样式入口
- 入口文件:`src/styles/index.scss`
-`src/main.ts` 中全局引入:
- `import './styles/index.scss'`
- 主要职责:
- 重置 margin / padding / box-sizing
- 全局字体、`html, body, #app` 基础样式
- 自定义 `.btn` / `.card` 等通用类
- 全局 Ant Design Vue 主题风格覆盖(如 `.ant-modal`, `.ant-btn` 等)
**如果你要改全局的按钮、弹窗、表单、输入框等基础风格:**
1. 打开 `src/styles/index.scss`
2. 找对应的选择器:
- 按钮:`.ant-btn` 下的几种状态(`&.ant-btn-default` / `&.ant-btn-primary` / `&.ant-btn-dangerous` 等)
- 弹窗:`.ant-modal` 内的 `.ant-modal-content` / `.ant-modal-header` / `.ant-modal-footer`
- 输入/选择等:`.ant-input`, `.ant-select-selector`, `.ant-input-number`, `.ant-picker`
3. 直接在这里调整颜色、圆角、阴影、间距。
4. 样式会作用于所有页面,无需在每个 `.vue` 里重复写。
> 建议:全局 Design System 统一改在这里,不要在业务页面里到处改 AntD 默认样式。
### 1.2 变量和混合(主题基础)
- 文件:`src/styles/variables.scss`
- 主要内容:
- 颜色:`$primary-color``$success-color``$gray-xxx`
- 间距:`$spacing-1 ~ $spacing-20`
- 圆角:`$radius-md``$radius-lg`
- 阴影:`$shadow-sm` / `$shadow-md` / `$shadow-lg`
- 常用 mixin`@mixin card-style``@mixin button-style`
**改全局配色 / 圆角 / 阴影的操作方式:**
1. 打开 `src/styles/variables.scss`
2. 修改对应变量:
- 主色:`$primary-color` / `$primary-light` / `$primary-dark`
- 背景:`$bg-primary` / `$bg-secondary`
- 阴影:`$shadow-md` / `$shadow-lg`
3. 不需要修改业务页面,使用这些变量的地方会统一生效。
> 如果要在页面里复用统一卡片/按钮样式,可以直接:
>
> ```scss
> .my-card {
> @include card-style;
> }
>
> .my-primary-button {
> @include button-style($primary-color, #fff);
> }
> ```
### 1.3 主题 store 与 CSS 变量
- 文件:`src/stores/hertz_theme.ts`
- 作用:
- 定义 `ThemeConfig`(导航栏背景、页面背景、卡片背景、主色、文字颜色)
- 使用 `document.documentElement.style.setProperty` 写入 CSS 变量:
- `--theme-header-bg`, `--theme-page-bg`, `--theme-card-bg`, `--theme-primary`, `--theme-text-primary`
- 使用方式:
- 在页面/组件的 SCSS 中,通过 `var(--theme-primary)` 等变量引用主题色:
- 示例:`color: var(--theme-text-primary, #1e293b);`
**修改主题默认值**
1. 打开 `src/stores/hertz_theme.ts`
2. 修改 `defaultTheme` 对象里的颜色值即可:
- 如:`primaryColor: '#FF4D4F'` 改成你的品牌色
3. 调用 `themeStore.loadTheme()` 时会自动应用到全局。
**页面内如何用这些主题变量?**
- 在 SCSS 中使用:
```scss
.some-block {
background: var(--theme-card-bg, #fff);
color: var(--theme-text-primary, #1e293b);
border-color: var(--theme-card-border, #e5e7eb);
}
```
---
## 2. 布局结构:管理端 / 用户端
### 2.1 管理端整体布局
- 入口布局:`src/views/admin_page/index.vue`
- 结构:
- 外层 `.admin-layout`
- 使用 `a-layout` + `a-layout-sider` + `a-layout-header` + `a-layout-content` + `a-layout-footer`
- 侧边菜单:`a-layout-sider` 内的 `a-menu`,使用 `admin_menu.ts` 生成菜单项
**修改管理端整体布局方式(比如侧边栏宽度、顶部高度):**
1. 打开 `src/views/admin_page/index.vue`
2. 找到模板部分:
- 侧边栏:`<a-layout-sider ... class="admin-sider">`
- 顶部:`<a-layout-header class="header">`
- 内容:`<a-layout-content class="content">`
3. 在同文件底部的 `<style scoped lang="scss">` 中调整:
- `.admin-layout``.admin-sider``.header``.content` 的 padding / background / shadow 等。
4. 如果要改变菜单布局(比如改成顶部导航):需要同时更新 template 结构和对应的 SCSS。
### 2.2 用户端整体布局
- 入口布局:`src/views/user_pages/index.vue`
- 结构:
- 顶部 `a-layout-header.user-header`:包含 logo + 顶部菜单 + 面包屑 + 布局切换按钮 + 主题按钮 + 用户下拉
- 主体 `a-layout.main-layout`,内部 `a-layout-content.user-content` 作为页面内容容器
- 中间区域通过 `currentComponent` 动态切换不同业务页YOLO 检测、AI 助手等)
**修改用户端整体布局(例如 header 高度、主内容宽度):**
1. 打开 `src/views/user_pages/index.vue`
2. 在 template 里找到:
- `<a-layout-header class="user-header">`
- `<a-layout class="main-layout">`
- `<a-layout-content class="user-content">`
3. 在同文件的 `<style scoped lang="scss">` 中调整:
- `.user-layout``.user-header``.main-layout``.user-content``.content-wrapper`
4. 你也可以在这里添加全局背景图(比如 `.user-layout` 里加 background只作用于用户端。
> 所有用户端的业务页面(`YoloDetection.vue`、`AiChat.vue` 等)都是渲染在 `user-content` 容器里,尽量保持这里的 padding / 背景一致,具体视觉再在业务页内做细化。
---
## 3. YOLO 检测页面修改指南(排版 / 布局 / 样式 / UI
文件路径:`src/views/user_pages/YoloDetection.vue`
这个页面已经内置了 **三种布局模式**,并且使用了大量局部 SCSS + AntD 组件。下面分步骤说明如何操作。
### 3.1 布局模式classic / vertical / grid
关键代码:
- 布局枚举与状态:
```ts
const layoutMode = ref<'classic' | 'vertical' | 'grid'>('classic')
const layoutKey = ref(0)
```
- 模板中的三个布局分支:
```vue
<!-- 经典两列布局 -->
<div v-if="layoutMode === 'classic'" class="detection-content classic-layout" :key="`classic-${layoutKey}`">
...
</div>
<!-- 上下布局 -->
<div v-if="layoutMode === 'vertical'" class="detection-content vertical-layout" :key="`vertical-${layoutKey}`">
...
</div>
<!-- 侧边栏详情布局 -->
<div v-if="layoutMode === 'grid'" class="detection-content sidebar-layout" :key="`grid-${layoutKey}`">
...
</div>
```
`layoutMode` 的切换来自用户端首页布局弹窗(`user_pages/index.vue`),会写入 `localStorage('yoloDetectionLayout')`
**如果你要改某一种布局的排版:**
1.`YoloDetection.vue` 模板里定位对应的 `div`
- 经典两列:`.classic-layout`
- 上下布局:`.vertical-layout`
- 侧边栏布局:`.sidebar-layout`
2. 在同文件的 `<style scoped lang="scss">` 中搜索这些 class例如搜索 `.classic-layout`
- 调整其中的 `a-row` / `a-col` 排布、卡片宽度、高度等。
3. 如果你只想保留一种布局,比如只保留经典两列:
- 可以在 template 中暂时注释掉 `vertical``grid` 这两段。
- 或者在 script 中将 `layoutMode` 的类型改小,只允许 `'classic'`
### 3.2 修改上传区域(左侧卡片)
上传部分主结构classic 布局示例):
```vue
<a-card title="文件上传" class="upload-card">
<div class="upload-area">
<a-upload-dragger ... class="beautiful-upload">
...
</a-upload-dragger>
<div v-if="fileList.length > 0" class="compact-preview">...
</div>
</div>
<div class="detection-params" v-if="fileList.length > 0">...
</div>
<div class="upload-actions" v-if="fileList.length > 0">...
</div>
</a-card>
```
对应样式(节选):
```scss
.yolo-detection-page {
.upload-card { ... }
.upload-area { ... }
.beautiful-upload { ... }
.compact-preview { ... }
.upload-actions { ... }
}
// 深度作用 AntD Upload
:deep(.ant-upload-dragger) {
border: 2px dashed #d1d5db;
border-radius: 12px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
...
}
```
**具体改法示例:**
- **想调整拖拽区域高度 / 圆角 / 背景:**
1.`<style scoped lang="scss">` 中搜索 `:deep(.ant-upload-dragger)`
2. 修改其中的 `border-radius``min-height``background` 等。
- **想改变已上传文件的缩略图排列方式:**
1. 搜索 `.compact-preview``.preview-grid``.preview-card` 等 class。
2. 例如,将 `display: grid` 的列数从 `3` 改为 `4`
- 修改 `grid-template-columns: repeat(3, 1fr);``repeat(4, 1fr);`
- **想给“开始检测”按钮加一个 loading 状态颜色:**
1. 上传按钮使用的是 `<a-button type="primary" :loading="detecting">`,全局颜色来自 `.ant-btn-primary`
2. 如果想只在该页面定制:
```scss
.yolo-detection-page {
.upload-actions {
:deep(.ant-btn-primary) {
background: #22c55e; // 单独改为绿色
}
}
}
```
### 3.3 修改检测结果区域排版
经典布局右侧结果卡片结构:
```vue
<a-card title="检测结果" class="result-card">
<div v-if="detectionResults.length === 0" class="no-results">...</div>
<div v-else class="results-list">
<div class="results-grid">
<div v-for="result in sortedDetectionResults" class="result-item">
<div class="image-comparison">...
<div class="result-details">...
</div>
</div>
</div>
</a-card>
```
对应样式关键点:
- `.results-list`:控制滚动区域、高度和滚动条样式
- `.result-item`:单个结果卡片的背景、边框、阴影
- `.image-comparison` / `.image-pair` / `.image-wrapper`:左右对比图/视频布局
- `.result-details` / `.detection-stats` / `.detection-tags`:文本统计信息
**操作示例:**
- **改结果区域整体高度 / 是否滚动:**
1. 在 SCSS 中搜索 `.results-list`
2. 调整 `max-height` 或移除 `overflow-y: scroll !important;`,就可以变成自适应高度。
- **把“原图/检测结果”改成上下叠加而不是左右:**
1. 找到 `.image-pair`
```scss
.image-pair {
display: flex;
gap: 16px;
}
```
2. 改为列方向:
```scss
.image-pair {
display: flex;
flex-direction: column; // 上下排列
gap: 16px;
}
```
- **调整图片容器比例 / 加圆角阴影:**
1. 修改 `.image-wrapper``height: 160px;` 改为你想要的高度或使用 `aspect-ratio`
2. 修改 `border-radius` / `box-shadow` 实现更柔和的卡片效果。
### 3.4 深度选择器 `:deep` 使用说明
`YoloDetection.vue` 中多次使用了 `:deep(...)` 来覆盖 AntD 子组件样式,例如:
```scss
:deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
}
:deep(.ant-upload-list) {
display: none !important;
}
```
**规则:**
- 页面级样式修改尽量写在该 `.vue``<style scoped>` 中,用 `:deep` 定位到 AntD 的 DOM 结构。
- 全局通用样式统一放在 `src/styles/index.scss`,不要在页面里频繁写过多全局覆盖。
**如何找到 AntD 的 class 名称?**
1. 浏览器打开页面F12 查看对应组件 DOM 结构。
2. 复制最内层需要修改的 class`.ant-card-head`)。
3. 在该页面样式中写:
```scss
.my-custom-card {
:deep(.ant-card-head) {
// 自定义头部样式
}
}
```
---
## 4. 修改 AI 助手页面(整体方法类似)
AI 助手页面:`src/views/user_pages/AiChat.vue`
- 也有多种布局:`default`(左右)、`compact`(上下)、`wide`(全屏消息 + 浮动侧边栏)。
- 样式组织方式与 YOLO 检测类似:
- 外层 `.ai-chat-page`
- 内部 `.chat-container` / `.chat-sidebar` / `.chat-main` / `.messages-wrapper` / `.composer`
- 使用 `:deep` 覆盖 AntD 的 `a-card``a-input-search``a-list` 等组件
**如果你已经掌握了上面 YOLO 页面改法:**
- 改 AI 助手排版时只需:
1. 找到对应布局分支(`currentLayout === 'default' | 'compact' | 'wide'`
2.`<style scoped lang="scss">` 中编辑对应 class 的样式即可。
---
## 5. 实战建议:改 UI 的基本步骤
1. **先确认层级:**
- 是全局都要变?(颜色、按钮、弹窗 -> `styles/index.scss`, `styles/variables.scss`, `hertz_theme.ts`
- 只改某个模块YOLO / AI / 管理端 / 用户端布局 -> 各自 `.vue` 的 scoped 样式)
2. **用浏览器开发者工具查 DOM 和 class**
- 看清楚 AntD 组件最终渲染出的结构和 class
- 再决定用 `:deep(.ant-xxx)` 还是自定义的 `.my-block` 去写样式
3. **在对应 `.vue` 文件底部 `<style scoped lang="scss">` 中改局部样式:**
- 保持 class 命名有语义:`.yolo-detection-page``.upload-area``.results-list`
4. **共用样式尽量抽到全局:**
- 例如卡片风格在 `variables.scss` + `index.scss` 抽成 mixin 或全局 class
5. **改完后检查响应式:**
- `YoloDetection.vue` 和 AI 助手都内置了 `@media` 段,记得同步修改,以免移动端错位。
---
## 6. 如果要新增一个“风格类似 YOLO 的新页面”
1.`src/views/user_pages/` 下新建 `MyFeature.vue`
2. 参考 `YoloDetection.vue` 的结构:
- 顶部 `.page-header`
- 中间 `a-row` + `a-col` 做主布局
- 底部 `<style scoped lang="scss">` 里用 `.my-feature-page { ... }` 包裹所有样式
3. 使用全局变量和 mixin
```scss
.my-feature-page {
background: var(--theme-page-bg, #f5f5f5);
.my-card {
@include card-style;
}
}
```
4.`src/router/user_menu_ai.ts` 中为用户端菜单新增一项,对应 `MyFeature.vue`
---