更新前端项目
This commit is contained in:
10
hertz_server_diango_ui/.env
Normal file
10
hertz_server_diango_ui/.env
Normal 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
|
||||
1
hertz_server_diango_ui/.env.development
Normal file
1
hertz_server_diango_ui/.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=http://192.168.124.40:8000
|
||||
1
hertz_server_diango_ui/.env.production
Normal file
1
hertz_server_diango_ui/.env.production
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=http://192.168.124.40:8000
|
||||
@@ -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/ # 接口定义(auth、yolo、knowledge、…)
|
||||
│ ├─ 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"
|
||||
}
|
||||
}
|
||||
|
||||
1
hertz_server_diango_ui/components.d.ts
vendored
1
hertz_server_diango_ui/components.d.ts
vendored
@@ -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']
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('💡 提示: 启动项目后会在控制台看到真正的动态路由信息')
|
||||
|
||||
@@ -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后缀
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 处理图片加载错误
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
422
hertz_server_diango_ui/修改操作指南.md
Normal file
422
hertz_server_diango_ui/修改操作指南.md
Normal 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`。
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user