更新前端项目
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>
|
<h1>通用大模型模板 · Hertz Admin + AI</h1>
|
||||||
|
|
||||||
现代化、可即用的管理后台前端模板。聚焦“工程化 + 体验”,内置账号体系、权限路由、主题美化、知识库、YOLO 模型全流程(管理/类别/告警/历史)等典型模块。
|
现代化的管理后台前端模板,面向二次开发的前端工程师。内置账号体系、权限路由、主题美化、知识库、YOLO 模型全流程(管理 / 类别 / 告警 / 历史)等典型模块。
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
基于 Vite + Vue 3 + TypeScript + Ant Design Vue + Pinia + Vue Router 构建
|
基于 Vite + Vue 3 + TypeScript + Ant Design Vue + Pinia + Vue Router 构建
|
||||||
@@ -12,15 +12,16 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ 特性一览
|
## ✨ 特性(面向前端)
|
||||||
|
|
||||||
- 设计统一:全局“苹果风格”主题(卡片/弹窗/按钮/输入/分页),开箱即用且风格一致
|
- **工程化完善**:TS 强类型、模块化 API、统一请求封装、权限化菜单/路由
|
||||||
- 工程规范:TypeScript 强类型、请求与错误拦截、模块化 API、权限化菜单/路由
|
- **设计统一**:全局“超现代风格”主题,卡片 / 弹窗 / 按钮 / 输入 / 分页风格一致
|
||||||
- 典型业务:
|
- **业务可复用**:
|
||||||
- 知识库管理:分类树、列表搜索、编辑/发布,已优化分类切换闪烁
|
- 知识库管理:分类树 + 列表搜索 + 编辑/发布
|
||||||
- YOLO 模型:模型管理、模型类别管理、告警处理中心、检测历史管理
|
- YOLO 模型:模型管理、模型类别管理、告警处理中心、检测历史管理
|
||||||
- 认证体系:登录/注册(字段对齐、错误信息透出),可扩展验证码
|
- AI 助手:多会话列表 + 消息记录 + 多布局对话界面(含错误调试信息)
|
||||||
- 体验友好:延迟 loading 避免闪烁、毛玻璃质感、统一按钮与交互反馈
|
- 认证体系:登录/注册、验证码
|
||||||
|
- **可扩展**:清晰的目录划分和命名规范,方便直接加模块或替换现有实现
|
||||||
|
|
||||||
## 🧩 技术栈
|
## 🧩 技术栈
|
||||||
|
|
||||||
@@ -31,28 +32,158 @@
|
|||||||
- 状态:Pinia
|
- 状态:Pinia
|
||||||
- 路由:Vue Router
|
- 路由:Vue Router
|
||||||
|
|
||||||
## 📦 目录结构(核心)
|
## 📦 项目结构与职责
|
||||||
|
|
||||||
```
|
> 根目录:`通用大模型模板/`
|
||||||
|
|
||||||
|
```bash
|
||||||
通用大模型模板/
|
通用大模型模板/
|
||||||
└─ hertz_server_diango_ui_2/ # 前端工程(Vite)
|
└─ hertz_server_diango_ui_2/ # 前端工程(Vite)
|
||||||
├─ public/ # 公共静态资源
|
├─ public/ # 公共静态资源(不走打包器)
|
||||||
├─ src/
|
├─ src/
|
||||||
│ ├─ api/ # 接口定义(auth、yolo、knowledge、…)
|
│ ├─ api/ # 接口定义(auth / yolo / knowledge / captcha / ai ...)
|
||||||
│ ├─ locales/ # 国际化
|
│ │ └─ yolo.ts # YOLO 模型 & 检测 & 类别相关 API
|
||||||
│ ├─ router/ # 路由与菜单(admin_menu.ts 自动化)
|
│ ├─ locales/ # 国际化文案
|
||||||
│ ├─ stores/ # Pinia
|
│ ├─ router/ # 路由与菜单配置
|
||||||
│ ├─ styles/ # 全局样式与变量(index.scss、variables.scss)
|
│ │ ├─ admin_menu.ts # 管理端菜单 + 路由映射(权限 key)
|
||||||
│ ├─ utils/ # 工具(请求、权限、URL 等)
|
│ │ ├─ user_menu_ai.ts # 用户端菜单 + 路由映射(含 AI 助手)
|
||||||
│ └─ views/ # 页面
|
│ │ └─ index.ts # Vue Router 实例 + 全局路由守卫
|
||||||
│ ├─ admin_page/ # 管理端模块
|
│ ├─ stores/ # Pinia Store
|
||||||
│ ├─ user_pages/ # 用户端模块
|
│ │ ├─ hertz_app.ts # 全局应用设置(语言、布局、菜单折叠等)
|
||||||
│ └─ register.vue / Login.vue # 登录注册
|
│ │ ├─ hertz_user.ts # 用户 / 鉴权状态
|
||||||
├─ index.html
|
│ │ └─ hertz_theme.ts # 主题配置与 CSS 变量
|
||||||
├─ package.json
|
│ ├─ styles/ # 全局样式与变量
|
||||||
└─ vite.config.ts
|
│ │ ├─ 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
|
```bash
|
||||||
@@ -62,74 +193,66 @@ cd hertz_server_diango_ui_2
|
|||||||
# 安装依赖
|
# 安装依赖
|
||||||
npm i
|
npm i
|
||||||
|
|
||||||
# 开发启动
|
# 开发启动(默认 http://localhost:3001)
|
||||||
npm run dev
|
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`
|
- `src/router/admin_menu.ts`:单文件维护管理端菜单树 + 路由映射 + 权限标识
|
||||||
- 全局统一:Modal/Drawer/Button/Input/Select/Table/Pagination…
|
- 面包屑逻辑已整理:不再重复展示“首页/”,只保留当前层级链路
|
||||||
- 专门处理闪烁与焦点态的视觉细节
|
|
||||||
|
|
||||||
- 菜单与路由
|
- **YOLO 模块**
|
||||||
- `src/router/admin_menu.ts`:单文件维护菜单与路由,支持权限过滤与自动生成
|
- `ModelManagement.vue`:模型上传 / 列表 / 启用、拖拽上传区
|
||||||
- 统一面包屑:已移除“首页/”的冗余展示,仅保留当前层级
|
- `AlertLevelManagement.vue`:模型类别管理,支持单条 & 批量修改告警等级
|
||||||
|
- `DetectionHistoryManagement.vue`:检测历史列表、图片/视频预览
|
||||||
|
|
||||||
- 知识库管理
|
- **认证模块**
|
||||||
- `src/views/admin_page/KnowledgeBaseManagement.vue`
|
|
||||||
- 分类树 + 列表搜索 + 编辑/发布
|
|
||||||
- 已优化分类切换闪烁(分类卡片不 Loading、表格 Loading 延迟)
|
|
||||||
|
|
||||||
- YOLO 模块
|
|
||||||
- 模型管理:上传/列表/启用禁用(苹果风格拖拽区与卡片)
|
|
||||||
- 模型类别管理:别名编辑、等级切换
|
|
||||||
- 告警处理中心:统计卡片、筛选、批量处理、详情预览
|
|
||||||
- 检测历史管理:搜索、筛选、对比查看(图片/视频),已移除“下载结果”按钮(后端未实现)
|
|
||||||
|
|
||||||
- 认证模块
|
|
||||||
- API:`src/api/auth.ts`
|
- API:`src/api/auth.ts`
|
||||||
- 注册页:`src/views/register.vue` 已与后端对齐字段
|
- 页面:`src/views/Login.vue`、`src/views/register.vue`
|
||||||
- 提交字段:`username, password, confirm_password, email, phone, real_name`
|
- 注册表单字段已与后端约定一致:
|
||||||
- 兼容字段:`captcha, captcha_id`(未启用可传空串)
|
`username, password, confirm_password, email, phone, real_name, captcha, captcha_id`
|
||||||
- 统一错误提示透出
|
|
||||||
|
|
||||||
## 🧪 常见问题(FAQ)
|
## 🧪 常见问题(FAQ)
|
||||||
|
|
||||||
- 按钮样式与其他页面不一致?
|
- **需要改哪些地方才能连上新的后端 IP?**
|
||||||
- 已在 `src/styles/index.scss` 对 `.ant-btn` 全局统一。若仍不一致,检查局部覆盖或第三方样式。
|
- 只改:`.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/views/admin_page/` 下新增页面,如 `FooBarManagement.vue`
|
||||||
- 对接后端:在 `src/api/` 创建对应接口文件,使用统一 `request` 封装
|
- 在 `src/router/admin_menu.ts` 中增加菜单配置(path + component + permission)
|
||||||
|
|
||||||
## 📜 脚本列表
|
- **扩展接口**
|
||||||
|
- 在 `src/api/` 新增 `xxx.ts`,导出 `xxxApi` 对象
|
||||||
|
- 使用统一的 `request` 封装(`hertz_request.ts`),保持错误处理一致
|
||||||
|
|
||||||
|
- **改造主题 / 品牌色**
|
||||||
|
- 修改 `src/styles/variables.scss` 中的主色、背景色、圆角、阴影
|
||||||
|
- 如需大改导航栏、卡片风格,优先在全局样式里做统一,而不是每页重新写
|
||||||
|
|
||||||
|
## 📜 NPM 脚本
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview"
|
"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']
|
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
|
||||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||||
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
||||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,10 @@
|
|||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"daisyui": "^5.1.13",
|
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.36.0",
|
||||||
"eslint-plugin-vue": "^10.4.0",
|
"eslint-plugin-vue": "^10.4.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"sass-embedded": "^1.93.0",
|
"sass-embedded": "^1.93.0",
|
||||||
"tailwindcss": "^4.1.13",
|
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.44.0",
|
"typescript-eslint": "^8.44.0",
|
||||||
"unplugin-vue-components": "^29.1.0",
|
"unplugin-vue-components": "^29.1.0",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const checkEnvironmentVariables = () => {
|
|||||||
|
|
||||||
// 在Vite中,环境变量可能通过define选项直接定义
|
// 在Vite中,环境变量可能通过define选项直接定义
|
||||||
// 或者通过import.meta.env读取
|
// 或者通过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 appTitle = import.meta.env.VITE_APP_TITLE || 'Hertz Admin'
|
||||||
const appVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
|
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 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 = [
|
const optionalVars = [
|
||||||
{ key: 'VITE_DEV_SERVER_HOST', value: devServerHost },
|
{ key: 'VITE_DEV_SERVER_HOST', value: devServerHost },
|
||||||
@@ -72,7 +72,7 @@ export const validateEnvironment = () => {
|
|||||||
|
|
||||||
// 获取API基础地址
|
// 获取API基础地址
|
||||||
export const getApiBaseUrl = (): string => {
|
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',
|
version: import.meta.env.VITE_APP_VERSION || '1.0.0',
|
||||||
apiBaseUrl: getApiBaseUrl(),
|
apiBaseUrl: getApiBaseUrl(),
|
||||||
devServerHost: import.meta.env.VITE_DEV_SERVER_HOST || 'localhost',
|
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(' 💡 提示: 可以在浏览器中直接访问这些路径')
|
||||||
|
|
||||||
console.log('\n🌐 可用链接:')
|
console.log('\n🌐 可用链接:')
|
||||||
console.log(' http://localhost:3001/ - 首页 (需要登录)')
|
console.log(' http://localhost:3000/ - 首页 (需要登录)')
|
||||||
console.log(' http://localhost:3001/login - 登录页面')
|
console.log(' http://localhost:3000/login - 登录页面')
|
||||||
console.log(' http://localhost:3001/dashboard - 仪表板 (需要登录)')
|
console.log(' http://localhost:3000/dashboard - 仪表板 (需要登录)')
|
||||||
console.log(' http://localhost:3001/user - 用户管理 (需要登录)')
|
console.log(' http://localhost:3000/user - 用户管理 (需要登录)')
|
||||||
console.log(' http://localhost:3001/profile - 个人资料 (需要登录)')
|
console.log(' http://localhost:3000/profile - 个人资料 (需要登录)')
|
||||||
console.log(' http://localhost:3001/settings - 系统设置 (需要登录)')
|
console.log(' http://localhost:3000/settings - 系统设置 (需要登录)')
|
||||||
console.log(' http://localhost:3001/test - 样式测试 (公开)')
|
console.log(' http://localhost:3000/test - 样式测试 (公开)')
|
||||||
console.log(' http://localhost:3001/websocket-test - WebSocket测试 (公开)')
|
console.log(' http://localhost:3000/websocket-test - WebSocket测试 (公开)')
|
||||||
console.log(' http://localhost:3001/demo - 动态路由演示 (公开)')
|
console.log(' http://localhost:3000/demo - 动态路由演示 (公开)')
|
||||||
console.log(' http://localhost:3001/any-other-path - 404页面 (公开)')
|
console.log(' http://localhost:3000/any-other-path - 404页面 (公开)')
|
||||||
|
|
||||||
console.log('\n✅ 路由配置加载完成!')
|
console.log('\n✅ 路由配置加载完成!')
|
||||||
console.log('💡 提示: 启动项目后会在控制台看到真正的动态路由信息')
|
console.log('💡 提示: 启动项目后会在控制台看到真正的动态路由信息')
|
||||||
|
|||||||
@@ -24,10 +24,25 @@ export function getFullFileUrl(relativePath: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 在生产环境中,拼接完整的URL
|
// 在生产环境中,拼接完整的URL
|
||||||
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001'
|
const baseURL = getBackendBaseUrl()
|
||||||
return `${baseURL}${relativePath}`
|
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
|
* 获取API基础URL
|
||||||
* @returns API基础URL
|
* @returns API基础URL
|
||||||
@@ -36,7 +51,7 @@ export function getApiBaseUrl(): string {
|
|||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
return '' // 开发环境使用空字符串,通过Vite代理
|
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) {
|
if (import.meta.env.DEV) {
|
||||||
return '' // 开发环境使用空字符串,通过Vite代理
|
return '' // 开发环境使用空字符串,通过Vite代理
|
||||||
}
|
}
|
||||||
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001'
|
const baseURL = getBackendBaseUrl()
|
||||||
return baseURL.replace('/api', '') // 移除/api后缀
|
return baseURL.replace('/api', '') // 移除/api后缀
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,13 @@
|
|||||||
</template>
|
</template>
|
||||||
刷新
|
刷新
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!selectedRowKeys.length"
|
||||||
|
@click="openBatchEdit"
|
||||||
|
>
|
||||||
|
批量修改等级
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-right">
|
<div class="action-right">
|
||||||
<a-input-search
|
<a-input-search
|
||||||
@@ -35,6 +42,7 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="pagination"
|
:pagination="pagination"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
|
:row-selection="rowSelection"
|
||||||
class="levels-table"
|
class="levels-table"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
@@ -180,6 +188,41 @@
|
|||||||
</a-form>
|
</a-form>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -193,7 +236,7 @@ import {
|
|||||||
EditOutlined,
|
EditOutlined,
|
||||||
PoweroffOutlined
|
PoweroffOutlined
|
||||||
} from '@ant-design/icons-vue'
|
} 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'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
@@ -201,19 +244,36 @@ const loading = ref(false)
|
|||||||
const editing = ref(false)
|
const editing = ref(false)
|
||||||
const levels = ref<AlertLevel[]>([])
|
const levels = ref<AlertLevel[]>([])
|
||||||
const searchKeyword = ref('')
|
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 detailModalVisible = ref(false)
|
||||||
const editModalVisible = ref(false)
|
const editModalVisible = ref(false)
|
||||||
const currentLevel = ref<AlertLevel | null>(null)
|
const currentLevel = ref<AlertLevel | null>(null)
|
||||||
|
const batchEditModalVisible = ref(false)
|
||||||
|
|
||||||
// 编辑表单
|
// 编辑表单
|
||||||
const editForm = reactive({
|
const editForm = reactive({
|
||||||
alert_level: 'low' as 'low' | 'medium' | 'high'
|
alert_level: 'low' as 'low' | 'medium' | 'high'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 批量编辑表单
|
||||||
|
const batchEditForm = reactive({
|
||||||
|
alert_level: 'low' as 'low' | 'medium' | 'high'
|
||||||
|
})
|
||||||
|
|
||||||
// 表单引用
|
// 表单引用
|
||||||
const editFormRef = ref()
|
const editFormRef = ref()
|
||||||
|
const batchEditFormRef = ref()
|
||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则
|
||||||
const editRules = {
|
const editRules = {
|
||||||
@@ -222,14 +282,22 @@ const editRules = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页配置
|
// 分页配置(受控分页)
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showQuickJumper: 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'
|
return colorMap[level] || 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取警告等级列表
|
// 获取警告等级列表(仅显示当前启用模型的类别)
|
||||||
const fetchLevels = async () => {
|
const fetchLevels = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
console.log('🔍 开始获取警告等级列表...')
|
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()
|
const response = await yoloApi.getAlertLevels()
|
||||||
console.log('📋 警告等级API响应:', response)
|
console.log('📋 警告等级API响应:', response)
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
levels.value = response.data
|
let data = response.data
|
||||||
pagination.total = response.data.length
|
|
||||||
|
// 如果有启用模型,则按模型过滤类别
|
||||||
|
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)
|
console.log('✅ 警告等级获取成功:', levels.value)
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ 获取警告等级失败:', response.message)
|
console.error('❌ 获取警告等级失败:', response.message)
|
||||||
@@ -400,28 +495,28 @@ const handleEdit = async () => {
|
|||||||
if (!currentLevel.value) return
|
if (!currentLevel.value) return
|
||||||
|
|
||||||
editing.value = true
|
editing.value = true
|
||||||
console.log('🔍 开始切换警告等级...', currentLevel.value.id, editForm)
|
console.log('开始切换警告等级...', currentLevel.value.id, editForm)
|
||||||
|
|
||||||
// 构造更新数据 - 只传递 alert_level
|
// 构造更新数据 - 只传递 alert_level
|
||||||
const updateData = {
|
const updateData = {
|
||||||
alert_level: editForm.alert_level
|
alert_level: editForm.alert_level
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📤 发送更新数据:', updateData)
|
console.log('发送更新数据:', updateData)
|
||||||
|
|
||||||
const response = await yoloApi.updateAlertLevel(currentLevel.value.id.toString(), updateData)
|
const response = await yoloApi.updateAlertLevel(currentLevel.value.id.toString(), updateData)
|
||||||
console.log('📋 更新警告等级API响应:', response)
|
console.log('更新警告等级API响应:', response)
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
message.success(`警告等级已切换为: ${editForm.alert_level}`)
|
message.success(`警告等级已切换为: ${editForm.alert_level}`)
|
||||||
editModalVisible.value = false
|
editModalVisible.value = false
|
||||||
fetchLevels()
|
fetchLevels()
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ 切换警告等级失败:', response.message)
|
console.error('切换警告等级失败:', response.message)
|
||||||
message.error(response.message || '切换失败')
|
message.error(response.message || '切换失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 切换警告等级异常:', error)
|
console.error('切换警告等级异常:', error)
|
||||||
message.error('切换失败')
|
message.error('切换失败')
|
||||||
} finally {
|
} finally {
|
||||||
editing.value = false
|
editing.value = false
|
||||||
@@ -434,13 +529,78 @@ const handleEditCancel = () => {
|
|||||||
currentLevel.value = null
|
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) => {
|
const toggleStatus = async (level: AlertLevel) => {
|
||||||
try {
|
try {
|
||||||
console.log('🔍 开始切换警告等级状态...', level.id)
|
console.log('开始切换警告等级状态...', level.id)
|
||||||
|
|
||||||
const response = await yoloApi.toggleAlertLevelStatus(level.id.toString())
|
const response = await yoloApi.toggleAlertLevelStatus(level.id.toString())
|
||||||
console.log('📋 切换状态API响应:', response)
|
console.log('切换状态API响应:', response)
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
message.success(level.is_active ? '警告等级已禁用' : '警告等级已启用')
|
message.success(level.is_active ? '警告等级已禁用' : '警告等级已启用')
|
||||||
@@ -457,7 +617,9 @@ const toggleStatus = async (level: AlertLevel) => {
|
|||||||
|
|
||||||
// 搜索处理
|
// 搜索处理
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
// 搜索逻辑已在computed中处理
|
// 搜索逻辑已在computed中处理,这里只负责重置分页
|
||||||
|
pagination.current = 1
|
||||||
|
pagination.total = filteredLevels.value.length
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件挂载时获取数据
|
// 组件挂载时获取数据
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ import {
|
|||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { yoloApi, type DetectionHistoryRecord, type YoloModel } from '@/api/yolo'
|
import { yoloApi, type DetectionHistoryRecord, type YoloModel } from '@/api/yolo'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import { getFullFileUrl } from '@/utils/hertz_url'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -467,8 +468,7 @@ const formatDate = (dateString: string) => {
|
|||||||
// 获取图片URL
|
// 获取图片URL
|
||||||
const getImageUrl = (filePath: string) => {
|
const getImageUrl = (filePath: string) => {
|
||||||
if (!filePath) return ''
|
if (!filePath) return ''
|
||||||
if (filePath.startsWith('http')) return filePath
|
return getFullFileUrl(filePath)
|
||||||
return `http://localhost:3001${filePath}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理图片加载错误
|
// 处理图片加载错误
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ import {
|
|||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
ReloadOutlined
|
ReloadOutlined
|
||||||
} from '@ant-design/icons-vue'
|
} 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 { yoloDetector, type YOLODetectionResult } from '@/utils/yolo_frontend'
|
||||||
import { yoloApi } from '@/api/yolo'
|
import { yoloApi } from '@/api/yolo'
|
||||||
|
|
||||||
@@ -477,7 +477,7 @@ const inferenceMode = ref<'pt' | 'onnx'>('pt')
|
|||||||
|
|
||||||
// WebSocket连接(PT推理模式使用)
|
// WebSocket连接(PT推理模式使用)
|
||||||
let ws: WebSocket | null = null
|
let ws: WebSocket | null = null
|
||||||
const wsUrl = 'ws://localhost:8000/ws/yolo/live/'
|
const wsUrl = `${getWsBaseUrl()}/ws/yolo/live/`
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -1342,7 +1342,7 @@ const handleOnnxUpload = async () => {
|
|||||||
|
|
||||||
// 如果URL不是完整路径,构建完整URL
|
// 如果URL不是完整路径,构建完整URL
|
||||||
if (!labelsDownloadUrl.startsWith('http')) {
|
if (!labelsDownloadUrl.startsWith('http')) {
|
||||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001'
|
const baseUrl = getBackendBaseUrl()
|
||||||
labelsDownloadUrl = labelsDownloadUrl.startsWith('/')
|
labelsDownloadUrl = labelsDownloadUrl.startsWith('/')
|
||||||
? `${baseUrl}${labelsDownloadUrl}`
|
? `${baseUrl}${labelsDownloadUrl}`
|
||||||
: `${baseUrl}/${labelsDownloadUrl}`
|
: `${baseUrl}/${labelsDownloadUrl}`
|
||||||
@@ -1417,7 +1417,7 @@ const handleOnnxUpload = async () => {
|
|||||||
|
|
||||||
// 如果URL不是完整路径,构建完整URL
|
// 如果URL不是完整路径,构建完整URL
|
||||||
if (!downloadUrl.startsWith('http')) {
|
if (!downloadUrl.startsWith('http')) {
|
||||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001'
|
const baseUrl = getBackendBaseUrl()
|
||||||
downloadUrl = downloadUrl.startsWith('/')
|
downloadUrl = downloadUrl.startsWith('/')
|
||||||
? `${baseUrl}${downloadUrl}`
|
? `${baseUrl}${downloadUrl}`
|
||||||
: `${baseUrl}/${downloadUrl}`
|
: `${baseUrl}/${downloadUrl}`
|
||||||
|
|||||||
@@ -918,6 +918,7 @@ import {
|
|||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { yoloApi, detectionHistoryApi, type YoloDetection, type YoloModel } from '@/api/yolo'
|
import { yoloApi, detectionHistoryApi, type YoloDetection, type YoloModel } from '@/api/yolo'
|
||||||
import { useUserStore } from '@/stores/hertz_user'
|
import { useUserStore } from '@/stores/hertz_user'
|
||||||
|
import { getFullFileUrl } from '@/utils/hertz_url'
|
||||||
|
|
||||||
// 布局模式
|
// 布局模式
|
||||||
const layoutMode = ref<'classic' | 'vertical' | 'grid'>('classic')
|
const layoutMode = ref<'classic' | 'vertical' | 'grid'>('classic')
|
||||||
@@ -1141,25 +1142,14 @@ const startDetection = async () => {
|
|||||||
console.log('检测API响应:', response)
|
console.log('检测API响应:', response)
|
||||||
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
// 构建完整的图片URL
|
// 构建完整的图片URL(统一通过工具函数处理)
|
||||||
const baseUrl = 'http://localhost:3001'
|
|
||||||
|
|
||||||
console.log('🔍 原始URL数据:', {
|
console.log('🔍 原始URL数据:', {
|
||||||
original_file_url: response.data.original_file_url,
|
original_file_url: response.data.original_file_url,
|
||||||
result_file_url: response.data.result_file_url
|
result_file_url: response.data.result_file_url
|
||||||
})
|
})
|
||||||
|
|
||||||
// 确保URL路径正确
|
let originalImageUrl = getFullFileUrl(response.data.original_file_url)
|
||||||
let originalImageUrl = response.data.original_file_url
|
let resultImageUrl = getFullFileUrl(response.data.result_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}`
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🖼️ 构建后的图片URL:', {
|
console.log('🖼️ 构建后的图片URL:', {
|
||||||
original: originalImageUrl,
|
original: originalImageUrl,
|
||||||
|
|||||||
@@ -4362,6 +4362,11 @@ const handleLogout = () => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
line-height: 62px;
|
line-height: 62px;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
:deep(.ant-menu) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ant-menu-item),
|
:deep(.ant-menu-item),
|
||||||
:deep(.ant-menu-submenu) {
|
: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 vue from '@vitejs/plugin-vue'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
import fs from 'fs'
|
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: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
modelsManifestPlugin(),
|
modelsManifestPlugin(),
|
||||||
@@ -228,7 +233,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
// API代理转发到后端服务器
|
// API代理转发到后端服务器
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8000',
|
target: backendOrigin,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||||
@@ -267,7 +272,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
// 媒体文件代理转发到后端服务器
|
// 媒体文件代理转发到后端服务器
|
||||||
'/media': {
|
'/media': {
|
||||||
target: 'http://localhost:8000',
|
target: backendOrigin,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/media/, '/media'),
|
rewrite: (path) => path.replace(/^\/media/, '/media'),
|
||||||
@@ -308,7 +313,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
// 环境变量定义,确保在没有.env文件时也能正常工作
|
// 环境变量定义,确保在没有.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_TITLE__: JSON.stringify('Hertz Admin'),
|
||||||
__VITE_APP_VERSION__: JSON.stringify('1.0.0'),
|
__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