更新
11
.env
@@ -3,13 +3,14 @@ SECRET_KEY=django-insecure-0a1bx*8!97l^4z#ml#ufn_*9ut*)zlso$*k-g^h&(2=p@^51md
|
||||
DEBUG=True
|
||||
#ALLOWED_HOSTS=localhost,127.0.0.1,django-host,192.168.1.22
|
||||
ALLOWED_HOSTS=*
|
||||
# Database Configuration
|
||||
# 切换数据源,支持sqlite/mysql
|
||||
DB_ENGINE=sqlite
|
||||
USE_REDIS_AS_DB=True
|
||||
|
||||
# MySQL Configuration (when USE_REDIS_AS_DB=False)
|
||||
DB_NAME=hertz_server
|
||||
DB_USER=root
|
||||
DB_PASSWORD=root
|
||||
DB_PASSWORD=123456
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
|
||||
@@ -27,9 +28,9 @@ EMAIL_HOST=smtp.qq.com
|
||||
EMAIL_PORT=465
|
||||
EMAIL_USE_SSL=True
|
||||
EMAIL_USE_TLS=False
|
||||
EMAIL_HOST_USER=your_email@example.com
|
||||
EMAIL_HOST_PASSWORD=your_email_password_or_app_key
|
||||
DEFAULT_FROM_EMAIL=your_email@example.com
|
||||
EMAIL_HOST_USER=your_email@qq.com
|
||||
EMAIL_HOST_PASSWORD=your_email_password
|
||||
DEFAULT_FROM_EMAIL=your_email@qq.com
|
||||
|
||||
# 注册邮箱验证码开关(0=关闭,1=开启)
|
||||
REGISTER_EMAIL_VERIFICATION=0
|
||||
|
||||
3
.gitignore
vendored
@@ -6,6 +6,9 @@ __pycache__/
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# python envs
|
||||
venv/
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
|
||||
BIN
data/db.sqlite3
@@ -58,7 +58,7 @@ def generate_crud_menu(args):
|
||||
print("请重启服务器以同步菜单到数据库")
|
||||
|
||||
|
||||
def main():
|
||||
def menu_generator_main():
|
||||
parser = argparse.ArgumentParser(description='菜单生成器')
|
||||
subparsers = parser.add_subparsers(dest='command', help='可用命令')
|
||||
|
||||
@@ -81,4 +81,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
menu_generator_main()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# API 基础地址
|
||||
VITE_API_BASE_URL=http://127.0.0.1:8000
|
||||
VITE_API_BASE_URL=http://192.168.124.23:8000
|
||||
|
||||
# 应用配置
|
||||
VITE_APP_TITLE=Hertz Admin
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
VITE_API_BASE_URL=http://127.0.0.1:8000
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
VITE_TEMPLATE_SETUP_MODE=true
|
||||
@@ -1 +1,2 @@
|
||||
VITE_API_BASE_URL=http://127.0.0.1:8000
|
||||
VITE_API_BASE_URL=http://192.168.124.40:8002
|
||||
VITE_TEMPLATE_SETUP_MODE=true
|
||||
@@ -17,7 +17,7 @@
|
||||
- **工程化完善**:TS 强类型、模块化 API、统一请求封装、权限化菜单/路由
|
||||
- **设计统一**:全局“超现代风格”主题,卡片 / 弹窗 / 按钮 / 输入 / 分页风格一致
|
||||
- **业务可复用**:
|
||||
- 知识库管理:分类树 + 列表搜索 + 编辑/发布
|
||||
- 文章管理:分类树 + 列表搜索 + 编辑/发布
|
||||
- YOLO 模型:模型管理、模型类别管理、告警处理中心、检测历史管理
|
||||
- AI 助手:多会话列表 + 消息记录 + 多布局对话界面(含错误调试信息)
|
||||
- 认证体系:登录/注册、验证码
|
||||
@@ -246,6 +246,72 @@ npm run dev
|
||||
- 修改 `src/styles/variables.scss` 中的主色、背景色、圆角、阴影
|
||||
- 如需大改导航栏、卡片风格,优先在全局样式里做统一,而不是每页重新写
|
||||
|
||||
## 🧩 模块选择与模板模式
|
||||
|
||||
- **模块配置文件**
|
||||
- 路径:`src/config/hertz_modules.ts`
|
||||
- 内容:
|
||||
- 使用 `HERTZ_MODULES` 统一管理“管理端 / 用户端”各功能模块
|
||||
- 每个模块包含:`key`(模块标识)、`label`(展示名称)、`group`(admin/user)、`defaultEnabled`(是否默认启用)
|
||||
- 运行时通过 `isModuleEnabled` / `getEnabledModuleKeys` 控制路由和菜单是否展示对应模块。
|
||||
|
||||
- **模块选择页面(功能 DIY)**
|
||||
- 页面:`src/views/ModuleSetup.vue`
|
||||
- 路由:`/template/modules`
|
||||
- 说明:
|
||||
1. 勾选需要启用的模块,未勾选的模块在菜单和路由中隐藏(仅运行时屏蔽,不改动源码)。
|
||||
2. 点击“保存配置并刷新”可多次预览效果;点击“保存并跳转登录”会在保存后跳转到登录页。
|
||||
3. 选择结果会以 `hertz_enabled_modules` 的形式保存在浏览器 Local Storage 中。
|
||||
|
||||
- **模板模式开关**
|
||||
- 通过环境变量控制:`VITE_TEMPLATE_SETUP_MODE`
|
||||
- 建议在开发环境 (`.env.development`) 中开启:
|
||||
|
||||
```bash
|
||||
VITE_TEMPLATE_SETUP_MODE=true
|
||||
```
|
||||
|
||||
- 当模板模式开启且浏览器中 **没有** `hertz_enabled_modules` 记录时,路由守卫会在首次进入时自动重定向到 `/template/modules`,强制先完成模块选择。
|
||||
- 如果已经配置过模块,下次 `npm run dev` 将直接进入系统。如需重新进入模块选择页:
|
||||
1. 打开浏览器开发者工具 → Application → Local Storage
|
||||
2. 选择当前站点,删除键 `hertz_enabled_modules`
|
||||
3. 刷新页面即可再次进入模块选择流程。
|
||||
|
||||
## ✂️ 一键裁剪(npm run prune)
|
||||
|
||||
> 适用于已经确定“哪些功能模块不再需要”的场景,用于真正瘦身前端代码体积。建议在执行前先提交一次 Git。
|
||||
|
||||
- **脚本位置与命令**
|
||||
- 脚本:`scripts/prune-modules.mjs`
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
npm run prune
|
||||
```
|
||||
|
||||
- **推荐使用流程**
|
||||
1. 启动开发环境:`npm run dev`。
|
||||
2. 打开 `/template/modules`,通过勾选确认“需要保留的模块”,用“保存配置并刷新”反复调试菜单/路由效果。
|
||||
3. 确认无误后,关闭开发服务器。
|
||||
4. 在终端执行 `npm run prune`,按照 CLI 提示:
|
||||
- 选择要“裁剪掉”的模块(通常是你在模块选择页面中未勾选的模块)。
|
||||
- 选择裁剪模式:
|
||||
- **模式 1:仅屏蔽**
|
||||
- 修改 `admin_menu.ts` / `user_menu_ai.ts` 中对应模块的 `moduleKey`,加上 `__pruned__` 前缀
|
||||
- 注释组件映射行,使这些模块在菜单和路由中完全隐藏
|
||||
- **不删除任何 `.vue` 文件**,方便后续恢复
|
||||
- **模式 2:删除**
|
||||
- 在模式 1 的基础上,额外删除对应模块的视图文件,如 `src/views/admin_page/UserManagement.vue` 等
|
||||
- 这是不可逆操作,建议先在模式 1 下验证,再使用模式 2 做最终瘦身
|
||||
|
||||
- **影响范围(前端)**
|
||||
- 管理端:
|
||||
- `src/router/admin_menu.ts` 中对应模块的菜单配置和组件映射
|
||||
- `src/views/admin_page/*.vue` 中不需要的页面(仅在删除模式下移除)
|
||||
- 用户端:
|
||||
- `src/router/user_menu_ai.ts` 中对应模块配置
|
||||
- `src/views/user_pages/*.vue` 中不需要的页面(仅在删除模式下移除)
|
||||
|
||||
## 📜 NPM 脚本
|
||||
|
||||
```json
|
||||
@@ -253,7 +319,8 @@ npm run dev
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"prune": "node scripts/prune-modules.mjs"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1
hertz_server_diango_ui/components.d.ts
vendored
@@ -16,6 +16,7 @@ declare module 'vue' {
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ACard: typeof import('ant-design-vue/es')['Card']
|
||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
||||
ACol: typeof import('ant-design-vue/es')['Col']
|
||||
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
|
||||
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
|
||||
|
||||
19
hertz_server_diango_ui/package-lock.json
generated
@@ -25,12 +25,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",
|
||||
@@ -2663,16 +2661,6 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.1.13",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.1.13.tgz",
|
||||
"integrity": "sha512-KWPF/4R+EHTJRqKZFNmSDPfAZ5xeS6YWB/2kS7Y6wGKg+atscUi2DOp6HoDD/OgGML0PJTtTpgwpTfeHVfjk7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
@@ -5141,13 +5129,6 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"prune": "node scripts/prune-modules.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^24.5.2",
|
||||
|
||||
392
hertz_server_diango_ui/scripts/prune-modules.mjs
Normal file
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// 一键裁剪脚本:根据功能模块删除或屏蔽对应的菜单配置和页面文件
|
||||
// 设计原则:
|
||||
// - 先通过运行时模块开关/页面确认要保留哪些模块
|
||||
// - 然后运行本脚本,选择要“裁剪掉”的模块,以及裁剪模式:
|
||||
// 1) 仅屏蔽(修改 moduleKey,使其永远不会被启用,保留页面文件)
|
||||
// 2) 删除(在 1 的基础上,再删除对应 .vue 页面文件)
|
||||
// - 脚本只操作前端代码,不影响后端
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import readline from 'readline'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const projectRoot = path.resolve(__dirname, '..')
|
||||
|
||||
/** 模块定义(与 src/config/hertz_modules.ts 保持一致) */
|
||||
const MODULES = [
|
||||
{ key: 'admin.user-management', label: '管理端 · 用户管理', group: 'admin' },
|
||||
{ key: 'admin.department-management', label: '管理端 · 部门管理', group: 'admin' },
|
||||
{ key: 'admin.menu-management', label: '管理端 · 菜单管理', group: 'admin' },
|
||||
{ key: 'admin.role-management', label: '管理端 · 角色管理', group: 'admin' },
|
||||
{ key: 'admin.notification-management', label: '管理端 · 通知管理', group: 'admin' },
|
||||
{ key: 'admin.log-management', label: '管理端 · 日志管理', group: 'admin' },
|
||||
{ key: 'admin.knowledge-base', label: '管理端 · 文章管理', group: 'admin' },
|
||||
{ key: 'admin.yolo-model', label: '管理端 · YOLO 模型相关', group: 'admin' },
|
||||
|
||||
{ key: 'user.system-monitor', label: '用户端 · 系统监控', group: 'user' },
|
||||
{ key: 'user.ai-chat', label: '用户端 · AI 助手', group: 'user' },
|
||||
{ key: 'user.yolo-detection', label: '用户端 · YOLO 检测', group: 'user' },
|
||||
{ key: 'user.live-detection', label: '用户端 · 实时检测', group: 'user' },
|
||||
{ key: 'user.detection-history', label: '用户端 · 检测历史', group: 'user' },
|
||||
{ key: 'user.alert-center', label: '用户端 · 告警中心', group: 'user' },
|
||||
{ key: 'user.notice-center', label: '用户端 · 通知中心', group: 'user' },
|
||||
{ key: 'user.knowledge-center', label: '用户端 · 知识库中心', group: 'user' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 每个模块对应的裁剪配置:
|
||||
* - adminModuleKey / userModuleKey: 在路由配置文件中的 moduleKey 值
|
||||
* - adminComponentNames / userComponentNames: 在组件映射对象中的组件名(*.vue)
|
||||
* - viewFiles: 可以安全删除的页面文件(相对项目根路径)
|
||||
*/
|
||||
const PRUNE_CONFIG = {
|
||||
'admin.user-management': {
|
||||
adminModuleKey: 'admin.user-management',
|
||||
adminComponentNames: ['UserManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/UserManagement.vue'],
|
||||
},
|
||||
'admin.department-management': {
|
||||
adminModuleKey: 'admin.department-management',
|
||||
adminComponentNames: ['DepartmentManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/DepartmentManagement.vue'],
|
||||
},
|
||||
'admin.menu-management': {
|
||||
adminModuleKey: 'admin.menu-management',
|
||||
adminComponentNames: ['MenuManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/MenuManagement.vue'],
|
||||
},
|
||||
'admin.role-management': {
|
||||
adminModuleKey: 'admin.role-management',
|
||||
adminComponentNames: ['Role.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/Role.vue'],
|
||||
},
|
||||
'admin.notification-management': {
|
||||
adminModuleKey: 'admin.notification-management',
|
||||
adminComponentNames: ['NotificationManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/NotificationManagement.vue'],
|
||||
},
|
||||
'admin.log-management': {
|
||||
adminModuleKey: 'admin.log-management',
|
||||
adminComponentNames: ['LogManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/LogManagement.vue'],
|
||||
},
|
||||
'admin.knowledge-base': {
|
||||
adminModuleKey: 'admin.knowledge-base',
|
||||
adminComponentNames: ['KnowledgeBaseManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/KnowledgeBaseManagement.vue'],
|
||||
},
|
||||
'admin.yolo-model': {
|
||||
adminModuleKey: 'admin.yolo-model',
|
||||
adminComponentNames: [
|
||||
'ModelManagement.vue',
|
||||
'AlertLevelManagement.vue',
|
||||
'AlertProcessingCenter.vue',
|
||||
'DetectionHistoryManagement.vue',
|
||||
],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: [
|
||||
'src/views/admin_page/ModelManagement.vue',
|
||||
'src/views/admin_page/AlertLevelManagement.vue',
|
||||
'src/views/admin_page/AlertProcessingCenter.vue',
|
||||
'src/views/admin_page/DetectionHistoryManagement.vue',
|
||||
],
|
||||
},
|
||||
'user.system-monitor': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.system-monitor',
|
||||
userComponentNames: ['SystemMonitor.vue'],
|
||||
viewFiles: ['src/views/user_pages/SystemMonitor.vue'],
|
||||
},
|
||||
'user.ai-chat': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.ai-chat',
|
||||
userComponentNames: ['AiChat.vue'],
|
||||
viewFiles: ['src/views/user_pages/AiChat.vue'],
|
||||
},
|
||||
'user.yolo-detection': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.yolo-detection',
|
||||
userComponentNames: ['YoloDetection.vue'],
|
||||
viewFiles: ['src/views/user_pages/YoloDetection.vue'],
|
||||
},
|
||||
'user.live-detection': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.live-detection',
|
||||
userComponentNames: ['LiveDetection.vue'],
|
||||
viewFiles: ['src/views/user_pages/LiveDetection.vue'],
|
||||
},
|
||||
'user.detection-history': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.detection-history',
|
||||
userComponentNames: ['DetectionHistory.vue'],
|
||||
viewFiles: ['src/views/user_pages/DetectionHistory.vue'],
|
||||
},
|
||||
'user.alert-center': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.alert-center',
|
||||
userComponentNames: ['AlertCenter.vue'],
|
||||
viewFiles: ['src/views/user_pages/AlertCenter.vue'],
|
||||
},
|
||||
'user.notice-center': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.notice-center',
|
||||
userComponentNames: ['NoticeCenter.vue'],
|
||||
viewFiles: ['src/views/user_pages/NoticeCenter.vue'],
|
||||
},
|
||||
'user.knowledge-center': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.knowledge-center',
|
||||
userComponentNames: ['KnowledgeCenter.vue'],
|
||||
// 注意:这里只删除 KnowledgeCenter.vue,保留 KnowledgeDetail.vue,避免复杂路由修改
|
||||
viewFiles: ['src/views/user_pages/KnowledgeCenter.vue'],
|
||||
},
|
||||
}
|
||||
|
||||
const ADMIN_MENU_FILE = 'src/router/admin_menu.ts'
|
||||
const USER_MENU_FILE = 'src/router/user_menu_ai.ts'
|
||||
|
||||
/**
|
||||
* 简单的 CLI 交互封装
|
||||
*/
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||
|
||||
function ask(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer.trim())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function resolvePath(relativePath) {
|
||||
return path.resolve(projectRoot, relativePath)
|
||||
}
|
||||
|
||||
function readTextFile(relativePath) {
|
||||
const full = resolvePath(relativePath)
|
||||
if (!fs.existsSync(full)) {
|
||||
return null
|
||||
}
|
||||
return fs.readFileSync(full, 'utf8')
|
||||
}
|
||||
|
||||
function writeTextFile(relativePath, content) {
|
||||
const full = resolvePath(relativePath)
|
||||
fs.writeFileSync(full, content, 'utf8')
|
||||
}
|
||||
|
||||
function commentComponentLines(content, componentNames) {
|
||||
if (!componentNames || componentNames.length === 0) return content
|
||||
const lines = content.split('\n')
|
||||
const nameSet = new Set(componentNames)
|
||||
|
||||
const updated = lines.map((line) => {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith('//')) return line
|
||||
|
||||
for (const name of nameSet) {
|
||||
if (line.includes(`'${name}'`)) {
|
||||
return `// ${line}`
|
||||
}
|
||||
}
|
||||
return line
|
||||
})
|
||||
|
||||
return updated.join('\n')
|
||||
}
|
||||
|
||||
function updateModuleKey(content, originalKey) {
|
||||
if (!originalKey) return content
|
||||
const patterns = [
|
||||
`moduleKey: '${originalKey}'`,
|
||||
`moduleKey: "${originalKey}"`,
|
||||
]
|
||||
const pruned = `moduleKey: '__pruned__${originalKey}'`
|
||||
|
||||
if (content.includes(pruned)) {
|
||||
return content
|
||||
}
|
||||
|
||||
let updated = content
|
||||
let found = false
|
||||
|
||||
for (const p of patterns) {
|
||||
if (updated.includes(p)) {
|
||||
updated = updated.replace(p, pruned)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
console.warn(`⚠️ 未在文件中找到 moduleKey: ${originalKey}`)
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
function collectViewFilesForModules(selectedKeys) {
|
||||
const files = new Set()
|
||||
for (const key of selectedKeys) {
|
||||
const cfg = PRUNE_CONFIG[key]
|
||||
if (!cfg) continue
|
||||
for (const f of cfg.viewFiles) {
|
||||
files.add(f)
|
||||
}
|
||||
}
|
||||
return Array.from(files)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('===== Hertz 模板 · 一键裁剪脚本 =====')
|
||||
console.log('说明:')
|
||||
console.log('1. 建议先在浏览器里通过“模板模式 + 模块选择页”确认要保留的模块')
|
||||
console.log('2. 然后关闭 dev 服务器,运行本脚本选择要裁剪掉的模块')
|
||||
console.log('3. 先可选择“仅屏蔽”,确认无误后,再选择“删除”彻底缩减代码体积')
|
||||
console.log('')
|
||||
|
||||
console.log('当前可裁剪模块:')
|
||||
MODULES.forEach((m, index) => {
|
||||
console.log(`${index + 1}. [${m.group}] ${m.label} (${m.key})`)
|
||||
})
|
||||
console.log('')
|
||||
|
||||
const indexAnswer = await ask('请输入要“裁剪掉”的模块序号(多个用逗号分隔,例如 2,4,7),或直接回车取消:')
|
||||
if (!indexAnswer) {
|
||||
console.log('未选择任何模块,退出。')
|
||||
rl.close()
|
||||
return
|
||||
}
|
||||
|
||||
const indexes = indexAnswer
|
||||
.split(',')
|
||||
.map((s) => parseInt(s.trim(), 10))
|
||||
.filter((n) => !Number.isNaN(n) && n >= 1 && n <= MODULES.length)
|
||||
|
||||
if (indexes.length === 0) {
|
||||
console.log('未解析出有效的序号,退出。')
|
||||
rl.close()
|
||||
return
|
||||
}
|
||||
|
||||
const selectedModules = Array.from(new Set(indexes.map((i) => MODULES[i - 1])))
|
||||
console.log('\n将要裁剪的模块:')
|
||||
selectedModules.forEach((m) => {
|
||||
console.log(`- [${m.group}] ${m.label} (${m.key})`)
|
||||
})
|
||||
|
||||
console.log('\n裁剪模式:')
|
||||
console.log('1) 仅屏蔽模块:')
|
||||
console.log(' - 修改 router 配置中的 moduleKey 为 __pruned__...')
|
||||
console.log(' - 生成的菜单和路由中将完全隐藏这些模块')
|
||||
console.log(' - 不删除任何 .vue 页面文件(可随时恢复)')
|
||||
console.log('2) 删除模块:')
|
||||
console.log(' - 在 1 的基础上,额外删除对应的 .vue 页面文件')
|
||||
console.log(' - 删除操作不可逆,请确保已经提交或备份代码\n')
|
||||
|
||||
const modeAnswer = await ask('请选择裁剪模式(1 = 仅屏蔽,2 = 删除):')
|
||||
const mode = modeAnswer === '2' ? 'delete' : 'comment'
|
||||
|
||||
const viewFiles = collectViewFilesForModules(selectedModules.map((m) => m.key))
|
||||
|
||||
console.log('\n即将进行如下修改:')
|
||||
console.log('- 修改文件: src/router/admin_menu.ts(按需)')
|
||||
console.log('- 修改文件: src/router/user_menu_ai.ts(按需)')
|
||||
if (mode === 'delete') {
|
||||
console.log('- 删除页面文件:')
|
||||
viewFiles.forEach((f) => console.log(` · ${f}`))
|
||||
} else {
|
||||
console.log('- 不删除任何页面文件,仅屏蔽模块')
|
||||
}
|
||||
|
||||
const confirm = await ask('\n确认执行这些修改吗?(y/N): ')
|
||||
if (confirm.toLowerCase() !== 'y') {
|
||||
console.log('已取消操作。')
|
||||
rl.close()
|
||||
return
|
||||
}
|
||||
|
||||
// 1) 修改 admin_menu.ts
|
||||
let adminMenuContent = readTextFile(ADMIN_MENU_FILE)
|
||||
if (adminMenuContent) {
|
||||
const adminKeys = selectedModules.map((m) => m.key).filter((k) => PRUNE_CONFIG[k]?.adminModuleKey)
|
||||
if (adminKeys.length > 0) {
|
||||
for (const key of adminKeys) {
|
||||
const cfg = PRUNE_CONFIG[key]
|
||||
adminMenuContent = updateModuleKey(adminMenuContent, cfg.adminModuleKey)
|
||||
adminMenuContent = commentComponentLines(adminMenuContent, cfg.adminComponentNames)
|
||||
}
|
||||
writeTextFile(ADMIN_MENU_FILE, adminMenuContent)
|
||||
console.log('✅ 已更新 src/router/admin_menu.ts')
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 修改 user_menu_ai.ts
|
||||
let userMenuContent = readTextFile(USER_MENU_FILE)
|
||||
if (userMenuContent) {
|
||||
const userKeys = selectedModules.map((m) => m.key).filter((k) => PRUNE_CONFIG[k]?.userModuleKey)
|
||||
if (userKeys.length > 0) {
|
||||
for (const key of userKeys) {
|
||||
const cfg = PRUNE_CONFIG[key]
|
||||
userMenuContent = updateModuleKey(userMenuContent, cfg.userModuleKey)
|
||||
userMenuContent = commentComponentLines(userMenuContent, cfg.userComponentNames)
|
||||
}
|
||||
writeTextFile(USER_MENU_FILE, userMenuContent)
|
||||
console.log('✅ 已更新 src/router/user_menu_ai.ts')
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 删除 .vue 页面文件(仅在 delete 模式下)
|
||||
if (mode === 'delete') {
|
||||
console.log('\n开始删除页面文件...')
|
||||
for (const relative of viewFiles) {
|
||||
const full = resolvePath(relative)
|
||||
if (fs.existsSync(full)) {
|
||||
fs.rmSync(full)
|
||||
console.log(`🗑️ 已删除: ${relative}`)
|
||||
} else {
|
||||
console.log(`⚠️ 文件不存在,跳过: ${relative}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n🎉 裁剪完成。建议执行以下操作检查:')
|
||||
console.log('- 重新运行: npm run dev')
|
||||
console.log('- 在浏览器中确认菜单和路由是否符合预期')
|
||||
console.log('- 如需恢复,请使用 Git 回退或重新拷贝模板')
|
||||
|
||||
rl.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('执行过程中发生错误:', err)
|
||||
rl.close()
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -14,3 +14,4 @@ export * from './ai'
|
||||
// export * from './admin'
|
||||
export * from './log'
|
||||
export * from './knowledge'
|
||||
export * from './kb'
|
||||
|
||||
131
hertz_server_diango_ui/src/api/kb.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用响应结构(与后端 HertzResponse 对齐)
|
||||
export interface KbApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 知识库条目
|
||||
export interface KbItem {
|
||||
id: number
|
||||
title: string
|
||||
modality: 'text' | 'code' | 'image' | 'audio' | 'video' | string
|
||||
source_type: 'text' | 'file' | 'url' | string
|
||||
chunk_count?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
created_chunk_count?: number
|
||||
// 允许后端扩展字段
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface KbItemListParams {
|
||||
query?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface KbItemListData {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
list: KbItem[]
|
||||
}
|
||||
|
||||
// 语义搜索
|
||||
export interface KbSearchParams {
|
||||
q: string
|
||||
k?: number
|
||||
}
|
||||
|
||||
// 问答(RAG)
|
||||
export interface KbQaPayload {
|
||||
question: string
|
||||
k?: number
|
||||
}
|
||||
|
||||
export interface KbQaData {
|
||||
answer: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// 图谱查询参数(实体 / 关系)
|
||||
export interface KbGraphListParams {
|
||||
query?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
// 关系检索可选参数
|
||||
source?: number
|
||||
target?: number
|
||||
relation_type?: string
|
||||
}
|
||||
|
||||
export const kbApi = {
|
||||
// 知识库条目:列表
|
||||
listItems(params?: KbItemListParams): Promise<KbApiResponse<KbItemListData>> {
|
||||
return request.get('/api/kb/items/list/', { params })
|
||||
},
|
||||
|
||||
// 语义搜索
|
||||
search(params: KbSearchParams): Promise<KbApiResponse<any>> {
|
||||
return request.get('/api/kb/search/', { params })
|
||||
},
|
||||
|
||||
// 问答(RAG)
|
||||
qa(payload: KbQaPayload): Promise<KbApiResponse<KbQaData>> {
|
||||
return request.post('/api/kb/qa/', payload)
|
||||
},
|
||||
|
||||
// 图谱:实体列表
|
||||
listEntities(params?: KbGraphListParams): Promise<KbApiResponse<any>> {
|
||||
return request.get('/api/kb/graph/entities/', { params })
|
||||
},
|
||||
|
||||
// 图谱:关系列表
|
||||
listRelations(params?: KbGraphListParams): Promise<KbApiResponse<any>> {
|
||||
return request.get('/api/kb/graph/relations/', { params })
|
||||
},
|
||||
|
||||
// 知识库条目:创建(JSON 文本)
|
||||
createItemJson(payload: { title: string; modality?: string; source_type?: string; content?: string; metadata?: any }): Promise<KbApiResponse<KbItem>> {
|
||||
return request.post('/api/kb/items/create/', payload)
|
||||
},
|
||||
|
||||
// 知识库条目:创建(文件上传)
|
||||
createItemFile(formData: FormData): Promise<KbApiResponse<KbItem>> {
|
||||
return request.post('/api/kb/items/create/', formData)
|
||||
},
|
||||
|
||||
// 图谱:创建实体
|
||||
createEntity(payload: { name: string; type: string; properties?: any }): Promise<KbApiResponse<any>> {
|
||||
return request.post('/api/kb/graph/entities/', payload)
|
||||
},
|
||||
|
||||
// 图谱:更新实体
|
||||
updateEntity(id: number, payload: { name?: string; type?: string; properties?: any }): Promise<KbApiResponse<any>> {
|
||||
return request.put(`/api/kb/graph/entities/${id}/`, payload)
|
||||
},
|
||||
|
||||
// 图谱:删除实体
|
||||
deleteEntity(id: number): Promise<KbApiResponse<null>> {
|
||||
return request.delete(`/api/kb/graph/entities/${id}/`)
|
||||
},
|
||||
|
||||
// 图谱:创建关系
|
||||
createRelation(payload: { source: number; target: number; relation_type: string; properties?: any; source_chunk?: number }): Promise<KbApiResponse<any>> {
|
||||
return request.post('/api/kb/graph/relations/', payload)
|
||||
},
|
||||
|
||||
// 图谱:删除关系
|
||||
deleteRelation(id: number): Promise<KbApiResponse<null>> {
|
||||
return request.delete(`/api/kb/graph/relations/${id}/`)
|
||||
},
|
||||
|
||||
// 图谱:自动抽取实体与关系
|
||||
extractGraph(payload: { text?: string; item_id?: number }): Promise<KbApiResponse<{ entities: number; relations: number }>> {
|
||||
return request.post('/api/kb/graph/extract/', payload)
|
||||
},
|
||||
}
|
||||
@@ -103,6 +103,12 @@ export const userApi = {
|
||||
return request.put('/api/auth/user/info/update/', data)
|
||||
},
|
||||
|
||||
uploadAvatar: (file: File): Promise<ApiResponse<User>> => {
|
||||
const formData = new FormData()
|
||||
formData.append('avatar', file)
|
||||
return request.upload('/api/auth/user/avatar/upload/', formData)
|
||||
},
|
||||
|
||||
// 分配用户角色
|
||||
assignRoles: (data: AssignRolesParams): Promise<ApiResponse<any>> => {
|
||||
return request.post('/api/users/assign-roles/', data)
|
||||
|
||||
@@ -67,6 +67,114 @@ export interface YoloModelListResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// 数据集管理相关类型
|
||||
export interface YoloDatasetSummary {
|
||||
id: number
|
||||
name: string
|
||||
version?: string
|
||||
root_folder_path: string
|
||||
data_yaml_path: string
|
||||
nc?: number
|
||||
description?: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
export interface YoloDatasetDetail extends YoloDatasetSummary {
|
||||
names?: string[]
|
||||
train_images_count?: number
|
||||
train_labels_count?: number
|
||||
val_images_count?: number
|
||||
val_labels_count?: number
|
||||
test_images_count?: number
|
||||
test_labels_count?: number
|
||||
}
|
||||
|
||||
export interface YoloDatasetSampleItem {
|
||||
image: string
|
||||
image_size?: number
|
||||
label?: string
|
||||
filename: string
|
||||
}
|
||||
|
||||
// YOLO 训练任务相关类型
|
||||
export type YoloTrainStatus =
|
||||
| 'queued'
|
||||
| 'running'
|
||||
| 'canceling'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'canceled'
|
||||
|
||||
export interface YoloTrainDatasetOption {
|
||||
id: number
|
||||
name: string
|
||||
version?: string
|
||||
yaml: string
|
||||
}
|
||||
|
||||
export interface YoloTrainVersionOption {
|
||||
family: 'v8' | '11' | '12'
|
||||
config_path: string
|
||||
sizes: string[]
|
||||
}
|
||||
|
||||
export interface YoloTrainOptionsResponse {
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: {
|
||||
datasets: YoloTrainDatasetOption[]
|
||||
versions: YoloTrainVersionOption[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface YoloTrainingJob {
|
||||
id: number
|
||||
dataset: number
|
||||
dataset_name: string
|
||||
model_family: 'v8' | '11' | '12'
|
||||
model_size?: 'n' | 's' | 'm' | 'l' | 'x'
|
||||
weight_path?: string
|
||||
config_path?: string
|
||||
status: YoloTrainStatus
|
||||
logs_path?: string
|
||||
runs_path?: string
|
||||
best_model_path?: string
|
||||
last_model_path?: string
|
||||
progress: number
|
||||
epochs: number
|
||||
imgsz: number
|
||||
batch: number
|
||||
device: string
|
||||
optimizer: 'SGD' | 'Adam' | 'AdamW' | 'RMSProp'
|
||||
error_message?: string
|
||||
created_at: string
|
||||
started_at?: string | null
|
||||
finished_at?: string | null
|
||||
}
|
||||
|
||||
export interface StartTrainingPayload {
|
||||
dataset_id: number
|
||||
model_family: 'v8' | '11' | '12'
|
||||
model_size?: 'n' | 's' | 'm' | 'l' | 'x'
|
||||
epochs?: number
|
||||
imgsz?: number
|
||||
batch?: number
|
||||
device?: string
|
||||
optimizer?: 'SGD' | 'Adam' | 'AdamW' | 'RMSProp'
|
||||
}
|
||||
|
||||
export interface YoloTrainLogsResponse {
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: {
|
||||
content: string
|
||||
next_offset: number
|
||||
finished: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// YOLO检测API
|
||||
export const yoloApi = {
|
||||
// 执行YOLO检测
|
||||
@@ -111,7 +219,8 @@ export const yoloApi = {
|
||||
|
||||
// 获取当前启用的YOLO模型信息
|
||||
async getCurrentEnabledModel(): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
return request.get('/api/yolo/models/enabled/')
|
||||
// 关闭全局错误提示,由调用方(如 YOLO 检测页面)自行处理“未启用模型”等业务文案
|
||||
return request.get('/api/yolo/models/enabled/', { showError: false })
|
||||
},
|
||||
|
||||
// 获取模型详情
|
||||
@@ -196,6 +305,112 @@ export const yoloApi = {
|
||||
return request.get('/api/yolo/stats/')
|
||||
},
|
||||
|
||||
// 数据集管理相关接口
|
||||
// 上传数据集
|
||||
async uploadDataset(formData: FormData): Promise<{ success: boolean; data?: YoloDatasetDetail; message?: string }> {
|
||||
return request.upload('/api/yolo/datasets/upload/', formData)
|
||||
},
|
||||
|
||||
// 获取数据集列表
|
||||
async getDatasets(): Promise<{ success: boolean; data?: YoloDatasetSummary[]; message?: string }> {
|
||||
return request.get('/api/yolo/datasets/')
|
||||
},
|
||||
|
||||
// 获取数据集详情
|
||||
async getDatasetDetail(datasetId: number): Promise<{ success: boolean; data?: YoloDatasetDetail; message?: string }> {
|
||||
return request.get(`/api/yolo/datasets/${datasetId}/`)
|
||||
},
|
||||
|
||||
// 删除数据集
|
||||
async deleteDataset(datasetId: number): Promise<{ success: boolean; message?: string }> {
|
||||
return request.post(`/api/yolo/datasets/${datasetId}/delete/`)
|
||||
},
|
||||
|
||||
// 获取数据集样本
|
||||
async getDatasetSamples(
|
||||
datasetId: number,
|
||||
params: { split?: 'train' | 'val' | 'test'; limit?: number; offset?: number } = {}
|
||||
): Promise<{
|
||||
success: boolean
|
||||
data?: { items: YoloDatasetSampleItem[]; total: number }
|
||||
message?: string
|
||||
}> {
|
||||
return request.get(`/api/yolo/datasets/${datasetId}/samples/`, { params })
|
||||
},
|
||||
|
||||
// YOLO 训练任务相关接口
|
||||
// 获取训练选项(可用数据集与模型版本)
|
||||
async getTrainOptions(): Promise<YoloTrainOptionsResponse> {
|
||||
return request.get('/api/yolo/train/options/')
|
||||
},
|
||||
|
||||
// 获取训练任务列表
|
||||
async getTrainJobs(): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: YoloTrainingJob[]
|
||||
}> {
|
||||
return request.get('/api/yolo/train/jobs/')
|
||||
},
|
||||
|
||||
// 创建并启动训练任务
|
||||
async startTrainJob(payload: StartTrainingPayload): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: YoloTrainingJob
|
||||
}> {
|
||||
return request.post('/api/yolo/train/jobs/start/', payload)
|
||||
},
|
||||
|
||||
// 获取训练任务详情
|
||||
async getTrainJobDetail(id: number): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: YoloTrainingJob
|
||||
}> {
|
||||
return request.get(`/api/yolo/train/jobs/${id}/`)
|
||||
},
|
||||
|
||||
// 获取训练任务日志(分页读取)
|
||||
async getTrainJobLogs(
|
||||
id: number,
|
||||
params: { offset?: number; max?: number } = {}
|
||||
): Promise<YoloTrainLogsResponse> {
|
||||
return request.get(`/api/yolo/train/jobs/${id}/logs/`, { params })
|
||||
},
|
||||
|
||||
// 取消训练任务
|
||||
async cancelTrainJob(id: number): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: YoloTrainingJob
|
||||
}> {
|
||||
return request.post(`/api/yolo/train/jobs/${id}/cancel/`)
|
||||
},
|
||||
|
||||
// 下载训练结果(ZIP)
|
||||
async downloadTrainJobResult(id: number): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: { url: string; size: number }
|
||||
}> {
|
||||
return request.get(`/api/yolo/train/jobs/${id}/download/`)
|
||||
},
|
||||
|
||||
// 删除训练任务
|
||||
async deleteTrainJob(id: number): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
}> {
|
||||
return request.post(`/api/yolo/train/jobs/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 警告等级管理相关接口
|
||||
// 获取警告等级列表
|
||||
async getAlertLevels(): Promise<{ success: boolean; data?: AlertLevel[]; message?: string }> {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br>
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ msg: string }>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
85
hertz_server_diango_ui/src/config/hertz_modules.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
export type HertzModuleGroup = 'admin' | 'user'
|
||||
|
||||
export interface HertzModule {
|
||||
key: string
|
||||
label: string
|
||||
group: HertzModuleGroup
|
||||
description?: string
|
||||
defaultEnabled: boolean
|
||||
}
|
||||
|
||||
export const HERTZ_MODULES: HertzModule[] = [
|
||||
{ key: 'admin.user-management', label: '管理端 · 用户管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.department-management', label: '管理端 · 部门管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.menu-management', label: '管理端 · 菜单管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.role-management', label: '管理端 · 角色管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.notification-management', label: '管理端 · 通知管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.log-management', label: '管理端 · 日志管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.knowledge-base', label: '管理端 · 文章管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.yolo-model', label: '管理端 · YOLO 模型相关', group: 'admin', defaultEnabled: true },
|
||||
|
||||
{ key: 'user.system-monitor', label: '用户端 · 系统监控', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.ai-chat', label: '用户端 · AI 助手', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.yolo-detection', label: '用户端 · YOLO 检测', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.live-detection', label: '用户端 · 实时检测', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.detection-history', label: '用户端 · 检测历史', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.alert-center', label: '用户端 · 告警中心', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.notice-center', label: '用户端 · 通知中心', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.knowledge-center', label: '用户端 · 文章中心', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.kb-center', label: '用户端 · 知识库中心', group: 'user', defaultEnabled: true },
|
||||
]
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'hertz_enabled_modules'
|
||||
|
||||
export function getEnabledModuleKeys(): string[] {
|
||||
const fallback = HERTZ_MODULES.filter(m => m.defaultEnabled).map(m => m.key)
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return fallback
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = window.localStorage.getItem(LOCAL_STORAGE_KEY)
|
||||
if (!stored) return fallback
|
||||
const parsed = JSON.parse(stored)
|
||||
if (Array.isArray(parsed)) {
|
||||
const valid = parsed.filter((k): k is string => typeof k === 'string')
|
||||
// 自动合并新增的默认启用模块,避免新模块在已有选择下被永久隐藏
|
||||
const missingDefaults = HERTZ_MODULES
|
||||
.filter(m => m.defaultEnabled && !valid.includes(m.key))
|
||||
.map(m => m.key)
|
||||
return [...valid, ...missingDefaults]
|
||||
}
|
||||
return fallback
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
export function setEnabledModuleKeys(keys: string[]): void {
|
||||
if (typeof window === 'undefined') return
|
||||
try {
|
||||
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(keys))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function isModuleEnabled(moduleKey?: string, enabledKeys?: string[]): boolean {
|
||||
if (!moduleKey) return true
|
||||
const keys = enabledKeys ?? getEnabledModuleKeys()
|
||||
return keys.indexOf(moduleKey) !== -1
|
||||
}
|
||||
|
||||
export function getModulesByGroup(group: HertzModuleGroup): HertzModule[] {
|
||||
return HERTZ_MODULES.filter(m => m.group === group)
|
||||
}
|
||||
|
||||
export function hasModuleSelection(): boolean {
|
||||
if (typeof window === 'undefined') return false
|
||||
try {
|
||||
return window.localStorage.getItem(LOCAL_STORAGE_KEY) !== null
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 会话与消息类型
|
||||
export interface AIChatItem {
|
||||
id: number
|
||||
title: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
latest_message?: string
|
||||
}
|
||||
|
||||
export interface AIChatDetail {
|
||||
id: number
|
||||
title: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AIChatMessage {
|
||||
id: number
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ChatListData {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
chats: AIChatItem[]
|
||||
}
|
||||
|
||||
export interface ChatDetailData {
|
||||
chat: AIChatDetail
|
||||
messages: AIChatMessage[]
|
||||
}
|
||||
|
||||
export interface SendMessageData {
|
||||
user_message: AIChatMessage
|
||||
ai_message: AIChatMessage
|
||||
}
|
||||
|
||||
export const aiApi = {
|
||||
listChats: (params?: { query?: string; page?: number; page_size?: number }): Promise<ApiResponse<ChatListData>> =>
|
||||
request.get('/api/ai/chats/', { params, showError: false }),
|
||||
|
||||
createChat: (body?: { title?: string }): Promise<ApiResponse<AIChatDetail>> =>
|
||||
request.post('/api/ai/chats/create/', body || { title: '新对话' }),
|
||||
|
||||
getChatDetail: (chatId: number): Promise<ApiResponse<ChatDetailData>> =>
|
||||
request.get(`/api/ai/chats/${chatId}/`),
|
||||
|
||||
updateChat: (chatId: number, body: { title: string }): Promise<ApiResponse<null>> =>
|
||||
request.put(`/api/ai/chats/${chatId}/update/`, body),
|
||||
|
||||
deleteChats: (chatIds: number[]): Promise<ApiResponse<null>> =>
|
||||
request.post('/api/ai/chats/delete/', { chat_ids: chatIds }),
|
||||
|
||||
sendMessage: (chatId: number, body: { content: string }): Promise<ApiResponse<SendMessageData>> =>
|
||||
request.post(`/api/ai/chats/${chatId}/send/`, body),
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
// 统一的菜单项配置接口
|
||||
export interface UserMenuConfig {
|
||||
key: string
|
||||
label: string
|
||||
icon?: string
|
||||
path: string
|
||||
component: string // 组件路径,相对于 @/views/user_pages/
|
||||
children?: UserMenuConfig[]
|
||||
disabled?: boolean
|
||||
meta?: {
|
||||
title?: string
|
||||
requiresAuth?: boolean
|
||||
roles?: string[]
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单项接口定义(用于前端显示)
|
||||
export interface MenuItem {
|
||||
key: string
|
||||
label: string
|
||||
icon?: string
|
||||
path?: string
|
||||
children?: MenuItem[]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
// 统一配置 - 同时用于菜单和路由
|
||||
export const userMenuConfigs: UserMenuConfig[] = [
|
||||
{
|
||||
key: 'dashboard',
|
||||
label: '首页',
|
||||
icon: 'DashboardOutlined',
|
||||
path: '/dashboard',
|
||||
component: 'index.vue',
|
||||
meta: { title: '用户首页', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
key: 'profile',
|
||||
label: '个人信息',
|
||||
icon: 'UserOutlined',
|
||||
path: '/user/profile',
|
||||
component: 'Profile.vue',
|
||||
meta: { title: '个人信息', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
key: 'documents',
|
||||
label: '文档管理',
|
||||
icon: 'FileTextOutlined',
|
||||
path: '/user/documents',
|
||||
component: 'Documents.vue',
|
||||
meta: { title: '文档管理', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
key: 'messages',
|
||||
label: '消息中心',
|
||||
icon: 'MessageOutlined',
|
||||
path: '/user/messages',
|
||||
component: 'Messages.vue',
|
||||
meta: { title: '消息中心', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
key: 'system-monitor',
|
||||
label: '系统监控',
|
||||
icon: 'DashboardOutlined',
|
||||
path: '/user/system-monitor',
|
||||
component: 'SystemMonitor.vue',
|
||||
meta: { title: '系统监控', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
key: 'ai-chat',
|
||||
label: 'AI助手',
|
||||
icon: 'MessageOutlined',
|
||||
path: '/user/ai-chat',
|
||||
component: 'AiChat.vue',
|
||||
meta: { title: 'AI助手', requiresAuth: true }
|
||||
},
|
||||
]
|
||||
|
||||
// 显式组件映射 - 避免Vite动态导入限制
|
||||
const explicitComponentMap: Record<string, any> = {
|
||||
'index.vue': defineAsyncComponent(() => import('@/views/user_pages/index.vue')),
|
||||
'Profile.vue': defineAsyncComponent(() => import('@/views/user_pages/Profile.vue')),
|
||||
'Documents.vue': defineAsyncComponent(() => import('@/views/user_pages/Documents.vue')),
|
||||
'Messages.vue': defineAsyncComponent(() => import('@/views/user_pages/Messages.vue')),
|
||||
'SystemMonitor.vue': defineAsyncComponent(() => import('@/views/user_pages/SystemMonitor.vue')),
|
||||
'AiChat.vue': defineAsyncComponent(() => import('@/views/user_pages/AiChat.vue')),
|
||||
}
|
||||
|
||||
// 自动生成菜单项(用于前端显示)
|
||||
export const userMenuItems: MenuItem[] = userMenuConfigs.map(config => ({
|
||||
key: config.key,
|
||||
label: config.label,
|
||||
icon: config.icon,
|
||||
path: config.path,
|
||||
disabled: config.disabled,
|
||||
children: config.children?.map(child => ({
|
||||
key: child.key,
|
||||
label: child.label,
|
||||
icon: child.icon,
|
||||
path: child.path,
|
||||
disabled: child.disabled
|
||||
}))
|
||||
}))
|
||||
|
||||
// 组件映射表 - 用于解决Vite动态导入限制
|
||||
const componentMap: Record<string, () => Promise<any>> = {
|
||||
'index.vue': () => import('@/views/user_pages/index.vue'),
|
||||
'Profile.vue': () => import('@/views/user_pages/Profile.vue'),
|
||||
'Documents.vue': () => import('@/views/user_pages/Documents.vue'),
|
||||
'Messages.vue': () => import('@/views/user_pages/Messages.vue'),
|
||||
'SystemMonitor.vue': () => import('@/views/user_pages/SystemMonitor.vue'),
|
||||
'AiChat.vue': () => import('@/views/user_pages/AiChat.vue'),
|
||||
}
|
||||
|
||||
// 自动生成路由配置
|
||||
export const userRoutes: RouteRecordRaw[] = userMenuConfigs.map(config => {
|
||||
const route: RouteRecordRaw = {
|
||||
path: config.path,
|
||||
name: `User${config.key.charAt(0).toUpperCase() + config.key.slice(1)}`,
|
||||
component: componentMap[config.component] || (() => import('@/views/NotFound.vue')),
|
||||
meta: {
|
||||
title: config.meta?.title || config.label,
|
||||
requiresAuth: config.meta?.requiresAuth ?? true,
|
||||
...config.meta
|
||||
}
|
||||
}
|
||||
|
||||
if (config.children && config.children.length > 0) {
|
||||
route.children = config.children.map(child => ({
|
||||
path: child.path,
|
||||
name: `User${child.key.charAt(0).toUpperCase() + child.key.slice(1)}`,
|
||||
component: componentMap[child.component] || (() => import('@/views/NotFound.vue')),
|
||||
meta: {
|
||||
title: child.meta?.title || child.label,
|
||||
requiresAuth: child.meta?.requiresAuth ?? true,
|
||||
...child.meta
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return route
|
||||
})
|
||||
|
||||
// 根据菜单项生成路由路径
|
||||
export function getMenuPath(menuKey: string): string {
|
||||
const findPath = (items: MenuItem[], key: string): string | null => {
|
||||
for (const item of items) {
|
||||
if (item.key === key && item.path) return item.path
|
||||
if (item.children) {
|
||||
const childPath = findPath(item.children, key)
|
||||
if (childPath) return childPath
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return findPath(userMenuItems, menuKey) || '/dashboard'
|
||||
}
|
||||
|
||||
// 获取菜单的面包屑路径
|
||||
export function getMenuBreadcrumb(menuKey: string): string[] {
|
||||
const findBreadcrumb = (items: MenuItem[], key: string, path: string[] = []): string[] | null => {
|
||||
for (const item of items) {
|
||||
const currentPath = [...path, item.label]
|
||||
if (item.key === menuKey) return currentPath
|
||||
if (item.children) {
|
||||
const childPath = findBreadcrumb(item.children, key, currentPath)
|
||||
if (childPath) return childPath
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return findBreadcrumb(userMenuItems, menuKey) || ['仪表盘']
|
||||
}
|
||||
|
||||
// 自动生成组件映射(基于配置和显式映射)
|
||||
export const generateComponentMap = () => {
|
||||
const map: Record<string, any> = {}
|
||||
const processConfigs = (configs: UserMenuConfig[]) => {
|
||||
configs.forEach(config => {
|
||||
if (explicitComponentMap[config.component]) {
|
||||
map[config.key] = explicitComponentMap[config.component]
|
||||
} else {
|
||||
map[config.key] = defineAsyncComponent(() => import('@/views/NotFound.vue'))
|
||||
}
|
||||
if (config.children) processConfigs(config.children)
|
||||
})
|
||||
}
|
||||
processConfigs(userMenuConfigs)
|
||||
return map
|
||||
}
|
||||
|
||||
// 导出自动生成的组件映射
|
||||
export const userComponentMap = generateComponentMap()
|
||||
|
||||
// 根据用户权限过滤菜单项
|
||||
export const getFilteredUserMenuItems = (userRoles: string[], userPermissions: string[]): MenuItem[] => {
|
||||
return userMenuConfigs
|
||||
.filter(config => {
|
||||
if (!config.meta?.roles || config.meta.roles.length === 0) return true
|
||||
return config.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
|
||||
})
|
||||
.map(config => ({
|
||||
key: config.key,
|
||||
label: config.label,
|
||||
icon: config.icon,
|
||||
path: config.path,
|
||||
disabled: config.disabled,
|
||||
children: config.children?.filter(child => {
|
||||
if (!child.meta?.roles || child.meta.roles.length === 0) return true
|
||||
return child.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
|
||||
}).map(child => ({
|
||||
key: child.key,
|
||||
label: child.label,
|
||||
icon: child.icon,
|
||||
path: child.path,
|
||||
disabled: child.disabled
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
// 检查用户是否有访问特定菜单的权限
|
||||
export const hasUserMenuPermission = (menuKey: string, userRoles: string[]): boolean => {
|
||||
const menuConfig = userMenuConfigs.find(config => config.key === menuKey)
|
||||
if (!menuConfig) return false
|
||||
if (!menuConfig.meta?.roles || menuConfig.meta.roles.length === 0) return true
|
||||
return menuConfig.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
<template>
|
||||
<div class="ai-chat-page">
|
||||
<a-page-header title="AI助手" sub-title="与 AI 进行智能对话">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-input-search v-model:value="query" placeholder="搜索会话标题" style="width: 240px" @search="fetchChats" />
|
||||
<a-button @click="fetchChats" :loading="loadingChats">
|
||||
<ReloadOutlined />
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button type="primary" @click="createChat" :loading="creating">
|
||||
<PlusOutlined />
|
||||
新建对话
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<!-- 左侧:会话列表 -->
|
||||
<a-col :xs="24" :md="8" :lg="6">
|
||||
<a-card title="我的对话" bordered>
|
||||
<a-list
|
||||
:data-source="chatList"
|
||||
item-layout="horizontal"
|
||||
:loading="loadingChats"
|
||||
:pagination="{ pageSize: pageSize, total: total, current: page, onChange: onPageChange }"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item :class="{ active: item.id === currentChatId }" @click="selectChat(item.id)">
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<div class="chat-title-row">
|
||||
<span class="chat-title">{{ item.title }}</span>
|
||||
<a-space>
|
||||
<a-button size="small" type="text" @click.stop="openRename(item)">
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
<a-popconfirm title="确认删除该对话?" @confirm="deleteChat(item.id)">
|
||||
<a-button size="small" danger type="text" @click.stop>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="chat-desc">{{ item.latest_message || '暂无消息' }}</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧:消息区 -->
|
||||
<a-col :xs="24" :md="16" :lg="18">
|
||||
<a-card :title="currentChat?.title || '请选择或新建对话'" bordered class="chat-card">
|
||||
<div class="messages" ref="messagesEl">
|
||||
<template v-if="messages.length">
|
||||
<div v-for="m in messages" :key="m.id" :class="['msg', m.role]">
|
||||
<div class="bubble">
|
||||
<div class="content" v-html="renderContent(m.content)"></div>
|
||||
<div class="time">{{ formatTime(m.created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-empty v-else description="暂无消息" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="composer">
|
||||
<a-textarea v-model:value="input" :rows="3" placeholder="输入你的问题..." :disabled="!currentChatId" />
|
||||
<a-space style="margin-top: 8px;">
|
||||
<a-button type="primary" :disabled="!canSend" :loading="sending" @click="send">
|
||||
<SendOutlined />
|
||||
发送
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 重命名对话 -->
|
||||
<a-modal v-model:open="renameOpen" title="重命名对话" @ok="doRename" :confirm-loading="renaming">
|
||||
<a-input v-model:value="renameTitle" placeholder="请输入新标题" />
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, SendOutlined } from '@ant-design/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { aiApi, type AIChatItem, type AIChatDetail, type AIChatMessage } from '@/api/ai'
|
||||
|
||||
// 会话列表状态
|
||||
const chatList = ref<AIChatItem[]>([])
|
||||
const query = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const loadingChats = ref(false)
|
||||
const creating = ref(false)
|
||||
|
||||
// 当前会话与消息
|
||||
const currentChatId = ref<number | null>(null)
|
||||
const currentChat = ref<AIChatDetail | null>(null)
|
||||
const messages = ref<AIChatMessage[]>([])
|
||||
const loadingMessages = ref(false)
|
||||
const sending = ref(false)
|
||||
|
||||
// 重命名
|
||||
const renameOpen = ref(false)
|
||||
const renameTitle = ref('')
|
||||
const renaming = ref(false)
|
||||
let renameTargetId: number | null = null
|
||||
|
||||
const messagesEl = ref<HTMLElement | null>(null)
|
||||
|
||||
const canSend = computed(() => !!currentChatId.value && !!input.value.trim())
|
||||
const input = ref('')
|
||||
|
||||
const fetchChats = async () => {
|
||||
loadingChats.value = true
|
||||
try {
|
||||
const res = await aiApi.listChats({ query: query.value || undefined, page: page.value, page_size: pageSize.value })
|
||||
if (res.success) {
|
||||
chatList.value = res.data.chats || []
|
||||
total.value = res.data.total || 0
|
||||
// 保持选择
|
||||
if (!currentChatId.value && chatList.value.length) selectChat(chatList.value[0].id)
|
||||
} else {
|
||||
message.error(res.message || '获取对话列表失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 403) {
|
||||
message.warning('暂无权限访问AI助手,请联系管理员开通权限')
|
||||
} else {
|
||||
message.error(e?.message || '网络错误')
|
||||
}
|
||||
} finally {
|
||||
loadingChats.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onPageChange = (p: number) => { page.value = p; fetchChats() }
|
||||
|
||||
const selectChat = async (id: number) => {
|
||||
if (currentChatId.value === id && messages.value.length) return
|
||||
currentChatId.value = id
|
||||
loadingMessages.value = true
|
||||
try {
|
||||
const res = await aiApi.getChatDetail(id)
|
||||
if (res.success) {
|
||||
currentChat.value = res.data.chat
|
||||
messages.value = res.data.messages || []
|
||||
await nextTick(); scrollToBottom()
|
||||
} else {
|
||||
message.error(res.message || '获取会话详情失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '网络错误')
|
||||
} finally {
|
||||
loadingMessages.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createChat = async () => {
|
||||
creating.value = true
|
||||
try {
|
||||
const res = await aiApi.createChat({ title: '新对话' })
|
||||
if (res.success) {
|
||||
message.success('创建成功')
|
||||
await fetchChats()
|
||||
selectChat(res.data.id)
|
||||
} else {
|
||||
message.error(res.message || '创建失败')
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '网络错误')
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openRename = (item: AIChatItem) => {
|
||||
renameTargetId = item.id
|
||||
renameTitle.value = item.title
|
||||
renameOpen.value = true
|
||||
}
|
||||
|
||||
const doRename = async () => {
|
||||
if (!renameTargetId || !renameTitle.value.trim()) { message.warning('标题不能为空'); return }
|
||||
renaming.value = true
|
||||
try {
|
||||
const res = await aiApi.updateChat(renameTargetId, { title: renameTitle.value.trim() })
|
||||
if (res.success) { message.success('重命名成功'); renameOpen.value = false; await fetchChats() }
|
||||
else { message.error(res.message || '重命名失败') }
|
||||
} catch (e: any) { message.error(e?.message || '网络错误') }
|
||||
finally { renaming.value = false }
|
||||
}
|
||||
|
||||
const deleteChat = async (id: number) => {
|
||||
try {
|
||||
const res = await aiApi.deleteChats([id])
|
||||
if (res.success) {
|
||||
message.success('删除成功')
|
||||
await fetchChats()
|
||||
if (currentChatId.value === id) { currentChatId.value = null; currentChat.value = null; messages.value = [] }
|
||||
} else {
|
||||
message.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (e: any) { message.error(e?.message || '网络错误') }
|
||||
}
|
||||
|
||||
const send = async () => {
|
||||
if (!canSend.value || !currentChatId.value) return
|
||||
const content = input.value.trim()
|
||||
sending.value = true
|
||||
try {
|
||||
const res = await aiApi.sendMessage(currentChatId.value, { content })
|
||||
if (res.success) {
|
||||
messages.value.push(res.data.user_message, res.data.ai_message)
|
||||
input.value = ''
|
||||
await nextTick(); scrollToBottom(); fetchChats() // 更新列表预览
|
||||
} else {
|
||||
message.error(res.message || '发送失败')
|
||||
}
|
||||
} catch (e: any) { message.error(e?.message || '网络错误') }
|
||||
finally { sending.value = false }
|
||||
}
|
||||
|
||||
const formatTime = (t: string) => dayjs(t).format('YYYY-MM-DD HH:mm:ss')
|
||||
const renderContent = (c: string) => c.replace(/\n/g, '<br/>')
|
||||
const scrollToBottom = () => { const el = messagesEl.value; if (el) el.scrollTop = el.scrollHeight }
|
||||
|
||||
onMounted(() => { fetchChats() })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ai-chat-page { padding: 16px; }
|
||||
.chat-title-row { display: flex; justify-content: space-between; align-items: center; }
|
||||
.chat-desc { color: #666; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.messages { min-height: 420px; max-height: 60vh; overflow-y: auto; padding: 8px; background: #fafafa; border-radius: 8px; }
|
||||
.msg { display: flex; margin-bottom: 12px; }
|
||||
.msg .bubble { max-width: 80%; padding: 10px 12px; border-radius: 8px; position: relative; }
|
||||
.msg .time { margin-top: 6px; font-size: 12px; color: #999; }
|
||||
.msg.user { justify-content: flex-end; }
|
||||
.msg.user .bubble { background: #e6f7ff; }
|
||||
.msg.assistant { justify-content: flex-start; }
|
||||
.msg.assistant .bubble { background: #f6ffed; }
|
||||
.composer { margin-top: 8px; }
|
||||
.chat-card :deep(.ant-card-head) { background: #fff; }
|
||||
.active { background: #f0f7ff; }
|
||||
</style>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,4 +1,5 @@
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import { getEnabledModuleKeys, isModuleEnabled } from "@/config/hertz_modules";
|
||||
|
||||
// 角色权限枚举
|
||||
export enum UserRole {
|
||||
@@ -19,6 +20,7 @@ export interface AdminMenuItem {
|
||||
roles?: UserRole[]; // 允许访问的角色,不设置则使用默认管理员角色
|
||||
permission?: string; // 所需权限标识符
|
||||
children?: AdminMenuItem[]; // 子菜单
|
||||
moduleKey?: string;
|
||||
}
|
||||
|
||||
// 🎯 统一配置中心 - 只需要在这里修改菜单配置
|
||||
@@ -38,6 +40,7 @@ export const ADMIN_MENU_CONFIG: AdminMenuItem[] = [
|
||||
path: "/admin/user-management",
|
||||
component: "UserManagement.vue",
|
||||
permission: "system:user:list", // 需要用户列表权限
|
||||
moduleKey: "admin.user-management",
|
||||
},
|
||||
{
|
||||
key: "department-management",
|
||||
@@ -45,7 +48,8 @@ export const ADMIN_MENU_CONFIG: AdminMenuItem[] = [
|
||||
icon: "SettingOutlined",
|
||||
path: "/admin/department-management",
|
||||
component: "DepartmentManagement.vue",
|
||||
permission: "system:dept:list", // 需要部门列表权限
|
||||
permission: "system:dept:list", // 需要部门列表权限
|
||||
moduleKey: "admin.department-management",
|
||||
},
|
||||
{
|
||||
key: "menu-management",
|
||||
@@ -54,6 +58,7 @@ export const ADMIN_MENU_CONFIG: AdminMenuItem[] = [
|
||||
path: "/admin/menu-management",
|
||||
component: "MenuManagement.vue",
|
||||
permission: "system:menu:list", // 需要菜单列表权限
|
||||
moduleKey: "admin.menu-management",
|
||||
},
|
||||
{
|
||||
key: "teacher",
|
||||
@@ -62,6 +67,7 @@ export const ADMIN_MENU_CONFIG: AdminMenuItem[] = [
|
||||
path: "/admin/teacher",
|
||||
component: "Role.vue",
|
||||
permission: "system:role:list", // 需要角色列表权限
|
||||
moduleKey: "admin.role-management",
|
||||
},
|
||||
{
|
||||
key: "notification-management",
|
||||
@@ -70,6 +76,7 @@ export const ADMIN_MENU_CONFIG: AdminMenuItem[] = [
|
||||
path: "/admin/notification-management",
|
||||
component: "NotificationManagement.vue",
|
||||
permission: "studio:notice:list", // 需要通知列表权限
|
||||
moduleKey: "admin.notification-management",
|
||||
},
|
||||
{
|
||||
key: "log-management",
|
||||
@@ -78,15 +85,17 @@ export const ADMIN_MENU_CONFIG: AdminMenuItem[] = [
|
||||
path: "/admin/log-management",
|
||||
component: "LogManagement.vue",
|
||||
permission: "log.view_operationlog", // 查看操作日志权限
|
||||
moduleKey: "admin.log-management",
|
||||
},
|
||||
{
|
||||
key: "knowledge-base",
|
||||
title: "知识库管理",
|
||||
title: "文章管理",
|
||||
icon: "DatabaseOutlined",
|
||||
path: "/admin/knowledge-base",
|
||||
component: "KnowledgeBaseManagement.vue",
|
||||
path: "/admin/article-management",
|
||||
component: "ArticleManagement.vue",
|
||||
// 菜单访问权限:需要具备文章列表权限
|
||||
permission: "system:knowledge:article:list",
|
||||
moduleKey: "admin.knowledge-base",
|
||||
},
|
||||
{
|
||||
key: "yolo-model",
|
||||
@@ -95,6 +104,7 @@ export const ADMIN_MENU_CONFIG: AdminMenuItem[] = [
|
||||
path: "/admin/yolo-model",
|
||||
component: "ModelManagement.vue", // 默认显示模型管理页面
|
||||
// 父菜单不设置权限,由子菜单的权限决定是否显示
|
||||
moduleKey: "admin.yolo-model",
|
||||
children: [
|
||||
{
|
||||
key: "model-management",
|
||||
@@ -104,6 +114,20 @@ export const ADMIN_MENU_CONFIG: AdminMenuItem[] = [
|
||||
component: "ModelManagement.vue",
|
||||
permission: "system:yolo:model:list",
|
||||
},
|
||||
{
|
||||
key: "dataset-management",
|
||||
title: "数据集管理",
|
||||
icon: "DatabaseOutlined",
|
||||
path: "/admin/dataset-management",
|
||||
component: "DatasetManagement.vue",
|
||||
},
|
||||
{
|
||||
key: "yolo-train-management",
|
||||
title: "YOLO训练",
|
||||
icon: "HistoryOutlined",
|
||||
path: "/admin/yolo-train",
|
||||
component: "YoloTrainManagement.vue",
|
||||
},
|
||||
{
|
||||
key: "alert-level-management",
|
||||
title: "模型类别管理",
|
||||
@@ -144,8 +168,10 @@ const COMPONENT_MAP: { [key: string]: () => Promise<any> } = {
|
||||
'MenuManagement.vue': () => import("@/views/admin_page/MenuManagement.vue"),
|
||||
'NotificationManagement.vue': () => import("@/views/admin_page/NotificationManagement.vue"),
|
||||
'LogManagement.vue': () => import("@/views/admin_page/LogManagement.vue"),
|
||||
'KnowledgeBaseManagement.vue': () => import("@/views/admin_page/KnowledgeBaseManagement.vue"),
|
||||
'ArticleManagement.vue': () => import("@/views/admin_page/ArticleManagement.vue"),
|
||||
'ModelManagement.vue': () => import("@/views/admin_page/ModelManagement.vue"),
|
||||
'DatasetManagement.vue': () => import("@/views/admin_page/DatasetManagement.vue"),
|
||||
'YoloTrainManagement.vue': () => import("@/views/admin_page/YoloTrainManagement.vue"),
|
||||
'AlertLevelManagement.vue': () => import("@/views/admin_page/AlertLevelManagement.vue"),
|
||||
'AlertProcessingCenter.vue': () => import("@/views/admin_page/AlertProcessingCenter.vue"),
|
||||
'DetectionHistoryManagement.vue': () => import("@/views/admin_page/DetectionHistoryManagement.vue"),
|
||||
@@ -154,8 +180,12 @@ const COMPONENT_MAP: { [key: string]: () => Promise<any> } = {
|
||||
// 🚀 自动生成路由配置
|
||||
function generateAdminRoutes(): RouteRecordRaw {
|
||||
const children: RouteRecordRaw[] = [];
|
||||
const enabledModuleKeys = getEnabledModuleKeys();
|
||||
|
||||
ADMIN_MENU_CONFIG.forEach(item => {
|
||||
if (!isModuleEnabled(item.moduleKey, enabledModuleKeys)) {
|
||||
return;
|
||||
}
|
||||
// 如果有子菜单,将子菜单作为独立的路由项
|
||||
if (item.children && item.children.length > 0) {
|
||||
// 为每个子菜单创建独立的路由
|
||||
@@ -334,8 +364,13 @@ export const getFilteredMenuConfig = (userRoles: string[], userPermissions: stri
|
||||
// 对 super_admin / system_admin 开放所有管理菜单(忽略权限字符串过滤)
|
||||
const isPrivilegedAdmin = userRoles.includes('super_admin') || userRoles.includes('system_admin');
|
||||
|
||||
// 过滤菜单项 - 基于权限字符串检查
|
||||
const enabledModuleKeys = getEnabledModuleKeys();
|
||||
|
||||
// 过滤菜单项 - 基于模块开关和权限字符串检查
|
||||
const filteredMenus = ADMIN_MENU_CONFIG.filter(menuItem => {
|
||||
if (!isModuleEnabled(menuItem.moduleKey, enabledModuleKeys)) {
|
||||
return false;
|
||||
}
|
||||
console.log(`🔍 检查菜单项: ${menuItem.title} (${menuItem.key})`, {
|
||||
hasPermission: !!menuItem.permission,
|
||||
permission: menuItem.permission,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { RouteRecordRaw } from "vue-router";
|
||||
import { useUserStore } from "@/stores/hertz_user";
|
||||
import { adminMenuRoutes, UserRole } from "./admin_menu";
|
||||
import { userRoutes } from "./user_menu_ai";
|
||||
import { hasModuleSelection } from "@/config/hertz_modules";
|
||||
|
||||
// 固定路由配置
|
||||
const fixedRoutes: RouteRecordRaw[] = [
|
||||
@@ -25,6 +26,15 @@ const fixedRoutes: RouteRecordRaw[] = [
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/template/modules",
|
||||
name: "ModuleSetup",
|
||||
component: () => import("@/views/ModuleSetup.vue"),
|
||||
meta: {
|
||||
title: "模块配置",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
name: "Register",
|
||||
@@ -160,6 +170,16 @@ router.beforeEach((to, _from, next) => {
|
||||
console.log('📋 用户信息:', userStore.userInfo);
|
||||
console.log('🔄 重定向计数:', redirectCount);
|
||||
|
||||
// 模板模式:首次必须先完成模块选择
|
||||
const isTemplateMode = import.meta.env.VITE_TEMPLATE_SETUP_MODE === 'true';
|
||||
if (isTemplateMode && to.name !== "ModuleSetup") {
|
||||
if (!hasModuleSelection()) {
|
||||
console.log('🧩 模板模式开启,尚未选择模块,重定向到模块配置页');
|
||||
next({ name: "ModuleSetup", query: { redirect: to.fullPath } });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置页面标题
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} - 管理系统`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { getEnabledModuleKeys, isModuleEnabled } from '@/config/hertz_modules'
|
||||
|
||||
export interface UserMenuConfig {
|
||||
key: string
|
||||
@@ -15,6 +16,7 @@ export interface UserMenuConfig {
|
||||
roles?: string[]
|
||||
[key: string]: any
|
||||
}
|
||||
moduleKey?: string
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
@@ -30,16 +32,23 @@ export const userMenuConfigs: UserMenuConfig[] = [
|
||||
{ key: 'dashboard', label: '首页', icon: 'DashboardOutlined', path: '/dashboard', component: 'index.vue', meta: { title: '用户首页', requiresAuth: true } },
|
||||
{ key: 'profile', label: '个人信息', icon: 'UserOutlined', path: '/user/profile', component: 'Profile.vue', meta: { title: '个人信息', requiresAuth: true, hideInMenu: true } },
|
||||
// { key: 'documents', label: '文档管理', icon: 'FileTextOutlined', path: '/user/documents', component: 'Documents.vue', meta: { title: '文档管理', requiresAuth: true } },
|
||||
{ key: 'system-monitor', label: '系统监控', icon: 'DashboardOutlined', path: '/user/system-monitor', component: 'SystemMonitor.vue', meta: { title: '系统监控', requiresAuth: true } },
|
||||
{ key: 'ai-chat', label: 'AI助手', icon: 'MessageOutlined', path: '/user/ai-chat', component: 'AiChat.vue', meta: { title: 'AI助手', requiresAuth: true } },
|
||||
{ key: 'yolo-detection', label: 'YOLO检测', icon: 'ScanOutlined', path: '/user/yolo-detection', component: 'YoloDetection.vue', meta: { title: 'YOLO检测中心', requiresAuth: true } },
|
||||
{ key: 'live-detection', label: '实时检测', icon: 'VideoCameraOutlined', path: '/user/live-detection', component: 'LiveDetection.vue', meta: { title: '实时检测', requiresAuth: true } },
|
||||
{ key: 'detection-history', label: '检测历史', icon: 'HistoryOutlined', path: '/user/detection-history', component: 'DetectionHistory.vue', meta: { title: '检测历史记录', requiresAuth: true } },
|
||||
{ key: 'alert-center', label: '告警中心', icon: 'ExclamationCircleOutlined', path: '/user/alert-center', component: 'AlertCenter.vue', meta: { title: '告警中心', requiresAuth: true } },
|
||||
{ key: 'notice-center', label: '通知中心', icon: 'BellOutlined', path: '/user/notice', component: 'NoticeCenter.vue', meta: { title: '通知中心', requiresAuth: true } },
|
||||
{ key: 'knowledge-center', label: '知识库中心', icon: 'DatabaseOutlined', path: '/user/knowledge', component: 'KnowledgeCenter.vue', meta: { title: '知识库中心', requiresAuth: true } },
|
||||
{ key: 'system-monitor', label: '系统监控', icon: 'DashboardOutlined', path: '/user/system-monitor', component: 'SystemMonitor.vue', meta: { title: '系统监控', requiresAuth: true }, moduleKey: 'user.system-monitor' },
|
||||
{ key: 'ai-chat', label: 'AI助手', icon: 'MessageOutlined', path: '/user/ai-chat', component: 'AiChat.vue', meta: { title: 'AI助手', requiresAuth: true }, moduleKey: 'user.ai-chat' },
|
||||
{ key: 'yolo-detection', label: 'YOLO检测', icon: 'ScanOutlined', path: '/user/yolo-detection', component: 'YoloDetection.vue', meta: { title: 'YOLO检测中心', requiresAuth: true }, moduleKey: 'user.yolo-detection' },
|
||||
{ key: 'live-detection', label: '实时检测', icon: 'VideoCameraOutlined', path: '/user/live-detection', component: 'LiveDetection.vue', meta: { title: '实时检测', requiresAuth: true }, moduleKey: 'user.live-detection' },
|
||||
{ key: 'detection-history', label: '检测历史', icon: 'HistoryOutlined', path: '/user/detection-history', component: 'DetectionHistory.vue', meta: { title: '检测历史记录', requiresAuth: true }, moduleKey: 'user.detection-history' },
|
||||
{ key: 'alert-center', label: '告警中心', icon: 'ExclamationCircleOutlined', path: '/user/alert-center', component: 'AlertCenter.vue', meta: { title: '告警中心', requiresAuth: true }, moduleKey: 'user.alert-center' },
|
||||
{ key: 'notice-center', label: '通知中心', icon: 'BellOutlined', path: '/user/notice', component: 'NoticeCenter.vue', meta: { title: '通知中心', requiresAuth: true }, moduleKey: 'user.notice-center' },
|
||||
{ key: 'knowledge-center', label: '文章中心', icon: 'DatabaseOutlined', path: '/user/knowledge', component: 'ArticleCenter.vue', meta: { title: '文章中心', requiresAuth: true }, moduleKey: 'user.knowledge-center' },
|
||||
{ key: 'kb-center', label: '知识库中心', icon: 'DatabaseOutlined', path: '/user/kb-center', component: 'KbCenter.vue', meta: { title: '知识库中心', requiresAuth: true }, moduleKey: 'user.kb-center' },
|
||||
]
|
||||
|
||||
const enabledModuleKeys = getEnabledModuleKeys()
|
||||
|
||||
const effectiveUserMenuConfigs: UserMenuConfig[] = userMenuConfigs.filter(config =>
|
||||
isModuleEnabled(config.moduleKey, enabledModuleKeys)
|
||||
)
|
||||
|
||||
const explicitComponentMap: Record<string, any> = {
|
||||
'index.vue': defineAsyncComponent(() => import('@/views/user_pages/index.vue')),
|
||||
'Profile.vue': defineAsyncComponent(() => import('@/views/user_pages/Profile.vue')),
|
||||
@@ -52,10 +61,11 @@ const explicitComponentMap: Record<string, any> = {
|
||||
'DetectionHistory.vue': defineAsyncComponent(() => import('@/views/user_pages/DetectionHistory.vue')),
|
||||
'AlertCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/AlertCenter.vue')),
|
||||
'NoticeCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/NoticeCenter.vue')),
|
||||
'KnowledgeCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/KnowledgeCenter.vue')),
|
||||
'ArticleCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/ArticleCenter.vue')),
|
||||
'KbCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/KbCenter.vue')),
|
||||
}
|
||||
|
||||
export const userMenuItems: MenuItem[] = userMenuConfigs.map(config => ({
|
||||
export const userMenuItems: MenuItem[] = effectiveUserMenuConfigs.map(config => ({
|
||||
key: config.key,
|
||||
label: config.label,
|
||||
icon: config.icon,
|
||||
@@ -76,10 +86,11 @@ const componentMap: Record<string, () => Promise<any>> = {
|
||||
'DetectionHistory.vue': () => import('@/views/user_pages/DetectionHistory.vue'),
|
||||
'AlertCenter.vue': () => import('@/views/user_pages/AlertCenter.vue'),
|
||||
'NoticeCenter.vue': () => import('@/views/user_pages/NoticeCenter.vue'),
|
||||
'KnowledgeCenter.vue': () => import('@/views/user_pages/KnowledgeCenter.vue'),
|
||||
'ArticleCenter.vue': () => import('@/views/user_pages/ArticleCenter.vue'),
|
||||
'KbCenter.vue': () => import('@/views/user_pages/KbCenter.vue'),
|
||||
}
|
||||
|
||||
const baseRoutes: RouteRecordRaw[] = userMenuConfigs.map(config => {
|
||||
const baseRoutes: RouteRecordRaw[] = effectiveUserMenuConfigs.map(config => {
|
||||
const route: RouteRecordRaw = {
|
||||
path: config.path,
|
||||
name: `User${config.key.charAt(0).toUpperCase() + config.key.slice(1)}`,
|
||||
@@ -101,7 +112,7 @@ const baseRoutes: RouteRecordRaw[] = userMenuConfigs.map(config => {
|
||||
const knowledgeDetailRoute: RouteRecordRaw = {
|
||||
path: '/user/knowledge/:id',
|
||||
name: 'UserKnowledgeDetail',
|
||||
component: () => import('@/views/user_pages/KnowledgeDetail.vue'),
|
||||
component: () => import('@/views/user_pages/ArticleDetail.vue'),
|
||||
meta: { title: '文章详情', requiresAuth: true, hideInMenu: true }
|
||||
}
|
||||
|
||||
@@ -148,14 +159,14 @@ export const generateComponentMap = () => {
|
||||
if (config.children) processConfigs(config.children)
|
||||
})
|
||||
}
|
||||
processConfigs(userMenuConfigs)
|
||||
processConfigs(effectiveUserMenuConfigs)
|
||||
return map
|
||||
}
|
||||
|
||||
export const userComponentMap = generateComponentMap()
|
||||
|
||||
export const getFilteredUserMenuItems = (userRoles: string[], userPermissions: string[]): MenuItem[] => {
|
||||
return userMenuConfigs
|
||||
return effectiveUserMenuConfigs
|
||||
.filter(config => {
|
||||
// 隐藏菜单中不显示的项(如个人信息,只在用户下拉菜单中显示)
|
||||
if (config.meta?.hideInMenu) return false
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ChangePasswordParams } from '@/api/password'
|
||||
import { roleApi } from '@/api/role'
|
||||
import { initializeMenuMapping } from '@/utils/menu_mapping'
|
||||
import { logoutUser } from '@/api/auth'
|
||||
import { hasModuleSelection } from '@/config/hertz_modules'
|
||||
|
||||
// 用户信息接口
|
||||
interface UserInfo {
|
||||
@@ -69,8 +70,11 @@ export const useUserStore = defineStore('user', () => {
|
||||
localStorage.setItem('userInfo', JSON.stringify(response.user_info))
|
||||
}
|
||||
|
||||
// 获取用户菜单权限
|
||||
await fetchUserMenuPermissions()
|
||||
// 获取用户菜单权限(模板模式首次运行时跳过)
|
||||
const isTemplateMode = import.meta.env.VITE_TEMPLATE_SETUP_MODE === 'true'
|
||||
if (!isTemplateMode || hasModuleSelection()) {
|
||||
await fetchUserMenuPermissions()
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
@@ -143,8 +147,11 @@ export const useUserStore = defineStore('user', () => {
|
||||
console.log('👤 恢复的用户信息:', parsedUserInfo)
|
||||
console.log('🔐 登录状态:', isLoggedIn.value)
|
||||
|
||||
// 获取用户菜单权限
|
||||
await fetchUserMenuPermissions()
|
||||
// 获取用户菜单权限(模板模式首次运行时跳过)
|
||||
const isTemplateMode = import.meta.env.VITE_TEMPLATE_SETUP_MODE === 'true'
|
||||
if (!isTemplateMode || hasModuleSelection()) {
|
||||
await fetchUserMenuPermissions()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 解析用户信息失败:', error)
|
||||
clearAuth()
|
||||
@@ -181,6 +188,14 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const adminRoleCodes = ['admin', 'system_admin', 'super_admin']
|
||||
const hasAdminRole = userInfo.value.roles.some(role => adminRoleCodes.includes(role.role_code))
|
||||
|
||||
if (!hasAdminRole) {
|
||||
userMenuPermissions.value = []
|
||||
return []
|
||||
}
|
||||
|
||||
// 获取用户所有角色的菜单权限
|
||||
const allMenuPermissions = new Set<number>()
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
149
hertz_server_diango_ui/src/views/ModuleSetup.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="module-setup">
|
||||
<a-card title="功能模块配置" class="module-setup-card">
|
||||
<div class="module-setup-intro">
|
||||
<p class="intro-title">使用说明</p>
|
||||
<p>1)本页面用于功能模块 DIY:勾选需要启用的模块,未勾选的模块将在菜单和路由中隐藏,仅作为运行时屏蔽,不影响源码文件。</p>
|
||||
<p>2)配置保存成功后,选择结果会以 <code>hertz_enabled_modules</code> 的形式保存在浏览器 Local Storage 中,下次执行 <code>npm run dev</code> 时如果存在该记录,将直接进入系统而不再展示本配置页。</p>
|
||||
<p>3)如需重新调整模块,请打开浏览器开发者工具 → <strong>Application</strong> → <strong>Local Storage</strong> → 选择当前站点,删除键 <code>hertz_enabled_modules</code>,然后刷新页面即可重新回到本页重新选择。</p>
|
||||
<a-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
message="一键裁剪(可选)"
|
||||
description="在本页确认模块选择并关闭运行环境后,可在终端运行 npm run prune,按提示对未勾选模块进行一键裁剪(支持仅屏蔽或直接删除相关页面)。"
|
||||
/>
|
||||
</div>
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="admin" tab="管理端模块">
|
||||
<a-checkbox-group v-model:value="adminSelected" class="module-group">
|
||||
<div
|
||||
v-for="m in adminModules"
|
||||
:key="m.key"
|
||||
class="module-item"
|
||||
>
|
||||
<a-checkbox :value="m.key">
|
||||
<span class="module-label">{{ m.label }}</span>
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</a-checkbox-group>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="user" tab="用户端模块">
|
||||
<a-checkbox-group v-model:value="userSelected" class="module-group">
|
||||
<div
|
||||
v-for="m in userModules"
|
||||
:key="m.key"
|
||||
class="module-item"
|
||||
>
|
||||
<a-checkbox :value="m.key">
|
||||
<span class="module-label">{{ m.label }}</span>
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</a-checkbox-group>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<div class="module-setup-actions">
|
||||
<a-button style="margin-right: 8px" @click="resetToDefault">恢复默认</a-button>
|
||||
<a-button style="margin-right: 8px" @click="saveModules">保存配置并刷新</a-button>
|
||||
<a-button type="primary" @click="saveModulesAndGoLogin">保存并跳转登录</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import {
|
||||
HERTZ_MODULES,
|
||||
getEnabledModuleKeys,
|
||||
getModulesByGroup,
|
||||
setEnabledModuleKeys,
|
||||
type HertzModuleGroup,
|
||||
} from '@/config/hertz_modules'
|
||||
|
||||
const activeKey = ref<HertzModuleGroup>('admin')
|
||||
|
||||
const adminModules = computed(() => getModulesByGroup('admin'))
|
||||
const userModules = computed(() => getModulesByGroup('user'))
|
||||
|
||||
const adminSelected = ref<string[]>([])
|
||||
const userSelected = ref<string[]>([])
|
||||
|
||||
const loadCurrentSelection = () => {
|
||||
const enabled = getEnabledModuleKeys()
|
||||
adminSelected.value = enabled.filter(k => k.indexOf('admin.') === 0)
|
||||
userSelected.value = enabled.filter(k => k.indexOf('user.') === 0)
|
||||
}
|
||||
|
||||
const resetToDefault = () => {
|
||||
const defaultEnabled = HERTZ_MODULES.filter(m => m.defaultEnabled).map(m => m.key)
|
||||
setEnabledModuleKeys(defaultEnabled)
|
||||
loadCurrentSelection()
|
||||
}
|
||||
|
||||
const saveModules = () => {
|
||||
const merged = adminSelected.value.concat(userSelected.value)
|
||||
setEnabledModuleKeys(merged)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
const saveModulesAndGoLogin = () => {
|
||||
const merged = adminSelected.value.concat(userSelected.value)
|
||||
setEnabledModuleKeys(merged)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCurrentSelection()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.module-setup {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 48px 16px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.module-setup-card {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.module-setup-intro {
|
||||
margin-bottom: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.intro-title {
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.module-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.module-label {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.module-setup-actions {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -7,8 +7,8 @@
|
||||
<DatabaseOutlined class="header-icon" />
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h1 class="page-title">知识库管理</h1>
|
||||
<p class="page-description">集中管理内部知识文档,支持分类、标签和搜索</p>
|
||||
<h1 class="page-title">文章管理</h1>
|
||||
<p class="page-description">集中管理文章内容,支持分类、标签和搜索</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,9 +60,9 @@
|
||||
|
||||
<!-- 内容区:分类树 + 文档列表 -->
|
||||
<div class="content-container">
|
||||
<a-row :gutter="16">
|
||||
<a-row :gutter="[8, 16]">
|
||||
<!-- 左侧分类树 -->
|
||||
<a-col :xs="24" :md="6">
|
||||
<a-col :xs="24" :md="5">
|
||||
<div class="category-container">
|
||||
<a-card title="分类" size="small" :loading="false" :bordered="false">
|
||||
<a-tree
|
||||
@@ -77,7 +77,7 @@
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧文档列表 -->
|
||||
<a-col :xs="24" :md="18">
|
||||
<a-col :xs="24" :md="19">
|
||||
<div class="table-container">
|
||||
<a-card title="文档列表" size="small" :bordered="false">
|
||||
<a-table
|
||||
@@ -88,32 +88,42 @@
|
||||
row-key="id"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status_display'">
|
||||
<a-tag :color="statusColor(record.status)">
|
||||
{{ record.status_display }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handlePublish(record)" :disabled="record.status === 'published'">
|
||||
发布
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleArchive(record)" :disabled="record.status === 'archived'">
|
||||
归档
|
||||
</a-button>
|
||||
<a-popconfirm title="确认删除该文档?" @confirm="() => handleDelete(record)">
|
||||
<a-button danger type="link" size="small">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status_display'">
|
||||
<a-tag :color="statusColor(record.status)">
|
||||
{{ record.status_display }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space wrap size="small">
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handlePublish(record)"
|
||||
:disabled="record.status === 'published'"
|
||||
>
|
||||
发布
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleArchive(record)"
|
||||
:disabled="record.status === 'archived'"
|
||||
>
|
||||
归档
|
||||
</a-button>
|
||||
<a-popconfirm title="确认删除该文档?" @confirm="() => handleDelete(record)">
|
||||
<a-button danger type="link" size="small">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
@@ -144,7 +154,13 @@
|
||||
</a-drawer>
|
||||
|
||||
<!-- 新建/编辑文档弹窗 -->
|
||||
<a-modal v-model:visible="editVisible" :title="editMode === 'create' ? '新建文档' : '编辑文档'" @ok="handleSave" :confirm-loading="saving" width="720px">
|
||||
<a-modal
|
||||
v-model:visible="editVisible"
|
||||
:title="editMode === 'create' ? '新建文档' : '编辑文档'"
|
||||
@ok="handleSave"
|
||||
:confirm-loading="saving"
|
||||
width="720px"
|
||||
>
|
||||
<a-form :model="editForm" layout="vertical">
|
||||
<a-form-item label="标题" required>
|
||||
<a-input v-model:value="editForm.title" placeholder="请输入文档标题" />
|
||||
@@ -162,93 +178,112 @@
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="内容">
|
||||
<a-textarea v-model:value="editForm.content" rows="8" placeholder="这里是内容编辑区域(可接入富文本编辑器)" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 分类管理弹窗 -->
|
||||
<a-modal v-model:visible="categoryVisible" title="分类管理" width="800px" :footer="null">
|
||||
<div class="category-toolbar">
|
||||
<a-space wrap>
|
||||
<a-button type="primary" @click="handleCategoryAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建分类
|
||||
</a-button>
|
||||
<a-input-search
|
||||
v-model:value="categorySearchName"
|
||||
placeholder="按名称搜索分类"
|
||||
style="width: 280px"
|
||||
@search="fetchCategoryListImmediate"
|
||||
allow-clear
|
||||
:loading="categoryLoading"
|
||||
>
|
||||
<template #enterButton>
|
||||
<a-button type="primary">搜索</a-button>
|
||||
</template>
|
||||
</a-input-search>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="categoryColumns"
|
||||
:data-source="categoryList"
|
||||
:loading="categoryLoading"
|
||||
:pagination="categoryPagination"
|
||||
row-key="id"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'children_count'">
|
||||
{{ record.children_count ?? 0 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'articles_count'">
|
||||
{{ record.articles_count ?? 0 }}
|
||||
</template>
|
||||
<template v-if="column.key === 'is_active'">
|
||||
<a-tag :color="record.is_active ? 'green' : 'red'">
|
||||
{{ record.is_active ? '启用' : '停用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleCategoryToggleActive(record)">
|
||||
{{ record.is_active ? '停用' : '启用' }}
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleCategoryEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确认删除该分类?" @confirm="() => handleCategoryDelete(record)">
|
||||
<a-button danger type="link" size="small">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 分类编辑弹窗 -->
|
||||
<a-modal v-model:visible="categoryEditVisible" :title="categoryEditMode === 'create' ? '新建分类' : '编辑分类'" @ok="handleCategorySave" :confirm-loading="categorySaving" width="640px">
|
||||
<a-form :model="categoryForm" layout="vertical">
|
||||
<a-form-item label="名称" required>
|
||||
<a-input v-model:value="categoryForm.name" placeholder="请输入分类名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="父级分类">
|
||||
<a-select v-model:value="categoryForm.parent" :options="categoryOptions" placeholder="请选择父级分类(可选)" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序">
|
||||
<a-input-number v-model:value="categoryForm.sort_order" :min="0" :max="9999" style="width: 160px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="启用状态">
|
||||
<a-switch v-model:checked="categoryForm.is_active" checked-children="启用" un-checked-children="停用" />
|
||||
</a-form-item>
|
||||
<a-form-item label="描述">
|
||||
<a-textarea v-model:value="categoryForm.description" rows="4" placeholder="请输入分类描述(可选)" />
|
||||
<a-textarea
|
||||
v-model:value="editForm.content"
|
||||
rows="8"
|
||||
placeholder="这里是内容编辑区域(可接入富文本编辑器)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
<!-- 分类管理弹窗 -->
|
||||
<a-modal v-model:visible="categoryVisible" title="分类管理" width="800px" :footer="null">
|
||||
<div class="category-toolbar">
|
||||
<a-space wrap>
|
||||
<a-button type="primary" @click="handleCategoryAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建分类
|
||||
</a-button>
|
||||
<a-input-search
|
||||
v-model:value="categorySearchName"
|
||||
placeholder="按名称搜索分类"
|
||||
style="width: 280px"
|
||||
@search="fetchCategoryListImmediate"
|
||||
allow-clear
|
||||
:loading="categoryLoading"
|
||||
>
|
||||
<template #enterButton>
|
||||
<a-button type="primary">搜索</a-button>
|
||||
</template>
|
||||
</a-input-search>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="categoryColumns"
|
||||
:data-source="categoryList"
|
||||
:loading="categoryLoading"
|
||||
:pagination="categoryPagination"
|
||||
row-key="id"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'children_count'">
|
||||
{{ record.children_count ?? 0 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'articles_count'">
|
||||
{{ record.articles_count ?? 0 }}
|
||||
</template>
|
||||
<template v-if="column.key === 'is_active'">
|
||||
<a-tag :color="record.is_active ? 'green' : 'red'">
|
||||
{{ record.is_active ? '启用' : '停用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleCategoryToggleActive(record)">
|
||||
{{ record.is_active ? '停用' : '启用' }}
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleCategoryEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确认删除该分类?" @confirm="() => handleCategoryDelete(record)">
|
||||
<a-button danger type="link" size="small">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 分类编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="categoryEditVisible"
|
||||
:title="categoryEditMode === 'create' ? '新建分类' : '编辑分类'"
|
||||
@ok="handleCategorySave"
|
||||
:confirm-loading="categorySaving"
|
||||
width="640px"
|
||||
>
|
||||
<a-form :model="categoryForm" layout="vertical">
|
||||
<a-form-item label="名称" required>
|
||||
<a-input v-model:value="categoryForm.name" placeholder="请输入分类名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="父级分类">
|
||||
<a-select
|
||||
v-model:value="categoryForm.parent"
|
||||
:options="categoryOptions"
|
||||
placeholder="请选择父级分类(可选)"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="排序">
|
||||
<a-input-number
|
||||
v-model:value="categoryForm.sort_order"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="启用状态">
|
||||
<a-switch v-model:checked="categoryForm.is_active" checked-children="启用" un-checked-children="停用" />
|
||||
</a-form-item>
|
||||
<a-form-item label="描述">
|
||||
<a-textarea v-model:value="categoryForm.description" rows="4" placeholder="请输入分类描述(可选)" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -265,9 +300,6 @@ import {
|
||||
} from '@ant-design/icons-vue'
|
||||
import { knowledgeApi, type KnowledgeArticleListItem, type KnowledgeArticleDetail, type KnowledgeCategory } from '@/api/knowledge'
|
||||
|
||||
|
||||
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -275,7 +307,7 @@ const loading = ref(false)
|
||||
const searchText = ref('')
|
||||
|
||||
// 分页(服务端)
|
||||
const pagination = ref({ current: 1, pageSize: 10, total: 0, showSizeChanger: true, pageSizeOptions: ['10','20','50'] })
|
||||
const pagination = ref({ current: 1, pageSize: 10, total: 0, showSizeChanger: true, pageSizeOptions: ['10', '20', '50'] })
|
||||
|
||||
// 分类树(接口)
|
||||
const categoryTree = ref<any[]>([{ title: '全部', key: 'all' }])
|
||||
@@ -286,8 +318,18 @@ const columns = [
|
||||
{ title: '标题', dataIndex: 'title', key: 'title', ellipsis: true },
|
||||
{ title: '分类', dataIndex: 'category_name', key: 'category_name', ellipsis: true },
|
||||
{ title: '状态', dataIndex: 'status_display', key: 'status_display' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
{ title: '发布时间', dataIndex: 'published_at', key: 'published_at' },
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
customRender: ({ text }: { text: string }) => formatDateOnly(text)
|
||||
},
|
||||
{
|
||||
title: '发布时间',
|
||||
dataIndex: 'published_at',
|
||||
key: 'published_at',
|
||||
customRender: ({ text }: { text: string }) => (text ? formatDateOnly(text) : '-')
|
||||
},
|
||||
{ title: '操作', key: 'actions' }
|
||||
]
|
||||
|
||||
@@ -316,14 +358,15 @@ const categoryOptions = computed(() => {
|
||||
// 状态颜色
|
||||
const statusColor = (s: 'draft' | 'published' | 'archived') => {
|
||||
switch (s) {
|
||||
case 'published': return 'green'
|
||||
case 'archived': return 'orange'
|
||||
default: return 'blue'
|
||||
case 'published':
|
||||
return 'green'
|
||||
case 'archived':
|
||||
return 'orange'
|
||||
default:
|
||||
return 'blue'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 详情态
|
||||
const detailOpen = ref(false)
|
||||
const currentDoc = ref<KnowledgeArticleDetail | null>(null)
|
||||
@@ -332,7 +375,15 @@ const currentDoc = ref<KnowledgeArticleDetail | null>(null)
|
||||
const editVisible = ref(false)
|
||||
const editMode = ref<'create' | 'update'>('create')
|
||||
const saving = ref(false)
|
||||
const editForm = ref<any>({ id: 0, title: '', category: undefined as number | undefined, tagsArray: [] as string[], status: 'draft' as 'draft' | 'published', content: '', summary: '' })
|
||||
const editForm = ref<any>({
|
||||
id: 0,
|
||||
title: '',
|
||||
category: undefined as number | undefined,
|
||||
tagsArray: [] as string[],
|
||||
status: 'draft' as 'draft' | 'published',
|
||||
content: '',
|
||||
summary: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategories()
|
||||
@@ -341,7 +392,6 @@ onMounted(() => {
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
|
||||
const res = await knowledgeApi.getCategoryTree()
|
||||
const data = (res.data || []) as KnowledgeCategory[]
|
||||
const mapNode = (n: KnowledgeCategory): any => ({
|
||||
@@ -364,7 +414,7 @@ const fetchArticles = async () => {
|
||||
|
||||
const params: any = {
|
||||
page: needsClientFilter ? 1 : pagination.value.current,
|
||||
page_size: needsClientFilter ? 50 : pagination.value.pageSize, // 如果需要客户端筛选,获取更多数据
|
||||
page_size: needsClientFilter ? 50 : pagination.value.pageSize // 如果需要客户端筛选,获取更多数据
|
||||
}
|
||||
|
||||
// 先尝试后端搜索
|
||||
@@ -389,10 +439,12 @@ const fetchArticles = async () => {
|
||||
const tags = (article.tags_list || []).join(' ').toLowerCase()
|
||||
const summary = (article.summary || '').toLowerCase()
|
||||
const categoryName = (article.category_name || '').toLowerCase()
|
||||
return title.includes(keywordLower) ||
|
||||
tags.includes(keywordLower) ||
|
||||
summary.includes(keywordLower) ||
|
||||
categoryName.includes(keywordLower)
|
||||
return (
|
||||
title.includes(keywordLower) ||
|
||||
tags.includes(keywordLower) ||
|
||||
summary.includes(keywordLower) ||
|
||||
categoryName.includes(keywordLower)
|
||||
)
|
||||
})
|
||||
|
||||
// 客户端分页
|
||||
@@ -451,9 +503,16 @@ const handleView = async (record: KnowledgeArticleListItem) => {
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
|
||||
editMode.value = 'create'
|
||||
editForm.value = { id: 0, title: '', category: undefined, tagsArray: [], status: 'draft', content: '', summary: '' }
|
||||
editForm.value = {
|
||||
id: 0,
|
||||
title: '',
|
||||
category: undefined,
|
||||
tagsArray: [],
|
||||
status: 'draft',
|
||||
content: '',
|
||||
summary: ''
|
||||
}
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
@@ -526,7 +585,7 @@ const handleSave = async () => {
|
||||
category: Number(categoryVal),
|
||||
status: editForm.value.status || 'draft',
|
||||
tags: tags || undefined,
|
||||
sort_order: 0,
|
||||
sort_order: 0
|
||||
}
|
||||
|
||||
if (editMode.value === 'create') {
|
||||
@@ -576,7 +635,6 @@ const handleArchive = async (record: KnowledgeArticleListItem) => {
|
||||
}
|
||||
|
||||
const openCategoryManager = () => {
|
||||
|
||||
categoryVisible.value = true
|
||||
fetchCategoryList()
|
||||
}
|
||||
@@ -607,10 +665,12 @@ pagination.value = {
|
||||
const tags = (article.tags_list || []).join(' ').toLowerCase()
|
||||
const summary = (article.summary || '').toLowerCase()
|
||||
const categoryName = (article.category_name || '').toLowerCase()
|
||||
return title.includes(keyword) ||
|
||||
tags.includes(keyword) ||
|
||||
summary.includes(keyword) ||
|
||||
categoryName.includes(keyword)
|
||||
return (
|
||||
title.includes(keyword) ||
|
||||
tags.includes(keyword) ||
|
||||
summary.includes(keyword) ||
|
||||
categoryName.includes(keyword)
|
||||
)
|
||||
})
|
||||
|
||||
// 客户端分页
|
||||
@@ -640,10 +700,12 @@ pagination.value = {
|
||||
const tags = (article.tags_list || []).join(' ').toLowerCase()
|
||||
const summary = (article.summary || '').toLowerCase()
|
||||
const categoryName = (article.category_name || '').toLowerCase()
|
||||
return title.includes(keyword) ||
|
||||
tags.includes(keyword) ||
|
||||
summary.includes(keyword) ||
|
||||
categoryName.includes(keyword)
|
||||
return (
|
||||
title.includes(keyword) ||
|
||||
tags.includes(keyword) ||
|
||||
summary.includes(keyword) ||
|
||||
categoryName.includes(keyword)
|
||||
)
|
||||
})
|
||||
|
||||
// 客户端分页
|
||||
@@ -665,7 +727,13 @@ const categoryVisible = ref(false)
|
||||
const categoryLoading = ref(false)
|
||||
const categoryList = ref<KnowledgeCategory[]>([])
|
||||
const categorySearchName = ref('')
|
||||
const categoryPagination = ref({ current: 1, pageSize: 10, total: 0, showSizeChanger: true, pageSizeOptions: ['10','20','50'] })
|
||||
const categoryPagination = ref({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50']
|
||||
})
|
||||
const categoryColumns = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '父级', dataIndex: 'parent_name', key: 'parent_name' },
|
||||
@@ -673,20 +741,27 @@ const categoryColumns = [
|
||||
{ title: '子分类数', dataIndex: 'children_count', key: 'children_count' },
|
||||
{ title: '文章数', dataIndex: 'articles_count', key: 'articles_count' },
|
||||
{ title: '状态', key: 'is_active' },
|
||||
{ title: '操作', key: 'actions' },
|
||||
{ title: '操作', key: 'actions' }
|
||||
]
|
||||
|
||||
const categoryEditVisible = ref(false)
|
||||
const categoryEditMode = ref<'create' | 'update'>('create')
|
||||
const categorySaving = ref(false)
|
||||
const categoryForm = ref<Partial<KnowledgeCategory>>({ id: 0, name: '', description: '', parent: undefined, sort_order: 0, is_active: true })
|
||||
const categoryForm = ref<Partial<KnowledgeCategory>>({
|
||||
id: 0,
|
||||
name: '',
|
||||
description: '',
|
||||
parent: undefined,
|
||||
sort_order: 0,
|
||||
is_active: true
|
||||
})
|
||||
|
||||
const fetchCategoryList = async () => {
|
||||
categoryLoading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: categoryPagination.value.current,
|
||||
page_size: categoryPagination.value.pageSize,
|
||||
page_size: categoryPagination.value.pageSize
|
||||
}
|
||||
const kw = categorySearchName.value.trim()
|
||||
if (kw) params.name = kw
|
||||
@@ -721,16 +796,27 @@ categoryPagination.value = {
|
||||
}
|
||||
|
||||
const handleCategoryAdd = () => {
|
||||
|
||||
categoryEditMode.value = 'create'
|
||||
categoryForm.value = { name: '', description: '', parent: undefined, sort_order: 0, is_active: true }
|
||||
categoryForm.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
parent: undefined,
|
||||
sort_order: 0,
|
||||
is_active: true
|
||||
}
|
||||
categoryEditVisible.value = true
|
||||
}
|
||||
|
||||
const handleCategoryEdit = (record: KnowledgeCategory) => {
|
||||
|
||||
categoryEditMode.value = 'update'
|
||||
categoryForm.value = { id: record.id, name: record.name, description: record.description, parent: record.parent ?? undefined, sort_order: record.sort_order, is_active: record.is_active }
|
||||
categoryForm.value = {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
description: record.description,
|
||||
parent: record.parent ?? undefined,
|
||||
sort_order: record.sort_order,
|
||||
is_active: record.is_active
|
||||
}
|
||||
categoryEditVisible.value = true
|
||||
}
|
||||
|
||||
@@ -742,7 +828,7 @@ const handleCategorySave = async () => {
|
||||
description: categoryForm.value.description || undefined,
|
||||
parent: categoryForm.value.parent ?? undefined,
|
||||
sort_order: categoryForm.value.sort_order ?? 0,
|
||||
is_active: categoryForm.value.is_active ?? true,
|
||||
is_active: categoryForm.value.is_active ?? true
|
||||
}
|
||||
|
||||
if (categoryEditMode.value === 'create') {
|
||||
@@ -788,6 +874,13 @@ const handleCategoryToggleActive = async (record: KnowledgeCategory) => {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 仅格式化为日期(YYYY-MM-DD),用于列表展示
|
||||
const formatDateOnly = (val?: string): string => {
|
||||
if (!val) return '-'
|
||||
const str = String(val)
|
||||
return str.length >= 10 ? str.slice(0, 10) : str
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -921,6 +1014,10 @@ const handleCategoryToggleActive = async (record: KnowledgeCategory) => {
|
||||
}
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
:deep(.ant-input-search) {
|
||||
.ant-input {
|
||||
border-radius: 8px;
|
||||
@@ -1012,7 +1109,7 @@ const handleCategoryToggleActive = async (record: KnowledgeCategory) => {
|
||||
font-weight: 600;
|
||||
color: #1d1d1f;
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 16px;
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
@@ -1025,7 +1122,7 @@ const handleCategoryToggleActive = async (record: KnowledgeCategory) => {
|
||||
}
|
||||
|
||||
> td {
|
||||
padding: 16px;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
color: #1d1d1f;
|
||||
}
|
||||
@@ -506,7 +506,7 @@ const quickActions = [
|
||||
{ key: 'role', label: '角色管理', icon: DatabaseOutlined, gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
|
||||
{ key: 'notification', label: '通知管理', icon: BellOutlined, gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
|
||||
{ key: 'log', label: '日志管理', icon: FileSearchOutlined, gradient: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' },
|
||||
{ key: 'knowledge', label: '知识库管理', icon: BookOutlined, gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
|
||||
{ key: 'knowledge', label: '文章管理', icon: BookOutlined, gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
|
||||
{ key: 'model-management', label: '模型管理', icon: RobotOutlined, gradient: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)' },
|
||||
{ key: 'alert-level-management', label: '类别管理', icon: WarningOutlined, gradient: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)' },
|
||||
{ key: 'alert-processing-center', label: '告警中心', icon: BellOutlined, gradient: 'linear-gradient(135deg, #ff6e7f 0%, #bfe9ff 100%)' },
|
||||
@@ -594,8 +594,8 @@ const handleQuickAction = (action: string) => {
|
||||
message.success('跳转到日志管理页面')
|
||||
break
|
||||
case 'knowledge':
|
||||
router.push('/admin/knowledge-base')
|
||||
message.success('跳转到知识库管理页面')
|
||||
router.push('/admin/article-management')
|
||||
message.success('跳转到文章管理页面')
|
||||
break
|
||||
case 'model-management':
|
||||
router.push('/admin/model-management')
|
||||
|
||||
1404
hertz_server_diango_ui/src/views/admin_page/DatasetManagement.vue
Normal file
@@ -47,7 +47,6 @@
|
||||
@change="handleTableChange"
|
||||
row-key="dept_id"
|
||||
size="middle"
|
||||
:scroll="{ x: 1200 }"
|
||||
>
|
||||
<!-- 部门名称列 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
@@ -325,49 +324,41 @@ const columns = [
|
||||
title: '部门名称',
|
||||
dataIndex: 'dept_name',
|
||||
key: 'dept_name',
|
||||
width: 200,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '部门编码',
|
||||
dataIndex: 'dept_code',
|
||||
key: 'dept_code',
|
||||
width: 120
|
||||
key: 'dept_code'
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'leader',
|
||||
key: 'leader',
|
||||
width: 100
|
||||
key: 'leader'
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 120
|
||||
key: 'phone'
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
width: 180
|
||||
key: 'email'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80
|
||||
key: 'status'
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort_order',
|
||||
key: 'sort_order',
|
||||
width: 80
|
||||
key: 'sort_order'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
@@ -768,7 +759,7 @@ onMounted(() => {
|
||||
font-weight: 600;
|
||||
color: #1d1d1f;
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 16px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
@@ -781,7 +772,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
> td {
|
||||
padding: 16px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
color: #1d1d1f;
|
||||
}
|
||||
|
||||
@@ -874,7 +874,7 @@ function rowClassName(record: OperationLogItem) {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-input-affix-wrapper),
|
||||
:deep(.ant-input-number),
|
||||
:deep(.ant-select-selector),
|
||||
:deep(.ant-picker) {
|
||||
|
||||
@@ -16,16 +16,23 @@
|
||||
<!-- 操作栏 - 苹果风格 -->
|
||||
<div class="action-bar">
|
||||
<div class="search-section">
|
||||
<a-input-search
|
||||
<a-input
|
||||
v-model:value="searchText"
|
||||
placeholder="搜索菜单名称或编码"
|
||||
style="width: 300px"
|
||||
@search="handleSearchImmediate"
|
||||
@pressEnter="handleSearchImmediate"
|
||||
@input="handleSearch"
|
||||
allow-clear
|
||||
:loading="loading"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="search-btn"
|
||||
style="margin-left: 12px"
|
||||
:loading="loading"
|
||||
@click="handleSearchImmediate"
|
||||
>
|
||||
查询
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="button-section">
|
||||
<a-button type="primary" @click="handleAdd" class="action-btn-primary">
|
||||
@@ -48,7 +55,6 @@
|
||||
:pagination="false"
|
||||
row-key="menu_id"
|
||||
size="middle"
|
||||
:scroll="{ x: 1200 }"
|
||||
childrenColumnName="children"
|
||||
:default-expand-all-rows="false"
|
||||
:expanded-row-keys="expandedRowKeys"
|
||||
@@ -308,8 +314,6 @@ const modalMode = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const searchText = ref('')
|
||||
|
||||
|
||||
|
||||
// 菜单详情相关
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
@@ -379,60 +383,44 @@ const columns = [
|
||||
{
|
||||
title: '菜单名称',
|
||||
dataIndex: 'menu_name',
|
||||
key: 'menu_name',
|
||||
width: 200,
|
||||
fixed: 'left'
|
||||
key: 'menu_name'
|
||||
},
|
||||
{
|
||||
title: '菜单编码',
|
||||
dataIndex: 'menu_code',
|
||||
key: 'menu_code',
|
||||
width: 150
|
||||
key: 'menu_code'
|
||||
},
|
||||
{
|
||||
title: '菜单类型',
|
||||
dataIndex: 'menu_type',
|
||||
key: 'menu_type',
|
||||
width: 100
|
||||
key: 'menu_type'
|
||||
},
|
||||
{
|
||||
title: '路径',
|
||||
dataIndex: 'path',
|
||||
key: 'path',
|
||||
width: 200
|
||||
key: 'path'
|
||||
},
|
||||
{
|
||||
title: '图标',
|
||||
dataIndex: 'icon',
|
||||
key: 'icon',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort_order',
|
||||
key: 'sort_order',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right'
|
||||
key: 'action'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -664,8 +652,6 @@ const refreshData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 搜索防抖
|
||||
let searchTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
@@ -689,10 +675,7 @@ const handleSearchImmediate = () => {
|
||||
// 搜索时不需要重新获取数据,filteredMenuData会自动过滤
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 展开/收起菜单
|
||||
const onExpand = (expanded: boolean, record: Menu) => {
|
||||
if (expanded) {
|
||||
if (!expandedRowKeys.value.includes(record.menu_id)) {
|
||||
@@ -1001,7 +984,7 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
|
||||
:deep(.ant-input-search) {
|
||||
.ant-input {
|
||||
.ant-input-affix-wrapper {
|
||||
border-radius: 12px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
@@ -1011,7 +994,7 @@ onMounted(() => {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
&.ant-input-affix-wrapper-focused {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
@@ -1093,7 +1076,7 @@ onMounted(() => {
|
||||
font-weight: 600;
|
||||
color: #1d1d1f;
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 16px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
@@ -1106,7 +1089,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
> td {
|
||||
padding: 16px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
color: #1d1d1f;
|
||||
}
|
||||
|
||||
@@ -16,14 +16,22 @@
|
||||
<!-- 操作栏 - 苹果风格 -->
|
||||
<div class="action-bar">
|
||||
<div class="search-section">
|
||||
<a-input-search
|
||||
<a-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索通知标题、内容..."
|
||||
style="width: 300px"
|
||||
@search="handleSearch"
|
||||
@pressEnter="handleSearch"
|
||||
allow-clear
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="search-btn"
|
||||
style="margin-left: 12px"
|
||||
:loading="loading"
|
||||
@click="handleSearch"
|
||||
>
|
||||
查询
|
||||
</a-button>
|
||||
<a-select
|
||||
v-model:value="statusFilter"
|
||||
placeholder="状态筛选"
|
||||
@@ -85,7 +93,7 @@
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'title'">
|
||||
<div class="title-container">
|
||||
<span class="title-text">{{ record.title }}</span>
|
||||
<span class="title-text">{{ formatTitleShort(record.title) }}</span>
|
||||
<span v-if="record.is_top" class="top-tag">置顶</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -339,57 +347,59 @@ interface CommonResponse {
|
||||
data?: any
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
// 表格列定义(为避免横向滚动,控制每列更紧凑的宽度)
|
||||
const columns = [
|
||||
{
|
||||
title: '通知标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
ellipsis: true
|
||||
ellipsis: true,
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '通知类型',
|
||||
dataIndex: 'notice_type',
|
||||
key: 'notice_type',
|
||||
width: 100
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
key: 'priority',
|
||||
width: 80
|
||||
width: 70
|
||||
},
|
||||
{
|
||||
title: '是否置顶',
|
||||
dataIndex: 'is_top',
|
||||
key: 'is_top',
|
||||
width: 80,
|
||||
width: 70,
|
||||
customRender: ({ text }) => text ? '是' : '否'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'create_time',
|
||||
key: 'create_time',
|
||||
width: 180
|
||||
width: 90,
|
||||
customRender: ({ text }) => formatDateOnly(text as string)
|
||||
},
|
||||
{
|
||||
title: '过期时间',
|
||||
dataIndex: 'expire_time',
|
||||
key: 'expire_time',
|
||||
width: 180,
|
||||
customRender: ({ text }) => text || '永久有效'
|
||||
width: 90,
|
||||
customRender: ({ text }) => (text ? formatDateOnly(text as string) : '永久有效')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 280,
|
||||
fixed: 'right'
|
||||
fixed: 'right',
|
||||
width: 210
|
||||
}
|
||||
]
|
||||
|
||||
@@ -553,6 +563,20 @@ const getPriorityColor = (priority: number): string => {
|
||||
return colorMap[priority] || 'default'
|
||||
}
|
||||
|
||||
// 仅格式化为日期(YYYY-MM-DD),用于列表展示
|
||||
const formatDateOnly = (val?: string): string => {
|
||||
if (!val) return '-'
|
||||
const str = String(val)
|
||||
return str.length >= 10 ? str.slice(0, 10) : str
|
||||
}
|
||||
|
||||
// 通知标题仅保留前4个字符,多余用 ... 表示
|
||||
const formatTitleShort = (val?: string): string => {
|
||||
if (!val) return '-'
|
||||
const str = String(val)
|
||||
return str.length <= 4 ? str : str.slice(0, 4) + '...'
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
if (formRef.value) {
|
||||
@@ -614,13 +638,20 @@ const fetchNoticeList = async () => {
|
||||
|
||||
if (response.success && response.data) {
|
||||
// 处理数据,确保每个记录都有正确的id映射
|
||||
let processedList = response.data.notices.map(notice => ({
|
||||
...notice,
|
||||
// 规范化状态,兼容 status_display 或字符串状态
|
||||
status: getStatusCode(notice),
|
||||
notice_id: Number(notice.notice_id),
|
||||
id: Number(notice.notice_id) // 映射notice_id到id,兼容现有代码,并统一为数字
|
||||
}))
|
||||
let processedList = response.data.notices.map(notice => {
|
||||
const createTime = notice.create_time || notice.created_at
|
||||
const updateTime = notice.update_time || notice.updated_at
|
||||
|
||||
return {
|
||||
...notice,
|
||||
create_time: createTime,
|
||||
update_time: updateTime,
|
||||
// 规范化状态,兼容 status_display 或字符串状态
|
||||
status: getStatusCode(notice),
|
||||
notice_id: Number(notice.notice_id),
|
||||
id: Number(notice.notice_id) // 映射notice_id到id,兼容现有代码,并统一为数字
|
||||
}
|
||||
})
|
||||
|
||||
// 如果后端不支持搜索或筛选,在前端进行客户端筛选
|
||||
if (needsClientFilter) {
|
||||
@@ -724,7 +755,14 @@ const fetchNoticeDetail = async (id: number) => {
|
||||
detailLoading.value = true
|
||||
const response = await request.get<NoticeDetailResponse>(`/api/notice/admin/detail/${id}/`)
|
||||
if (response.success && response.data) {
|
||||
notificationDetail.value = response.data
|
||||
const data = response.data
|
||||
const createTime = data.create_time || data.created_at
|
||||
const updateTime = data.update_time || data.updated_at
|
||||
notificationDetail.value = {
|
||||
...data,
|
||||
create_time: createTime,
|
||||
update_time: updateTime
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取通知详情失败:', error)
|
||||
@@ -1108,7 +1146,7 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
|
||||
:deep(.ant-input-search) {
|
||||
.ant-input {
|
||||
.ant-input-affix-wrapper {
|
||||
border-radius: 12px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
@@ -1118,7 +1156,7 @@ onMounted(() => {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
&.ant-input-affix-wrapper-focused {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
@@ -1240,7 +1278,7 @@ onMounted(() => {
|
||||
font-weight: 600;
|
||||
color: #1d1d1f;
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 16px;
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
@@ -1253,7 +1291,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
> td {
|
||||
padding: 16px;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
color: #1d1d1f;
|
||||
}
|
||||
|
||||
@@ -16,14 +16,22 @@
|
||||
<!-- 操作栏 - 苹果风格 -->
|
||||
<div class="action-bar">
|
||||
<div class="search-section">
|
||||
<a-input-search
|
||||
<a-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索用户名、邮箱..."
|
||||
style="width: 300px"
|
||||
@search="handleSearch"
|
||||
@pressEnter="handleSearch"
|
||||
allow-clear
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="search-btn"
|
||||
style="margin-left: 12px"
|
||||
:loading="loading"
|
||||
@click="handleSearch"
|
||||
>
|
||||
查询
|
||||
</a-button>
|
||||
<a-select
|
||||
v-model:value="statusFilter"
|
||||
placeholder="用户状态"
|
||||
@@ -1153,7 +1161,7 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
|
||||
:deep(.ant-input-search) {
|
||||
.ant-input {
|
||||
.ant-input-affix-wrapper {
|
||||
border-radius: 12px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
@@ -1163,7 +1171,7 @@ onMounted(() => {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
&.ant-input-affix-wrapper-focused {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
1305
hertz_server_diango_ui/src/views/admin_page/YoloTrainManagement.vue
Normal file
@@ -223,12 +223,21 @@
|
||||
<div v-else-if="currentUserInfo" class="user-info-content">
|
||||
<!-- 用户头像区域 -->
|
||||
<div class="user-avatar-section">
|
||||
<a-avatar :size="80" :src="currentUserInfo.avatar" class="large-avatar">
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
{{ currentUserInfo.username?.charAt(0)?.toUpperCase() }}
|
||||
</a-avatar>
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
:before-upload="handleAdminAvatarBeforeUpload"
|
||||
>
|
||||
<a-avatar
|
||||
:size="80"
|
||||
:src="currentUserInfo.avatar || userStore.userInfo?.avatar"
|
||||
class="large-avatar"
|
||||
>
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
{{ currentUserInfo.username?.charAt(0)?.toUpperCase() }}
|
||||
</a-avatar>
|
||||
</a-upload>
|
||||
<h3 class="user-display-name">
|
||||
{{ currentUserInfo.real_name || currentUserInfo.username }}
|
||||
</h3>
|
||||
@@ -718,6 +727,53 @@ const fetchCurrentUserInfo = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdminAvatarBeforeUpload = async (file: File) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
if (!isImage) {
|
||||
message.error('只能上传图片文件作为头像')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
message.error('头像图片大小不能超过 2MB')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await userApi.uploadAvatar(file)
|
||||
const updated = res?.data ?? res
|
||||
const newAvatar = updated?.avatar || updated?.avatar_url
|
||||
|
||||
if (newAvatar) {
|
||||
if (currentUserInfo.value) {
|
||||
currentUserInfo.value.avatar = newAvatar
|
||||
}
|
||||
|
||||
if (userInfoForm.value) {
|
||||
(userInfoForm.value as any).avatar = newAvatar
|
||||
}
|
||||
|
||||
if (userStore.userInfo) {
|
||||
userStore.userInfo.avatar = newAvatar
|
||||
localStorage.setItem('userInfo', JSON.stringify(userStore.userInfo))
|
||||
}
|
||||
|
||||
const ok = typeof res?.success === 'boolean' ? res.success : true
|
||||
if (ok) {
|
||||
message.success(res?.message || '头像上传成功')
|
||||
} else {
|
||||
message.error(res?.message || '头像上传失败')
|
||||
}
|
||||
} else {
|
||||
message.error(res?.message || '头像上传失败:未返回头像地址')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '头像上传失败,请稍后重试')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 开始编辑用户信息
|
||||
const startEditUserInfo = () => {
|
||||
if (currentUserInfo.value) {
|
||||
@@ -742,7 +798,13 @@ const handleUserInfoSave = async () => {
|
||||
|
||||
console.log('🔄 开始保存用户信息:', userInfoForm.value)
|
||||
|
||||
const response = await userApi.updateUserInfo(userInfoForm.value)
|
||||
const payload: any = { ...userInfoForm.value }
|
||||
// 头像在单独的上传接口中更新,这里不再提交 avatar 字段,避免 URL 校验报错
|
||||
if ('avatar' in payload) {
|
||||
delete payload.avatar
|
||||
}
|
||||
|
||||
const response = await userApi.updateUserInfo(payload)
|
||||
console.log('📋 更新用户信息API响应:', response)
|
||||
|
||||
if (response.success && response.data) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<template>这是我user_pages</template>
|
||||
@@ -57,11 +57,27 @@
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="真实姓名" name="real_name">
|
||||
<a-input v-model:value="form.real_name" placeholder="请输入真实姓名" size="large" />
|
||||
<a-input
|
||||
v-model:value="form.real_name"
|
||||
placeholder="请输入真实姓名"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<IdcardOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input v-model:value="form.phone" placeholder="请输入手机号" size="large" />
|
||||
<a-input
|
||||
v-model:value="form.phone"
|
||||
placeholder="请输入手机号"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<PhoneOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
@@ -125,7 +141,9 @@ import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
UserOutlined,
|
||||
LockOutlined,
|
||||
MailOutlined
|
||||
MailOutlined,
|
||||
IdcardOutlined,
|
||||
PhoneOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { registerUser } from '@/api/auth'
|
||||
|
||||
|
||||
@@ -140,7 +140,17 @@
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="bubble">
|
||||
<div class="content" v-html="renderContent(m.content)"></div>
|
||||
<div class="content">
|
||||
<div v-if="m.id === loadingMessageId && m.role === 'assistant'" class="typing-indicator">
|
||||
<div class="typing-circle"></div>
|
||||
<div class="typing-circle"></div>
|
||||
<div class="typing-circle"></div>
|
||||
<div class="typing-shadow"></div>
|
||||
<div class="typing-shadow"></div>
|
||||
<div class="typing-shadow"></div>
|
||||
</div>
|
||||
<div v-else v-html="renderContent(m.content)"></div>
|
||||
</div>
|
||||
<div class="time">{{ formatTime(m.created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,28 +168,28 @@
|
||||
|
||||
<div class="composer" v-if="currentChatId">
|
||||
<div class="input-container">
|
||||
<a-textarea
|
||||
v-model:value="input"
|
||||
:rows="3"
|
||||
placeholder="输入你的问题..."
|
||||
:disabled="!currentChatId || sending"
|
||||
@pressEnter="onEnterSend"
|
||||
<a-textarea
|
||||
v-model:value="input"
|
||||
:rows="3"
|
||||
placeholder="输入你的问题..."
|
||||
:disabled="!currentChatId"
|
||||
@pressEnter="onEnterSend"
|
||||
@keydown="onComposerKeydown"
|
||||
class="message-input"
|
||||
/>
|
||||
<div class="input-actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!canSend"
|
||||
:loading="sending"
|
||||
@click="send"
|
||||
class="send-btn"
|
||||
size="large"
|
||||
>
|
||||
<template #icon>
|
||||
<SendOutlined />
|
||||
<SendOutlined />
|
||||
</template>
|
||||
发送消息
|
||||
</a-button>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,7 +260,17 @@
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="bubble">
|
||||
<div class="content" v-html="renderContent(m.content)"></div>
|
||||
<div class="content">
|
||||
<div v-if="m.id === loadingMessageId && m.role === 'assistant'" class="typing-indicator">
|
||||
<div class="typing-circle"></div>
|
||||
<div class="typing-circle"></div>
|
||||
<div class="typing-circle"></div>
|
||||
<div class="typing-shadow"></div>
|
||||
<div class="typing-shadow"></div>
|
||||
<div class="typing-shadow"></div>
|
||||
</div>
|
||||
<div v-else v-html="renderContent(m.content)"></div>
|
||||
</div>
|
||||
<div class="time">{{ formatTime(m.created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -272,7 +292,7 @@
|
||||
v-model:value="input"
|
||||
:rows="3"
|
||||
placeholder="输入你的问题..."
|
||||
:disabled="!currentChatId || sending"
|
||||
:disabled="!currentChatId"
|
||||
@pressEnter="onEnterSend"
|
||||
class="message-input"
|
||||
/>
|
||||
@@ -280,7 +300,6 @@
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!canSend"
|
||||
:loading="sending"
|
||||
@click="send"
|
||||
class="send-btn"
|
||||
size="large"
|
||||
@@ -382,7 +401,17 @@
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="bubble">
|
||||
<div class="content" v-html="renderContent(m.content)"></div>
|
||||
<div class="content">
|
||||
<div v-if="m.id === loadingMessageId && m.role === 'assistant'" class="typing-indicator">
|
||||
<div class="typing-circle"></div>
|
||||
<div class="typing-circle"></div>
|
||||
<div class="typing-circle"></div>
|
||||
<div class="typing-shadow"></div>
|
||||
<div class="typing-shadow"></div>
|
||||
<div class="typing-shadow"></div>
|
||||
</div>
|
||||
<div v-else v-html="renderContent(m.content)"></div>
|
||||
</div>
|
||||
<div class="time">{{ formatTime(m.created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -404,7 +433,7 @@
|
||||
v-model:value="input"
|
||||
:rows="3"
|
||||
placeholder="输入你的问题..."
|
||||
:disabled="!currentChatId || sending"
|
||||
:disabled="!currentChatId"
|
||||
@pressEnter="onEnterSend"
|
||||
class="message-input"
|
||||
/>
|
||||
@@ -412,7 +441,6 @@
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!canSend"
|
||||
:loading="sending"
|
||||
@click="send"
|
||||
class="send-btn"
|
||||
size="large"
|
||||
@@ -541,11 +569,20 @@ const renaming = ref(false)
|
||||
let renameTargetId: number | null = null
|
||||
|
||||
const messagesEl = ref<HTMLElement | null>(null)
|
||||
const loadingMessageId = ref<number | null>(null)
|
||||
|
||||
const canSend = computed(() => !!currentChatId.value && !!input.value.trim() && !sending.value)
|
||||
const canSend = computed(() => !!currentChatId.value && !!input.value.trim())
|
||||
const input = ref('')
|
||||
const searchRef = ref<any>(null)
|
||||
|
||||
let tempMessageId = -1
|
||||
const createLocalMessage = (role: 'user' | 'assistant', content: string): AIChatMessage => ({
|
||||
id: tempMessageId--,
|
||||
role,
|
||||
content,
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
|
||||
const fetchChats = async () => {
|
||||
loadingChats.value = true
|
||||
try {
|
||||
@@ -691,18 +728,40 @@ const deleteChat = async (id: number) => {
|
||||
const send = async () => {
|
||||
if (!canSend.value || !currentChatId.value) return
|
||||
const content = input.value.trim()
|
||||
sending.value = true
|
||||
|
||||
// 本地先插入用户消息
|
||||
const userMsg = createLocalMessage('user', content)
|
||||
messages.value.push(userMsg)
|
||||
input.value = ''
|
||||
await nextTick(); scrollToBottom()
|
||||
|
||||
// 本地插入 AI 加载中消息
|
||||
const loadingMsg = createLocalMessage('assistant', 'AI 正在思考中,请稍候...')
|
||||
messages.value.push(loadingMsg)
|
||||
const loadingId = loadingMsg.id
|
||||
loadingMessageId.value = loadingId
|
||||
await nextTick(); scrollToBottom()
|
||||
|
||||
try {
|
||||
const res = await aiApi.sendMessage(currentChatId.value, { content })
|
||||
if (res.success) {
|
||||
messages.value.push(res.data.user_message, res.data.ai_message)
|
||||
input.value = ''
|
||||
// 移除加载中消息
|
||||
const idx = messages.value.findIndex(m => m.id === loadingId)
|
||||
if (idx !== -1) messages.value.splice(idx, 1)
|
||||
// 追加真正的 AI 回复
|
||||
messages.value.push(res.data.ai_message)
|
||||
await nextTick(); scrollToBottom(); fetchChats()
|
||||
} else {
|
||||
const idx = messages.value.findIndex(m => m.id === loadingId)
|
||||
if (idx !== -1) messages.value.splice(idx, 1)
|
||||
message.error(res.message || '发送失败')
|
||||
}
|
||||
} catch (e: any) { message.error(e?.message || '网络错误') }
|
||||
finally { sending.value = false }
|
||||
} catch (e: any) {
|
||||
const idx = messages.value.findIndex(m => m.id === loadingId)
|
||||
if (idx !== -1) messages.value.splice(idx, 1)
|
||||
message.error(e?.message || '网络错误')
|
||||
}
|
||||
loadingMessageId.value = null
|
||||
}
|
||||
|
||||
const formatTime = (t: string) => dayjs(t).format('YYYY-MM-DD HH:mm:ss')
|
||||
@@ -1006,6 +1065,61 @@ onUnmounted(() => {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text-primary, #1e293b);
|
||||
|
||||
// AI 正在思考加载动画(来自 Uiverse.io aaronross1,适配聊天气泡)
|
||||
.typing-indicator {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.typing-circle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background-color: #000;
|
||||
left: 15%;
|
||||
transform-origin: 50%;
|
||||
animation: typing-circle7124 0.5s alternate infinite ease;
|
||||
}
|
||||
|
||||
.typing-circle:nth-child(2) {
|
||||
left: 45%;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.typing-circle:nth-child(3) {
|
||||
left: auto;
|
||||
right: 15%;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.typing-shadow {
|
||||
width: 5px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
transform-origin: 50%;
|
||||
z-index: 0;
|
||||
left: 15%;
|
||||
filter: blur(1px);
|
||||
animation: typing-shadow046 0.5s alternate infinite ease;
|
||||
}
|
||||
|
||||
.typing-shadow:nth-child(4) {
|
||||
left: 45%;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.typing-shadow:nth-child(5) {
|
||||
left: auto;
|
||||
right: 15%;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
@@ -1487,4 +1601,39 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typing-circle7124 {
|
||||
0% {
|
||||
top: 20px;
|
||||
height: 5px;
|
||||
border-radius: 50px 50px 25px 25px;
|
||||
transform: scaleX(1.7);
|
||||
}
|
||||
|
||||
40% {
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typing-shadow046 {
|
||||
0% {
|
||||
transform: scaleX(1.5);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scaleX(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleX(0.2);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">
|
||||
<BookOutlined class="title-icon" />
|
||||
知识库中心
|
||||
文章中心
|
||||
</h1>
|
||||
<p class="page-description">探索智能知识,提升工作效率</p>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="panel-icon">
|
||||
<BookOutlined />
|
||||
</div>
|
||||
<h3 class="panel-title">知识分类</h3>
|
||||
<h3 class="panel-title">文章分类</h3>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<a-spin :spinning="categoryLoading">
|
||||
@@ -64,7 +64,7 @@
|
||||
<FileTextOutlined />
|
||||
</div>
|
||||
<div class="title-group">
|
||||
<h3 class="panel-title">知识库</h3>
|
||||
<h3 class="panel-title">文章列表</h3>
|
||||
<span class="panel-subtitle">发现更多精彩内容</span>
|
||||
</div>
|
||||
</div>
|
||||
2499
hertz_server_diango_ui/src/views/user_pages/KbCenter.vue
Normal file
@@ -13,14 +13,24 @@
|
||||
<div class="user-info-section">
|
||||
<div class="user-info-card">
|
||||
<div class="user-avatar-section">
|
||||
<a-avatar :size="100" class="avatar">
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<a-upload
|
||||
:show-upload-list="false"
|
||||
:before-upload="handleAvatarBeforeUpload"
|
||||
>
|
||||
<a-avatar
|
||||
:size="100"
|
||||
class="avatar"
|
||||
:src="userForm.avatar || userStore.userInfo?.avatar"
|
||||
>
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</a-upload>
|
||||
<div class="avatar-info">
|
||||
<div class="username">{{ userForm.username || '用户' }}</div>
|
||||
<div class="user-role">普通用户</div>
|
||||
<div class="avatar-hint">点击头像更换图片</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,19 +204,24 @@ import {
|
||||
SaveOutlined,
|
||||
ReloadOutlined,
|
||||
MailOutlined,
|
||||
PhoneOutlined
|
||||
PhoneOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { userApi, type User } from '@/api/user'
|
||||
import { useUserStore } from '@/stores/hertz_user'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const loading = ref(false)
|
||||
const avatarUploading = ref(false)
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userForm = ref<Partial<User>>({
|
||||
username: '',
|
||||
email: '',
|
||||
real_name: '',
|
||||
phone: '',
|
||||
avatar: '',
|
||||
gender: 0,
|
||||
birthday: ''
|
||||
birthday: '',
|
||||
})
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
@@ -215,7 +230,7 @@ const fetchUserInfo = async () => {
|
||||
if (response.success) {
|
||||
userForm.value = {
|
||||
...response.data,
|
||||
birthday: response.data.birthday ? dayjs(response.data.birthday) : undefined
|
||||
birthday: response.data.birthday ? dayjs(response.data.birthday) : undefined,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -227,9 +242,16 @@ const fetchUserInfo = async () => {
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const submitData = {
|
||||
const submitData: any = {
|
||||
...userForm.value,
|
||||
birthday: userForm.value.birthday ? dayjs(userForm.value.birthday).format('YYYY-MM-DD') : undefined
|
||||
birthday: userForm.value.birthday
|
||||
? dayjs(userForm.value.birthday as any).format('YYYY-MM-DD')
|
||||
: undefined,
|
||||
}
|
||||
|
||||
// 头像通过单独的上传接口处理,这里不再提交 avatar 字段,避免 URL 校验报错
|
||||
if ('avatar' in submitData) {
|
||||
delete submitData.avatar
|
||||
}
|
||||
|
||||
const response = await userApi.updateUserInfo(submitData)
|
||||
@@ -255,20 +277,69 @@ const resetForm = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAvatarBeforeUpload = async (file: File) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
if (!isImage) {
|
||||
message.error('只能上传图片文件作为头像')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
message.error('头像图片大小不能超过 2MB')
|
||||
return false
|
||||
}
|
||||
|
||||
avatarUploading.value = true
|
||||
try {
|
||||
const res: any = await userApi.uploadAvatar(file)
|
||||
|
||||
// 兼容两种返回结构:
|
||||
// 1) 标准 { success, message, data: { avatar, ... } }
|
||||
// 2) 直接返回用户对象 { avatar, ... }
|
||||
const updatedUser = res?.data ?? res
|
||||
// 后端返回字段为 data.avatar_url,这里兼容 avatar 和 avatar_url
|
||||
const newAvatar = updatedUser?.avatar || updatedUser?.avatar_url
|
||||
|
||||
if (newAvatar) {
|
||||
// 更新当前页面表单
|
||||
userForm.value.avatar = newAvatar
|
||||
|
||||
// 更新全局用户 store,导航栏头像立即刷新
|
||||
if (userStore.userInfo) {
|
||||
userStore.userInfo.avatar = newAvatar
|
||||
localStorage.setItem('userInfo', JSON.stringify(userStore.userInfo))
|
||||
}
|
||||
|
||||
const ok = typeof res?.success === 'boolean' ? res.success : true
|
||||
if (ok) {
|
||||
message.success(res?.message || '头像上传成功')
|
||||
} else {
|
||||
message.error(res?.message || '头像上传失败')
|
||||
}
|
||||
} else {
|
||||
message.error(res?.message || '头像上传失败:未返回头像地址')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '头像上传失败,请稍后重试')
|
||||
} finally {
|
||||
avatarUploading.value = false
|
||||
}
|
||||
|
||||
// 阻止 a-upload 自己提交,由我们手动处理
|
||||
return false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchUserInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables';
|
||||
|
||||
.profile-page {
|
||||
padding: 24px;
|
||||
background: var(--theme-page-bg, #f5f5f5);
|
||||
min-height: 100vh;
|
||||
|
||||
// 页面头部 - 与知识库中心一致
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
@@ -295,7 +366,6 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 用户信息卡片
|
||||
.user-info-section {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto 24px;
|
||||
@@ -315,6 +385,7 @@ onMounted(() => {
|
||||
.avatar {
|
||||
background: linear-gradient(135deg, var(--theme-primary, #3b82f6) 0%, var(--theme-primary, #2563eb) 100%);
|
||||
border: 3px solid var(--theme-card-border, #e5e7eb);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar-info {
|
||||
@@ -330,12 +401,17 @@ onMounted(() => {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.avatar-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--theme-text-secondary, #94a3b8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 资料表单容器
|
||||
.profile-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
@@ -347,7 +423,6 @@ onMounted(() => {
|
||||
border: 1px solid var(--theme-card-border, #e5e7eb);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.panel-header {
|
||||
border-bottom: 1px solid var(--theme-card-border, #e5e7eb);
|
||||
@@ -415,66 +490,6 @@ onMounted(() => {
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: var(--theme-text-primary, #1e293b);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-input {
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--theme-card-border, #e5e7eb);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: var(--theme-primary, #3b82f6);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--theme-card-hover-bg, #f9fafb);
|
||||
color: var(--theme-text-secondary, #6b7280);
|
||||
border-color: var(--theme-card-border, #e5e7eb);
|
||||
}
|
||||
}
|
||||
|
||||
.form-select {
|
||||
:deep(.ant-select-selector) {
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--theme-card-border, #e5e7eb);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-primary, #3b82f6);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-select-focused .ant-select-selector) {
|
||||
border-color: var(--theme-primary, #3b82f6);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.form-date-picker {
|
||||
:deep(.ant-picker) {
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--theme-card-border, #e5e7eb);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-primary, #3b82f6);
|
||||
}
|
||||
|
||||
&.ant-picker-focused {
|
||||
border-color: var(--theme-primary, #3b82f6);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,81 +502,13 @@ onMounted(() => {
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--theme-card-border, #e5e7eb);
|
||||
|
||||
.submit-btn {
|
||||
border-radius: 8px;
|
||||
height: 44px;
|
||||
padding: 0 32px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn,
|
||||
.reset-btn {
|
||||
border-radius: 8px;
|
||||
height: 44px;
|
||||
padding: 0 32px;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-primary, #3b82f6);
|
||||
color: var(--theme-primary, #3b82f6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.profile-page {
|
||||
padding: 16px;
|
||||
|
||||
.user-info-section {
|
||||
.user-info-card {
|
||||
padding: 24px;
|
||||
|
||||
.user-avatar-section {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
.profile-wrapper {
|
||||
.panel-content {
|
||||
padding: 20px;
|
||||
|
||||
.profile-form {
|
||||
.form-grid {
|
||||
gap: 24px;
|
||||
|
||||
.form-section {
|
||||
.form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.submit-btn,
|
||||
.reset-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,7 +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'
|
||||
import { getFullFileUrl, isImageFile, isVideoFile } from '@/utils/hertz_url'
|
||||
|
||||
// 布局模式
|
||||
const layoutMode = ref<'classic' | 'vertical' | 'grid'>('classic')
|
||||
@@ -1033,14 +1033,31 @@ interface DetectionResult {
|
||||
|
||||
// 上传前处理
|
||||
const beforeUpload = (file: any) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isVideo = file.type.startsWith('video/')
|
||||
const mimeType = file.type || ''
|
||||
const fileName = file.name || ''
|
||||
|
||||
const isImageByMime = mimeType.startsWith('image/')
|
||||
const isVideoByMime = mimeType.startsWith('video/')
|
||||
const isImageByExt = isImageFile(fileName)
|
||||
const isVideoByExt = isVideoFile(fileName)
|
||||
|
||||
const isImage = isImageByMime || (!isVideoByMime && isImageByExt)
|
||||
const isVideo = isVideoByMime || (!isImageByMime && isVideoByExt)
|
||||
|
||||
if (!isImage && !isVideo) {
|
||||
message.error('只能上传图片或视频文件!')
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果浏览器没有提供 MIME 类型,根据判断结果补一个,方便后续逻辑使用
|
||||
if (!file.type) {
|
||||
if (isVideo) {
|
||||
file.type = 'video/*'
|
||||
} else if (isImage) {
|
||||
file.type = 'image/*'
|
||||
}
|
||||
}
|
||||
|
||||
// 图片文件大小限制
|
||||
if (isImage) {
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
@@ -1102,7 +1119,7 @@ const startDetection = async () => {
|
||||
}
|
||||
|
||||
if (!currentModel.value) {
|
||||
message.error('当前没有可用的检测模型,请稍后再试')
|
||||
message.error('当前未启用 YOLO 检测模型,请先在模型管理中启用模型后再使用此功能')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1142,32 +1159,25 @@ const startDetection = async () => {
|
||||
console.log('检测API响应:', response)
|
||||
|
||||
if (response.data) {
|
||||
// 构建完整的图片URL(统一通过工具函数处理)
|
||||
// 直接使用后端返回的URL地址,不再通过getFullFileUrl处理
|
||||
console.log('🔍 原始URL数据:', {
|
||||
original_file_url: response.data.original_file_url,
|
||||
result_file_url: response.data.result_file_url
|
||||
})
|
||||
|
||||
let originalImageUrl = getFullFileUrl(response.data.original_file_url)
|
||||
let resultImageUrl = getFullFileUrl(response.data.result_file_url)
|
||||
let originalImageUrl = response.data.original_file_url
|
||||
let resultImageUrl = response.data.result_file_url
|
||||
|
||||
console.log('🖼️ 构建后的图片URL:', {
|
||||
console.log('🖼️ 直接使用的URL地址:', {
|
||||
original: originalImageUrl,
|
||||
result: resultImageUrl
|
||||
})
|
||||
|
||||
// 测试URL是否可访问
|
||||
console.log('🧪 测试图片URL可访问性...')
|
||||
fetch(originalImageUrl, { method: 'HEAD' })
|
||||
.then(res => console.log('✅ 原图URL可访问:', res.status))
|
||||
.catch(err => console.error('❌ 原图URL不可访问:', err))
|
||||
|
||||
fetch(resultImageUrl, { method: 'HEAD' })
|
||||
.then(res => console.log('✅ 结果图URL可访问:', res.status))
|
||||
.catch(err => console.error('❌ 结果图URL不可访问:', err))
|
||||
|
||||
// 判断文件类型
|
||||
const fileType = actualFile.type.startsWith('video/') ? 'video' : 'image'
|
||||
// 判断文件类型(同时参考 MIME 和文件扩展名,避免视频被识别成图片)
|
||||
const actualMimeType = (actualFile as any).type || ''
|
||||
const actualName = (actualFile as any).name || file.name || ''
|
||||
const isVideoFileType = actualMimeType.startsWith('video/') || isVideoFile(actualName)
|
||||
const fileType: 'image' | 'video' = isVideoFileType ? 'video' : 'image'
|
||||
|
||||
// 对于视频文件,使用后端返回的URL;对于图片文件,使用本地预览URL
|
||||
const displayImageUrl = fileType === 'video' ? originalImageUrl : imageUrl
|
||||
@@ -1197,31 +1207,60 @@ const startDetection = async () => {
|
||||
timestamp: Date.now() // 添加时间戳
|
||||
}
|
||||
|
||||
// 预加载图片,确保能正确显示
|
||||
if (resultImageUrl) {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
result.resultImageLoaded = true
|
||||
}
|
||||
img.onerror = () => {
|
||||
result.resultImageError = true
|
||||
}
|
||||
img.src = resultImageUrl
|
||||
}
|
||||
// 预加载图片,确保能正确显示(仅针对图片类型,视频不使用 Image 预加载)
|
||||
if (fileType === 'image') {
|
||||
// 创建Promise数组来等待所有图片加载完成
|
||||
const loadPromises: Promise<void>[] = []
|
||||
|
||||
if (originalImageUrl) {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
result.originalImageLoaded = true
|
||||
if (resultImageUrl) {
|
||||
const resultPromise = new Promise<void>((resolve) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
result.resultImageLoaded = true
|
||||
console.log('✅ 结果图片预加载成功:', resultImageUrl)
|
||||
resolve()
|
||||
}
|
||||
img.onerror = () => {
|
||||
result.resultImageError = true
|
||||
console.error('❌ 结果图片预加载失败:', resultImageUrl)
|
||||
resolve()
|
||||
}
|
||||
img.src = resultImageUrl
|
||||
})
|
||||
loadPromises.push(resultPromise)
|
||||
}
|
||||
img.onerror = () => {
|
||||
result.originalImageError = true
|
||||
}
|
||||
img.src = originalImageUrl
|
||||
}
|
||||
|
||||
// 将新结果添加到数组开头,确保最新结果显示在最上面
|
||||
detectionResults.value.unshift(result)
|
||||
if (originalImageUrl) {
|
||||
const originalPromise = new Promise<void>((resolve) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
result.originalImageLoaded = true
|
||||
console.log('✅ 原图片预加载成功:', originalImageUrl)
|
||||
resolve()
|
||||
}
|
||||
img.onerror = () => {
|
||||
result.originalImageError = true
|
||||
console.error('❌ 原图片预加载失败:', originalImageUrl)
|
||||
resolve()
|
||||
}
|
||||
img.src = originalImageUrl
|
||||
})
|
||||
loadPromises.push(originalPromise)
|
||||
}
|
||||
|
||||
// 等待所有图片预加载完成后再添加到结果数组
|
||||
Promise.all(loadPromises).then(() => {
|
||||
console.log('📸 所有图片预加载完成,添加到结果数组')
|
||||
detectionResults.value.unshift(result)
|
||||
})
|
||||
} else if (fileType === 'video') {
|
||||
// 对于视频文件,直接设置加载状态为true,让视频元素自己处理加载
|
||||
result.originalImageLoaded = true
|
||||
result.resultImageLoaded = true
|
||||
console.log('📹 视频文件,直接设置加载状态为true')
|
||||
// 视频直接添加到结果数组
|
||||
detectionResults.value.unshift(result)
|
||||
}
|
||||
console.log('检测结果:', result)
|
||||
|
||||
// 检查警告级别并显示警告框
|
||||
@@ -1500,7 +1539,7 @@ const retryImageLoad = (result: DetectionResult, type: 'original' | 'result') =>
|
||||
if (type === 'original') {
|
||||
result.originalImageError = false
|
||||
result.originalImageLoaded = false
|
||||
} else {
|
||||
} else {
|
||||
result.resultImageError = false
|
||||
result.resultImageLoaded = false
|
||||
}
|
||||
@@ -1509,79 +1548,58 @@ const retryImageLoad = (result: DetectionResult, type: 'original' | 'result') =>
|
||||
|
||||
console.log('🔄 重试加载媒体:', url)
|
||||
|
||||
// 先测试URL是否可访问
|
||||
fetch(url, { method: 'HEAD' })
|
||||
.then(response => {
|
||||
console.log('📡 URL测试响应:', response.status, response.statusText)
|
||||
if (response.ok) {
|
||||
// URL可访问,尝试加载图片
|
||||
const img = new Image()
|
||||
const img = new Image()
|
||||
|
||||
img.onload = () => {
|
||||
if (type === 'original') {
|
||||
result.originalImageLoaded = true
|
||||
} else {
|
||||
result.resultImageLoaded = true
|
||||
}
|
||||
console.log('✅ 图片重试加载成功:', url)
|
||||
message.success('图片加载成功')
|
||||
}
|
||||
img.onload = () => {
|
||||
if (type === 'original') {
|
||||
result.originalImageLoaded = true
|
||||
} else {
|
||||
result.resultImageLoaded = true
|
||||
}
|
||||
console.log('✅ 图片重试加载成功:', url)
|
||||
message.success('图片加载成功')
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
if (type === 'original') {
|
||||
result.originalImageError = true
|
||||
} else {
|
||||
result.resultImageError = true
|
||||
}
|
||||
console.error('❌ 图片重试加载失败:', url)
|
||||
message.error('图片加载失败')
|
||||
}
|
||||
img.onerror = () => {
|
||||
if (type === 'original') {
|
||||
result.originalImageError = true
|
||||
} else {
|
||||
result.resultImageError = true
|
||||
}
|
||||
console.error('❌ 图片重试加载失败:', url)
|
||||
message.error('图片加载失败')
|
||||
}
|
||||
|
||||
img.src = url
|
||||
} else {
|
||||
console.error('❌ URL不可访问:', response.status, response.statusText)
|
||||
if (type === 'original') {
|
||||
result.originalImageError = true
|
||||
} else {
|
||||
result.resultImageError = true
|
||||
}
|
||||
message.error(`图片URL不可访问 (${response.status})`)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ URL测试失败:', error)
|
||||
if (type === 'original') {
|
||||
result.originalImageError = true
|
||||
} else {
|
||||
result.resultImageError = true
|
||||
}
|
||||
message.error('网络连接失败')
|
||||
})
|
||||
img.src = url
|
||||
}
|
||||
|
||||
// 加载当前启用的YOLO模型
|
||||
const loadCurrentModel = async () => {
|
||||
try {
|
||||
console.log('正在加载当前启用的YOLO模型...')
|
||||
const response = await yoloApi.getCurrentEnabledModel()
|
||||
const response: any = await yoloApi.getCurrentEnabledModel()
|
||||
|
||||
console.log('API响应:', response)
|
||||
|
||||
if (response.success && response.data) {
|
||||
if (response && response.success && response.data) {
|
||||
currentModel.value = response.data
|
||||
console.log('加载到的当前模型:', currentModel.value)
|
||||
message.success(`已加载模型: ${currentModel.value.name} v${currentModel.value.version}`)
|
||||
} else {
|
||||
console.error('加载当前模型失败:', response?.message || '未知错误')
|
||||
console.warn('当前没有可用的YOLO模型:', response?.message || '未返回模型数据')
|
||||
currentModel.value = null
|
||||
// 不显示警告消息,避免干扰用户体验
|
||||
console.warn('当前没有可用的YOLO模型,请联系管理员启用模型')
|
||||
message.warning(response?.message || '当前未启用 YOLO 检测模型,请先在模型管理中启用模型后再使用此功能')
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('加载当前模型时出错:', error)
|
||||
currentModel.value = null
|
||||
// 不显示错误消息,避免干扰用户体验
|
||||
console.warn('加载模型失败,请检查网络连接')
|
||||
const status = error?.response?.status
|
||||
const backendMessage = error?.response?.data?.message
|
||||
if (status === 404) {
|
||||
message.warning(backendMessage || '当前未启用 YOLO 检测模型,请先在模型管理中启用模型后再使用此功能')
|
||||
} else {
|
||||
message.error(backendMessage || '加载 YOLO 模型失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,12 +60,12 @@
|
||||
<!-- 用户信息下拉菜单 -->
|
||||
<a-dropdown placement="bottomRight" class="header-item">
|
||||
<a-button type="text" class="user-info-btn">
|
||||
<a-avatar :src="userInfo?.avatar" :size="32">
|
||||
<a-avatar :src="userStore.userInfo?.avatar" :size="32">
|
||||
<template #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<span class="username">{{ userInfo?.real_name || userInfo?.username }}</span>
|
||||
<span class="username">{{ userStore.userInfo?.real_name || userStore.userInfo?.username }}</span>
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
@@ -3308,8 +3308,8 @@ const initMap = async () => {
|
||||
// 检查容器是否存在
|
||||
const container = document.getElementById('tencent-map-container')
|
||||
if (!container) {
|
||||
console.error('地图容器不存在')
|
||||
message.error('地图容器不存在')
|
||||
// 在非仪表盘或容器尚未渲染时,静默跳过初始化,避免在其他页面弹出错误
|
||||
console.warn('地图容器不存在,跳过地图初始化')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4160,9 +4160,9 @@ onMounted(() => {
|
||||
// 初始化语音合成
|
||||
initSpeechSynthesis()
|
||||
|
||||
// 初始化地图(延迟执行,确保 DOM 已渲染)
|
||||
// 初始化地图(延迟执行,确保 DOM 已渲染,仅在首页生效)
|
||||
setTimeout(() => {
|
||||
initMap()
|
||||
checkAndInitMap()
|
||||
}, 500)
|
||||
|
||||
// 自动检测位置并查询天气(延迟执行,避免影响页面加载)
|
||||
|
||||
@@ -8,27 +8,33 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
from django.conf import settings
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
|
||||
|
||||
# Import Django first to ensure proper initialization
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
# Initialize Django ASGI application early to ensure the AppRegistry
|
||||
# is populated before importing code that may import ORM models.
|
||||
django_asgi_app = get_asgi_application()
|
||||
|
||||
# Import other modules AFTER Django setup
|
||||
from django.conf import settings
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
|
||||
# Import websocket routing AFTER Django setup to avoid AppRegistryNotReady
|
||||
from hertz_demo import routing as demo_routing
|
||||
from hertz_studio_django_yolo import routing as yolo_routing
|
||||
|
||||
# Merge websocket routes
|
||||
websocket_urlpatterns = (
|
||||
demo_routing.websocket_urlpatterns +
|
||||
yolo_routing.websocket_urlpatterns
|
||||
)
|
||||
if 'hertz_studio_django_yolo' in settings.INSTALLED_APPS:
|
||||
from hertz_studio_django_yolo import routing as yolo_routing
|
||||
websocket_urlpatterns = (
|
||||
demo_routing.websocket_urlpatterns +
|
||||
yolo_routing.websocket_urlpatterns
|
||||
)
|
||||
else:
|
||||
websocket_urlpatterns = demo_routing.websocket_urlpatterns
|
||||
|
||||
# 在开发环境下放宽Origin校验,便于第三方客户端(如 Apifox、wscat)调试
|
||||
websocket_app = AuthMiddlewareStack(
|
||||
@@ -47,3 +53,4 @@ else:
|
||||
"http": django_asgi_app,
|
||||
"websocket": AllowedHostsOriginValidator(websocket_app),
|
||||
})
|
||||
|
||||
|
||||
@@ -62,8 +62,12 @@ DEBUG = config('DEBUG', default=True, cast=bool)
|
||||
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||
|
||||
# Database switch configuration
|
||||
# Database engine configuration (sqlite/mysql) with backward compatibility
|
||||
# Prefer `DB_ENGINE` env var; fallback to legacy `USE_REDIS_AS_DB`
|
||||
DB_ENGINE = config('DB_ENGINE', default=None)
|
||||
USE_REDIS_AS_DB = config('USE_REDIS_AS_DB', default=True, cast=bool)
|
||||
if DB_ENGINE is None:
|
||||
DB_ENGINE = 'sqlite' if USE_REDIS_AS_DB else 'mysql'
|
||||
|
||||
# Application definition
|
||||
|
||||
@@ -82,13 +86,19 @@ INSTALLED_APPS = [
|
||||
'drf_spectacular',
|
||||
|
||||
# 必备注册的app,不要删
|
||||
'hertz_demo', # 初始化演示模块
|
||||
'hertz_demo', # 初始化演示模块
|
||||
'hertz_studio_django_captcha', # 验证码模块
|
||||
'hertz_studio_django_auth', # 权限模块
|
||||
'hertz_studio_django_system_monitor', # 系统监测模块
|
||||
'hertz_studio_django_log', # 日志管理模块
|
||||
'hertz_studio_django_notice', # 通知模块
|
||||
|
||||
# ======在下面导入你需要的app======
|
||||
'hertz_studio_django_ai', #ai聊天模块
|
||||
'hertz_studio_django_kb', # 知识库 ai和kb库是相互绑定的
|
||||
'hertz_studio_django_wiki', # 文章模块
|
||||
'hertz_studio_django_yolo', # YOLO目标检测模块
|
||||
'hertz_studio_django_yolo_train', # Yolo训练模块
|
||||
|
||||
]
|
||||
|
||||
@@ -128,21 +138,17 @@ WSGI_APPLICATION = 'hertz_server_django.wsgi.application'
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
if USE_REDIS_AS_DB:
|
||||
# Redis as primary database (for caching and session storage)
|
||||
if DB_ENGINE == 'sqlite':
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'data/db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
# Use Redis for sessions
|
||||
# Use Redis-backed sessions when on SQLite (optional, keeps prior behavior)
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
||||
SESSION_CACHE_ALIAS = 'default'
|
||||
|
||||
else:
|
||||
# MySQL database configuration
|
||||
elif DB_ENGINE == 'mysql':
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
@@ -156,6 +162,14 @@ else:
|
||||
},
|
||||
}
|
||||
}
|
||||
else:
|
||||
# Fallback to SQLite for unexpected values
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'data/db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
# Redis
|
||||
CACHES = {
|
||||
|
||||
@@ -45,8 +45,25 @@ urlpatterns = [
|
||||
# Hertz Log routes
|
||||
path('api/log/', include('hertz_studio_django_log.urls')),
|
||||
|
||||
# Hertz Notice routes
|
||||
path('api/notice/', include('hertz_studio_django_notice.urls')),
|
||||
|
||||
# ===========在下面添加你需要的路由===========
|
||||
# Hertz AI routes
|
||||
path('api/ai/', include('hertz_studio_django_ai.urls')),
|
||||
|
||||
# Hertz Knowledge Base routes
|
||||
path('api/kb/', include('hertz_studio_django_kb.urls')),
|
||||
|
||||
# Hertz Wiki routes
|
||||
path('api/wiki/', include('hertz_studio_django_wiki.urls')),
|
||||
|
||||
# Hertz YOLO routes
|
||||
path('api/yolo/', include('hertz_studio_django_yolo.urls')),
|
||||
|
||||
# YOLO 训练管理
|
||||
path('api/yolo/train/', include('hertz_studio_django_yolo_train.urls')),
|
||||
|
||||
|
||||
]
|
||||
|
||||
|
||||
146
hertz_studio_django_utils/yolo/convert_paths_to_relative.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
将数据库中的绝对路径转换为相对路径的脚本
|
||||
执行方式: python convert_paths_to_relative.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# 设置Django环境
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
|
||||
django.setup()
|
||||
|
||||
from django.conf import settings
|
||||
from hertz_studio_django_yolo.models import YoloModel, Dataset
|
||||
|
||||
|
||||
def convert_to_relative_path(absolute_path):
|
||||
"""将绝对路径转换为相对于MEDIA_ROOT的相对路径"""
|
||||
if not absolute_path:
|
||||
return None
|
||||
|
||||
# 如果已经是相对路径,直接返回
|
||||
if not os.path.isabs(absolute_path):
|
||||
return absolute_path
|
||||
|
||||
try:
|
||||
# 计算相对路径
|
||||
relative_path = os.path.relpath(absolute_path, settings.MEDIA_ROOT)
|
||||
return relative_path
|
||||
except ValueError:
|
||||
# 如果路径不在MEDIA_ROOT下,返回原路径
|
||||
print(f"警告: 路径 {absolute_path} 不在 MEDIA_ROOT 下")
|
||||
return absolute_path
|
||||
|
||||
|
||||
def convert_yolo_models():
|
||||
"""转换YoloModel中的路径"""
|
||||
print("开始转换 YoloModel 表中的路径...")
|
||||
|
||||
models = YoloModel.objects.all()
|
||||
updated_count = 0
|
||||
|
||||
for model in models:
|
||||
updated = False
|
||||
|
||||
# 转换 model_folder_path
|
||||
if model.model_folder_path and os.path.isabs(model.model_folder_path):
|
||||
old_path = model.model_folder_path
|
||||
model.model_folder_path = convert_to_relative_path(old_path)
|
||||
print(f" 模型 {model.name}: model_folder_path")
|
||||
print(f" 原路径: {old_path}")
|
||||
print(f" 新路径: {model.model_folder_path}")
|
||||
updated = True
|
||||
|
||||
# 转换 best_model_path
|
||||
if model.best_model_path and os.path.isabs(model.best_model_path):
|
||||
old_path = model.best_model_path
|
||||
model.best_model_path = convert_to_relative_path(old_path)
|
||||
print(f" 模型 {model.name}: best_model_path")
|
||||
print(f" 原路径: {old_path}")
|
||||
print(f" 新路径: {model.best_model_path}")
|
||||
updated = True
|
||||
|
||||
# 转换 last_model_path
|
||||
if model.last_model_path and os.path.isabs(model.last_model_path):
|
||||
old_path = model.last_model_path
|
||||
model.last_model_path = convert_to_relative_path(old_path)
|
||||
print(f" 模型 {model.name}: last_model_path")
|
||||
print(f" 原路径: {old_path}")
|
||||
print(f" 新路径: {model.last_model_path}")
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
model.save()
|
||||
updated_count += 1
|
||||
|
||||
print(f"YoloModel 转换完成! 更新了 {updated_count} 个模型记录\n")
|
||||
|
||||
|
||||
def convert_datasets():
|
||||
"""转换Dataset中的路径"""
|
||||
print("开始转换 Dataset 表中的路径...")
|
||||
|
||||
datasets = Dataset.objects.all()
|
||||
updated_count = 0
|
||||
|
||||
for dataset in datasets:
|
||||
updated = False
|
||||
|
||||
# 转换 root_folder_path
|
||||
if dataset.root_folder_path and os.path.isabs(dataset.root_folder_path):
|
||||
old_path = dataset.root_folder_path
|
||||
dataset.root_folder_path = convert_to_relative_path(old_path)
|
||||
print(f" 数据集 {dataset.name}: root_folder_path")
|
||||
print(f" 原路径: {old_path}")
|
||||
print(f" 新路径: {dataset.root_folder_path}")
|
||||
updated = True
|
||||
|
||||
# 转换 data_yaml_path
|
||||
if dataset.data_yaml_path and os.path.isabs(dataset.data_yaml_path):
|
||||
old_path = dataset.data_yaml_path
|
||||
dataset.data_yaml_path = convert_to_relative_path(old_path)
|
||||
print(f" 数据集 {dataset.name}: data_yaml_path")
|
||||
print(f" 原路径: {old_path}")
|
||||
print(f" 新路径: {dataset.data_yaml_path}")
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
dataset.save()
|
||||
updated_count += 1
|
||||
|
||||
print(f"Dataset 转换完成! 更新了 {updated_count} 个数据集记录\n")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 80)
|
||||
print("路径转换工具 - 将绝对路径转换为相对路径")
|
||||
print("=" * 80)
|
||||
print(f"MEDIA_ROOT: {settings.MEDIA_ROOT}\n")
|
||||
|
||||
# 确认执行
|
||||
confirm = input("是否开始转换? (输入 yes 确认): ")
|
||||
if confirm.lower() != 'yes':
|
||||
print("操作已取消")
|
||||
return
|
||||
|
||||
try:
|
||||
# 转换YoloModel
|
||||
convert_yolo_models()
|
||||
|
||||
# 转换Dataset
|
||||
convert_datasets()
|
||||
|
||||
print("=" * 80)
|
||||
print("所有路径转换完成!")
|
||||
print("=" * 80)
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -28,10 +28,11 @@ requests>=2.32.3
|
||||
hertz-studio-django-ai
|
||||
hertz-studio-django-auth
|
||||
hertz-studio-django-captcha
|
||||
hertz-studio-django-codegen
|
||||
hertz-studio-django-kb
|
||||
hertz-studio-django-log
|
||||
hertz-studio-django-notice
|
||||
hertz-studio-django-system-monitor
|
||||
hertz-studio-django-wiki
|
||||
hertz-studio-django-yolo
|
||||
hertz-studio-django-codegen
|
||||
hertz-studio-django-yolo-train
|
||||
@@ -294,6 +294,32 @@ def init_superuser():
|
||||
print(f"超级管理员账号创建成功: {superuser.username}")
|
||||
return superuser
|
||||
|
||||
def init_demo_user():
|
||||
from hertz_studio_django_auth.models import HertzUser, HertzUserRole, HertzRole
|
||||
print("正在初始化普通用户账号...")
|
||||
if HertzUser.objects.filter(username='demo').exists():
|
||||
print("普通用户账号已存在,跳过创建")
|
||||
user = HertzUser.objects.get(username='demo')
|
||||
else:
|
||||
user = HertzUser.objects.create_user(
|
||||
username='demo',
|
||||
email='demo@hertz.com',
|
||||
password='123456',
|
||||
real_name='普通用户',
|
||||
status=1
|
||||
)
|
||||
print(f"普通用户账号创建成功: {user.username}")
|
||||
try:
|
||||
role = HertzRole.objects.get(role_id=3)
|
||||
user_role, created = HertzUserRole.objects.get_or_create(user=user, role=role)
|
||||
if created:
|
||||
print(f"为用户 {user.username} 分配角色ID: {role.role_id}")
|
||||
else:
|
||||
print(f"用户 {user.username} 已拥有角色ID: {role.role_id}")
|
||||
except HertzRole.DoesNotExist:
|
||||
print("角色ID=3不存在,跳过分配")
|
||||
return user
|
||||
|
||||
|
||||
def init_departments():
|
||||
"""
|
||||
@@ -607,41 +633,6 @@ def assign_user_roles(superuser, roles):
|
||||
print(f"用户 {superuser.username} 已拥有角色: {super_admin_role.role_name}")
|
||||
|
||||
|
||||
def init_yolo_data():
|
||||
"""
|
||||
初始化YOLO模块数据
|
||||
"""
|
||||
print("初始化YOLO模块数据...")
|
||||
|
||||
try:
|
||||
from hertz_studio_django_yolo.models import YoloModelConfig
|
||||
|
||||
# 检查是否已存在默认模型配置
|
||||
if YoloModelConfig.objects.filter(is_default=True).exists():
|
||||
print("✅ YOLO默认模型配置已存在")
|
||||
return
|
||||
|
||||
# 创建默认YOLO模型配置
|
||||
default_model = YoloModelConfig.objects.create(
|
||||
model_name="YOLOv12古建筑检测模型",
|
||||
model_path="static/models/yolov12/weights/best.pt",
|
||||
model_version="1.0.0",
|
||||
description="用于古建筑识别的YOLOv12模型",
|
||||
confidence_threshold=0.5,
|
||||
iou_threshold=0.45,
|
||||
max_detections=1000,
|
||||
input_size="640",
|
||||
class_labels='["古塔", "古桥", "古建筑", "传统建筑", "文物建筑"]',
|
||||
is_active=True,
|
||||
is_default=True
|
||||
)
|
||||
|
||||
print(f"✅ 创建默认YOLO模型配置: {default_model.model_name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 初始化YOLO模块数据失败: {e}")
|
||||
|
||||
|
||||
def sync_generated_menus():
|
||||
"""
|
||||
同步代码生成器生成的菜单权限
|
||||
@@ -883,7 +874,6 @@ def init_database():
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# 1. 初始化超级管理员
|
||||
superuser = init_superuser()
|
||||
|
||||
# 2. 初始化部门
|
||||
@@ -904,11 +894,11 @@ def init_database():
|
||||
# 7. 为生成的菜单分配权限
|
||||
assign_generated_menu_permissions(generated_menus)
|
||||
|
||||
# 8. 分配用户角色
|
||||
assign_user_roles(superuser, roles)
|
||||
demo_user = init_demo_user()
|
||||
|
||||
# 9. 初始化YOLO模块数据
|
||||
init_yolo_data()
|
||||
# init_yolo_data()
|
||||
|
||||
# 10. 创建菜单生成器命令行工具
|
||||
create_menu_generator_command()
|
||||
@@ -927,13 +917,15 @@ def init_database():
|
||||
print(f"密码: hertz")
|
||||
print(f"邮箱: admin@hertz.com")
|
||||
print("")
|
||||
print("YOLO模型配置:")
|
||||
print(f"模型路径: static/models/yolov12/weights/best.pt")
|
||||
print("")
|
||||
print("菜单生成器工具:")
|
||||
print(f"使用命令: python generate_menu.py crud <模块名> <模型名>")
|
||||
print("")
|
||||
print("请妥善保管管理员账号信息!")
|
||||
print("")
|
||||
print("普通用户账号信息:")
|
||||
print("用户名: demo")
|
||||
print("密码: 123456")
|
||||
|
||||
except Exception as e:
|
||||
print(f"数据库初始化失败: {str(e)}")
|
||||
@@ -1097,7 +1089,6 @@ def check_database_exists():
|
||||
db_path = Path(db_config['NAME'])
|
||||
return db_path.exists()
|
||||
else:
|
||||
# 对于其他数据库类型,尝试连接来检查
|
||||
try:
|
||||
from django.db import connection
|
||||
connection.ensure_connection()
|
||||
@@ -1105,6 +1096,31 @@ def check_database_exists():
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def create_mysql_database_if_missing():
|
||||
from django.conf import settings
|
||||
db = settings.DATABASES['default']
|
||||
if db['ENGINE'] != 'django.db.backends.mysql':
|
||||
return False
|
||||
name = db['NAME']
|
||||
host = db.get('HOST') or 'localhost'
|
||||
user = db.get('USER') or 'root'
|
||||
password = db.get('PASSWORD') or ''
|
||||
port = int(db.get('PORT') or 3306)
|
||||
try:
|
||||
import MySQLdb
|
||||
except Exception:
|
||||
return False
|
||||
try:
|
||||
conn = MySQLdb.connect(host=host, user=user, passwd=password, port=port)
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"CREATE DATABASE IF NOT EXISTS `{name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def run_migrations():
|
||||
"""
|
||||
@@ -1187,6 +1203,7 @@ def main():
|
||||
print("📊 步骤1: 检查数据库状态...")
|
||||
if not check_database_exists():
|
||||
print("❌ 数据库不存在,需要创建")
|
||||
created = create_mysql_database_if_missing()
|
||||
need_migration = True
|
||||
else:
|
||||
print("✅ 数据库文件存在")
|
||||
|
||||
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 212 KiB |
@@ -1,106 +0,0 @@
|
||||
task: detect
|
||||
mode: train
|
||||
model: ultralytics/cfg/models/12/yolo12.yaml
|
||||
data: data/data.yaml
|
||||
epochs: 100
|
||||
time: null
|
||||
patience: 100
|
||||
batch: 8
|
||||
imgsz: 640
|
||||
save: true
|
||||
save_period: -1
|
||||
cache: false
|
||||
device: '0'
|
||||
workers: 4
|
||||
project: runs
|
||||
name: 建筑2
|
||||
exist_ok: false
|
||||
pretrained: yolo12n.pt
|
||||
optimizer: SGD
|
||||
verbose: true
|
||||
seed: 0
|
||||
deterministic: true
|
||||
single_cls: false
|
||||
rect: false
|
||||
cos_lr: false
|
||||
close_mosaic: 0
|
||||
resume: false
|
||||
amp: true
|
||||
fraction: 1.0
|
||||
profile: false
|
||||
freeze: null
|
||||
multi_scale: false
|
||||
compile: false
|
||||
overlap_mask: true
|
||||
mask_ratio: 4
|
||||
dropout: 0.0
|
||||
val: true
|
||||
split: val
|
||||
save_json: false
|
||||
conf: null
|
||||
iou: 0.7
|
||||
max_det: 300
|
||||
half: false
|
||||
dnn: false
|
||||
plots: true
|
||||
source: null
|
||||
vid_stride: 1
|
||||
stream_buffer: false
|
||||
visualize: false
|
||||
augment: false
|
||||
agnostic_nms: false
|
||||
classes: null
|
||||
retina_masks: false
|
||||
embed: null
|
||||
show: false
|
||||
save_frames: false
|
||||
save_txt: false
|
||||
save_conf: false
|
||||
save_crop: false
|
||||
show_labels: true
|
||||
show_conf: true
|
||||
show_boxes: true
|
||||
line_width: null
|
||||
format: torchscript
|
||||
keras: false
|
||||
optimize: false
|
||||
int8: false
|
||||
dynamic: false
|
||||
simplify: true
|
||||
opset: null
|
||||
workspace: null
|
||||
nms: false
|
||||
lr0: 0.01
|
||||
lrf: 0.01
|
||||
momentum: 0.937
|
||||
weight_decay: 0.0005
|
||||
warmup_epochs: 3.0
|
||||
warmup_momentum: 0.8
|
||||
warmup_bias_lr: 0.1
|
||||
box: 7.5
|
||||
cls: 0.5
|
||||
dfl: 1.5
|
||||
pose: 12.0
|
||||
kobj: 1.0
|
||||
nbs: 64
|
||||
hsv_h: 0.015
|
||||
hsv_s: 0.7
|
||||
hsv_v: 0.4
|
||||
degrees: 0.0
|
||||
translate: 0.1
|
||||
scale: 0.5
|
||||
shear: 0.0
|
||||
perspective: 0.0
|
||||
flipud: 0.0
|
||||
fliplr: 0.5
|
||||
bgr: 0.0
|
||||
mosaic: 1.0
|
||||
mixup: 0.0
|
||||
cutmix: 0.0
|
||||
copy_paste: 0.0
|
||||
copy_paste_mode: flip
|
||||
auto_augment: randaugment
|
||||
erasing: 0.4
|
||||
cfg: null
|
||||
tracker: botsort.yaml
|
||||
save_dir: C:\2025.8\train\YOLOv12\YOLOv12\runs\建筑2
|
||||
|
Before Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 196 KiB |
@@ -1,101 +0,0 @@
|
||||
epoch,time,train/box_loss,train/cls_loss,train/dfl_loss,metrics/precision(B),metrics/recall(B),metrics/mAP50(B),metrics/mAP50-95(B),val/box_loss,val/cls_loss,val/dfl_loss,lr/pg0,lr/pg1,lr/pg2
|
||||
1,20.0086,1.08731,3.29955,1.64504,0.48095,0.36519,0.39922,0.21803,1.4547,2.91476,2.19748,0.0701807,0.00331325,0.00331325
|
||||
2,37.4561,1.10527,2.42227,1.62543,0.45751,0.44294,0.42037,0.22736,1.58274,2.95344,2.38848,0.0401149,0.00658079,0.00658079
|
||||
3,53.5831,1.18287,2.1408,1.65928,0.44406,0.5402,0.53045,0.267,1.44637,3.10211,2.38715,0.00998312,0.00978232,0.00978232
|
||||
4,69.7641,1.2564,2.15102,1.71232,0.42506,0.47244,0.42852,0.22216,1.46367,2.98094,2.31821,0.009703,0.009703,0.009703
|
||||
5,85.9353,1.23832,2.04544,1.69687,0.56321,0.63046,0.57009,0.29777,1.53055,1.90867,2.26696,0.009604,0.009604,0.009604
|
||||
6,101.832,1.19918,1.92689,1.67477,0.70549,0.61898,0.69763,0.38758,1.43295,1.68339,2.16059,0.009505,0.009505,0.009505
|
||||
7,117.773,1.22633,1.84729,1.6803,0.553,0.67332,0.66166,0.34056,1.58437,1.59916,2.26187,0.009406,0.009406,0.009406
|
||||
8,133.716,1.15681,1.7694,1.62475,0.65653,0.68416,0.68772,0.355,1.49044,1.58497,2.2459,0.009307,0.009307,0.009307
|
||||
9,149.682,1.1569,1.69483,1.62545,0.7688,0.67236,0.77061,0.45366,1.32424,1.32885,1.98086,0.009208,0.009208,0.009208
|
||||
10,165.68,1.15321,1.61623,1.61874,0.69611,0.75215,0.75817,0.4151,1.43079,1.33073,2.13068,0.009109,0.009109,0.009109
|
||||
11,181.822,1.14882,1.53541,1.59886,0.75853,0.7246,0.78361,0.48015,1.24253,1.28601,1.92595,0.00901,0.00901,0.00901
|
||||
12,197.889,1.13351,1.49226,1.60018,0.71891,0.78305,0.78574,0.46692,1.30974,1.17806,1.93744,0.008911,0.008911,0.008911
|
||||
13,213.936,1.12,1.44936,1.57138,0.76987,0.77484,0.83633,0.49014,1.30538,1.10903,1.96262,0.008812,0.008812,0.008812
|
||||
14,229.905,1.09812,1.40376,1.56604,0.85048,0.82277,0.86213,0.53598,1.24135,1.00046,1.90721,0.008713,0.008713,0.008713
|
||||
15,245.888,1.0883,1.36922,1.55153,0.81135,0.79338,0.86585,0.49846,1.27613,1.06052,1.92691,0.008614,0.008614,0.008614
|
||||
16,261.957,1.07151,1.3544,1.54787,0.79833,0.82635,0.87057,0.53621,1.21024,1.04704,1.82224,0.008515,0.008515,0.008515
|
||||
17,277.964,1.05532,1.29671,1.52115,0.82433,0.84293,0.87414,0.55596,1.19172,0.93791,1.79585,0.008416,0.008416,0.008416
|
||||
18,293.854,1.06292,1.25445,1.51939,0.80981,0.80042,0.85706,0.53513,1.20435,0.9763,1.85792,0.008317,0.008317,0.008317
|
||||
19,309.903,1.04243,1.26345,1.51501,0.82473,0.78456,0.85556,0.52423,1.26536,1.09649,1.85899,0.008218,0.008218,0.008218
|
||||
20,325.895,1.02684,1.22845,1.49715,0.78029,0.8402,0.87547,0.56629,1.21743,0.93182,1.78994,0.008119,0.008119,0.008119
|
||||
21,341.856,1.00893,1.21206,1.49205,0.8397,0.81854,0.88349,0.57237,1.16211,0.90322,1.72301,0.00802,0.00802,0.00802
|
||||
22,357.71,1.04323,1.16636,1.51546,0.82415,0.80944,0.85666,0.56374,1.12557,0.88955,1.72481,0.007921,0.007921,0.007921
|
||||
23,373.601,1.01213,1.16652,1.48291,0.89755,0.84814,0.92614,0.61678,1.0727,0.82181,1.66517,0.007822,0.007822,0.007822
|
||||
24,389.775,1.01485,1.1677,1.49783,0.82869,0.82688,0.89032,0.57284,1.15754,0.86914,1.75619,0.007723,0.007723,0.007723
|
||||
25,405.662,1.0297,1.14486,1.4959,0.85883,0.86808,0.90549,0.58164,1.18685,0.90579,1.7652,0.007624,0.007624,0.007624
|
||||
26,421.518,1.00351,1.12364,1.48466,0.91116,0.86016,0.90481,0.59292,1.16494,0.82749,1.7473,0.007525,0.007525,0.007525
|
||||
27,437.468,0.99466,1.10594,1.48,0.88077,0.87488,0.91148,0.59708,1.12196,0.86732,1.66219,0.007426,0.007426,0.007426
|
||||
28,453.355,0.97246,1.09983,1.45414,0.8509,0.88423,0.92609,0.59958,1.08857,0.82737,1.66404,0.007327,0.007327,0.007327
|
||||
29,469.354,0.959,1.03722,1.43833,0.87257,0.89723,0.9292,0.63013,1.05661,0.79721,1.65584,0.007228,0.007228,0.007228
|
||||
30,485.279,0.94641,1.0268,1.43645,0.87653,0.87173,0.92362,0.6152,1.09542,0.75908,1.668,0.007129,0.007129,0.007129
|
||||
31,501.258,0.96459,1.0295,1.4477,0.85852,0.85356,0.91341,0.60373,1.10817,0.84379,1.65368,0.00703,0.00703,0.00703
|
||||
32,517.168,0.9447,1.04712,1.43485,0.89923,0.89345,0.93876,0.62992,1.08927,0.72365,1.6683,0.006931,0.006931,0.006931
|
||||
33,533.108,0.96199,1.00588,1.44815,0.87076,0.88306,0.91325,0.6119,1.10563,0.7969,1.66875,0.006832,0.006832,0.006832
|
||||
34,549.012,0.93536,0.99021,1.42286,0.8548,0.89202,0.91598,0.59513,1.07788,0.79095,1.64711,0.006733,0.006733,0.006733
|
||||
35,564.887,0.94238,1.03363,1.43755,0.89576,0.89663,0.94787,0.63072,1.08729,0.71356,1.64567,0.006634,0.006634,0.006634
|
||||
36,580.805,0.94078,0.96053,1.41823,0.89222,0.9035,0.93538,0.64363,1.04054,0.67852,1.59891,0.006535,0.006535,0.006535
|
||||
37,596.784,0.94003,0.94652,1.42158,0.88453,0.88348,0.94349,0.64272,1.06216,0.73028,1.60311,0.006436,0.006436,0.006436
|
||||
38,612.423,0.92788,0.95855,1.42125,0.88936,0.91974,0.93403,0.6147,1.08269,0.72013,1.61904,0.006337,0.006337,0.006337
|
||||
39,628.076,0.9224,0.94874,1.40102,0.89073,0.9069,0.92887,0.63473,1.0353,0.69181,1.58564,0.006238,0.006238,0.006238
|
||||
40,643.77,0.89775,0.94759,1.39744,0.90121,0.88407,0.9443,0.65067,1.0286,0.7247,1.55336,0.006139,0.006139,0.006139
|
||||
41,659.544,0.92422,0.92201,1.4103,0.91164,0.92449,0.95193,0.65305,1.02727,0.65603,1.55055,0.00604,0.00604,0.00604
|
||||
42,675.22,0.90217,0.95037,1.40697,0.90345,0.88679,0.93082,0.63708,1.03697,0.69057,1.54943,0.005941,0.005941,0.005941
|
||||
43,690.878,0.87991,0.89616,1.37762,0.92201,0.88928,0.95304,0.65937,0.96974,0.67072,1.51294,0.005842,0.005842,0.005842
|
||||
44,706.637,0.88529,0.89891,1.38099,0.90464,0.91058,0.95737,0.64397,1.03433,0.65809,1.55709,0.005743,0.005743,0.005743
|
||||
45,722.328,0.89267,0.88466,1.38184,0.89532,0.90185,0.93743,0.63438,1.02933,0.6711,1.56103,0.005644,0.005644,0.005644
|
||||
46,738.079,0.87848,0.87502,1.38415,0.90984,0.90932,0.94224,0.65532,0.99621,0.65005,1.54755,0.005545,0.005545,0.005545
|
||||
47,753.783,0.86936,0.86761,1.36124,0.90837,0.92106,0.95072,0.6533,1.00146,0.64086,1.54653,0.005446,0.005446,0.005446
|
||||
48,769.457,0.88033,0.8537,1.37431,0.90748,0.90495,0.958,0.66689,1.02737,0.63689,1.55787,0.005347,0.005347,0.005347
|
||||
49,785.148,0.86988,0.85134,1.36982,0.88654,0.91463,0.94567,0.65455,1.01259,0.64571,1.54479,0.005248,0.005248,0.005248
|
||||
50,800.893,0.88317,0.8575,1.37127,0.91587,0.90751,0.94408,0.63446,1.02748,0.6757,1.5421,0.005149,0.005149,0.005149
|
||||
51,816.553,0.87948,0.87439,1.37605,0.93772,0.92013,0.95022,0.65743,1.00579,0.63395,1.53519,0.00505,0.00505,0.00505
|
||||
52,832.289,0.86492,0.83698,1.36435,0.92391,0.90861,0.95711,0.65756,1.04998,0.6317,1.57745,0.004951,0.004951,0.004951
|
||||
53,848.093,0.85355,0.82554,1.35923,0.91881,0.94257,0.95839,0.66649,1.01111,0.61029,1.52965,0.004852,0.004852,0.004852
|
||||
54,863.775,0.8535,0.83035,1.3572,0.91154,0.9155,0.94293,0.64606,1.02703,0.63016,1.54362,0.004753,0.004753,0.004753
|
||||
55,879.543,0.85986,0.83531,1.35698,0.90236,0.93667,0.96041,0.67001,1.0152,0.63869,1.54171,0.004654,0.004654,0.004654
|
||||
56,895.268,0.84045,0.79273,1.34856,0.89532,0.92493,0.95921,0.67304,1.00989,0.64012,1.54289,0.004555,0.004555,0.004555
|
||||
57,911.011,0.82598,0.8,1.33521,0.91024,0.90031,0.96198,0.68098,0.98671,0.63172,1.52354,0.004456,0.004456,0.004456
|
||||
58,926.714,0.84181,0.77648,1.34796,0.91338,0.93502,0.95999,0.67984,0.98774,0.59608,1.49965,0.004357,0.004357,0.004357
|
||||
59,942.543,0.83298,0.78212,1.3426,0.94031,0.94731,0.96849,0.67095,1.00989,0.58775,1.52084,0.004258,0.004258,0.004258
|
||||
60,958.212,0.83207,0.78794,1.33592,0.93456,0.93818,0.95865,0.67999,0.99628,0.58504,1.52259,0.004159,0.004159,0.004159
|
||||
61,973.921,0.82934,0.76291,1.33786,0.94081,0.91384,0.94778,0.65941,1.01695,0.5969,1.526,0.00406,0.00406,0.00406
|
||||
62,989.588,0.82385,0.76501,1.31992,0.93582,0.92592,0.95652,0.67726,0.99442,0.5805,1.52139,0.003961,0.003961,0.003961
|
||||
63,1005.27,0.82826,0.74738,1.33216,0.92745,0.93277,0.95931,0.66606,0.98922,0.59454,1.50615,0.003862,0.003862,0.003862
|
||||
64,1021.05,0.80678,0.72549,1.32648,0.91708,0.9377,0.95986,0.66584,0.99548,0.59683,1.52291,0.003763,0.003763,0.003763
|
||||
65,1036.89,0.81482,0.72911,1.32164,0.93121,0.93781,0.95586,0.67407,1.0136,0.57583,1.54121,0.003664,0.003664,0.003664
|
||||
66,1052.64,0.79386,0.71331,1.30697,0.93732,0.93754,0.96547,0.68623,0.97395,0.56161,1.49551,0.003565,0.003565,0.003565
|
||||
67,1068.43,0.79758,0.71484,1.31935,0.91717,0.93342,0.96536,0.68752,0.97895,0.57296,1.50173,0.003466,0.003466,0.003466
|
||||
68,1084.23,0.79346,0.71871,1.31773,0.91666,0.95392,0.95066,0.6665,1.01352,0.57563,1.53215,0.003367,0.003367,0.003367
|
||||
69,1100,0.78618,0.71187,1.31807,0.94808,0.945,0.97505,0.69891,0.96108,0.54733,1.46854,0.003268,0.003268,0.003268
|
||||
70,1115.68,0.79627,0.69645,1.30445,0.92507,0.96131,0.96442,0.69506,0.97216,0.55866,1.48312,0.003169,0.003169,0.003169
|
||||
71,1131.46,0.76443,0.70608,1.29604,0.94985,0.9588,0.97657,0.70639,0.96117,0.54299,1.46808,0.00307,0.00307,0.00307
|
||||
72,1147.25,0.78344,0.68905,1.30023,0.95568,0.95506,0.97787,0.69443,0.97204,0.53025,1.47856,0.002971,0.002971,0.002971
|
||||
73,1163,0.76713,0.68651,1.28648,0.94512,0.93645,0.96365,0.6831,0.99244,0.54819,1.50867,0.002872,0.002872,0.002872
|
||||
74,1178.73,0.78028,0.69619,1.30254,0.95322,0.94042,0.96886,0.68547,0.96242,0.55545,1.49758,0.002773,0.002773,0.002773
|
||||
75,1194.43,0.77224,0.69314,1.29348,0.95179,0.93399,0.97174,0.69176,0.96883,0.55448,1.48271,0.002674,0.002674,0.002674
|
||||
76,1210.25,0.75916,0.67695,1.2806,0.94949,0.94603,0.97058,0.70176,0.95191,0.54891,1.47248,0.002575,0.002575,0.002575
|
||||
77,1225.98,0.74762,0.66489,1.28089,0.94943,0.94766,0.97236,0.69255,0.98364,0.55388,1.48251,0.002476,0.002476,0.002476
|
||||
78,1241.66,0.77711,0.67409,1.2883,0.93344,0.95383,0.96868,0.67911,0.97057,0.54697,1.48187,0.002377,0.002377,0.002377
|
||||
79,1257.35,0.74631,0.66574,1.27737,0.95085,0.94672,0.97652,0.69682,0.95676,0.54795,1.46933,0.002278,0.002278,0.002278
|
||||
80,1273.15,0.75217,0.6506,1.28044,0.93861,0.96425,0.97701,0.6801,0.99862,0.53986,1.50843,0.002179,0.002179,0.002179
|
||||
81,1288.83,0.74646,0.64203,1.28248,0.94694,0.95524,0.97378,0.69948,0.9393,0.53228,1.45059,0.00208,0.00208,0.00208
|
||||
82,1304.49,0.76051,0.64159,1.29044,0.94279,0.96615,0.97642,0.71067,0.94991,0.52283,1.45191,0.001981,0.001981,0.001981
|
||||
83,1320.18,0.74572,0.65008,1.26769,0.9648,0.95015,0.97473,0.69887,0.96052,0.53792,1.47686,0.001882,0.001882,0.001882
|
||||
84,1335.93,0.73985,0.6404,1.26772,0.95366,0.94533,0.97208,0.70438,0.95165,0.52846,1.45992,0.001783,0.001783,0.001783
|
||||
85,1352,0.74811,0.62183,1.27641,0.94718,0.9572,0.97351,0.70502,0.96988,0.52138,1.48555,0.001684,0.001684,0.001684
|
||||
86,1367.67,0.74665,0.63662,1.2715,0.95091,0.95529,0.97714,0.70807,0.95641,0.5126,1.46246,0.001585,0.001585,0.001585
|
||||
87,1383.33,0.71769,0.63289,1.2628,0.94342,0.9504,0.97286,0.69575,0.95349,0.52387,1.4662,0.001486,0.001486,0.001486
|
||||
88,1399,0.73196,0.62775,1.26479,0.92059,0.96043,0.97218,0.69775,0.95618,0.53638,1.48112,0.001387,0.001387,0.001387
|
||||
89,1414.78,0.70978,0.60203,1.24537,0.92296,0.95359,0.9652,0.69841,0.9556,0.51803,1.46562,0.001288,0.001288,0.001288
|
||||
90,1430.43,0.72264,0.59842,1.25778,0.94076,0.9349,0.97077,0.69505,0.97915,0.53102,1.48364,0.001189,0.001189,0.001189
|
||||
91,1446.11,0.69944,0.60885,1.23989,0.95493,0.95098,0.97538,0.71147,0.95821,0.50546,1.45932,0.00109,0.00109,0.00109
|
||||
92,1461.79,0.7136,0.58259,1.25125,0.93353,0.95799,0.97267,0.70024,0.97099,0.51186,1.47324,0.000991,0.000991,0.000991
|
||||
93,1477.44,0.71531,0.6023,1.2503,0.94569,0.95561,0.97551,0.71119,0.96279,0.49675,1.45994,0.000892,0.000892,0.000892
|
||||
94,1493.19,0.70482,0.59368,1.24567,0.95518,0.94865,0.97496,0.71021,0.96272,0.50238,1.46949,0.000793,0.000793,0.000793
|
||||
95,1508.84,0.70726,0.6036,1.2466,0.93383,0.9483,0.97548,0.70424,0.96704,0.50892,1.47366,0.000694,0.000694,0.000694
|
||||
96,1524.51,0.69484,0.58516,1.23633,0.94152,0.95618,0.97524,0.70887,0.96193,0.50233,1.46647,0.000595,0.000595,0.000595
|
||||
97,1540.23,0.702,0.57676,1.24112,0.94631,0.94809,0.9713,0.70927,0.95388,0.50872,1.45941,0.000496,0.000496,0.000496
|
||||
98,1555.97,0.7011,0.56706,1.23848,0.94278,0.95722,0.97388,0.71225,0.95844,0.50285,1.46779,0.000397,0.000397,0.000397
|
||||
99,1571.61,0.68355,0.5717,1.24052,0.94328,0.94665,0.97367,0.70978,0.95599,0.50202,1.45859,0.000298,0.000298,0.000298
|
||||
100,1587.33,0.68724,0.5822,1.22946,0.94701,0.95346,0.97446,0.71402,0.95374,0.50433,1.46168,0.000199,0.000199,0.000199
|
||||
|
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 674 KiB |
|
Before Width: | Height: | Size: 495 KiB |
|
Before Width: | Height: | Size: 512 KiB |
|
Before Width: | Height: | Size: 705 KiB |
|
Before Width: | Height: | Size: 712 KiB |
|
Before Width: | Height: | Size: 730 KiB |
|
Before Width: | Height: | Size: 741 KiB |
|
Before Width: | Height: | Size: 717 KiB |
|
Before Width: | Height: | Size: 737 KiB |
@@ -7,6 +7,7 @@
|
||||
- `Python 3.10+`(建议 3.12.3)
|
||||
- 操作系统:Windows
|
||||
- redis:本地 `Redis` 服务(默认地址 `redis://127.0.0.1:6379`)
|
||||
- mysql(支持切换mysql):在.env文件里面将mysql配置修改为你的数据库信息
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +37,7 @@ pip install hertz-studio-django-yolo -i https://hertz:hertz@hzpypi.hzsystems.cn/
|
||||
|
||||
### 2)下载全部库
|
||||
|
||||
pip install -r requirements.txt -i https://hzpypi.hzsystems.cn/simple/
|
||||
pip install -r requirements.txt -i https://hertz:hertz@hzpypi.hzsystems.cn/simple/
|
||||
|
||||
|
||||
|
||||
@@ -130,4 +131,4 @@ ffmpeg压缩包在根目录下面的static目录下,如下图。
|
||||
## 九、**快速启动**
|
||||
|
||||
- 安装依赖:`pip install -r requirements.txt -i https://hertz:hertz@hzpypi.hzsystems.cn/simple/`
|
||||
- 启动服务:`python start_server.py --port 8000`
|
||||
- 启动服务:`python start_server.py`
|
||||
|
||||