├── .gitignore ├── CHANGELOG.md ├── CLEANUP_TEMP_FILES.md ├── Dockerfile ├── README.md ├── assets ├── logo.png ├── mulcontent.png ├── pdf_chat.png ├── pdf_helper.png ├── ui_1.png ├── ui_2.png ├── user-setting.png └── wecom.jpg ├── docker-compose.yml ├── mineru_volumes.py ├── nginx.conf ├── scripts ├── generate_env.bat ├── generate_env.sh └── install.sh ├── server ├── app.py ├── database.py ├── download_models_hf.py ├── magic-pdf.json ├── requirements.txt ├── routes │ ├── __init__.py │ ├── files │ │ └── routes.py │ ├── knowledgebases │ │ └── routes.py │ ├── teams │ │ ├── __init__.py │ │ └── routes.py │ ├── tenants │ │ ├── __init__.py │ │ └── routes.py │ └── users │ │ ├── __init__.py │ │ └── routes.py ├── services │ ├── files │ │ ├── base_service.py │ │ ├── document_service.py │ │ ├── file2document_service.py │ │ ├── file_service.py │ │ ├── models │ │ │ └── __init__.py │ │ ├── service.py │ │ └── utils.py │ ├── knowflow │ │ ├── README.md │ │ ├── __init__.py │ │ ├── config.json │ │ ├── ragflow_chat.py │ │ └── requirements.txt │ ├── knowledgebases │ │ ├── __init__.py │ │ ├── document_parser.py │ │ ├── mineru_parse │ │ │ ├── __init__.py │ │ │ ├── download_models_hf.py │ │ │ ├── file_converter.py │ │ │ ├── mineru_test.py │ │ │ ├── minio_server.py │ │ │ ├── process_pdf.py │ │ │ ├── ragflow_build.py │ │ │ └── utils.py │ │ ├── service.py │ │ └── utils.py │ ├── teams │ │ ├── __init__.py │ │ └── service.py │ ├── tenants │ │ ├── __init__.py │ │ └── service.py │ └── users │ │ ├── __init__.py │ │ └── service.py └── utils.py └── web ├── .editorconfig ├── .env.staging ├── .gitignore ├── .npmrc ├── eslint.config.js ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── app-loading.css ├── detect-ie.js ├── favicon.ico └── favicon.ico1 ├── src ├── App.vue ├── common │ ├── apis │ │ ├── configs │ │ │ ├── index.ts │ │ │ └── type.ts │ │ ├── files │ │ │ ├── index.ts │ │ │ └── type.ts │ │ ├── kbs │ │ │ ├── document.ts │ │ │ ├── knowledgebase.ts │ │ │ └── type.ts │ │ ├── tables │ │ │ ├── index.ts │ │ │ └── type.ts │ │ ├── teams │ │ │ ├── index.ts │ │ │ └── type.ts │ │ └── users │ │ │ ├── index.ts │ │ │ └── type.ts │ ├── assets │ │ ├── icons │ │ │ ├── dashboard.svg │ │ │ ├── file.svg │ │ │ ├── fullscreen-exit.svg │ │ │ ├── fullscreen.svg │ │ │ ├── kb.svg │ │ │ ├── keyboard-down.svg │ │ │ ├── keyboard-enter.svg │ │ │ ├── keyboard-esc.svg │ │ │ ├── keyboard-up.svg │ │ │ ├── preserve-color │ │ │ │ └── README.md │ │ │ ├── search.svg │ │ │ ├── team-management.svg │ │ │ ├── user-config.svg │ │ │ └── user-management.svg │ │ ├── images │ │ │ └── layouts │ │ │ │ ├── logo-text-1.png │ │ │ │ ├── logo-text-2.png │ │ │ │ └── logo.png │ │ └── styles │ │ │ ├── element-plus.css │ │ │ ├── element-plus.scss │ │ │ ├── index.scss │ │ │ ├── mixins.scss │ │ │ ├── theme │ │ │ ├── core │ │ │ │ ├── element-plus.scss │ │ │ │ ├── index.scss │ │ │ │ └── layouts.scss │ │ │ ├── dark-blue │ │ │ │ ├── index.scss │ │ │ │ └── variables.scss │ │ │ ├── dark │ │ │ │ ├── index.scss │ │ │ │ └── variables.scss │ │ │ └── register.scss │ │ │ ├── transition.scss │ │ │ ├── variables.css │ │ │ ├── view-transition.scss │ │ │ ├── vxe-table.css │ │ │ └── vxe-table.scss │ ├── components │ │ ├── Notify │ │ │ ├── List.vue │ │ │ ├── data.ts │ │ │ ├── index.vue │ │ │ └── type.ts │ │ ├── Screenfull │ │ │ └── index.vue │ │ ├── SearchMenu │ │ │ ├── Footer.vue │ │ │ ├── Modal.vue │ │ │ ├── Result.vue │ │ │ └── index.vue │ │ └── ThemeSwitch │ │ │ └── index.vue │ ├── composables │ │ ├── useDevice.ts │ │ ├── useFetchSelect.ts │ │ ├── useFullscreenLoading.ts │ │ ├── useGreyAndColorWeakness.ts │ │ ├── useLayoutMode.ts │ │ ├── usePagination.ts │ │ ├── usePany.ts │ │ ├── useRouteListener.ts │ │ ├── useTheme.ts │ │ ├── useTitle.ts │ │ └── useWatermark.ts │ ├── constants │ │ ├── app-key.ts │ │ └── cache-key.ts │ └── utils │ │ ├── cache │ │ ├── cookies.ts │ │ └── local-storage.ts │ │ ├── css.ts │ │ ├── datetime.ts │ │ ├── permission.ts │ │ └── validate.ts ├── http │ └── axios.ts ├── layouts │ ├── components │ │ ├── AppMain │ │ │ └── index.vue │ │ ├── Breadcrumb │ │ │ └── index.vue │ │ ├── DocumentParseProgress │ │ │ └── index.vue │ │ ├── Footer │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ ├── Logo │ │ │ └── index.vue │ │ ├── NavigationBar │ │ │ └── index.vue │ │ ├── RightPanel │ │ │ └── index.vue │ │ ├── Settings │ │ │ ├── SelectLayoutMode.vue │ │ │ └── index.vue │ │ ├── Sidebar │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ └── index.vue │ │ ├── TagsView │ │ │ ├── ScrollPane.vue │ │ │ └── index.vue │ │ └── index.ts │ ├── composables │ │ └── useResize.ts │ ├── config.ts │ ├── index.vue │ └── modes │ │ ├── LeftMode.vue │ │ ├── LeftTopMode.vue │ │ └── TopMode.vue ├── main.ts ├── pages │ ├── dashboard │ │ ├── components │ │ │ ├── Admin.vue │ │ │ └── Editor.vue │ │ ├── images │ │ │ └── dashboard.svg │ │ └── index.vue │ ├── error │ │ ├── 403.vue │ │ ├── 404.vue │ │ ├── components │ │ │ └── Layout.vue │ │ └── images │ │ │ ├── 403.svg │ │ │ └── 404.svg │ ├── file │ │ └── index.vue │ ├── knowledgebase │ │ └── index.vue │ ├── login │ │ ├── apis │ │ │ ├── index.ts │ │ │ └── type.ts │ │ ├── components │ │ │ └── Owl.vue │ │ ├── composables │ │ │ └── useFocus.ts │ │ ├── images │ │ │ ├── close-eyes.png │ │ │ ├── face.png │ │ │ ├── hand-down-left.png │ │ │ ├── hand-down-right.png │ │ │ ├── hand-up-left.png │ │ │ └── hand-up-right.png │ │ └── index.vue │ ├── redirect │ │ └── index.vue │ ├── team-management │ │ └── index.vue │ ├── user-config │ │ └── index.vue │ └── user-management │ │ └── index.vue ├── pinia │ ├── index.ts │ └── stores │ │ ├── app.ts │ │ ├── permission.ts │ │ ├── settings.ts │ │ ├── tags-view.ts │ │ └── user.ts ├── plugins │ ├── element-plus-icons.ts │ ├── index.ts │ ├── permission-directive.ts │ ├── svg-icon.ts │ └── vxe-table.ts └── router │ ├── config.ts │ ├── guard.ts │ ├── helper.ts │ ├── index.ts │ └── whitelist.ts ├── tests ├── components │ └── Notify.test.ts ├── demo.test.ts └── utils │ └── validate.test.ts ├── tsconfig.json ├── types ├── api.d.ts ├── auto │ ├── auto-imports.d.ts │ ├── components.d.ts │ ├── svg-component-global.d.ts │ └── svg-component.d.ts ├── directives.d.ts ├── env.d.ts └── vue-router.d.ts ├── uno.config.ts └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # 通用忽略规则 2 | .DS_Store 3 | *.log 4 | *.tmp 5 | *.bak 6 | *.swp 7 | *~ 8 | .idea/ 9 | .vscode/ 10 | *.code-workspace 11 | 12 | # Node.js 相关 13 | node_modules/ 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | .pnpm-debug.log* 18 | dist/ 19 | build/ 20 | .cache/ 21 | .temp/ 22 | 23 | # Python 相关 24 | __pycache__/ 25 | *.py[cod] 26 | *.so 27 | .Python 28 | venv/ 29 | env/ 30 | *.egg-info/ 31 | .venv/ 32 | .eggs/ 33 | *.egg 34 | 35 | # Docker 相关 36 | docker-compose.override.yml 37 | **/.env 38 | .env 39 | .env.local 40 | .env.development 41 | .env.test 42 | .env.production 43 | 44 | # 前端特定 45 | web/.env 46 | web/.env.local 47 | web/public/storage 48 | web/public/hot 49 | web/public/js/ 50 | web/public/css/ 51 | web/public/mix-manifest.json 52 | web/storage/*.key 53 | web/storage/framework/cache/ 54 | web/storage/framework/sessions/ 55 | web/storage/framework/views/ 56 | web/bootstrap/cache/ 57 | 58 | # 后端特定 59 | server/instance/ 60 | server/.flaskenv 61 | server/migrations/ 62 | server/__pycache__/ 63 | 64 | # 测试相关 65 | coverage/ 66 | .nyc_output/ 67 | *.lcov 68 | 69 | # 编辑器 70 | .idea 71 | *.suo 72 | *.ntvs* 73 | *.njsproj 74 | *.sln 75 | *.sw? 76 | 77 | 78 | # 忽略 server/output/ 目录下的所有文件和子目录 79 | server/output/* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # KnowFlow 更新日志 2 | 3 | 4 | ## [v0.4.0] - 2025-05-29(兼容 RAGFlow v0.19.0) 5 | ### 新增 6 | - 支持更多的文件格式,包含 doc、ppt、docx、url、excel 等文件格式,具体格式如下: 7 | 8 | ".123", ".602", ".abw", ".bib", ".bmp", ".cdr", ".cgm", ".cmx", ".csv", ".cwk", ".dbf", ".dif", 9 | ".doc", ".docm", ".docx", ".dot", ".dotm", ".dotx", ".dxf", ".emf", ".eps", ".epub", ".fodg", 10 | ".fodp", ".fods", ".fodt", ".fopd", ".gif", ".htm", ".html", ".hwp", ".jpeg", ".jpg", ".key", 11 | ".ltx", ".lwp", ".mcw", ".met", ".mml", ".mw", ".numbers", ".odd", ".odg", ".odm", ".odp", 12 | ".ods", ".odt", ".otg", ".oth", ".otp", ".ots", ".ott", ".pages", ".pbm", ".pcd", ".pct", 13 | ".pcx", ".pdb", ".pgm", ".png", ".pot", ".potm", ".potx", ".ppm", ".pps", ".ppt", ".pptm", 14 | ".pptx", ".psd", ".psw", ".pub", ".pwp", ".pxl", ".ras", ".rtf", ".sda", ".sdc", ".sdd", 15 | ".sdp", ".sdw", ".sgl", ".slk", ".smf", ".stc", ".std", ".sti", ".stw", ".svg", ".svm", 16 | ".swf", ".sxc", ".sxd", ".sxg", ".sxi", ".sxm", ".sxw", ".tga", ".tif", ".tiff", ".txt", 17 | ".uof", ".uop", ".uos", ".uot", ".vdx", ".vor", ".vsd", ".vsdm", ".vsdx", ".wb2", ".wk1", 18 | ".wks", ".wmf", ".wpd", ".wpg", ".wps", ".xbm", ".xhtml", ".xls", ".xlsb", ".xlsm", ".xlsx", 19 | ".xlt", ".xltm", ".xltx", ".xlw", ".xml", ".xpm", ".zabw" 20 | 21 | - Markdown 文件分块规则和官方保持一致,支持图片和表格向量化 22 | 23 | ### 优化 24 | - 简化配置流程,避免配置错误导致链接失败 25 | - 支持在 .env 配置是否保存 MinerU 生成产物 26 | 27 | 28 | ## [v0.3.0] - 2025-05-02(兼容 RAGFlow v0.18.0) 29 | ### 新增 30 | - 开源 KnowFlow 前端 dist 产物 31 | - 移除了向量模型配置,默认为最后更新的向量模型 32 | - 适配 RAGFlow v0.18.0 33 | 34 | 35 | ## [v0.2.0] - 2025-04-24(仅支持源码,镜像包尚未构建) 36 | ### 新增 37 | - 适配 RAGFlow Plus 的知识库管理 38 | - 支持自定义 chunk 以及坐标回溯 39 | - 支持企业微信三方接入 40 | 41 | 42 | ## [v0.1.2] - 2025-04-17 43 | ### 新增 44 | - 图文回答支持在前端页面一键解析,无需复杂配置 45 | 46 | ## [v0.1.1] - 2025-04-11 47 | ### 新增 48 | - 回答结果支持图片展示 49 | 50 | ## [v0.1.0] - 2025-04-10 51 | ### 新增 52 | - 用户后台管理系统(用户管理、团队管理、模型配置管理) 53 | -------------------------------------------------------------------------------- /CLEANUP_TEMP_FILES.md: -------------------------------------------------------------------------------- 1 | # 临时文件清理控制 2 | 3 | ## 环境变量说明 4 | 5 | ### CLEANUP_TEMP_FILES 6 | 7 | 控制系统是否自动清理处理过程中生成的临时文件。 8 | 9 | **默认值**: `true` 10 | 11 | **可选值**: 12 | - `true`, `1`, `yes`, `on` - 启用自动清理(默认) 13 | - `false`, `0`, `no`, `off` - 禁用自动清理 14 | 15 | ## 使用方法 16 | 17 | 在你的 `.env` 文件中添加: 18 | 19 | ```bash 20 | # 启用自动清理(默认行为) 21 | CLEANUP_TEMP_FILES=true 22 | 23 | # 或者禁用自动清理(用于调试) 24 | CLEANUP_TEMP_FILES=false 25 | ``` 26 | 27 | ## 影响的功能 28 | 29 | 当 `CLEANUP_TEMP_FILES=false` 时,以下临时文件将被保留: 30 | 31 | 1. **文档解析临时文件** (`document_parser.py`) 32 | - 临时 PDF 文件 33 | - 临时图片目录 34 | 35 | 2. **MinIO 图片上传** (`minio_server.py`) 36 | - 处理后的图片文件 37 | 38 | 3. **RAGFlow 资源创建** (`ragflow_build.py`) 39 | - Markdown 文件及其目录 40 | 41 | ## 使用场景 42 | 43 | ### 生产环境 44 | ```bash 45 | CLEANUP_TEMP_FILES=true 46 | ``` 47 | - 自动清理临时文件,节省磁盘空间 48 | - 避免临时文件积累 49 | 50 | ### 开发/调试环境 51 | ```bash 52 | CLEANUP_TEMP_FILES=false 53 | ``` 54 | - 保留临时文件用于调试 55 | - 可以检查中间处理结果 56 | - 便于问题排查 57 | 58 | ## 注意事项 59 | 60 | 1. **磁盘空间**: 禁用清理可能导致临时文件积累,注意监控磁盘使用情况 61 | 2. **调试完成**: 调试完成后建议重新启用自动清理 62 | 3. **权限**: 确保应用有权限删除临时文件目录 63 | 64 | ## 日志信息 65 | 66 | 启用清理时的日志: 67 | ``` 68 | [INFO] 已清理临时文件目录: /tmp/some_temp_dir 69 | [INFO] 已删除临时图片文件: /tmp/image.jpg 70 | ``` 71 | 72 | 禁用清理时的日志: 73 | ``` 74 | [INFO] 环境变量 CLEANUP_TEMP_FILES 设置为 false,保留临时文件: /tmp/some_temp_dir 75 | [INFO] 保留临时图片文件: /tmp/image.jpg 76 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 前端构建阶段 2 | FROM node:18 AS frontend-builder 3 | WORKDIR /app/frontend 4 | COPY web /app/frontend 5 | # 安装 pnpm 6 | RUN npm install -g pnpm 7 | # 设置环境变量禁用交互式提示 8 | ENV CI=true 9 | # 安装依赖并构建 10 | RUN pnpm i && pnpm build 11 | 12 | # 前端服务阶段 13 | FROM nginx:alpine AS frontend 14 | COPY nginx.conf /etc/nginx/conf.d/default.conf 15 | COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html 16 | # 暴露前端端口 17 | EXPOSE 80 18 | CMD ["nginx", "-g", "daemon off;"] 19 | 20 | # 后端构建阶段 21 | FROM python:3.10.16-slim AS backend 22 | WORKDIR /app 23 | 24 | # 安装系统依赖 25 | RUN apt-get update && apt-get install -y --no-install-recommends \ 26 | libgl1-mesa-glx \ 27 | libglib2.0-0 \ 28 | libsm6 \ 29 | libxext6 \ 30 | libxrender-dev \ 31 | && rm -rf /var/lib/apt/lists/* 32 | 33 | COPY server/requirements.txt /app/ 34 | # 安装依赖 35 | RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple \ 36 | && rm -rf ~/.cache/pip 37 | # 复制后端代码 38 | COPY server /app 39 | 40 | # /root/.cache/huggingface/hub/models--opendatalab--PDF-Extract-Kit-1.0/snapshots/95817b4b2321769155f05c8d7e2f5a6b6da9e662/models 41 | COPY server/magic-pdf.json /root/magic-pdf.json 42 | 43 | # 暴露后端端口 44 | EXPOSE 5000 45 | CMD ["python3.10", "app.py"] -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/assets/logo.png -------------------------------------------------------------------------------- /assets/mulcontent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/assets/mulcontent.png -------------------------------------------------------------------------------- /assets/pdf_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/assets/pdf_chat.png -------------------------------------------------------------------------------- /assets/pdf_helper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/assets/pdf_helper.png -------------------------------------------------------------------------------- /assets/ui_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/assets/ui_1.png -------------------------------------------------------------------------------- /assets/ui_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/assets/ui_2.png -------------------------------------------------------------------------------- /assets/user-setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/assets/user-setting.png -------------------------------------------------------------------------------- /assets/wecom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/assets/wecom.jpg -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | container_name: knowflow-frontend 4 | image: zxwei/knowflow-web:v0.4.0 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | target: frontend 9 | ports: 10 | - "8888:80" 11 | depends_on: 12 | - backend 13 | environment: 14 | - API_BASE_URL=/api 15 | networks: 16 | - management_network 17 | 18 | backend: 19 | container_name: knowflow-backend 20 | image: zxwei/knowflow-server:v0.4.0 21 | build: 22 | context: . 23 | dockerfile: Dockerfile 24 | target: backend 25 | deploy: 26 | resources: 27 | reservations: 28 | devices: 29 | - driver: nvidia 30 | count: all 31 | capabilities: [gpu] 32 | ports: 33 | - "5000:5000" 34 | environment: 35 | - RAGFLOW_API_KEY=${RAGFLOW_API_KEY} 36 | - RAGFLOW_BASE_URL=${RAGFLOW_BASE_URL} 37 | - DB_HOST=${DB_HOST} 38 | - MINIO_HOST=${MINIO_HOST} 39 | - ES_HOST=${ES_HOST} 40 | - ES_PORT=${ES_PORT} 41 | - FLASK_ENV=development 42 | - CORS_ALLOWED_ORIGINS=http://frontend 43 | - GOTENBERG_URL=http://gotenberg:3000 44 | - MANAGEMENT_ADMIN_USERNAME=${MANAGEMENT_ADMIN_USERNAME:-admin} 45 | - MANAGEMENT_ADMIN_PASSWORD=${MANAGEMENT_ADMIN_PASSWORD:-12345678} 46 | - MANAGEMENT_JWT_SECRET=${MANAGEMENT_JWT_SECRET:-12345678} 47 | volumes: 48 | - ${MINERU_MODLES_DIR}:/root/.cache/huggingface/hub 49 | - ${MINERU_MAGIC_PDF_JSON_PATH}:/root/magic-pdf.json 50 | extra_hosts: 51 | - "host.docker.internal:host-gateway" 52 | networks: 53 | - management_network 54 | 55 | gotenberg: 56 | image: gotenberg/gotenberg:8 57 | ports: 58 | - "3000:3000" 59 | networks: 60 | - management_network 61 | 62 | 63 | networks: 64 | management_network: 65 | driver: bridge 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /mineru_volumes.py: -------------------------------------------------------------------------------- 1 | # 本项目 mineru 并未打包到镜像内,而是挂载到本地已下载的资源 2 | # 通过本脚本可以将 magic-pdf.json 文件路径添加到环境变量,实现自动挂载 3 | 4 | import os 5 | import json 6 | 7 | def update_env_file(env_path, updates): 8 | # 读取现有 .env 内容 9 | if os.path.exists(env_path): 10 | with open(env_path, 'r', encoding='utf-8') as f: 11 | lines = f.readlines() 12 | else: 13 | lines = [] 14 | 15 | # 构建新的 .env 内容 16 | env_dict = {} 17 | for line in lines: 18 | if '=' in line and not line.strip().startswith('#'): 19 | k, v = line.split('=', 1) 20 | env_dict[k.strip()] = v.strip().strip("'").strip('"') 21 | 22 | # 更新/追加变量 23 | env_dict.update(updates) 24 | 25 | # 打印 env_dict 26 | print(f"env_dict: {env_dict}") 27 | 28 | # 写回 .env 文件 29 | with open(env_path, 'w', encoding='utf-8') as f: 30 | for k, v in env_dict.items(): 31 | f.write(f"{k}='{v}'\n") 32 | 33 | 34 | if __name__ == "__main__": 35 | # 解析 magic-pdf.json 36 | home_dir = os.path.expanduser('~') 37 | 38 | mineru_models_dir = os.path.join(home_dir, '.cache/huggingface/hub/') 39 | 40 | # 打印处理后的路径 41 | print(f"mineru_models_dir: {mineru_models_dir}") 42 | 43 | # 更新 .env 文件 44 | env_path = os.path.join(os.path.dirname(__file__), '.env') 45 | 46 | print(f"env_path: {env_path}") 47 | 48 | # 复制 magic-pdf.json 到 server 目录,用于 docker 构建 49 | config_file = os.path.join(home_dir, 'magic-pdf.json') 50 | if not os.path.exists(config_file): 51 | print(f"配置文件不存在: {config_file}") 52 | exit(1) 53 | 54 | with open(config_file, 'r', encoding='utf-8') as f: 55 | config_json = json.load(f) 56 | 57 | def fix_cache_path(path): 58 | idx = path.find('.cache') 59 | if idx == -1: 60 | return path 61 | # 如果.cache前不是/root,则替换 62 | if not path.startswith('/root/.cache'): 63 | return '/root/' + path[idx:] 64 | return path 65 | 66 | if 'models-dir' in config_json: 67 | config_json['models-dir'] = fix_cache_path(config_json['models-dir']) 68 | if 'layoutreader-model-dir' in config_json: 69 | config_json['layoutreader-model-dir'] = fix_cache_path(config_json['layoutreader-model-dir']) 70 | 71 | server_dir = os.path.join(os.path.dirname(__file__), "server") 72 | target_config_file = os.path.join(server_dir, "magic-pdf.json") 73 | 74 | with open(target_config_file, 'w', encoding='utf-8') as f: 75 | json.dump(config_json, f, ensure_ascii=False, indent=4) 76 | print(f"已将修正后的配置文件复制到: {target_config_file}") 77 | 78 | # 更新环境变量,添加 MINERU_MAGIC_PDF_JSON_PATH 79 | update_env_file(env_path, { 80 | 'MINERU_MODLES_DIR': mineru_models_dir, 81 | 'MINERU_MAGIC_PDF_JSON_PATH': target_config_file 82 | }) 83 | 84 | print("已将配置文件路径添加到环境变量,可以通过 volume 挂载到容器内。") 85 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | # 设置客户端请求体大小限制 5 | client_max_body_size 500M; 6 | 7 | location / { 8 | root /usr/share/nginx/html; 9 | try_files $uri $uri/ /index.html; 10 | } 11 | 12 | location /v3-admin-vite/ { 13 | alias /usr/share/nginx/html/; 14 | try_files $uri $uri/ /index.html; 15 | } 16 | 17 | location /api/ { 18 | # 将所有以/api/开头的请求转发到后端服务(backend容器的5000端口) 19 | proxy_pass http://backend:5000/api/; 20 | # 设置代理请求头 21 | proxy_set_header Host $host; # 保留原始请求的Host头 22 | # 传递客户端真实IP 23 | proxy_set_header X-Real-IP $remote_addr; # 记录客户端IP 24 | # 添加X-Forwarded-For头 25 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链路追踪 26 | } 27 | } -------------------------------------------------------------------------------- /scripts/generate_env.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | :: 设置日志函数 5 | call :log_info "=== RAGFlow 配置生成器 ===" 6 | 7 | :: 获取项目根目录 8 | set "PROJECT_ROOT=%~dp0.." 9 | set "ENV_FILE=%PROJECT_ROOT%\.env" 10 | 11 | :: 检查 .env 文件是否存在 12 | if not exist "%ENV_FILE%" ( 13 | call :log_error ".env 文件不存在,请先创建 .env 文件并添加 RAGFLOW_API_KEY 和 RAGFLOW_BASE_URL" 14 | exit /b 1 15 | ) 16 | 17 | :: 获取宿主机 IP 18 | for /f "tokens=2 delims=:" %%a in ('ipconfig ^| findstr /i "IPv4"') do ( 19 | set "HOST_IP=%%a" 20 | set "HOST_IP=!HOST_IP: =!" 21 | goto :found_ip 22 | ) 23 | :found_ip 24 | 25 | :: 如果上述方法都失败,使用 localhost 26 | if "%HOST_IP%"=="" set "HOST_IP=localhost" 27 | 28 | :: 提示用户输入必要信息 29 | call :log_info "宿主机 IP: %HOST_IP%" 30 | echo. 31 | 32 | set /p ES_PORT="请输入 Elasticsearch 端口 (默认: 9200): " 33 | if "%ES_PORT%"=="" set "ES_PORT=9200" 34 | 35 | :: 创建临时文件 36 | set "TEMP_FILE=%TEMP%\ragflow_env_%RANDOM%.tmp" 37 | call :log_info "创建临时文件: %TEMP_FILE%" 38 | 39 | :: 定义需要更新的配置项 40 | set "ES_HOST=%HOST_IP%" 41 | set "ES_PORT=%ES_PORT%" 42 | set "DB_HOST=%HOST_IP%" 43 | set "MINIO_HOST=%HOST_IP%" 44 | set "REDIS_HOST=%HOST_IP%" 45 | 46 | :: 处理现有文件 47 | for /f "usebackq delims=" %%a in ("%ENV_FILE%") do ( 48 | set "line=%%a" 49 | 50 | :: 跳过空行 51 | if "!line!"=="" ( 52 | echo. >> "%TEMP_FILE%" 53 | goto :next_line 54 | ) 55 | 56 | :: 处理注释行 57 | if "!line:~0,1!"=="#" ( 58 | echo !line! >> "%TEMP_FILE%" 59 | goto :next_line 60 | ) 61 | 62 | :: 处理键值对 63 | for /f "tokens=1,* delims==" %%b in ("!line!") do ( 64 | set "key=%%b" 65 | set "key=!key: =!" 66 | 67 | :: 根据键名更新值 68 | if /i "!key!"=="ES_HOST" ( 69 | echo ES_HOST=!ES_HOST! >> "%TEMP_FILE%" 70 | set "ES_HOST=" 71 | ) else if /i "!key!"=="ES_PORT" ( 72 | echo ES_PORT=!ES_PORT! >> "%TEMP_FILE%" 73 | set "ES_PORT=" 74 | ) else if /i "!key!"=="DB_HOST" ( 75 | echo DB_HOST=!DB_HOST! >> "%TEMP_FILE%" 76 | set "DB_HOST=" 77 | ) else if /i "!key!"=="MINIO_HOST" ( 78 | echo MINIO_HOST=!MINIO_HOST! >> "%TEMP_FILE%" 79 | set "MINIO_HOST=" 80 | ) else if /i "!key!"=="REDIS_HOST" ( 81 | echo REDIS_HOST=!REDIS_HOST! >> "%TEMP_FILE%" 82 | set "REDIS_HOST=" 83 | ) else ( 84 | :: 保持原有值 85 | echo !line! >> "%TEMP_FILE%" 86 | ) 87 | ) 88 | :next_line 89 | ) 90 | 91 | :: 添加新的配置项 92 | echo. >> "%TEMP_FILE%" 93 | echo # 自动生成的配置 >> "%TEMP_FILE%" 94 | echo # 宿主机 IP: %HOST_IP% >> "%TEMP_FILE%" 95 | echo. >> "%TEMP_FILE%" 96 | 97 | :: 添加所有未处理的配置项 98 | if defined ES_HOST echo ES_HOST=!ES_HOST! >> "%TEMP_FILE%" 99 | if defined ES_PORT echo ES_PORT=!ES_PORT! >> "%TEMP_FILE%" 100 | if defined DB_HOST echo DB_HOST=!DB_HOST! >> "%TEMP_FILE%" 101 | if defined MINIO_HOST echo MINIO_HOST=!MINIO_HOST! >> "%TEMP_FILE%" 102 | if defined REDIS_HOST echo REDIS_HOST=!REDIS_HOST! >> "%TEMP_FILE%" 103 | 104 | :: 将临时文件内容写回 .env 文件 105 | move /y "%TEMP_FILE%" "%ENV_FILE%" > nul 106 | call :log_info "配置已更新: %ENV_FILE%" 107 | 108 | echo. 109 | call :log_info "配置已更新: %ENV_FILE%" 110 | echo 接下来您可以: 111 | echo 1. 运行 'docker compose up -d' 启动服务 112 | echo 2. 访问 http://%HOST_IP%:8888 进入管理界面 113 | 114 | goto :eof 115 | 116 | :log_info 117 | echo [INFO] %date% %time% - %~1 118 | goto :eof 119 | 120 | :log_error 121 | echo [ERROR] %date% %time% - %~1 >&2 122 | goto :eof -------------------------------------------------------------------------------- /scripts/generate_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 设置日志函数 4 | log_info() { 5 | echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1" 6 | } 7 | 8 | log_error() { 9 | echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $1" >&2 10 | } 11 | 12 | # 获取脚本所在目录的父目录(项目根目录) 13 | PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 14 | ENV_FILE="$PROJECT_ROOT/.env" 15 | 16 | # 检查 .env 文件是否存在 17 | if [ ! -f "$ENV_FILE" ]; then 18 | log_error ".env 文件不存在,请先创建 .env 文件并添加 RAGFLOW_API_KEY 和 RAGFLOW_BASE_URL" 19 | exit 1 20 | fi 21 | 22 | # 获取宿主机 IP 23 | if [[ "$OSTYPE" == "darwin"* ]]; then 24 | # macOS 25 | HOST_IP=$(ipconfig getifaddr en0 || ipconfig getifaddr en1) 26 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then 27 | # Linux 28 | HOST_IP=$(hostname -I | awk '{print $1}') 29 | elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then 30 | # Windows 31 | HOST_IP=$(ipconfig | findstr IPv4 | head -n 1 | awk '{print $NF}') 32 | else 33 | # 使用 Docker 方式获取 34 | HOST_IP=$(docker run --rm alpine ip route | awk 'NR==1 {print $3}') 35 | fi 36 | 37 | # 如果上述方法都失败,使用 localhost 38 | if [ -z "$HOST_IP" ]; then 39 | HOST_IP="localhost" 40 | fi 41 | 42 | # 提示用户输入必要信息 43 | log_info "=== RAGFlow 配置生成器 ===" 44 | log_info "宿主机 IP: $HOST_IP" 45 | echo 46 | 47 | read -p "请输入 Elasticsearch 端口 (默认: 9200): " ES_PORT 48 | ES_PORT=${ES_PORT:-9200} 49 | 50 | # 创建临时文件 51 | TEMP_FILE=$(mktemp) 52 | log_info "创建临时文件: $TEMP_FILE" 53 | 54 | # 定义需要更新的配置项 55 | ES_HOST="$HOST_IP" 56 | ES_PORT="$ES_PORT" 57 | DB_HOST="$HOST_IP" 58 | MINIO_HOST="$HOST_IP" 59 | REDIS_HOST="$HOST_IP" 60 | 61 | # 处理现有文件 62 | while IFS= read -r line || [ -n "$line" ]; do 63 | # 跳过空行 64 | if [[ -z "$line" ]]; then 65 | echo "" >> "$TEMP_FILE" 66 | continue 67 | fi 68 | 69 | # 处理注释行 70 | if [[ "$line" =~ ^[[:space:]]*# ]]; then 71 | echo "$line" >> "$TEMP_FILE" 72 | continue 73 | fi 74 | 75 | # 处理键值对 76 | if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then 77 | key="${BASH_REMATCH[1]}" 78 | key=$(echo "$key" | xargs) # 去除前后空格 79 | 80 | # 根据键名更新值 81 | case "$key" in 82 | "ES_HOST") 83 | echo "ES_HOST=$ES_HOST" >> "$TEMP_FILE" 84 | ES_HOST="" 85 | ;; 86 | "ES_PORT") 87 | echo "ES_PORT=$ES_PORT" >> "$TEMP_FILE" 88 | ES_PORT="" 89 | ;; 90 | "DB_HOST") 91 | echo "DB_HOST=$DB_HOST" >> "$TEMP_FILE" 92 | DB_HOST="" 93 | ;; 94 | "MINIO_HOST") 95 | echo "MINIO_HOST=$MINIO_HOST" >> "$TEMP_FILE" 96 | MINIO_HOST="" 97 | ;; 98 | "REDIS_HOST") 99 | echo "REDIS_HOST=$REDIS_HOST" >> "$TEMP_FILE" 100 | REDIS_HOST="" 101 | ;; 102 | *) 103 | # 保持原有值 104 | echo "$line" >> "$TEMP_FILE" 105 | ;; 106 | esac 107 | else 108 | # 保持非键值对的行不变 109 | echo "$line" >> "$TEMP_FILE" 110 | fi 111 | done < "$ENV_FILE" 112 | 113 | # 添加新的配置项 114 | echo "" >> "$TEMP_FILE" 115 | echo "# 自动生成的配置" >> "$TEMP_FILE" 116 | echo "# 宿主机 IP: $HOST_IP" >> "$TEMP_FILE" 117 | echo "" >> "$TEMP_FILE" 118 | 119 | # 添加所有未处理的配置项 120 | [ -n "$ES_HOST" ] && echo "ES_HOST=$ES_HOST" >> "$TEMP_FILE" 121 | [ -n "$ES_PORT" ] && echo "ES_PORT=$ES_PORT" >> "$TEMP_FILE" 122 | [ -n "$DB_HOST" ] && echo "DB_HOST=$DB_HOST" >> "$TEMP_FILE" 123 | [ -n "$MINIO_HOST" ] && echo "MINIO_HOST=$MINIO_HOST" >> "$TEMP_FILE" 124 | [ -n "$REDIS_HOST" ] && echo "REDIS_HOST=$REDIS_HOST" >> "$TEMP_FILE" 125 | 126 | # 将临时文件内容写回 .env 文件 127 | mv "$TEMP_FILE" "$ENV_FILE" 128 | log_info "配置已更新: $ENV_FILE" 129 | 130 | echo 131 | log_info "配置已更新: $ENV_FILE" 132 | echo "接下来您可以:" 133 | echo "1. 运行 'docker compose up -d' 启动服务" 134 | echo "2. 访问 http://$HOST_IP:8888 进入管理界面" -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 获取脚本所在目录的父目录(项目根目录) 4 | PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 5 | 6 | echo "=== RAGFlow 插件安装程序 ===" 7 | echo "项目根目录: $PROJECT_ROOT" 8 | echo 9 | 10 | # 检查 .env 文件是否存在 11 | if [ ! -f "$PROJECT_ROOT/.env" ]; then 12 | echo "错误: .env 文件不存在,请先创建 .env 文件并添加 RAGFLOW_API_KEY 和 RAGFLOW_BASE_URL" 13 | exit 1 14 | fi 15 | 16 | # 生成环境配置 17 | echo "生成环境配置..." 18 | chmod +x "$PROJECT_ROOT/scripts/generate_env.sh" 19 | "$PROJECT_ROOT/scripts/generate_env.sh" 20 | 21 | # 准备 Docker 挂载 22 | echo "准备 Docker 挂载..." 23 | python3 "$PROJECT_ROOT/mineru_volumes.py" 24 | 25 | 26 | echo 27 | echo "=== 安装完成!===" 28 | -------------------------------------------------------------------------------- /server/app.py: -------------------------------------------------------------------------------- 1 | import database 2 | import jwt 3 | import os 4 | from flask import Flask, jsonify, request 5 | from flask_cors import CORS 6 | from datetime import datetime, timedelta 7 | from routes import register_routes 8 | from dotenv import load_dotenv 9 | 10 | # 加载环境变量 11 | load_dotenv(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'docker', '.env')) 12 | 13 | app = Flask(__name__) 14 | # 启用CORS,允许前端访问 15 | CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True) 16 | 17 | # 注册所有路由 18 | register_routes(app) 19 | 20 | # 从环境变量获取配置 21 | ADMIN_USERNAME = os.getenv('MANAGEMENT_ADMIN_USERNAME', 'admin') 22 | ADMIN_PASSWORD = os.getenv('MANAGEMENT_ADMIN_PASSWORD', '12345678') 23 | JWT_SECRET = os.getenv('MANAGEMENT_JWT_SECRET', 'your-secret-key') 24 | 25 | # 生成token 26 | def generate_token(username): 27 | # 设置令牌过期时间(例如1小时后过期) 28 | expire_time = datetime.utcnow() + timedelta(hours=1) 29 | 30 | # 生成令牌 31 | token = jwt.encode({ 32 | 'username': username, 33 | 'exp': expire_time 34 | }, JWT_SECRET, algorithm='HS256') 35 | 36 | return token 37 | 38 | # 登录路由保留在主文件中 39 | @app.route('/api/v1/auth/login', methods=['POST']) 40 | def login(): 41 | data = request.get_json() 42 | username = data.get('username') 43 | password = data.get('password') 44 | 45 | # 创建用户名和密码的映射 46 | valid_users = { 47 | ADMIN_USERNAME: ADMIN_PASSWORD 48 | } 49 | 50 | # 验证用户名是否存在 51 | if not username or username not in valid_users: 52 | return {"code": 1, "message": "用户名不存在"}, 400 53 | 54 | # 验证密码是否正确 55 | if not password or password != valid_users[username]: 56 | return {"code": 1, "message": "密码错误"}, 400 57 | 58 | # 生成token 59 | token = generate_token(username) 60 | 61 | return {"code": 0, "data": {"token": token}, "message": "登录成功"} 62 | 63 | 64 | if __name__ == '__main__': 65 | app.run(host='0.0.0.0', port=5000,debug=True) -------------------------------------------------------------------------------- /server/download_models_hf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | 5 | import requests 6 | from huggingface_hub import snapshot_download 7 | 8 | 9 | def download_json(url): 10 | # 下载JSON文件 11 | response = requests.get(url) 12 | response.raise_for_status() # 检查请求是否成功 13 | return response.json() 14 | 15 | 16 | def download_and_modify_json(url, local_filename, modifications): 17 | if os.path.exists(local_filename): 18 | data = json.load(open(local_filename)) 19 | config_version = data.get('config_version', '0.0.0') 20 | if config_version < '1.2.0': 21 | data = download_json(url) 22 | else: 23 | data = download_json(url) 24 | 25 | # 修改内容 26 | for key, value in modifications.items(): 27 | data[key] = value 28 | 29 | # 保存修改后的内容 30 | with open(local_filename, 'w', encoding='utf-8') as f: 31 | json.dump(data, f, ensure_ascii=False, indent=4) 32 | 33 | 34 | if __name__ == '__main__': 35 | 36 | mineru_patterns = [ 37 | # "models/Layout/LayoutLMv3/*", 38 | "models/Layout/YOLO/*", 39 | "models/MFD/YOLO/*", 40 | "models/MFR/unimernet_hf_small_2503/*", 41 | "models/OCR/paddleocr_torch/*", 42 | # "models/TabRec/TableMaster/*", 43 | # "models/TabRec/StructEqTable/*", 44 | ] 45 | model_dir = snapshot_download('opendatalab/PDF-Extract-Kit-1.0', allow_patterns=mineru_patterns) 46 | 47 | layoutreader_pattern = [ 48 | "*.json", 49 | "*.safetensors", 50 | ] 51 | layoutreader_model_dir = snapshot_download('hantian/layoutreader', allow_patterns=layoutreader_pattern) 52 | 53 | model_dir = model_dir + '/models' 54 | print(f'model_dir is: {model_dir}') 55 | print(f'layoutreader_model_dir is: {layoutreader_model_dir}') 56 | 57 | # paddleocr_model_dir = model_dir + '/OCR/paddleocr' 58 | # user_paddleocr_dir = os.path.expanduser('~/.paddleocr') 59 | # if os.path.exists(user_paddleocr_dir): 60 | # shutil.rmtree(user_paddleocr_dir) 61 | # shutil.copytree(paddleocr_model_dir, user_paddleocr_dir) 62 | 63 | json_url = 'https://github.com/opendatalab/MinerU/raw/master/magic-pdf.template.json' 64 | config_file_name = 'magic-pdf.json' 65 | home_dir = os.path.expanduser('~') 66 | config_file = os.path.join(home_dir, config_file_name) 67 | 68 | json_mods = { 69 | 'models-dir': model_dir, 70 | 'layoutreader-model-dir': layoutreader_model_dir, 71 | } 72 | 73 | download_and_modify_json(json_url, config_file, json_mods) 74 | print(f'The configuration file has been configured successfully, the path is: {config_file}') 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /server/magic-pdf.json: -------------------------------------------------------------------------------- 1 | { 2 | "bucket_info": { 3 | "bucket-name-1": [ 4 | "ak", 5 | "sk", 6 | "endpoint" 7 | ], 8 | "bucket-name-2": [ 9 | "ak", 10 | "sk", 11 | "endpoint" 12 | ] 13 | }, 14 | "models-dir": "/root/.cache/huggingface/hub/models--opendatalab--PDF-Extract-Kit-1.0/snapshots/14efd64068741c8e1d79d635dd236a80a9db66ba/models", 15 | "layoutreader-model-dir": "/root/.cache/huggingface/hub/models--hantian--layoutreader/snapshots/641226775a0878b1014a96ad01b9642915136853", 16 | "device-mode": "cpu", 17 | "layout-config": { 18 | "model": "doclayout_yolo" 19 | }, 20 | "formula-config": { 21 | "mfd_model": "yolo_v8_mfd", 22 | "mfr_model": "unimernet_small", 23 | "enable": true 24 | }, 25 | "table-config": { 26 | "model": "rapid_table", 27 | "sub_model": "slanet_plus", 28 | "enable": true, 29 | "max_time": 400 30 | }, 31 | "latex-delimiter-config": { 32 | "display": { 33 | "left": "$$", 34 | "right": "$$" 35 | }, 36 | "inline": { 37 | "left": "$", 38 | "right": "$" 39 | } 40 | }, 41 | "llm-aided-config": { 42 | "formula_aided": { 43 | "api_key": "your_api_key", 44 | "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", 45 | "model": "qwen2.5-7b-instruct", 46 | "enable": false 47 | }, 48 | "text_aided": { 49 | "api_key": "your_api_key", 50 | "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", 51 | "model": "qwen2.5-7b-instruct", 52 | "enable": false 53 | }, 54 | "title_aided": { 55 | "api_key": "your_api_key", 56 | "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", 57 | "model": "qwen2.5-32b-instruct", 58 | "enable": false 59 | } 60 | }, 61 | "config_version": "1.2.1" 62 | } -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.1.0 2 | flask_cors==5.0.1 3 | mysql-connector-python==9.2.0 4 | pycryptodomex==3.20.0 5 | tabulate==0.9.0 6 | Werkzeug==3.1.3 7 | PyJWT==2.10.1 8 | dotenv==0.9.9 9 | magic-pdf[full]==1.3.10 10 | transformers==4.51.3 11 | elasticsearch==8.12.0 12 | minio==7.2.4 13 | strenum==0.4.15 14 | peewee==3.17.1 15 | pytz==2020.5 16 | tiktoken==0.9.0 17 | ragflow-sdk==0.18.0 18 | markdown==3.6 19 | markdown-to-json==2.1.1 20 | -------------------------------------------------------------------------------- /server/routes/__init__.py: -------------------------------------------------------------------------------- 1 | # 路由模块初始化 2 | from flask import Blueprint 3 | 4 | # 创建蓝图 5 | users_bp = Blueprint('users', __name__, url_prefix='/api/v1/users') 6 | teams_bp = Blueprint('teams', __name__, url_prefix='/api/v1/teams') 7 | tenants_bp = Blueprint('tenants', __name__, url_prefix='/api/v1/tenants') 8 | files_bp = Blueprint('files', __name__, url_prefix='/api/v1/files') 9 | knowledgebase_bp = Blueprint('knowledgebases', __name__, url_prefix='/api/v1/knowledgebases') 10 | 11 | # 导入路由 12 | from .users.routes import * 13 | from .teams.routes import * 14 | from .tenants.routes import * 15 | from .files.routes import * 16 | from .knowledgebases.routes import * 17 | 18 | def register_routes(app): 19 | """注册所有路由蓝图到应用""" 20 | app.register_blueprint(users_bp) 21 | app.register_blueprint(teams_bp) 22 | app.register_blueprint(tenants_bp) 23 | app.register_blueprint(files_bp) 24 | app.register_blueprint(knowledgebase_bp) -------------------------------------------------------------------------------- /server/routes/teams/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/server/routes/teams/__init__.py -------------------------------------------------------------------------------- /server/routes/tenants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/server/routes/tenants/__init__.py -------------------------------------------------------------------------------- /server/routes/tenants/routes.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, request 2 | from services.tenants.service import get_tenants_with_pagination, update_tenant 3 | from .. import tenants_bp 4 | 5 | @tenants_bp.route('', methods=['GET']) 6 | def get_tenants(): 7 | """获取租户列表的API端点,支持分页和条件查询""" 8 | try: 9 | # 获取查询参数 10 | current_page = int(request.args.get('currentPage', 1)) 11 | page_size = int(request.args.get('size', 10)) 12 | username = request.args.get('username', '') 13 | 14 | # 调用服务函数获取分页和筛选后的租户数据 15 | tenants, total = get_tenants_with_pagination(current_page, page_size, username) 16 | 17 | # 返回符合前端期望格式的数据 18 | return jsonify({ 19 | "code": 0, 20 | "data": { 21 | "list": tenants, 22 | "total": total 23 | }, 24 | "message": "获取租户列表成功" 25 | }) 26 | except Exception as e: 27 | # 错误处理 28 | return jsonify({ 29 | "code": 500, 30 | "message": f"获取租户列表失败: {str(e)}" 31 | }), 500 32 | 33 | @tenants_bp.route('/', methods=['PUT']) 34 | def update_tenant_route(tenant_id): 35 | """更新租户的API端点""" 36 | try: 37 | data = request.json 38 | success = update_tenant(tenant_id=tenant_id, tenant_data=data) 39 | if success: 40 | return jsonify({ 41 | "code": 0, 42 | "message": f"租户 {tenant_id} 更新成功" 43 | }) 44 | else: 45 | return jsonify({ 46 | "code": 404, 47 | "message": f"租户 {tenant_id} 不存在或更新失败" 48 | }), 404 49 | except Exception as e: 50 | return jsonify({ 51 | "code": 500, 52 | "message": f"更新租户失败: {str(e)}" 53 | }), 500 -------------------------------------------------------------------------------- /server/routes/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/server/routes/users/__init__.py -------------------------------------------------------------------------------- /server/routes/users/routes.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, request 2 | from services.users.service import get_users_with_pagination, delete_user, create_user, update_user, reset_user_password 3 | from .. import users_bp 4 | 5 | @users_bp.route('', methods=['GET']) 6 | def get_users(): 7 | """获取用户的API端点,支持分页和条件查询""" 8 | try: 9 | # 获取查询参数 10 | current_page = int(request.args.get('currentPage', 1)) 11 | page_size = int(request.args.get('size', 10)) 12 | username = request.args.get('username', '') 13 | email = request.args.get('email', '') 14 | 15 | # 调用服务函数获取分页和筛选后的用户数据 16 | users, total = get_users_with_pagination(current_page, page_size, username, email) 17 | 18 | # 返回符合前端期望格式的数据 19 | return jsonify({ 20 | "code": 0, # 成功状态码 21 | "data": { 22 | "list": users, 23 | "total": total 24 | }, 25 | "message": "获取用户列表成功" 26 | }) 27 | except Exception as e: 28 | # 错误处理 29 | return jsonify({ 30 | "code": 500, 31 | "message": f"获取用户列表失败: {str(e)}" 32 | }), 500 33 | 34 | @users_bp.route('/', methods=['DELETE']) 35 | def delete_user_route(user_id): 36 | """删除用户的API端点""" 37 | delete_user(user_id) 38 | return jsonify({ 39 | "code": 0, 40 | "message": f"用户 {user_id} 删除成功" 41 | }) 42 | 43 | @users_bp.route('', methods=['POST']) 44 | def create_user_route(): 45 | """创建用户的API端点""" 46 | data = request.json 47 | # 创建用户 48 | try: 49 | success = create_user(user_data=data) 50 | if success: 51 | return jsonify({ 52 | "code": 0, 53 | "message": "用户创建成功" 54 | }) 55 | else: 56 | return jsonify({ 57 | "code": 400, 58 | "message": "用户创建失败" 59 | }), 400 60 | except Exception as e: 61 | return jsonify({ 62 | "code": 500, 63 | "message": f"用户创建失败: {str(e)}" 64 | }), 500 65 | 66 | @users_bp.route('/', methods=['PUT']) 67 | def update_user_route(user_id): 68 | """更新用户的API端点""" 69 | data = request.json 70 | user_id = data.get('id') 71 | update_user(user_id=user_id, user_data=data) 72 | return jsonify({ 73 | "code": 0, 74 | "message": f"用户 {user_id} 更新成功" 75 | }) 76 | 77 | @users_bp.route('/me', methods=['GET']) 78 | def get_current_user(): 79 | return jsonify({ 80 | "code": 0, 81 | "data": { 82 | "username": "admin", 83 | "roles": ["admin"] 84 | }, 85 | "message": "获取用户信息成功" 86 | }) 87 | 88 | @users_bp.route('//reset-password', methods=['PUT']) 89 | def reset_password_route(user_id): 90 | """ 91 | 重置用户密码的API端点 92 | Args: 93 | user_id (str): 需要重置密码的用户ID 94 | Returns: 95 | Response: JSON响应 96 | """ 97 | try: 98 | data = request.json 99 | new_password = data.get('password') 100 | 101 | # 校验密码是否存在 102 | if not new_password: 103 | return jsonify({"code": 400, "message": "缺少新密码参数 'password'"}), 400 104 | 105 | # 调用 service 函数重置密码 106 | success = reset_user_password(user_id=user_id, new_password=new_password) 107 | 108 | if success: 109 | return jsonify({ 110 | "code": 0, 111 | "message": f"用户密码重置成功" 112 | }) 113 | else: 114 | # service 层可能因为用户不存在或其他原因返回 False 115 | return jsonify({"code": 404, "message": f"用户未找到或密码重置失败"}), 404 116 | except Exception as e: 117 | # 统一处理异常 118 | return jsonify({ 119 | "code": 500, 120 | "message": f"重置密码失败: {str(e)}" 121 | }), 500 -------------------------------------------------------------------------------- /server/services/files/base_service.py: -------------------------------------------------------------------------------- 1 | from peewee import Model 2 | from typing import Type, TypeVar, Dict, Any 3 | 4 | T = TypeVar('T', bound=Model) 5 | 6 | class BaseService: 7 | model: Type[T] 8 | 9 | @classmethod 10 | def get_by_id(cls, id: str) -> T: 11 | return cls.model.get_by_id(id) 12 | 13 | @classmethod 14 | def insert(cls, data: Dict[str, Any]) -> T: 15 | return cls.model.create(**data) 16 | 17 | @classmethod 18 | def delete_by_id(cls, id: str) -> int: 19 | return cls.model.delete().where(cls.model.id == id).execute() 20 | 21 | @classmethod 22 | def query(cls, **kwargs) -> list[T]: 23 | return list(cls.model.select().where(*[ 24 | getattr(cls.model, k) == v for k, v in kwargs.items() 25 | ])) -------------------------------------------------------------------------------- /server/services/files/document_service.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | from .base_service import BaseService 3 | from .models import Document 4 | from .utils import get_uuid, StatusEnum 5 | 6 | class DocumentService(BaseService): 7 | model = Document 8 | 9 | @classmethod 10 | def create_document(cls, kb_id: str, name: str, location: str, size: int, file_type: str, created_by: str = None, parser_id: str = None, parser_config: dict = None) -> Document: 11 | """ 12 | 创建文档记录 13 | 14 | Args: 15 | kb_id: 知识库ID 16 | name: 文件名 17 | location: 存储位置 18 | size: 文件大小 19 | file_type: 文件类型 20 | created_by: 创建者ID 21 | parser_id: 解析器ID 22 | parser_config: 解析器配置 23 | 24 | Returns: 25 | Document: 创建的文档对象 26 | """ 27 | doc_id = get_uuid() 28 | 29 | # 构建基本文档数据 30 | doc_data = { 31 | 'id': doc_id, 32 | 'kb_id': kb_id, 33 | 'name': name, 34 | 'location': location, 35 | 'size': size, 36 | 'type': file_type, 37 | 'created_by': created_by or 'system', 38 | 'parser_id': parser_id or '', 39 | 'parser_config': parser_config or {"pages": [[1, 1000000]]}, 40 | 'source_type': 'local', 41 | 'token_num': 0, 42 | 'chunk_num': 0, 43 | 'progress': 0, 44 | 'progress_msg': '', 45 | 'run': '0', # 未开始解析 46 | 'status': StatusEnum.VALID.value 47 | } 48 | 49 | return cls.insert(doc_data) 50 | 51 | @classmethod 52 | def get_by_kb_id(cls, kb_id: str) -> list[Document]: 53 | return cls.query(kb_id=kb_id) -------------------------------------------------------------------------------- /server/services/files/file2document_service.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | from .base_service import BaseService 3 | from .models import File2Document 4 | 5 | class File2DocumentService(BaseService): 6 | model = File2Document 7 | 8 | @classmethod 9 | def create_mapping(cls, file_id: str, document_id: str) -> File2Document: 10 | return cls.insert({ 11 | 'file_id': file_id, 12 | 'document_id': document_id 13 | }) 14 | 15 | @classmethod 16 | def get_by_document_id(cls, document_id: str) -> list[File2Document]: 17 | return cls.query(document_id=document_id) 18 | 19 | @classmethod 20 | def get_by_file_id(cls, file_id: str) -> list[File2Document]: 21 | return cls.query(file_id=file_id) -------------------------------------------------------------------------------- /server/services/files/file_service.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | from .base_service import BaseService 3 | from .models import File 4 | from .utils import FileType, get_uuid 5 | 6 | class FileService(BaseService): 7 | model = File 8 | 9 | @classmethod 10 | def create_file(cls, parent_id: str, name: str, location: str, size: int, file_type: str) -> File: 11 | return cls.insert({ 12 | 'parent_id': parent_id, 13 | 'name': name, 14 | 'location': location, 15 | 'size': size, 16 | 'type': file_type, 17 | 'source_type': 'knowledgebase' 18 | }) 19 | 20 | @classmethod 21 | def get_parser(cls, file_type, filename, tenant_id): 22 | """获取适合文件类型的解析器ID""" 23 | # 这里可能需要根据实际情况调整 24 | if file_type == FileType.PDF.value: 25 | return "pdf_parser" 26 | elif file_type == FileType.WORD.value: 27 | return "word_parser" 28 | elif file_type == FileType.EXCEL.value: 29 | return "excel_parser" 30 | elif file_type == FileType.PPT.value: 31 | return "ppt_parser" 32 | elif file_type == FileType.VISUAL.value: 33 | return "image_parser" 34 | elif file_type == FileType.TEXT.value: # 添加对文本文件的支持 35 | return "text_parser" 36 | else: 37 | return "default_parser" 38 | 39 | @classmethod 40 | def get_by_parent_id(cls, parent_id: str) -> list[File]: 41 | return cls.query(parent_id=parent_id) 42 | 43 | @classmethod 44 | def generate_bucket_name(cls): 45 | """生成随机存储桶名称""" 46 | return f"kb-{get_uuid()}" -------------------------------------------------------------------------------- /server/services/files/models/__init__.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | import os 3 | from datetime import datetime 4 | from database import DB_CONFIG 5 | 6 | # 使用MySQL数据库 7 | db = MySQLDatabase( 8 | DB_CONFIG["database"], 9 | host=DB_CONFIG["host"], 10 | port=DB_CONFIG["port"], 11 | user=DB_CONFIG["user"], 12 | password=DB_CONFIG["password"] 13 | ) 14 | 15 | class BaseModel(Model): 16 | # 添加共同的时间戳字段 17 | create_time = BigIntegerField(null=True) 18 | create_date = CharField(null=True) 19 | update_time = BigIntegerField(null=True) 20 | update_date = CharField(null=True) 21 | 22 | class Meta: 23 | database = db 24 | 25 | class Document(BaseModel): 26 | id = CharField(primary_key=True) 27 | thumbnail = TextField(null=True) 28 | kb_id = CharField(index=True) 29 | parser_id = CharField(null=True, index=True) 30 | parser_config = TextField(null=True) # JSONField在SQLite中用TextField替代 31 | source_type = CharField(default="local", index=True) 32 | type = CharField(index=True) 33 | created_by = CharField(null=True, index=True) 34 | name = CharField(null=True, index=True) 35 | location = CharField(null=True) 36 | size = IntegerField(default=0) 37 | token_num = IntegerField(default=0) 38 | chunk_num = IntegerField(default=0) 39 | progress = FloatField(default=0) 40 | progress_msg = TextField(null=True, default="") 41 | process_begin_at = DateTimeField(null=True) 42 | process_duation = FloatField(default=0) 43 | meta_fields = TextField(null=True) # JSONField 44 | run = CharField(default="0") 45 | status = CharField(default="1") 46 | 47 | class Meta: 48 | db_table = "document" 49 | 50 | class File(BaseModel): 51 | id = CharField(primary_key=True) 52 | parent_id = CharField(index=True) 53 | tenant_id = CharField(null=True, index=True) 54 | created_by = CharField(null=True, index=True) 55 | name = CharField(index=True) 56 | location = CharField(null=True) 57 | size = IntegerField(default=0) 58 | type = CharField(index=True) 59 | source_type = CharField(default="", index=True) 60 | 61 | class Meta: 62 | db_table = "file" 63 | 64 | class File2Document(BaseModel): 65 | id = CharField(primary_key=True) 66 | file_id = CharField(index=True) 67 | document_id = CharField(index=True) 68 | 69 | class Meta: 70 | db_table = "file2document" -------------------------------------------------------------------------------- /server/services/files/utils.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from strenum import StrEnum 3 | from enum import Enum 4 | 5 | 6 | # 参考:api.db 7 | class FileType(StrEnum): 8 | FOLDER = "folder" 9 | PDF = "pdf" 10 | WORD = "word" 11 | EXCEL = "excel" 12 | PPT = "ppt" 13 | VISUAL = "visual" 14 | TEXT = "txt" 15 | OTHER = "other" 16 | 17 | class FileSource(StrEnum): 18 | LOCAL = "" 19 | KNOWLEDGEBASE = "knowledgebase" 20 | S3 = "s3" 21 | 22 | class StatusEnum(Enum): 23 | VALID = "1" 24 | INVALID = "0" 25 | 26 | # 参考:api.utils 27 | def get_uuid(): 28 | return uuid.uuid1().hex -------------------------------------------------------------------------------- /server/services/knowflow/README.md: -------------------------------------------------------------------------------- 1 | # RAGFlow Chat 插件用于 ChatGPT-on-WeChat 2 | 本文件夹包含 ragflow_chat 插件的源代码,该插件扩展了 RAGFlow API 的核心功能,支持基于检索增强生成(RAG)的对话交互。该插件可无缝集成到 ChatGPT-on-WeChat 项目中,使微信及其他平台能够在聊天交互中利用 RAGFlow 提供的知识检索能力。 3 | 4 | ### 功能特性 5 | - 对话交互 :将微信的对话界面与强大的 RAG(检索增强生成)能力结合。 6 | - 基于知识的回复 :通过检索外部知识源中的相关数据并将其融入聊天回复,丰富对话内容。 7 | - 多平台支持 :可在微信、企业微信以及 ChatGPT-on-WeChat 框架支持的多种平台上运行。 8 | 9 | ### 插件与 ChatGPT-on-WeChat 配置说明 10 | 注意 :本集成涉及两个不同的配置文件——一个用于 ChatGPT-on-WeChat 核心项目,另一个专用于 ragflow_chat 插件。请务必正确配置两者,以确保顺利集成。 11 | ChatGPT-on-WeChat 根配置( config.json ) 12 | 该文件位于 ChatGPT-on-WeChat 项目的根目录,用于定义通信渠道和整体行为。例如,它负责配置微信、企业微信以及飞书、钉钉等服务。 13 | 14 | 微信渠道的 config.json 示例: 15 | 16 | ```json 17 | { 18 | "channel_type": "wechatmp", 19 | "wechatmp_app_id": "YOUR_APP_ID", 20 | "wechatmp_app_secret": "YOUR_APP_SECRET", 21 | "wechatmp_token": "YOUR_TOKEN", 22 | "wechatmp_port": 80, 23 | ... 24 | } 25 | ``` 26 | 27 | 该文件也可修改以支持其他通信平台,例如: 28 | 29 | - 个人微信 ( channel_type: wx ) 30 | - 微信公众号 ( wechatmp 或 wechatmp_service ) 31 | - 企业微信 ( wechatcom_app ) 32 | - 飞书 ( feishu ) 33 | - 钉钉 ( dingtalk ) 34 | 详细配置选项请参见官方 LinkAI 文档 。 35 | RAGFlow Chat 插件配置( plugins/ragflow_chat/config.json ) 36 | 该配置文件专用于 ragflow_chat 插件,用于设置与 RAGFlow 服务器的通信。请确保你的 RAGFlow 服务器已启动,并将插件的 config.json 文件更新为你的服务器信息: 37 | 38 | ragflow_chat 的 config.json 示例: 39 | 40 | ```json 41 | { 42 | "api_key": "ragflow-xxxx", 43 | "host_address": "xxxx", 44 | "dialog_id": "助理 ID", 45 | "conversation_id":"会话 ID" 46 | } 47 | ``` 48 | 49 | 该文件必须正确指向你的 RAGFlow 实例,`api_key` 和 `host_address` 字段需正确设置。 `dialog_id`, `ragflow_api_key` 可在 RAGFlow 前端页面 url 或者调试模式中获取。 50 | 51 | ### 使用要求 52 | 在使用本插件前,请确保: 53 | 54 | 1. 你已安装并配置好 `ChatGPT-on-WeChat`。 55 | 2. 将 knowflow 文件夹放到 `chatgpt-on-wechat/plugins/` 目录下 56 | 3. 在 knowflow 目录下运行 `pip instal -r requirement.txt` 57 | 4. 重启 ChatGPT-on-WeChat 服务 58 | 5. 你已部署并运行 RAGFlow 服务器 59 | 请确保上述两个 config.json 文件(ChatGPT-on-WeChat 和 RAGFlow Chat 插件)均已按示例正确配置。 -------------------------------------------------------------------------------- /server/services/knowflow/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2025 The InfiniFlow Authors. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from beartype.claw import beartype_this_package 18 | beartype_this_package() 19 | 20 | from .ragflow_chat import RAGFlowChat 21 | 22 | __all__ = [ 23 | "RAGFlowChat" 24 | ] -------------------------------------------------------------------------------- /server/services/knowflow/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "ragflow-M4NzNjYzQwMGJiZTExZjA5MTY1MTZhZG", 3 | "host_address": "www.knowflowchat.cn", 4 | "dialog_id": "924be6b21bfd11f0a0cb72197409366f", 5 | "conversation_id":"7f0df2f0e30644fc9df64e49111341cf" 6 | } -------------------------------------------------------------------------------- /server/services/knowflow/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /server/services/knowledgebases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/server/services/knowledgebases/__init__.py -------------------------------------------------------------------------------- /server/services/knowledgebases/mineru_parse/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/server/services/knowledgebases/mineru_parse/__init__.py -------------------------------------------------------------------------------- /server/services/knowledgebases/mineru_parse/download_models_hf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | import requests 5 | from huggingface_hub import snapshot_download 6 | 7 | 8 | def download_json(url): 9 | # 下载JSON文件 10 | response = requests.get(url) 11 | response.raise_for_status() # 检查请求是否成功 12 | return response.json() 13 | 14 | 15 | def download_and_modify_json(url, local_filename, modifications): 16 | if os.path.exists(local_filename): 17 | data = json.load(open(local_filename)) 18 | config_version = data.get('config_version', '0.0.0') 19 | if config_version < '1.2.0': 20 | data = download_json(url) 21 | else: 22 | data = download_json(url) 23 | 24 | # 修改内容 25 | for key, value in modifications.items(): 26 | data[key] = value 27 | 28 | # 保存修改后的内容 29 | with open(local_filename, 'w', encoding='utf-8') as f: 30 | json.dump(data, f, ensure_ascii=False, indent=4) 31 | 32 | 33 | if __name__ == '__main__': 34 | 35 | mineru_patterns = [ 36 | # "models/Layout/LayoutLMv3/*", 37 | "models/Layout/YOLO/*", 38 | "models/MFD/YOLO/*", 39 | "models/MFR/unimernet_hf_small_2503/*", 40 | "models/OCR/paddleocr_torch/*", 41 | # "models/TabRec/TableMaster/*", 42 | # "models/TabRec/StructEqTable/*", 43 | ] 44 | model_dir = snapshot_download('opendatalab/PDF-Extract-Kit-1.0', allow_patterns=mineru_patterns) 45 | 46 | layoutreader_pattern = [ 47 | "*.json", 48 | "*.safetensors", 49 | ] 50 | layoutreader_model_dir = snapshot_download('hantian/layoutreader', allow_patterns=layoutreader_pattern) 51 | 52 | model_dir = model_dir + '/models' 53 | print(f'model_dir is: {model_dir}') 54 | print(f'layoutreader_model_dir is: {layoutreader_model_dir}') 55 | 56 | # paddleocr_model_dir = model_dir + '/OCR/paddleocr' 57 | # user_paddleocr_dir = os.path.expanduser('~/.paddleocr') 58 | # if os.path.exists(user_paddleocr_dir): 59 | # shutil.rmtree(user_paddleocr_dir) 60 | # shutil.copytree(paddleocr_model_dir, user_paddleocr_dir) 61 | 62 | json_url = 'https://github.com/opendatalab/MinerU/raw/master/magic-pdf.template.json' 63 | config_file_name = 'magic-pdf.json' 64 | home_dir = os.path.expanduser('~') 65 | config_file = os.path.join(home_dir, config_file_name) 66 | 67 | json_mods = { 68 | 'models-dir': model_dir, 69 | 'layoutreader-model-dir': layoutreader_model_dir, 70 | } 71 | 72 | download_and_modify_json(json_url, config_file, json_mods) 73 | print(f'The configuration file has been configured successfully, the path is: {config_file}') 74 | -------------------------------------------------------------------------------- /server/services/knowledgebases/mineru_parse/process_pdf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from .mineru_test import process_pdf_with_minerU 5 | from .ragflow_build import create_ragflow_resources 6 | # 聊天助手 Prompt 模板: 7 | # 请参考{knowledge}内容回答用户问题。 8 | # 如果知识库内容包含图片,请在回答中包含图片URL。 9 | # 注意这个 html 格式的 URL 是来自知识库本身,URL 不能做任何改动。 10 | # 示例如下:图片。 11 | # 请确保回答简洁、专业,将图片自然地融入回答内容中。 12 | 13 | 14 | 15 | def _safe_process_pdf(pdf_path, update_progress): 16 | """封装PDF处理,便于异常捕获和扩展""" 17 | return process_pdf_with_minerU(pdf_path, update_progress) 18 | 19 | def _safe_create_ragflow(doc_id, kb_id, md_file_path, image_dir, update_progress): 20 | """封装RAGFlow资源创建,便于异常捕获和扩展""" 21 | return create_ragflow_resources(doc_id, kb_id, md_file_path, image_dir, update_progress) 22 | 23 | def process_pdf_entry(doc_id, pdf_path, kb_id, update_progress): 24 | """ 25 | 供外部调用的PDF处理接口 26 | Args: 27 | doc_id (str): 文档ID 28 | pdf_path (str): PDF文件路径 29 | kb_id (str): 知识库ID 30 | update_progress (function): 进度回调 31 | Returns: 32 | dict: 处理结果 33 | """ 34 | try: 35 | md_file_path = _safe_process_pdf(pdf_path, update_progress) 36 | images_dir = os.path.join(os.path.dirname(md_file_path), 'images') 37 | result = _safe_create_ragflow(doc_id, kb_id, md_file_path, images_dir, update_progress) 38 | return result 39 | except Exception as e: 40 | print(e) 41 | return 0 -------------------------------------------------------------------------------- /server/services/knowledgebases/utils.py: -------------------------------------------------------------------------------- 1 | from ragflow_sdk import RAGFlow 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | 6 | 7 | def _validate_environment(): 8 | """验证环境变量配置""" 9 | load_dotenv() 10 | api_key = os.getenv('RAGFLOW_API_KEY') 11 | base_url = os.getenv('RAGFLOW_BASE_URL') 12 | if not api_key: 13 | raise ValueError("错误:请在.env文件中设置RAGFLOW_API_KEY或使用--api_key参数指定。") 14 | if not base_url: 15 | raise ValueError("错误:请在.env文件中设置RAGFLOW_BASE_URL或使用--server_ip参数指定。") 16 | return api_key, base_url 17 | 18 | def get_doc_content(dataset_id, doc_id): 19 | api_key, base_url = _validate_environment() 20 | rag_object = RAGFlow(api_key=api_key, base_url=base_url) 21 | datasets = rag_object.list_datasets(id=dataset_id) 22 | if not datasets: 23 | raise Exception(f"未找到指定 dataset_id: {dataset_id}") 24 | dataset = datasets[0] 25 | docs = dataset.list_documents(id=doc_id) 26 | if not docs: 27 | raise Exception(f"未找到指定 doc_id: {doc_id}") 28 | doc = docs[0] 29 | return doc.download() 30 | -------------------------------------------------------------------------------- /server/services/teams/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/server/services/teams/__init__.py -------------------------------------------------------------------------------- /server/services/tenants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/server/services/tenants/__init__.py -------------------------------------------------------------------------------- /server/services/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/server/services/users/__init__.py -------------------------------------------------------------------------------- /server/utils.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import base64 3 | from flask import jsonify 4 | from Cryptodome.PublicKey import RSA 5 | from Cryptodome.Cipher import PKCS1_v1_5 6 | from werkzeug.security import generate_password_hash 7 | 8 | 9 | # 生成随机的 UUID 作为 id 10 | def generate_uuid(): 11 | return uuid.uuid1().hex 12 | 13 | # RSA 加密密码 14 | def rsa_psw(password: str) -> str: 15 | pub_key = """-----BEGIN PUBLIC KEY----- 16 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB 17 | -----END PUBLIC KEY-----""" 18 | 19 | rsa_key = RSA.import_key(pub_key) 20 | cipher = PKCS1_v1_5.new(rsa_key) 21 | encrypted_data = cipher.encrypt(base64.b64encode(password.encode())) 22 | return base64.b64encode(encrypted_data).decode() 23 | 24 | # 加密密码 25 | def encrypt_password(raw_password: str) -> str: 26 | base64_password = base64.b64encode(raw_password.encode()).decode() 27 | return generate_password_hash(base64_password) 28 | 29 | # 标准响应格式 30 | def success_response(data=None, message="操作成功", code=0): 31 | return jsonify({ 32 | "code": code, 33 | "message": message, 34 | "data": data 35 | }) 36 | 37 | # 错误响应格式 38 | def error_response(message="操作失败", code=500, details=None): 39 | """标准错误响应格式""" 40 | response = { 41 | "code": code, 42 | "message": message 43 | } 44 | if details: 45 | response["details"] = details 46 | return jsonify(response), code if code >= 400 else 500 -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | # 配置项文档:https://editorconfig.org(修改配置后重启编辑器) 2 | 3 | ## 告知 EditorConfig 插件,当前即是根文件 4 | root = true 5 | 6 | ## 适用全部文件 7 | [*] 8 | ### 设置字符集 9 | charset = utf-8 10 | ### 缩进风格 space | tab,建议 space 11 | indent_style = space 12 | ### 缩进的空格数 13 | indent_size = 2 14 | ### 换行符类型 lf | cr | crlf,一般都是设置为 lf 15 | end_of_line = lf 16 | ### 是否在文件末尾插入空白行 17 | insert_final_newline = true 18 | ### 是否删除一行中的前后空格 19 | trim_trailing_whitespace = true 20 | 21 | ## 适用 .md 文件 22 | [*.md] 23 | insert_final_newline = false 24 | trim_trailing_whitespace = false 25 | -------------------------------------------------------------------------------- /web/.env.staging: -------------------------------------------------------------------------------- 1 | # 预发布环境的环境变量(命名必须以 VITE_ 开头) 2 | 3 | ## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径) 4 | VITE_BASE_URL = http://localhost:5000 5 | 6 | ## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/ 域名下就需要填写 /) 7 | VITE_PUBLIC_PATH = / 8 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Common 2 | dist 3 | node_modules 4 | .eslintcache 5 | vite.config.*.timestamp* 6 | 7 | # MacOS 8 | .DS_Store 9 | 10 | # Local env files 11 | *.local 12 | 13 | # Logs 14 | *.log 15 | 16 | # Use the pnpm 17 | package-lock.json 18 | yarn.lock 19 | -------------------------------------------------------------------------------- /web/.npmrc: -------------------------------------------------------------------------------- 1 | # China mirror of npm 2 | registry = https://registry.npmmirror.com 3 | 4 | # 安装依赖时锁定版本号 5 | save-exact = true 6 | -------------------------------------------------------------------------------- /web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from "@antfu/eslint-config" 2 | 3 | // 更多自定义配置可查阅仓库:https://github.com/antfu/eslint-config 4 | export default antfu( 5 | { 6 | // 使用外部格式化程序格式化 css、html、markdown 等文件 7 | formatters: true, 8 | // 启用样式规则 9 | stylistic: { 10 | // 缩进级别 11 | indent: 2, 12 | // 引号风格 'single' | 'double' 13 | quotes: "double", 14 | // 是否启用分号 15 | semi: false 16 | }, 17 | // 忽略文件 18 | ignores: [] 19 | }, 20 | { 21 | // 对所有文件都生效的规则 22 | rules: { 23 | // vue 24 | "vue/block-order": ["error", { order: ["script", "template", "style"] }], 25 | "vue/attributes-order": "off", 26 | // ts 27 | "ts/no-use-before-define": "off", 28 | // node 29 | "node/prefer-global/process": "off", 30 | // style 31 | "style/comma-dangle": ["error", "never"], 32 | "style/brace-style": ["error", "1tbs"], 33 | // regexp 34 | "regexp/no-unused-capturing-group": "off", 35 | // other 36 | "no-console": "off", 37 | "no-debugger": "off", 38 | "symbol-description": "off", 39 | "antfu/if-newline": "off", 40 | "unicorn/no-instanceof-builtins": "off" 41 | } 42 | } 43 | ) 44 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %VITE_APP_TITLE% 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v3-admin-vite", 3 | "type": "module", 4 | "version": "5.0.0-beta.5", 5 | "description": "A crafted admin template, built with Vue3, Vite, TypeScript, Element Plus, and more", 6 | "author": "pany <939630029@qq.com> (https://github.com/pany-ang)", 7 | "repository": "https://github.com/un-pany/v3-admin-vite", 8 | "scripts": { 9 | "dev": "vite", 10 | "build:staging": "vue-tsc && vite build --mode production", 11 | "build": "vue-tsc && vite build", 12 | "preview": "vite preview", 13 | "lint": "eslint . --fix", 14 | "prepare": "husky", 15 | "test": "vitest" 16 | }, 17 | "dependencies": { 18 | "@element-plus/icons-vue": "2.3.1", 19 | "axios": "1.8.4", 20 | "dayjs": "1.11.13", 21 | "element-plus": "2.9.7", 22 | "js-cookie": "3.0.5", 23 | "lodash-es": "4.17.21", 24 | "mitt": "3.0.1", 25 | "normalize.css": "8.0.1", 26 | "nprogress": "0.2.0", 27 | "path-browserify": "1.0.1", 28 | "path-to-regexp": "8.2.0", 29 | "pinia": "3.0.1", 30 | "screenfull": "6.0.2", 31 | "vue": "3.5.13", 32 | "vue-router": "4.5.0", 33 | "vxe-table": "4.6.25" 34 | }, 35 | "devDependencies": { 36 | "@antfu/eslint-config": "4.11.0", 37 | "@types/js-cookie": "3.0.6", 38 | "@types/lodash-es": "4.17.12", 39 | "@types/node": "22.13.14", 40 | "@types/nprogress": "0.2.3", 41 | "@types/path-browserify": "1.0.3", 42 | "@vitejs/plugin-vue": "5.2.3", 43 | "@vitejs/plugin-vue-jsx": "4.1.2", 44 | "@vue/test-utils": "2.4.6", 45 | "eslint": "9.23.0", 46 | "eslint-plugin-format": "1.0.1", 47 | "happy-dom": "17.4.4", 48 | "husky": "9.1.7", 49 | "lint-staged": "15.5.0", 50 | "sass": "1.78.0", 51 | "typescript": "5.8.2", 52 | "unocss": "66.1.0-beta.7", 53 | "unplugin-auto-import": "19.1.2", 54 | "unplugin-svg-component": "0.12.1", 55 | "unplugin-vue-components": "28.4.1", 56 | "vite": "6.2.3", 57 | "vite-svg-loader": "5.1.0", 58 | "vitest": "3.0.9", 59 | "vue-tsc": "2.2.8" 60 | }, 61 | "lint-staged": { 62 | "*": "eslint --fix" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /web/public/app-loading.css: -------------------------------------------------------------------------------- 1 | /* 白屏阶段会执行的 CSS 加载动画 */ 2 | 3 | #app-loading { 4 | position: relative; 5 | top: 45vh; 6 | margin: 0 auto; 7 | color: #409eff; 8 | font-size: 12px; 9 | } 10 | 11 | #app-loading, 12 | #app-loading::before, 13 | #app-loading::after { 14 | width: 2em; 15 | height: 2em; 16 | border-radius: 50%; 17 | animation: 2s ease-in-out infinite app-loading-animation; 18 | } 19 | 20 | #app-loading::before, 21 | #app-loading::after { 22 | content: ""; 23 | position: absolute; 24 | } 25 | 26 | #app-loading::before { 27 | left: -4em; 28 | animation-delay: -0.2s; 29 | } 30 | 31 | #app-loading::after { 32 | left: 4em; 33 | animation-delay: 0.2s; 34 | } 35 | 36 | @keyframes app-loading-animation { 37 | 0%, 38 | 80%, 39 | 100% { 40 | box-shadow: 0 2em 0 -2em; 41 | } 42 | 40% { 43 | box-shadow: 0 2em 0 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /web/public/detect-ie.js: -------------------------------------------------------------------------------- 1 | // Tip: Simple judgments may not fully cover 2 | if (/MSIE\s|Trident\//.test(navigator.userAgent)) { 3 | document.body.innerHTML = "Sorry, this browser is currently not supported. We recommend using the latest version of a modern browser. For example, Chrome/Firefox/Edge." 4 | } 5 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/favicon.ico1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/public/favicon.ico1 -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /web/src/common/apis/configs/index.ts: -------------------------------------------------------------------------------- 1 | import type * as Tables from "./type" 2 | import { request } from "@/http/axios" 3 | 4 | /** 改 */ 5 | export function updateTableDataApi(data: Tables.CreateOrUpdateTableRequestData) { 6 | return request({ 7 | url: `api/v1/tenants/${data.id}`, 8 | method: "put", 9 | data 10 | }) 11 | } 12 | 13 | /** 查 */ 14 | export function getTableDataApi(params: Tables.TableRequestData) { 15 | return request({ 16 | url: "api/v1/tenants", 17 | method: "get", 18 | params 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /web/src/common/apis/configs/type.ts: -------------------------------------------------------------------------------- 1 | export interface CreateOrUpdateTableRequestData { 2 | id?: number 3 | username: string 4 | chatModel?: string 5 | embeddingModel?: string 6 | } 7 | 8 | export interface TableRequestData { 9 | /** 当前页码 */ 10 | currentPage: number 11 | /** 查询条数 */ 12 | size: number 13 | /** 查询参数:用户名 */ 14 | username?: string 15 | } 16 | 17 | export interface TableData { 18 | id: number 19 | username: string 20 | chatModel: string 21 | embeddingModel: string 22 | createTime: string 23 | updateTime: string 24 | } 25 | 26 | export type TableResponseData = ApiResponseData<{ 27 | list: TableData[] 28 | total: number 29 | }> 30 | -------------------------------------------------------------------------------- /web/src/common/apis/files/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosResponse } from "axios" 2 | import type { FileData, PageQuery, PageResult } from "./type" 3 | import { request } from "@/http/axios" 4 | import axios from "axios" 5 | 6 | /** 7 | * 获取文件列表 8 | * @param params 查询参数 9 | */ 10 | export function getFileListApi(params: PageQuery & { name?: string }) { 11 | return request<{ data: PageResult, code: number, message: string }>({ 12 | url: "/api/v1/files", 13 | method: "get", 14 | params 15 | }) 16 | } 17 | 18 | /** 19 | * 下载文件 - 使用流式下载 20 | * @param fileId 文件ID 21 | * @param onDownloadProgress 下载进度回调 22 | */ 23 | export function downloadFileApi( 24 | fileId: string, 25 | onDownloadProgress?: (progressEvent: any) => void 26 | ): Promise> { 27 | console.log(`发起文件下载请求: ${fileId}`) 28 | const source = axios.CancelToken.source() 29 | 30 | return request({ 31 | url: `/api/v1/files/${fileId}/download`, 32 | method: "get", 33 | responseType: "blob", 34 | timeout: 300000, 35 | onDownloadProgress, 36 | cancelToken: source.token, 37 | validateStatus: (_status) => { 38 | // 允许所有状态码,以便在前端统一处理错误 39 | return true 40 | } 41 | }).then((response: unknown) => { 42 | const axiosResponse = response as AxiosResponse 43 | console.log(`下载响应: ${axiosResponse.status}`, axiosResponse.data) 44 | // 确保响应对象包含必要的属性 45 | if (axiosResponse.data instanceof Blob && axiosResponse.data.size > 0) { 46 | // 如果是成功的Blob响应,确保状态码为200 47 | if (!axiosResponse.status) axiosResponse.status = 200 48 | return axiosResponse 49 | } 50 | return axiosResponse 51 | }).catch((error: any) => { 52 | console.error("下载请求失败:", error) 53 | // 将错误信息转换为统一格式 54 | if (error.response) { 55 | error.response.data = { 56 | message: error.response.data?.message || "服务器错误" 57 | } 58 | } 59 | return Promise.reject(error) 60 | }) as Promise> 61 | } 62 | 63 | /** 64 | * 取消下载 65 | */ 66 | export function cancelDownload() { 67 | if (axios.isCancel(Error)) { 68 | axios.CancelToken.source().cancel("用户取消下载") 69 | } 70 | } 71 | 72 | /** 73 | * 删除文件 74 | * @param fileId 文件ID 75 | */ 76 | export function deleteFileApi(fileId: string) { 77 | return request<{ code: number, message: string }>({ 78 | url: `/api/v1/files/${fileId}`, 79 | method: "delete" 80 | }) 81 | } 82 | 83 | /** 84 | * 批量删除文件 85 | * @param fileIds 文件ID数组 86 | */ 87 | export function batchDeleteFilesApi(fileIds: string[]) { 88 | return request<{ code: number, message: string }>({ 89 | url: "/api/v1/files/batch", 90 | method: "delete", 91 | data: { ids: fileIds } 92 | }) 93 | } 94 | 95 | /** 96 | * 上传文件 97 | */ 98 | export function uploadFileApi(formData: FormData) { 99 | return request<{ 100 | code: number 101 | data: Array<{ 102 | name: string 103 | size: number 104 | type: string 105 | status: string 106 | }> 107 | message: string 108 | }>({ 109 | url: "/api/v1/files/upload", 110 | method: "post", 111 | data: formData, 112 | headers: { 113 | "Content-Type": "multipart/form-data" 114 | } 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /web/src/common/apis/files/type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 文件数据类型 3 | */ 4 | export interface FileData { 5 | /** 文件ID */ 6 | id: string 7 | /** 文件名称 */ 8 | name: string 9 | /** 文件大小(字节) */ 10 | size: number 11 | /** 文件类型 */ 12 | type: string 13 | /** 知识库ID */ 14 | kb_id: string 15 | /** 存储位置 */ 16 | location: string 17 | /** 创建时间 */ 18 | create_time?: number 19 | /** 更新时间 */ 20 | update_time?: number 21 | } 22 | 23 | /** 24 | * 文件列表结果 25 | */ 26 | export interface FileListResult { 27 | /** 文件列表 */ 28 | list: FileData[] 29 | /** 总条数 */ 30 | total: number 31 | } 32 | 33 | /** 34 | * 分页查询参数 35 | */ 36 | export interface PageQuery { 37 | /** 当前页码 */ 38 | currentPage: number 39 | /** 每页条数 */ 40 | size: number 41 | } 42 | 43 | /** 44 | * 分页结果 45 | */ 46 | export interface PageResult { 47 | /** 数据列表 */ 48 | list: T[] 49 | /** 总条数 */ 50 | total: number 51 | } 52 | 53 | /** 54 | * 通用响应结构 55 | */ 56 | export interface ApiResponse { 57 | /** 状态码 */ 58 | code: number 59 | /** 响应数据 */ 60 | data: T 61 | /** 响应消息 */ 62 | message: string 63 | } 64 | -------------------------------------------------------------------------------- /web/src/common/apis/kbs/knowledgebase.ts: -------------------------------------------------------------------------------- 1 | import { request } from "@/http/axios" 2 | 3 | // 获取知识库列表 4 | export function getKnowledgeBaseListApi(params: { 5 | currentPage: number 6 | size: number 7 | name?: string 8 | }) { 9 | return request({ 10 | url: "/api/v1/knowledgebases", 11 | method: "get", 12 | params 13 | }) 14 | } 15 | 16 | // 获取知识库详情 17 | export function getKnowledgeBaseDetailApi(id: string) { 18 | return request({ 19 | url: `/api/v1/knowledgebases/${id}`, 20 | method: "get" 21 | }) 22 | } 23 | 24 | // 创建知识库 25 | export function createKnowledgeBaseApi(data: { 26 | name: string 27 | description?: string 28 | language?: string 29 | permission?: string 30 | }) { 31 | return request({ 32 | url: "/api/v1/knowledgebases", 33 | method: "post", 34 | data 35 | }) 36 | } 37 | 38 | // 更新知识库 39 | export function updateKnowledgeBaseApi(id: string, data: { 40 | name?: string 41 | description?: string 42 | language?: string 43 | permission?: string 44 | }) { 45 | return request({ 46 | url: `/api/v1/knowledgebases/${id}`, 47 | method: "put", 48 | data 49 | }) 50 | } 51 | 52 | // 删除知识库 53 | export function deleteKnowledgeBaseApi(id: string) { 54 | return request({ 55 | url: `/api/v1/knowledgebases/${id}`, 56 | method: "delete" 57 | }) 58 | } 59 | 60 | // 批量删除知识库 61 | export function batchDeleteKnowledgeBaseApi(ids: string[]) { 62 | return request({ 63 | url: "/api/v1/knowledgebases/batch", 64 | method: "delete", 65 | data: { ids } 66 | }) 67 | } 68 | 69 | // 添加文档到知识库 70 | export function addDocumentToKnowledgeBaseApi(data: { 71 | kb_id: string 72 | file_ids: string[] 73 | }) { 74 | return request({ 75 | url: `/api/v1/knowledgebases/${data.kb_id}/documents`, 76 | method: "post", 77 | data: { file_ids: data.file_ids } 78 | }) 79 | } 80 | 81 | // 获取系统 Embedding 配置 82 | export function getSystemEmbeddingConfigApi() { 83 | return request({ 84 | url: "/api/v1/knowledgebases/system_embedding_config", // 确认 API 路径前缀是否正确 85 | method: "get" 86 | }) 87 | } 88 | 89 | // 设置系统 Embedding 配置 90 | export function setSystemEmbeddingConfigApi(data: { 91 | llm_name: string 92 | api_base: string 93 | api_key?: string 94 | }) { 95 | return request({ 96 | url: "/api/v1/knowledgebases/system_embedding_config", // 确认 API 路径前缀是否正确 97 | method: "post", 98 | data 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /web/src/common/apis/kbs/type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 文件数据类型 3 | */ 4 | export interface FileData { 5 | /** 文件ID */ 6 | id: string 7 | /** 文件名称 */ 8 | name: string 9 | /** 文件大小(字节) */ 10 | size: number 11 | /** 文件类型 */ 12 | type: string 13 | /** 知识库ID */ 14 | kb_id: string 15 | /** 存储位置 */ 16 | location: string 17 | /** 创建时间 */ 18 | create_time?: number 19 | /** 更新时间 */ 20 | update_time?: number 21 | } 22 | 23 | /** 24 | * 文件列表结果 25 | */ 26 | export interface FileListResult { 27 | /** 文件列表 */ 28 | list: FileData[] 29 | /** 总条数 */ 30 | total: number 31 | } 32 | 33 | /** 34 | * 分页查询参数 35 | */ 36 | export interface PageQuery { 37 | /** 当前页码 */ 38 | currentPage: number 39 | /** 每页条数 */ 40 | size: number 41 | } 42 | 43 | /** 44 | * 分页结果 45 | */ 46 | export interface PageResult { 47 | /** 数据列表 */ 48 | list: T[] 49 | /** 总条数 */ 50 | total: number 51 | } 52 | 53 | /** 54 | * 通用响应结构 55 | */ 56 | export interface ApiResponse { 57 | /** 状态码 */ 58 | code: number 59 | /** 响应数据 */ 60 | data: T 61 | /** 响应消息 */ 62 | message: string 63 | } 64 | -------------------------------------------------------------------------------- /web/src/common/apis/tables/index.ts: -------------------------------------------------------------------------------- 1 | import type * as Tables from "./type" 2 | import { request } from "@/http/axios" 3 | 4 | /** 增 */ 5 | export function createTableDataApi(data: Tables.CreateOrUpdateTableRequestData) { 6 | return request({ 7 | url: "api/v1/users", 8 | method: "post", 9 | data 10 | }) 11 | } 12 | 13 | /** 删 */ 14 | export function deleteTableDataApi(id: number) { 15 | return request({ 16 | url: `api/v1/users/${id}`, 17 | method: "delete" 18 | }) 19 | } 20 | 21 | /** 改 */ 22 | export function updateTableDataApi(data: Tables.CreateOrUpdateTableRequestData) { 23 | return request({ 24 | url: `api/v1/users/${data.id}`, 25 | method: "put", 26 | data 27 | }) 28 | } 29 | 30 | /** 查 */ 31 | export function getTableDataApi(params: Tables.TableRequestData) { 32 | return request({ 33 | url: "api/v1/users", 34 | method: "get", 35 | params 36 | }) 37 | } 38 | 39 | /** 40 | * 重置用户密码 41 | * @param userId 用户ID 42 | * @param password 新密码 43 | * @returns BaseResponse 44 | */ 45 | export function resetPasswordApi(userId: number, password: string) { 46 | return request({ 47 | url: `api/v1/users/${userId}/reset-password`, 48 | method: "put", 49 | data: { password } // 发送新密码 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /web/src/common/apis/tables/type.ts: -------------------------------------------------------------------------------- 1 | export interface CreateOrUpdateTableRequestData { 2 | id?: number 3 | username: string 4 | email?: string 5 | password?: string 6 | } 7 | 8 | export interface TableRequestData { 9 | /** 当前页码 */ 10 | currentPage: number 11 | /** 查询条数 */ 12 | size: number 13 | /** 查询参数:用户名 */ 14 | username?: string 15 | /** 查询参数:邮箱 */ 16 | email?: string 17 | } 18 | 19 | export interface TableData { 20 | id: number 21 | username: string 22 | email: string 23 | createTime: string 24 | updateTime: string 25 | } 26 | 27 | export type TableResponseData = ApiResponseData<{ 28 | list: TableData[] 29 | total: number 30 | }> 31 | -------------------------------------------------------------------------------- /web/src/common/apis/teams/index.ts: -------------------------------------------------------------------------------- 1 | import type * as Tables from "./type" 2 | import { request } from "@/http/axios" 3 | 4 | // 查询团队整体数据 5 | export function getTableDataApi(params: Tables.TableRequestData) { 6 | return request({ 7 | url: "api/v1/teams", 8 | method: "get", 9 | params 10 | }) 11 | } 12 | 13 | // 获取团队成员列表 14 | export function getTeamMembersApi(teamId: number) { 15 | return request({ 16 | url: `api/v1/teams/${teamId}/members`, 17 | method: "get" 18 | }) 19 | } 20 | 21 | // 添加团队成员 22 | export function addTeamMemberApi(data: { teamId: number, userId: number, role: string }) { 23 | return request({ 24 | url: `api/v1/teams/${data.teamId}/members`, 25 | method: "post", 26 | data 27 | }) 28 | } 29 | 30 | // 移除团队成员 31 | export function removeTeamMemberApi(data: { teamId: number, memberId: number }) { 32 | return request({ 33 | url: `api/v1/teams/${data.teamId}/members/${data.memberId}`, 34 | method: "delete" 35 | }) 36 | } 37 | 38 | /** 39 | * @description 获取用户列表 40 | * @param params 查询参数,例如 { size: number, currentPage: number, username: string } 41 | */ 42 | export function getUsersApi(params?: object) { 43 | return request({ 44 | url: "api/v1/users", 45 | method: "get", 46 | params 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /web/src/common/apis/teams/type.ts: -------------------------------------------------------------------------------- 1 | export interface CreateOrUpdateTableRequestData { 2 | id?: number 3 | username: string 4 | email?: string 5 | password?: string 6 | } 7 | 8 | export interface TableRequestData { 9 | /** 当前页码 */ 10 | currentPage: number 11 | /** 查询条数 */ 12 | size: number 13 | /** 查询参数:用户名 */ 14 | username?: string 15 | /** 查询参数:邮箱 */ 16 | email?: string 17 | /** 查询参数:团队名称 */ 18 | name?: string // 添加 name 属性 19 | } 20 | 21 | export interface TableData { 22 | id: number 23 | username: string 24 | email: string 25 | createTime: string 26 | updateTime: string 27 | } 28 | 29 | export type TableResponseData = ApiResponseData<{ 30 | list: TableData[] 31 | total: number 32 | }> 33 | -------------------------------------------------------------------------------- /web/src/common/apis/users/index.ts: -------------------------------------------------------------------------------- 1 | import type * as Users from "./type" 2 | import { request } from "@/http/axios" 3 | 4 | /** 获取当前登录用户详情 */ 5 | export function getCurrentUserApi() { 6 | return request({ 7 | url: "api/v1/users/me", 8 | method: "get" 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /web/src/common/apis/users/type.ts: -------------------------------------------------------------------------------- 1 | export type CurrentUserResponseData = ApiResponseData<{ username: string, roles: string[] }> 2 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/dashboard.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/fullscreen-exit.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/kb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 41 | 42 | 43 | 53 | 54 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/keyboard-down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/keyboard-enter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/keyboard-esc.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/keyboard-up.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/preserve-color/README.md: -------------------------------------------------------------------------------- 1 | ## 目录说明 2 | 3 | - `common/assets/icons/preserve-color` 目录下存放带颜色的 svg icon 4 | 5 | - `common/assets/icons` 目录存放的 svg icon 会被插件重写 `fill` 和 `stroke` 属性,使得图片自带的颜色丢失,从而继承父元素的颜色 6 | 7 | ## 使用说明 8 | 9 | `common/assets/icons/preserve-color` 目录下需要添加 `preserve-color/` 前缀,像这样: `` 10 | 11 | `common/assets/icons` 目录下则不需要,像这样: `` 12 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/team-management.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/user-config.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /web/src/common/assets/icons/user-management.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/src/common/assets/images/layouts/logo-text-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/src/common/assets/images/layouts/logo-text-1.png -------------------------------------------------------------------------------- /web/src/common/assets/images/layouts/logo-text-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/src/common/assets/images/layouts/logo-text-2.png -------------------------------------------------------------------------------- /web/src/common/assets/images/layouts/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/src/common/assets/images/layouts/logo.png -------------------------------------------------------------------------------- /web/src/common/assets/styles/element-plus.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @description dark-blue 主题模式下的 Element Plus CSS 变量 3 | * @description 在此查阅所有可自定义的变量:https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss 4 | * @description 也可以打开浏览器控制台选择元素,查看要覆盖的变量名 5 | */ 6 | 7 | /* 基础颜色 */ 8 | html.dark-blue { 9 | /* color-primary */ 10 | --el-color-primary: #00bb99; 11 | --el-color-primary-light-3: #00bb99b3; 12 | --el-color-primary-light-5: #00bb9980; 13 | --el-color-primary-light-7: #00bb994d; 14 | --el-color-primary-light-8: #00bb9933; 15 | --el-color-primary-light-9: #00bb991a; 16 | --el-color-primary-dark-2: #00bb99; 17 | /* color-success */ 18 | --el-color-success: #67c23a; 19 | --el-color-success-light-3: #67c23ab3; 20 | --el-color-success-light-5: #67c23a80; 21 | --el-color-success-light-7: #67c23a4d; 22 | --el-color-success-light-8: #67c23a33; 23 | --el-color-success-light-9: #67c23a1a; 24 | --el-color-success-dark-2: #67c23a; 25 | /* color-warning */ 26 | --el-color-warning: #e6a23c; 27 | --el-color-warning-light-3: #e6a23cb3; 28 | --el-color-warning-light-5: #e6a23c80; 29 | --el-color-warning-light-7: #e6a23c4d; 30 | --el-color-warning-light-8: #e6a23c33; 31 | --el-color-warning-light-9: #e6a23c1a; 32 | --el-color-warning-dark-2: #e6a23c; 33 | /* color-danger */ 34 | --el-color-danger: #f56c6c; 35 | --el-color-danger-light-3: #f56c6cb3; 36 | --el-color-danger-light-5: #f56c6c80; 37 | --el-color-danger-light-7: #f56c6c4d; 38 | --el-color-danger-light-8: #f56c6c33; 39 | --el-color-danger-light-9: #f56c6c1a; 40 | --el-color-danger-dark-2: #f56c6c; 41 | /* color-error */ 42 | --el-color-error: #f56c6c; 43 | --el-color-error-light-3: #f56c6cb3; 44 | --el-color-error-light-5: #f56c6c80; 45 | --el-color-error-light-7: #f56c6c4d; 46 | --el-color-error-light-8: #f56c6c33; 47 | --el-color-error-light-9: #f56c6c1a; 48 | --el-color-error-dark-2: #f56c6c; 49 | /* color-info */ 50 | --el-color-info: #909399; 51 | --el-color-info-light-3: #909399b3; 52 | --el-color-info-light-5: #90939980; 53 | --el-color-info-light-7: #9093994d; 54 | --el-color-info-light-8: #90939933; 55 | --el-color-info-light-9: #9093991a; 56 | --el-color-info-dark-2: #909399; 57 | /* text-color */ 58 | --el-text-color-primary: #e5eaf3; 59 | --el-text-color-regular: #cfd3dc; 60 | --el-text-color-secondary: #a3a6ad; 61 | --el-text-color-placeholder: #8d9095; 62 | --el-text-color-disabled: #6c6e72; 63 | /* border-color */ 64 | --el-border-color-darker: #003380; 65 | --el-border-color-dark: #003380; 66 | --el-border-color: #003380; 67 | --el-border-color-light: #003380; 68 | --el-border-color-lighter: #003380; 69 | --el-border-color-extra-light: #003380; 70 | /* fill-color */ 71 | --el-fill-color-darker: #002b6b; 72 | --el-fill-color-dark: #002b6b; 73 | --el-fill-color: #002b6b; 74 | --el-fill-color-light: #002359; 75 | --el-fill-color-lighter: #002359; 76 | --el-fill-color-blank: #001b44; 77 | --el-fill-color-extra-light: #001b44; 78 | /* bg-color */ 79 | --el-bg-color-page: #001535; 80 | --el-bg-color: #001b44; 81 | --el-bg-color-overlay: #002359; 82 | /* mask-color */ 83 | --el-mask-color: rgba(0, 0, 0, 0.5); 84 | --el-mask-color-extra-light: rgba(0, 0, 0, 0.3); 85 | } 86 | 87 | /* button */ 88 | html.dark-blue .el-button { 89 | --el-button-disabled-text-color: rgba(255, 255, 255, 0.5); 90 | } 91 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/element-plus.scss: -------------------------------------------------------------------------------- 1 | // 自定义 Element Plus 样式 2 | 3 | // 卡片 4 | .el-card { 5 | background-color: var(--el-bg-color) !important; 6 | } 7 | 8 | // 分页 9 | .el-pagination { 10 | // 参考 Bootstrap 的响应式设计 WIDTH = 768 11 | @media screen and (max-width: 768px) { 12 | .el-pagination__total, 13 | .el-pagination__sizes, 14 | .el-pagination__jump, 15 | .btn-prev, 16 | .btn-next { 17 | display: none; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | // 全局 CSS 变量 2 | @import "./variables.css"; 3 | // Transition 4 | @import "./transition.scss"; 5 | // Element Plus 6 | @import "./element-plus.css"; 7 | @import "./element-plus.scss"; 8 | // Vxe Table 9 | @import "./vxe-table.css"; 10 | @import "./vxe-table.scss"; 11 | // 注册多主题 12 | @import "./theme/register.scss"; 13 | // Mixins 14 | @import "./mixins.scss"; 15 | // View Transition 16 | @import "./view-transition.scss"; 17 | 18 | // 业务页面几乎都应该在根元素上挂载 class="app-container",以保持页面美观 19 | .app-container { 20 | padding: 20px; 21 | } 22 | 23 | html { 24 | height: 100%; 25 | // 灰色模式 26 | &.grey-mode { 27 | filter: grayscale(1); 28 | } 29 | // 色弱模式 30 | &.color-weakness { 31 | filter: invert(0.8); 32 | } 33 | } 34 | 35 | body { 36 | height: 100%; 37 | color: var(--v3-body-text-color); 38 | background-color: var(--v3-body-bg-color); 39 | -moz-osx-font-smoothing: grayscale; 40 | -webkit-font-smoothing: antialiased; 41 | font-family: 42 | Inter, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, 43 | sans-serif; 44 | @extend %scrollbar; 45 | } 46 | 47 | #app { 48 | height: 100%; 49 | } 50 | 51 | *, 52 | *::before, 53 | *::after { 54 | box-sizing: border-box; 55 | } 56 | 57 | a, 58 | a:focus, 59 | a:hover { 60 | color: inherit; 61 | outline: none; 62 | text-decoration: none; 63 | } 64 | 65 | div:focus { 66 | outline: none; 67 | } 68 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | // 清除浮动 2 | %clearfix { 3 | &::after { 4 | content: ""; 5 | display: table; 6 | clear: both; 7 | } 8 | } 9 | 10 | // 美化原生滚动条 11 | %scrollbar { 12 | // 整个滚动条 13 | &::-webkit-scrollbar { 14 | width: 8px; 15 | height: 8px; 16 | } 17 | // 滚动条上的滚动滑块 18 | &::-webkit-scrollbar-thumb { 19 | border-radius: 4px; 20 | background-color: #90939955; 21 | } 22 | &::-webkit-scrollbar-thumb:hover { 23 | background-color: #90939977; 24 | } 25 | &::-webkit-scrollbar-thumb:active { 26 | background-color: #90939999; 27 | } 28 | // 当同时有垂直滚动条和水平滚动条时交汇的部分 29 | &::-webkit-scrollbar-corner { 30 | background-color: transparent; 31 | } 32 | } 33 | 34 | // 文本溢出时显示省略号 35 | %ellipsis { 36 | // 隐藏溢出的文本 37 | overflow: hidden; 38 | // 防止文本换行 39 | white-space: nowrap; 40 | // 文本内容溢出容器时,文本末尾显示省略号 41 | text-overflow: ellipsis; 42 | } 43 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/core/element-plus.scss: -------------------------------------------------------------------------------- 1 | // Element Plus 相关 2 | 3 | // 侧边栏的 item 的 popper 4 | .el-popper { 5 | .el-menu { 6 | background-color: var(--el-bg-color); 7 | .el-menu-item { 8 | background-color: var(--el-bg-color); 9 | &.is-active, 10 | &:hover { 11 | background-color: var(--el-bg-color-overlay); 12 | color: #ffffff; 13 | } 14 | } 15 | .el-sub-menu__title { 16 | background-color: var(--el-bg-color); 17 | } 18 | .el-sub-menu { 19 | &.is-active { 20 | > .el-sub-menu__title { 21 | color: #ffffff; 22 | } 23 | } 24 | } 25 | } 26 | .el-menu--horizontal { 27 | border: none; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/core/index.scss: -------------------------------------------------------------------------------- 1 | .#{$theme-name} { 2 | @import "./layouts.scss"; 3 | @import "./element-plus.scss"; 4 | } 5 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/core/layouts.scss: -------------------------------------------------------------------------------- 1 | // Layout 相关 2 | 3 | .app-wrapper { 4 | // 侧边栏 5 | .sidebar-container { 6 | background-color: var(--el-bg-color); 7 | .el-menu { 8 | background-color: var(--el-bg-color); 9 | .el-menu-item { 10 | background-color: var(--el-bg-color); 11 | &.is-active, 12 | &:hover { 13 | background-color: var(--el-bg-color-overlay); 14 | color: #ffffff; 15 | } 16 | } 17 | } 18 | .el-sub-menu__title { 19 | background-color: var(--el-bg-color); 20 | } 21 | .el-sub-menu { 22 | &.is-active { 23 | > .el-sub-menu__title { 24 | color: #ffffff; 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | // 右侧设置面板 32 | .handle-button { 33 | background-color: lighten($theme-bg-color, 20%) !important; 34 | } 35 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/dark-blue/index.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.scss"; 2 | @import "../core/index.scss"; 3 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/dark-blue/variables.scss: -------------------------------------------------------------------------------- 1 | // dark-blue 主题下的变量 2 | 3 | // 主题名称 4 | $theme-name: "dark-blue"; 5 | // 主题背景颜色 6 | $theme-bg-color: #001b44; 7 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/dark/index.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.scss"; 2 | @import "../core/index.scss"; 3 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/dark/variables.scss: -------------------------------------------------------------------------------- 1 | // dark 主题下的变量 2 | 3 | // 主题名称 4 | $theme-name: "dark"; 5 | // 主题背景颜色 6 | $theme-bg-color: #141414; 7 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/theme/register.scss: -------------------------------------------------------------------------------- 1 | // 注册多主题 2 | @import "./dark/index.scss"; 3 | @import "./dark-blue/index.scss"; 4 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // https://cn.vuejs.org/guide/built-ins/transition 2 | 3 | // fade-transform 4 | .fade-transform-leave-active, 5 | .fade-transform-enter-active { 6 | transition: all 0.5s; 7 | } 8 | .fade-transform-enter { 9 | opacity: 0; 10 | transform: translateX(-30px); 11 | } 12 | .fade-transform-leave-to { 13 | opacity: 0; 14 | transform: translateX(30px); 15 | } 16 | 17 | // layout-logo-fade 18 | .layout-logo-fade-enter-active, 19 | .layout-logo-fade-leave-active { 20 | transition: opacity 1.5s; 21 | } 22 | .layout-logo-fade-enter-from, 23 | .layout-logo-fade-leave-to { 24 | opacity: 0; 25 | } 26 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/variables.css: -------------------------------------------------------------------------------- 1 | /* 全局 CSS 变量,这种变量不仅可以在 CSS 和 SCSS 中使用,还可以导入到 JS 中使用 */ 2 | 3 | :root { 4 | /* Body */ 5 | --v3-body-text-color: var(--el-text-color-primary); 6 | --v3-body-bg-color: var(--el-bg-color-page); 7 | /* Header 区域 = NavigationBar 组件 + TagsView 组件 */ 8 | --v3-header-height: calc( 9 | var(--v3-navigationbar-height) + var(--v3-tagsview-height) + var(--v3-header-border-bottom-width) 10 | ); 11 | --v3-header-bg-color: var(--el-bg-color); 12 | --v3-header-box-shadow: var(--el-box-shadow-lighter); 13 | --v3-header-border-bottom-width: 1px; 14 | --v3-header-border-bottom: var(--v3-header-border-bottom-width) solid var(--el-fill-color); 15 | /* NavigationBar 组件 */ 16 | --v3-navigationbar-height: 50px; 17 | --v3-navigationbar-text-color: var(--el-text-color-regular); 18 | /* Sidebar 组件(左侧模式全部生效、顶部模式全部不生效、混合模式非颜色部分生效) */ 19 | --v3-sidebar-width: 220px; 20 | --v3-sidebar-hide-width: 58px; 21 | --v3-sidebar-border-right: 1px solid var(--el-fill-color); 22 | --v3-sidebar-menu-item-height: 60px; 23 | --v3-sidebar-menu-tip-line-bg-color: var(--el-color-primary); 24 | --v3-sidebar-menu-bg-color: #001428; 25 | --v3-sidebar-menu-hover-bg-color: #409eff10; 26 | --v3-sidebar-menu-text-color: #cfd3dc; 27 | --v3-sidebar-menu-active-text-color: #ffffff; 28 | /* TagsView 组件 */ 29 | --v3-tagsview-height: 34px; 30 | --v3-tagsview-text-color: var(--el-text-color-regular); 31 | --v3-tagsview-tag-active-text-color: #ffffff; 32 | --v3-tagsview-tag-bg-color: var(--el-bg-color); 33 | --v3-tagsview-tag-active-bg-color: var(--el-color-primary); 34 | --v3-tagsview-tag-border-radius: 2px; 35 | --v3-tagsview-tag-border-color: var(--el-border-color-lighter); 36 | --v3-tagsview-tag-active-border-color: var(--el-color-primary); 37 | --v3-tagsview-tag-icon-hover-bg-color: #00000030; 38 | --v3-tagsview-tag-icon-hover-color: #ffffff; 39 | --v3-tagsview-contextmenu-text-color: var(--el-text-color-regular); 40 | --v3-tagsview-contextmenu-hover-text-color: var(--el-text-color-primary); 41 | --v3-tagsview-contextmenu-bg-color: var(--el-bg-color-overlay); 42 | --v3-tagsview-contextmenu-hover-bg-color: var(--el-fill-color); 43 | --v3-tagsview-contextmenu-box-shadow: var(--el-box-shadow); 44 | /* Hamburger 组件 */ 45 | --v3-hamburger-text-color: var(--el-text-color-primary); 46 | /* RightPanel 组件 */ 47 | --v3-rightpanel-button-bg-color: #001428; 48 | } 49 | 50 | /* 内容区放大时,将不需要的组件隐藏 */ 51 | body.content-large { 52 | /* Header 区域 = TagsView 组件 */ 53 | --v3-header-height: var(--v3-tagsview-height); 54 | /* NavigationBar 组件 */ 55 | --v3-navigationbar-height: 0px; 56 | /* Sidebar 组件 */ 57 | --v3-sidebar-width: 0px; 58 | --v3-sidebar-hide-width: 0px; 59 | } 60 | 61 | /* 内容区全屏时,将不需要的组件隐藏 */ 62 | body.content-full { 63 | /* Header 区域 */ 64 | --v3-header-height: 0px; 65 | /* NavigationBar 组件 */ 66 | --v3-navigationbar-height: 0px; 67 | /* Sidebar 组件 */ 68 | --v3-sidebar-width: 0px; 69 | --v3-sidebar-hide-width: 0px; 70 | /* TagsView 组件 */ 71 | --v3-tagsview-height: 0px; 72 | } 73 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/view-transition.scss: -------------------------------------------------------------------------------- 1 | // 控制切换主题时的动画效果(只在较新的浏览器上生效,例如 Chrome 111+) 2 | 3 | ::view-transition-old(root) { 4 | animation: none; 5 | mix-blend-mode: normal; 6 | } 7 | 8 | ::view-transition-new(root) { 9 | animation: 0.5s ease-in clip-animation; 10 | mix-blend-mode: normal; 11 | } 12 | 13 | @keyframes clip-animation { 14 | from { 15 | clip-path: circle(0px at var(--v3-theme-x) var(--v3-theme-y)); 16 | } 17 | to { 18 | clip-path: circle(var(--v3-theme-r) at var(--v3-theme-x) var(--v3-theme-y)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/src/common/assets/styles/vxe-table.scss: -------------------------------------------------------------------------------- 1 | // 自定义 Vxe Table 样式 2 | 3 | .vxe-grid { 4 | // 表单 5 | &--form-wrapper { 6 | .vxe-form { 7 | padding: 10px 20px; 8 | margin-bottom: 20px; 9 | } 10 | } 11 | 12 | // 工具栏 13 | &--toolbar-wrapper { 14 | .vxe-toolbar { 15 | padding: 20px; 16 | } 17 | } 18 | 19 | // 分页 20 | &--pager-wrapper { 21 | .vxe-pager { 22 | height: 70px; 23 | padding: 0 20px; 24 | &--wrapper { 25 | // 参考 Bootstrap 的响应式设计 WIDTH = 768 26 | @media screen and (max-width: 768px) { 27 | .vxe-pager--total, 28 | .vxe-pager--sizes, 29 | .vxe-pager--jump, 30 | .vxe-pager--jump-prev, 31 | .vxe-pager--jump-next { 32 | display: none; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/src/common/components/Notify/List.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | 36 | 61 | -------------------------------------------------------------------------------- /web/src/common/components/Notify/data.ts: -------------------------------------------------------------------------------- 1 | import type { NotifyItem } from "./type" 2 | 3 | export const notifyData: NotifyItem[] = [ 4 | { 5 | avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png", 6 | title: "V3 Admin Vite 上线啦", 7 | datetime: "两年前", 8 | description: "一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术" 9 | }, 10 | { 11 | avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png", 12 | title: "V3 Admin 上线啦", 13 | datetime: "三年前", 14 | description: "一个中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus 和 Pinia" 15 | } 16 | ] 17 | 18 | export const messageData: NotifyItem[] = [ 19 | { 20 | avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png", 21 | title: "来自楚门的世界", 22 | description: "如果再也不能见到你,祝你早安、午安和晚安", 23 | datetime: "1998-06-05" 24 | }, 25 | { 26 | avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png", 27 | title: "来自大话西游", 28 | description: "如果非要在这份爱上加上一个期限,我希望是一万年", 29 | datetime: "1995-02-04" 30 | }, 31 | { 32 | avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png", 33 | title: "来自龙猫", 34 | description: "心存善意,定能途遇天使", 35 | datetime: "1988-04-16" 36 | } 37 | ] 38 | 39 | export const todoData: NotifyItem[] = [ 40 | { 41 | title: "任务名称", 42 | description: "这家伙很懒,什么都没留下", 43 | extra: "未开始", 44 | status: "info" 45 | }, 46 | { 47 | title: "任务名称", 48 | description: "这家伙很懒,什么都没留下", 49 | extra: "进行中", 50 | status: "primary" 51 | }, 52 | { 53 | title: "任务名称", 54 | description: "这家伙很懒,什么都没留下", 55 | extra: "已超时", 56 | status: "danger" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /web/src/common/components/Notify/index.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 87 | 88 | 95 | -------------------------------------------------------------------------------- /web/src/common/components/Notify/type.ts: -------------------------------------------------------------------------------- 1 | export interface NotifyItem { 2 | avatar?: string 3 | title: string 4 | datetime?: string 5 | description?: string 6 | status?: "primary" | "success" | "info" | "warning" | "danger" 7 | extra?: string 8 | } 9 | -------------------------------------------------------------------------------- /web/src/common/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 103 | 104 | 112 | -------------------------------------------------------------------------------- /web/src/common/components/SearchMenu/Footer.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | 34 | 55 | -------------------------------------------------------------------------------- /web/src/common/components/SearchMenu/Result.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 88 | 89 | 116 | -------------------------------------------------------------------------------- /web/src/common/components/SearchMenu/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | 30 | -------------------------------------------------------------------------------- /web/src/common/components/ThemeSwitch/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | -------------------------------------------------------------------------------- /web/src/common/composables/useDevice.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore } from "@/pinia/stores/app" 2 | import { DeviceEnum } from "@@/constants/app-key" 3 | 4 | const appStore = useAppStore() 5 | 6 | const isMobile = computed(() => appStore.device === DeviceEnum.Mobile) 7 | const isDesktop = computed(() => appStore.device === DeviceEnum.Desktop) 8 | 9 | /** 设备类型 Composable */ 10 | export function useDevice() { 11 | return { isMobile, isDesktop } 12 | } 13 | -------------------------------------------------------------------------------- /web/src/common/composables/useFetchSelect.ts: -------------------------------------------------------------------------------- 1 | type OptionValue = string | number 2 | 3 | /** Select 需要的数据格式 */ 4 | interface SelectOption { 5 | value: OptionValue 6 | label: string 7 | disabled?: boolean 8 | } 9 | 10 | /** 接口响应格式 */ 11 | type ApiData = ApiResponseData 12 | 13 | /** 入参格式,暂时只需要传递 api 函数即可 */ 14 | interface FetchSelectProps { 15 | api: () => Promise 16 | } 17 | 18 | /** 下拉选择器 Composable */ 19 | export function useFetchSelect(props: FetchSelectProps) { 20 | const { api } = props 21 | 22 | const loading = ref(false) 23 | const options = ref([]) 24 | const value = ref("") 25 | 26 | // 调用接口获取数据 27 | const loadData = () => { 28 | loading.value = true 29 | options.value = [] 30 | api().then((res) => { 31 | options.value = res.data 32 | }).finally(() => { 33 | loading.value = false 34 | }) 35 | } 36 | 37 | onMounted(() => { 38 | loadData() 39 | }) 40 | 41 | return { loading, options, value } 42 | } 43 | -------------------------------------------------------------------------------- /web/src/common/composables/useFullscreenLoading.ts: -------------------------------------------------------------------------------- 1 | import type { LoadingOptions } from "element-plus" 2 | 3 | interface UseFullscreenLoading { 4 | ) => ReturnType>( 5 | fn: T, 6 | options?: LoadingOptions 7 | ): (...args: Parameters) => Promise> 8 | } 9 | 10 | interface LoadingInstance { 11 | close: () => void 12 | } 13 | 14 | const DEFAULT_OPTIONS = { 15 | lock: true, 16 | text: "加载中..." 17 | } 18 | 19 | /** 20 | * @name 全屏加载 Composable 21 | * @description 传入一个函数 fn,在它执行周期内,加上「全屏」Loading 22 | * @param fn 要执行的函数 23 | * @param options LoadingOptions 24 | * @returns 返回一个新的函数,该函数返回一个 Promise 25 | */ 26 | export const useFullscreenLoading: UseFullscreenLoading = (fn, options = {}) => { 27 | let loadingInstance: LoadingInstance 28 | return async (...args) => { 29 | try { 30 | loadingInstance = ElLoading.service({ ...DEFAULT_OPTIONS, ...options }) 31 | return await fn(...args) 32 | } finally { 33 | loadingInstance.close() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/src/common/composables/useGreyAndColorWeakness.ts: -------------------------------------------------------------------------------- 1 | import { useSettingsStore } from "@/pinia/stores/settings" 2 | 3 | const GREY_MODE = "grey-mode" 4 | const COLOR_WEAKNESS = "color-weakness" 5 | 6 | const classList = document.documentElement.classList 7 | 8 | /** 初始化 */ 9 | function initGreyAndColorWeakness() { 10 | const settingsStore = useSettingsStore() 11 | watchEffect(() => { 12 | classList.toggle(GREY_MODE, settingsStore.showGreyMode) 13 | classList.toggle(COLOR_WEAKNESS, settingsStore.showColorWeakness) 14 | }) 15 | } 16 | 17 | /** 灰色模式和色弱模式 Composable */ 18 | export function useGreyAndColorWeakness() { 19 | return { initGreyAndColorWeakness } 20 | } 21 | -------------------------------------------------------------------------------- /web/src/common/composables/useLayoutMode.ts: -------------------------------------------------------------------------------- 1 | import { useSettingsStore } from "@/pinia/stores/settings" 2 | import { LayoutModeEnum } from "@@/constants/app-key" 3 | 4 | const settingsStore = useSettingsStore() 5 | 6 | const isLeft = computed(() => settingsStore.layoutMode === LayoutModeEnum.Left) 7 | const isTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.Top) 8 | const isLeftTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.LeftTop) 9 | 10 | function setLayoutMode(mode: LayoutModeEnum) { 11 | settingsStore.layoutMode = mode 12 | } 13 | 14 | /** 布局模式 Composable */ 15 | export function useLayoutMode() { 16 | return { isLeft, isTop, isLeftTop, setLayoutMode } 17 | } 18 | -------------------------------------------------------------------------------- /web/src/common/composables/usePagination.ts: -------------------------------------------------------------------------------- 1 | interface PaginationData { 2 | total?: number 3 | currentPage?: number 4 | pageSizes?: number[] 5 | pageSize?: number 6 | layout?: string 7 | } 8 | 9 | /** 默认的分页参数 */ 10 | const DEFAULT_PAGINATION_DATA = { 11 | total: 0, 12 | currentPage: 1, 13 | pageSizes: [10, 20, 50], 14 | pageSize: 10, 15 | layout: "total, sizes, prev, pager, next, jumper" 16 | } 17 | 18 | /** 分页 Composable */ 19 | export function usePagination(initPaginationData: PaginationData = {}) { 20 | // 合并分页参数 21 | const paginationData = reactive({ ...DEFAULT_PAGINATION_DATA, ...initPaginationData }) 22 | // 改变当前页码 23 | const handleCurrentChange = (value: number) => { 24 | paginationData.currentPage = value 25 | } 26 | // 改变每页显示条数 27 | const handleSizeChange = (value: number) => { 28 | paginationData.pageSize = value 29 | } 30 | 31 | return { paginationData, handleCurrentChange, handleSizeChange } 32 | } 33 | -------------------------------------------------------------------------------- /web/src/common/composables/usePany.ts: -------------------------------------------------------------------------------- 1 | function initStarNotification() { 2 | // setTimeout(() => { 3 | // ElNotification({ 4 | // title: "为爱发电!", 5 | // type: "success", 6 | // message: h( 7 | // "div", 8 | // null, 9 | // [ 10 | // h("div", null, "所有源码均免费开源,如果对你有帮助,欢迎点个 Star 支持一下!"), 11 | // h("a", { style: "color: teal", target: "_blank", href: "https://github.com/un-pany/v3-admin-vite" }, "点击传送") 12 | // ] 13 | // ), 14 | // duration: 0, 15 | // position: "bottom-right" 16 | // }) 17 | // }, 0) 18 | } 19 | 20 | function initStoreNotification() { 21 | // setTimeout(() => { 22 | // ElNotification({ 23 | // title: "懒人服务?", 24 | // type: "warning", 25 | // message: h( 26 | // "div", 27 | // null, 28 | // [ 29 | // h("div", null, "不想自己动手,但想移除 TS 或其他模块?也有懒人套餐!"), 30 | // h("a", { style: "color: teal", target: "_blank", href: "https://github.com/un-pany/v3-admin-vite/issues/225" }, "点击查看") 31 | // ] 32 | // ), 33 | // duration: 0, 34 | // position: "bottom-right" 35 | // }) 36 | // }, 500) 37 | } 38 | 39 | export function usePany() { 40 | // return null 41 | return { initStarNotification, initStoreNotification } 42 | } 43 | -------------------------------------------------------------------------------- /web/src/common/composables/useRouteListener.ts: -------------------------------------------------------------------------------- 1 | import type { Handler } from "mitt" 2 | import type { RouteLocationNormalizedGeneric } from "vue-router" 3 | import mitt from "mitt" 4 | 5 | /** 回调函数的类型 */ 6 | type Callback = (route: RouteLocationNormalizedGeneric) => void 7 | 8 | const emitter = mitt() 9 | 10 | const key = Symbol("ROUTE_CHANGE") 11 | 12 | let latestRoute: RouteLocationNormalizedGeneric 13 | 14 | /** 设置最新的路由信息,触发路由变化事件 */ 15 | export function setRouteChange(to: RouteLocationNormalizedGeneric) { 16 | // 触发事件 17 | emitter.emit(key, to) 18 | // 缓存最新的路由信息 19 | latestRoute = to 20 | } 21 | 22 | /** 23 | * @name 订阅路由变化 Composable 24 | * @description 1. 单独用 watch 监听路由会浪费渲染性能 25 | * @description 2. 可优先选择使用该发布订阅模式去进行分发管理 26 | */ 27 | export function useRouteListener() { 28 | // 回调函数集合 29 | const callbackList: Callback[] = [] 30 | 31 | // 监听路由变化(可以选择立即执行) 32 | const listenerRouteChange = (callback: Callback, immediate = false) => { 33 | // 缓存回调函数 34 | callbackList.push(callback) 35 | // 监听事件 36 | emitter.on(key, callback as Handler) 37 | // 可以选择立即执行一次回调函数 38 | immediate && latestRoute && callback(latestRoute) 39 | } 40 | 41 | // 移除路由变化事件监听器 42 | const removeRouteListener = (callback: Callback) => { 43 | emitter.off(key, callback as Handler) 44 | } 45 | 46 | // 组件销毁前移除监听器 47 | onBeforeUnmount(() => { 48 | callbackList.forEach(removeRouteListener) 49 | }) 50 | 51 | return { listenerRouteChange, removeRouteListener } 52 | } 53 | -------------------------------------------------------------------------------- /web/src/common/composables/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { getActiveThemeName, setActiveThemeName } from "@@/utils/cache/local-storage" 2 | import { setCssVar } from "@@/utils/css" 3 | 4 | const DEFAULT_THEME_NAME = "normal" 5 | 6 | type DefaultThemeName = typeof DEFAULT_THEME_NAME 7 | 8 | /** 注册的主题名称, 其中 DefaultThemeName 是必填的 */ 9 | export type ThemeName = DefaultThemeName | "dark" | "dark-blue" 10 | 11 | interface ThemeList { 12 | title: string 13 | name: ThemeName 14 | } 15 | 16 | /** 主题列表 */ 17 | const themeList: ThemeList[] = [ 18 | { 19 | title: "默认", 20 | name: DEFAULT_THEME_NAME 21 | }, 22 | { 23 | title: "黑暗", 24 | name: "dark" 25 | }, 26 | { 27 | title: "深蓝", 28 | name: "dark-blue" 29 | } 30 | ] 31 | 32 | /** 正在应用的主题名称 */ 33 | const activeThemeName = ref(getActiveThemeName() || DEFAULT_THEME_NAME) 34 | 35 | /** 设置主题 */ 36 | function setTheme({ clientX, clientY }: MouseEvent, value: ThemeName) { 37 | const maxRadius = Math.hypot( 38 | Math.max(clientX, window.innerWidth - clientX), 39 | Math.max(clientY, window.innerHeight - clientY) 40 | ) 41 | setCssVar("--v3-theme-x", `${clientX}px`) 42 | setCssVar("--v3-theme-y", `${clientY}px`) 43 | setCssVar("--v3-theme-r", `${maxRadius}px`) 44 | const handler = () => { 45 | activeThemeName.value = value 46 | } 47 | document.startViewTransition ? document.startViewTransition(handler) : handler() 48 | } 49 | 50 | /** 在 html 根元素上挂载 class */ 51 | function addHtmlClass(value: ThemeName) { 52 | document.documentElement.classList.add(value) 53 | } 54 | 55 | /** 在 html 根元素上移除其他主题 class */ 56 | function removeHtmlClass(value: ThemeName) { 57 | const otherThemeNameList = themeList.map(item => item.name).filter(name => name !== value) 58 | document.documentElement.classList.remove(...otherThemeNameList) 59 | } 60 | 61 | /** 初始化 */ 62 | function initTheme() { 63 | // watchEffect 来收集副作用 64 | watchEffect(() => { 65 | const value = activeThemeName.value 66 | removeHtmlClass(value) 67 | addHtmlClass(value) 68 | setActiveThemeName(value) 69 | }) 70 | } 71 | 72 | /** 主题 Composable */ 73 | export function useTheme() { 74 | return { themeList, activeThemeName, initTheme, setTheme } 75 | } 76 | -------------------------------------------------------------------------------- /web/src/common/composables/useTitle.ts: -------------------------------------------------------------------------------- 1 | /** 项目标题 */ 2 | const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE ?? "V3 Admin Vite" 3 | 4 | /** 动态标题 */ 5 | const dynamicTitle = ref("") 6 | 7 | /** 设置标题 */ 8 | function setTitle(title?: string) { 9 | dynamicTitle.value = title ? `${VITE_APP_TITLE} | ${title}` : VITE_APP_TITLE 10 | } 11 | 12 | // 监听标题变化 13 | watch(dynamicTitle, (value, oldValue) => { 14 | if (document && value !== oldValue) { 15 | document.title = value 16 | } 17 | }) 18 | 19 | /** 标题 Composable */ 20 | export function useTitle() { 21 | return { setTitle } 22 | } 23 | -------------------------------------------------------------------------------- /web/src/common/constants/app-key.ts: -------------------------------------------------------------------------------- 1 | /** 设备类型 */ 2 | export enum DeviceEnum { 3 | Mobile, 4 | Desktop 5 | } 6 | 7 | /** 布局模式 */ 8 | export enum LayoutModeEnum { 9 | Left = "left", 10 | Top = "top", 11 | LeftTop = "left-top" 12 | } 13 | 14 | /** 侧边栏打开状态常量 */ 15 | export const SIDEBAR_OPENED = "opened" 16 | 17 | /** 侧边栏关闭状态常量 */ 18 | export const SIDEBAR_CLOSED = "closed" 19 | 20 | export type SidebarOpened = typeof SIDEBAR_OPENED 21 | 22 | export type SidebarClosed = typeof SIDEBAR_CLOSED 23 | -------------------------------------------------------------------------------- /web/src/common/constants/cache-key.ts: -------------------------------------------------------------------------------- 1 | const SYSTEM_NAME = "v3-admin-vite" 2 | 3 | /** 缓存数据时用到的 Key */ 4 | export class CacheKey { 5 | static readonly TOKEN = `${SYSTEM_NAME}-token-key` 6 | static readonly CONFIG_LAYOUT = `${SYSTEM_NAME}-config-layout-key` 7 | static readonly SIDEBAR_STATUS = `${SYSTEM_NAME}-sidebar-status-key` 8 | static readonly ACTIVE_THEME_NAME = `${SYSTEM_NAME}-active-theme-name-key` 9 | static readonly VISITED_VIEWS = `${SYSTEM_NAME}-visited-views-key` 10 | static readonly CACHED_VIEWS = `${SYSTEM_NAME}-cached-views-key` 11 | } 12 | -------------------------------------------------------------------------------- /web/src/common/utils/cache/cookies.ts: -------------------------------------------------------------------------------- 1 | // 统一处理 Cookie 2 | 3 | import { CacheKey } from "@@/constants/cache-key" 4 | import Cookies from "js-cookie" 5 | 6 | export function getToken() { 7 | return Cookies.get(CacheKey.TOKEN) 8 | } 9 | 10 | export function setToken(token: string) { 11 | Cookies.set(CacheKey.TOKEN, token) 12 | } 13 | 14 | export function removeToken() { 15 | Cookies.remove(CacheKey.TOKEN) 16 | } 17 | -------------------------------------------------------------------------------- /web/src/common/utils/cache/local-storage.ts: -------------------------------------------------------------------------------- 1 | // 统一处理 localStorage 2 | 3 | import type { LayoutsConfig } from "@/layouts/config" 4 | import type { TagView } from "@/pinia/stores/tags-view" 5 | import type { ThemeName } from "@@/composables/useTheme" 6 | import type { SidebarClosed, SidebarOpened } from "@@/constants/app-key" 7 | import { CacheKey } from "@@/constants/cache-key" 8 | 9 | // #region 系统布局配置 10 | export function getLayoutsConfig() { 11 | const json = localStorage.getItem(CacheKey.CONFIG_LAYOUT) 12 | return json ? (JSON.parse(json) as LayoutsConfig) : null 13 | } 14 | export function setLayoutsConfig(settings: LayoutsConfig) { 15 | localStorage.setItem(CacheKey.CONFIG_LAYOUT, JSON.stringify(settings)) 16 | } 17 | export function removeLayoutsConfig() { 18 | localStorage.removeItem(CacheKey.CONFIG_LAYOUT) 19 | } 20 | // #endregion 21 | 22 | // #region 侧边栏状态 23 | export function getSidebarStatus() { 24 | return localStorage.getItem(CacheKey.SIDEBAR_STATUS) 25 | } 26 | export function setSidebarStatus(sidebarStatus: SidebarOpened | SidebarClosed) { 27 | localStorage.setItem(CacheKey.SIDEBAR_STATUS, sidebarStatus) 28 | } 29 | // #endregion 30 | 31 | // #region 正在应用的主题名称 32 | export function getActiveThemeName() { 33 | return localStorage.getItem(CacheKey.ACTIVE_THEME_NAME) as ThemeName | null 34 | } 35 | export function setActiveThemeName(themeName: ThemeName) { 36 | localStorage.setItem(CacheKey.ACTIVE_THEME_NAME, themeName) 37 | } 38 | // #endregion 39 | 40 | // #region 标签栏 41 | export function getVisitedViews() { 42 | const json = localStorage.getItem(CacheKey.VISITED_VIEWS) 43 | return JSON.parse(json ?? "[]") as TagView[] 44 | } 45 | export function setVisitedViews(views: TagView[]) { 46 | views.forEach((view) => { 47 | // 删除不必要的属性,防止 JSON.stringify 处理到循环引用 48 | delete view.matched 49 | delete view.redirectedFrom 50 | }) 51 | localStorage.setItem(CacheKey.VISITED_VIEWS, JSON.stringify(views)) 52 | } 53 | export function getCachedViews() { 54 | const json = localStorage.getItem(CacheKey.CACHED_VIEWS) 55 | return JSON.parse(json ?? "[]") as string[] 56 | } 57 | export function setCachedViews(views: string[]) { 58 | localStorage.setItem(CacheKey.CACHED_VIEWS, JSON.stringify(views)) 59 | } 60 | // #endregion 61 | -------------------------------------------------------------------------------- /web/src/common/utils/css.ts: -------------------------------------------------------------------------------- 1 | /** 获取指定元素(默认全局)上的 CSS 变量的值 */ 2 | export function getCssVar(varName: string, element: HTMLElement = document.documentElement) { 3 | if (!varName?.startsWith("--")) { 4 | console.error("CSS 变量名应以 '--' 开头") 5 | return "" 6 | } 7 | // 没有拿到值时,会返回空串 8 | return getComputedStyle(element).getPropertyValue(varName) 9 | } 10 | 11 | /** 设置指定元素(默认全局)上的 CSS 变量的值 */ 12 | export function setCssVar(varName: string, value: string, element: HTMLElement = document.documentElement) { 13 | if (!varName?.startsWith("--")) { 14 | console.error("CSS 变量名应以 '--' 开头") 15 | return 16 | } 17 | element.style.setProperty(varName, value) 18 | } 19 | -------------------------------------------------------------------------------- /web/src/common/utils/datetime.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs" 2 | 3 | const INVALID_DATE = "N/A" 4 | 5 | /** 格式化日期时间 */ 6 | export function formatDateTime(datetime: string | number | Date = "", template: string = "YYYY-MM-DD HH:mm:ss") { 7 | const day = dayjs(datetime) 8 | return day.isValid() ? day.format(template) : INVALID_DATE 9 | } 10 | -------------------------------------------------------------------------------- /web/src/common/utils/permission.ts: -------------------------------------------------------------------------------- 1 | import { useUserStore } from "@/pinia/stores/user" 2 | import { isArray } from "@@/utils/validate" 3 | 4 | /** 全局权限判断函数,和权限指令 v-permission 功能类似 */ 5 | export function checkPermission(permissionRoles: string[]): boolean { 6 | if (isArray(permissionRoles) && permissionRoles.length > 0) { 7 | const { roles } = useUserStore() 8 | return roles.some(role => permissionRoles.includes(role)) 9 | } else { 10 | console.error("参数必须是一个数组且长度大于 0,参考:checkPermission(['admin', 'editor'])") 11 | return false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/src/common/utils/validate.ts: -------------------------------------------------------------------------------- 1 | /** 判断是否为数组 */ 2 | export function isArray(arg: T) { 3 | return Array.isArray ? Array.isArray(arg) : Object.prototype.toString.call(arg) === "[object Array]" 4 | } 5 | 6 | /** 判断是否为字符串 */ 7 | export function isString(str: unknown) { 8 | return typeof str === "string" || str instanceof String 9 | } 10 | 11 | /** 判断是否为外链 */ 12 | export function isExternal(path: string) { 13 | const reg = /^(https?:|mailto:|tel:)/ 14 | return reg.test(path) 15 | } 16 | -------------------------------------------------------------------------------- /web/src/layouts/components/AppMain/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 50 | -------------------------------------------------------------------------------- /web/src/layouts/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 52 | 53 | 64 | -------------------------------------------------------------------------------- /web/src/layouts/components/Footer/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /web/src/layouts/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /web/src/layouts/components/Logo/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | 31 | 65 | -------------------------------------------------------------------------------- /web/src/layouts/components/NavigationBar/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 65 | 66 | 127 | -------------------------------------------------------------------------------- /web/src/layouts/components/RightPanel/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 36 | -------------------------------------------------------------------------------- /web/src/layouts/components/Settings/SelectLayoutMode.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 40 | 41 | 104 | -------------------------------------------------------------------------------- /web/src/layouts/components/Settings/index.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 71 | 72 | 94 | -------------------------------------------------------------------------------- /web/src/layouts/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 79 | 80 | 99 | -------------------------------------------------------------------------------- /web/src/layouts/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /web/src/layouts/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AppMain } from "./AppMain/index.vue" 2 | export { default as Breadcrumb } from "./Breadcrumb/index.vue" 3 | export { default as Footer } from "./Footer/index.vue" 4 | export { default as Hamburger } from "./Hamburger/index.vue" 5 | export { default as Logo } from "./Logo/index.vue" 6 | export { default as NavigationBar } from "./NavigationBar/index.vue" 7 | export { default as RightPanel } from "./RightPanel/index.vue" 8 | export { default as Settings } from "./Settings/index.vue" 9 | export { default as Sidebar } from "./Sidebar/index.vue" 10 | export { default as TagsView } from "./TagsView/index.vue" 11 | -------------------------------------------------------------------------------- /web/src/layouts/composables/useResize.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore } from "@/pinia/stores/app" 2 | import { useRouteListener } from "@@/composables/useRouteListener" 3 | import { DeviceEnum } from "@@/constants/app-key" 4 | 5 | /** 参考 Bootstrap 的响应式设计将最大移动端宽度设置为 992 */ 6 | const MAX_MOBILE_WIDTH = 992 7 | 8 | /** 9 | * @name 浏览器宽度变化 Composable 10 | * @description 根据浏览器宽度变化,变换 Layout 布局 11 | */ 12 | export function useResize() { 13 | const appStore = useAppStore() 14 | const { listenerRouteChange } = useRouteListener() 15 | 16 | // 用于判断当前设备是否为移动端 17 | const isMobile = () => { 18 | const rect = document.body.getBoundingClientRect() 19 | return rect.width - 1 < MAX_MOBILE_WIDTH 20 | } 21 | 22 | // 用于处理窗口大小变化事件 23 | const resizeHandler = () => { 24 | if (!document.hidden) { 25 | const _isMobile = isMobile() 26 | appStore.toggleDevice(_isMobile ? DeviceEnum.Mobile : DeviceEnum.Desktop) 27 | _isMobile && appStore.closeSidebar(true) 28 | } 29 | } 30 | 31 | // 监听路由变化,根据设备类型调整布局 32 | listenerRouteChange(() => { 33 | if (appStore.device === DeviceEnum.Mobile && appStore.sidebar.opened) { 34 | appStore.closeSidebar(false) 35 | } 36 | }) 37 | 38 | // 在组件挂载前添加窗口大小变化事件监听器 39 | onBeforeMount(() => { 40 | window.addEventListener("resize", resizeHandler) 41 | }) 42 | 43 | // 在组件挂载后根据窗口大小判断设备类型并调整布局 44 | onMounted(() => { 45 | if (isMobile()) { 46 | appStore.toggleDevice(DeviceEnum.Mobile) 47 | appStore.closeSidebar(true) 48 | } 49 | }) 50 | 51 | // 在组件卸载前移除窗口大小变化事件监听器 52 | onBeforeUnmount(() => { 53 | window.removeEventListener("resize", resizeHandler) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /web/src/layouts/config.ts: -------------------------------------------------------------------------------- 1 | import { LayoutModeEnum } from "@@/constants/app-key" 2 | import { getLayoutsConfig } from "@@/utils/cache/local-storage" 3 | 4 | /** 项目配置类型 */ 5 | export interface LayoutsConfig { 6 | /** 是否显示设置按钮和面板 */ 7 | showSettings: boolean 8 | /** 布局模式 */ 9 | layoutMode: LayoutModeEnum 10 | /** 是否显示标签栏 */ 11 | showTagsView: boolean 12 | /** 是否显示 Logo */ 13 | showLogo: boolean 14 | /** 是否固定 Header */ 15 | fixedHeader: boolean 16 | /** 是否显示页脚 */ 17 | showFooter: boolean 18 | /** 是否显示消息通知 */ 19 | showNotify: boolean 20 | /** 是否显示切换主题按钮 */ 21 | showThemeSwitch: boolean 22 | /** 是否显示全屏按钮 */ 23 | showScreenfull: boolean 24 | /** 是否显示搜索按钮 */ 25 | showSearchMenu: boolean 26 | /** 是否缓存标签栏 */ 27 | cacheTagsView: boolean 28 | /** 开启系统水印 */ 29 | showWatermark: boolean 30 | /** 是否显示灰色模式 */ 31 | showGreyMode: boolean 32 | /** 是否显示色弱模式 */ 33 | showColorWeakness: boolean 34 | } 35 | 36 | /** 默认配置 */ 37 | const DEFAULT_CONFIG: LayoutsConfig = { 38 | layoutMode: LayoutModeEnum.LeftTop, 39 | showSettings: true, 40 | showTagsView: false, 41 | fixedHeader: true, 42 | showFooter: true, 43 | showLogo: true, 44 | showNotify: false, 45 | showThemeSwitch: true, 46 | showScreenfull: true, 47 | showSearchMenu: true, 48 | cacheTagsView: false, 49 | showWatermark: false, 50 | showGreyMode: false, 51 | showColorWeakness: false 52 | } 53 | 54 | /** 项目配置 */ 55 | export const layoutsConfig: LayoutsConfig = { ...DEFAULT_CONFIG, ...getLayoutsConfig() } 56 | -------------------------------------------------------------------------------- /web/src/layouts/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 50 | -------------------------------------------------------------------------------- /web/src/layouts/modes/LeftTopMode.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 37 | 38 | 112 | -------------------------------------------------------------------------------- /web/src/layouts/modes/TopMode.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | 27 | 75 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable perfectionist/sort-imports */ 2 | 3 | // core 4 | import { pinia } from "@/pinia" 5 | import { router } from "@/router" 6 | import { installPlugins } from "@/plugins" 7 | import App from "@/App.vue" 8 | // css 9 | import "normalize.css" 10 | import "nprogress/nprogress.css" 11 | import "element-plus/theme-chalk/dark/css-vars.css" 12 | import "vxe-table/lib/style.css" 13 | import "@@/assets/styles/index.scss" 14 | import "virtual:uno.css" 15 | 16 | // 创建应用实例 17 | const app = createApp(App) 18 | 19 | // 安装插件(全局组件、自定义指令等) 20 | installPlugins(app) 21 | 22 | // 安装 pinia 和 router 23 | app.use(pinia).use(router) 24 | 25 | // router 准备就绪后挂载应用 26 | router.isReady().then(() => { 27 | app.mount("#app") 28 | }) 29 | -------------------------------------------------------------------------------- /web/src/pages/dashboard/components/Admin.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /web/src/pages/dashboard/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /web/src/pages/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /web/src/pages/error/403.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /web/src/pages/error/404.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /web/src/pages/error/components/Layout.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /web/src/pages/login/apis/index.ts: -------------------------------------------------------------------------------- 1 | import type * as Auth from "./type" 2 | import { request } from "@/http/axios" 3 | 4 | /** 获取登录验证码 */ 5 | // export function getCaptchaApi() { 6 | // return request({ 7 | // url: "v1/auth/captcha", 8 | // method: "get" 9 | // }) 10 | // } 11 | 12 | /** 登录并返回 Token */ 13 | export function loginApi(data: Auth.LoginRequestData) { 14 | return request({ 15 | url: "/api/v1/auth/login", 16 | method: "post", 17 | data 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /web/src/pages/login/apis/type.ts: -------------------------------------------------------------------------------- 1 | export interface LoginRequestData { 2 | /** admin 或 editor */ 3 | username: "admin" | "editor" 4 | /** 密码 */ 5 | password: string 6 | } 7 | 8 | export type CaptchaResponseData = ApiResponseData 9 | 10 | export type LoginResponseData = ApiResponseData<{ token: string }> 11 | -------------------------------------------------------------------------------- /web/src/pages/login/components/Owl.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 93 | -------------------------------------------------------------------------------- /web/src/pages/login/composables/useFocus.ts: -------------------------------------------------------------------------------- 1 | /** 焦点 Composable */ 2 | export function useFocus() { 3 | // 是否有焦点 4 | const isFocus = ref(false) 5 | 6 | // 失去焦点 7 | const handleBlur = () => { 8 | isFocus.value = false 9 | } 10 | 11 | // 获取焦点 12 | const handleFocus = () => { 13 | isFocus.value = true 14 | } 15 | 16 | return { isFocus, handleBlur, handleFocus } 17 | } 18 | -------------------------------------------------------------------------------- /web/src/pages/login/images/close-eyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/src/pages/login/images/close-eyes.png -------------------------------------------------------------------------------- /web/src/pages/login/images/face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/src/pages/login/images/face.png -------------------------------------------------------------------------------- /web/src/pages/login/images/hand-down-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/src/pages/login/images/hand-down-left.png -------------------------------------------------------------------------------- /web/src/pages/login/images/hand-down-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/src/pages/login/images/hand-down-right.png -------------------------------------------------------------------------------- /web/src/pages/login/images/hand-up-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/src/pages/login/images/hand-up-left.png -------------------------------------------------------------------------------- /web/src/pages/login/images/hand-up-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weizxfree/KnowFlow/d128069af428438ecb6fbffdb4312b8b8b8b9379/web/src/pages/login/images/hand-up-right.png -------------------------------------------------------------------------------- /web/src/pages/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /web/src/pinia/index.ts: -------------------------------------------------------------------------------- 1 | export const pinia = createPinia() 2 | -------------------------------------------------------------------------------- /web/src/pinia/stores/app.ts: -------------------------------------------------------------------------------- 1 | import { pinia } from "@/pinia" 2 | import { DeviceEnum, SIDEBAR_CLOSED, SIDEBAR_OPENED } from "@@/constants/app-key" 3 | import { getSidebarStatus, setSidebarStatus } from "@@/utils/cache/local-storage" 4 | 5 | interface Sidebar { 6 | opened: boolean 7 | withoutAnimation: boolean 8 | } 9 | 10 | /** 设置侧边栏状态本地缓存 */ 11 | function handleSidebarStatus(opened: boolean) { 12 | opened ? setSidebarStatus(SIDEBAR_OPENED) : setSidebarStatus(SIDEBAR_CLOSED) 13 | } 14 | 15 | export const useAppStore = defineStore("app", () => { 16 | // 侧边栏状态 17 | const sidebar: Sidebar = reactive({ 18 | opened: getSidebarStatus() !== SIDEBAR_CLOSED, 19 | withoutAnimation: false 20 | }) 21 | 22 | // 设备类型 23 | const device = ref(DeviceEnum.Desktop) 24 | 25 | // 监听侧边栏 opened 状态 26 | watch( 27 | () => sidebar.opened, 28 | (opened) => { 29 | handleSidebarStatus(opened) 30 | } 31 | ) 32 | 33 | // 切换侧边栏 34 | const toggleSidebar = (withoutAnimation: boolean) => { 35 | sidebar.opened = !sidebar.opened 36 | sidebar.withoutAnimation = withoutAnimation 37 | } 38 | 39 | // 关闭侧边栏 40 | const closeSidebar = (withoutAnimation: boolean) => { 41 | sidebar.opened = false 42 | sidebar.withoutAnimation = withoutAnimation 43 | } 44 | 45 | // 切换设备类型 46 | const toggleDevice = (value: DeviceEnum) => { 47 | device.value = value 48 | } 49 | 50 | return { device, sidebar, toggleSidebar, closeSidebar, toggleDevice } 51 | }) 52 | 53 | /** 54 | * @description 在 SPA 应用中可用于在 pinia 实例被激活前使用 store 55 | * @description 在 SSR 应用中可用于在 setup 外使用 store 56 | */ 57 | export function useAppStoreOutside() { 58 | return useAppStore(pinia) 59 | } 60 | -------------------------------------------------------------------------------- /web/src/pinia/stores/permission.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from "vue-router" 2 | import { pinia } from "@/pinia" 3 | import { constantRoutes, dynamicRoutes } from "@/router" 4 | import { routerConfig } from "@/router/config" 5 | import { flatMultiLevelRoutes } from "@/router/helper" 6 | 7 | function hasPermission(roles: string[], route: RouteRecordRaw) { 8 | const routeRoles = route.meta?.roles 9 | return routeRoles ? roles.some(role => routeRoles.includes(role)) : true 10 | } 11 | 12 | function filterDynamicRoutes(routes: RouteRecordRaw[], roles: string[]) { 13 | const res: RouteRecordRaw[] = [] 14 | routes.forEach((route) => { 15 | const tempRoute = { ...route } 16 | if (hasPermission(roles, tempRoute)) { 17 | if (tempRoute.children) { 18 | tempRoute.children = filterDynamicRoutes(tempRoute.children, roles) 19 | } 20 | res.push(tempRoute) 21 | } 22 | }) 23 | return res 24 | } 25 | 26 | export const usePermissionStore = defineStore("permission", () => { 27 | // 可访问的路由 28 | const routes = ref([]) 29 | 30 | // 有访问权限的动态路由 31 | const addRoutes = ref([]) 32 | 33 | // 根据角色生成可访问的 Routes(可访问的路由 = 常驻路由 + 有访问权限的动态路由) 34 | const setRoutes = (roles: string[]) => { 35 | const accessedRoutes = filterDynamicRoutes(dynamicRoutes, roles) 36 | set(accessedRoutes) 37 | } 38 | 39 | // 所有路由 = 所有常驻路由 + 所有动态路由 40 | const setAllRoutes = () => { 41 | set(dynamicRoutes) 42 | } 43 | 44 | // 统一设置 45 | const set = (accessedRoutes: RouteRecordRaw[]) => { 46 | routes.value = constantRoutes.concat(accessedRoutes) 47 | addRoutes.value = routerConfig.thirdLevelRouteCache ? flatMultiLevelRoutes(accessedRoutes) : accessedRoutes 48 | } 49 | 50 | return { routes, addRoutes, setRoutes, setAllRoutes } 51 | }) 52 | 53 | /** 54 | * @description 在 SPA 应用中可用于在 pinia 实例被激活前使用 store 55 | * @description 在 SSR 应用中可用于在 setup 外使用 store 56 | */ 57 | export function usePermissionStoreOutside() { 58 | return usePermissionStore(pinia) 59 | } 60 | -------------------------------------------------------------------------------- /web/src/pinia/stores/settings.ts: -------------------------------------------------------------------------------- 1 | import type { LayoutsConfig } from "@/layouts/config" 2 | import type { Ref } from "vue" 3 | import { layoutsConfig } from "@/layouts/config" 4 | import { pinia } from "@/pinia" 5 | import { setLayoutsConfig } from "@@/utils/cache/local-storage" 6 | 7 | type SettingsStore = { 8 | // 使用映射类型来遍历 LayoutsConfig 对象的键 9 | [Key in keyof LayoutsConfig]: Ref 10 | } 11 | 12 | type SettingsStoreKey = keyof SettingsStore 13 | 14 | export const useSettingsStore = defineStore("settings", () => { 15 | // 状态对象 16 | const state = {} as SettingsStore 17 | // 遍历 LayoutsConfig 对象的键值对 18 | for (const [key, value] of Object.entries(layoutsConfig)) { 19 | // 使用类型断言来指定 key 的类型,将 value 包装在 ref 函数中,创建一个响应式变量 20 | const refValue = ref(value) 21 | // @ts-expect-error ignore 22 | state[key as SettingsStoreKey] = refValue 23 | // 监听每个响应式变量 24 | watch(refValue, () => { 25 | // 缓存 26 | const settings = getCacheData() 27 | setLayoutsConfig(settings) 28 | }) 29 | } 30 | // 获取要缓存的数据:将 state 对象转化为 settings 对象 31 | const getCacheData = () => { 32 | const settings = {} as LayoutsConfig 33 | for (const [key, value] of Object.entries(state)) { 34 | // @ts-expect-error ignore 35 | settings[key as SettingsStoreKey] = value.value 36 | } 37 | return settings 38 | } 39 | 40 | return state 41 | }) 42 | 43 | /** 44 | * @description 在 SPA 应用中可用于在 pinia 实例被激活前使用 store 45 | * @description 在 SSR 应用中可用于在 setup 外使用 store 46 | */ 47 | export function useSettingsStoreOutside() { 48 | return useSettingsStore(pinia) 49 | } 50 | -------------------------------------------------------------------------------- /web/src/pinia/stores/tags-view.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalizedGeneric } from "vue-router" 2 | import { pinia } from "@/pinia" 3 | import { getCachedViews, getVisitedViews, setCachedViews, setVisitedViews } from "@@/utils/cache/local-storage" 4 | import { useSettingsStore } from "./settings" 5 | 6 | export type TagView = Partial 7 | 8 | export const useTagsViewStore = defineStore("tags-view", () => { 9 | const { cacheTagsView } = useSettingsStore() 10 | const visitedViews = ref(cacheTagsView ? getVisitedViews() : []) 11 | const cachedViews = ref(cacheTagsView ? getCachedViews() : []) 12 | 13 | // 缓存标签栏数据 14 | watchEffect(() => { 15 | setVisitedViews(visitedViews.value) 16 | setCachedViews(cachedViews.value) 17 | }) 18 | 19 | // #region add 20 | const addVisitedView = (view: TagView) => { 21 | // 检查是否已经存在相同的 visitedView 22 | const index = visitedViews.value.findIndex(v => v.path === view.path) 23 | if (index !== -1) { 24 | // 防止 query 参数丢失 25 | visitedViews.value[index].fullPath !== view.fullPath && (visitedViews.value[index] = { ...view }) 26 | } else { 27 | // 添加新的 visitedView 28 | visitedViews.value.push({ ...view }) 29 | } 30 | } 31 | 32 | const addCachedView = (view: TagView) => { 33 | if (typeof view.name !== "string") return 34 | if (cachedViews.value.includes(view.name)) return 35 | if (view.meta?.keepAlive) { 36 | cachedViews.value.push(view.name) 37 | } 38 | } 39 | // #endregion 40 | 41 | // #region del 42 | const delVisitedView = (view: TagView) => { 43 | const index = visitedViews.value.findIndex(v => v.path === view.path) 44 | if (index !== -1) { 45 | visitedViews.value.splice(index, 1) 46 | } 47 | } 48 | 49 | const delCachedView = (view: TagView) => { 50 | if (typeof view.name !== "string") return 51 | const index = cachedViews.value.indexOf(view.name) 52 | if (index !== -1) { 53 | cachedViews.value.splice(index, 1) 54 | } 55 | } 56 | // #endregion 57 | 58 | // #region delOthers 59 | const delOthersVisitedViews = (view: TagView) => { 60 | visitedViews.value = visitedViews.value.filter((v) => { 61 | return v.meta?.affix || v.path === view.path 62 | }) 63 | } 64 | 65 | const delOthersCachedViews = (view: TagView) => { 66 | if (typeof view.name !== "string") return 67 | const index = cachedViews.value.indexOf(view.name) 68 | if (index !== -1) { 69 | cachedViews.value = cachedViews.value.slice(index, index + 1) 70 | } else { 71 | // 如果 index = -1, 没有缓存的 tags 72 | cachedViews.value = [] 73 | } 74 | } 75 | // #endregion 76 | 77 | // #region delAll 78 | const delAllVisitedViews = () => { 79 | // 保留固定的 tags 80 | visitedViews.value = visitedViews.value.filter(tag => tag.meta?.affix) 81 | } 82 | 83 | const delAllCachedViews = () => { 84 | cachedViews.value = [] 85 | } 86 | // #endregion 87 | 88 | return { 89 | visitedViews, 90 | cachedViews, 91 | addVisitedView, 92 | addCachedView, 93 | delVisitedView, 94 | delCachedView, 95 | delOthersVisitedViews, 96 | delOthersCachedViews, 97 | delAllVisitedViews, 98 | delAllCachedViews 99 | } 100 | }) 101 | 102 | /** 103 | * @description 在 SPA 应用中可用于在 pinia 实例被激活前使用 store 104 | * @description 在 SSR 应用中可用于在 setup 外使用 store 105 | */ 106 | export function useTagsViewStoreOutside() { 107 | return useTagsViewStore(pinia) 108 | } 109 | -------------------------------------------------------------------------------- /web/src/pinia/stores/user.ts: -------------------------------------------------------------------------------- 1 | import { pinia } from "@/pinia" 2 | import { resetRouter } from "@/router" 3 | import { routerConfig } from "@/router/config" 4 | import { getCurrentUserApi } from "@@/apis/users" 5 | import { setToken as _setToken, getToken, removeToken } from "@@/utils/cache/cookies" 6 | import { useSettingsStore } from "./settings" 7 | import { useTagsViewStore } from "./tags-view" 8 | 9 | export const useUserStore = defineStore("user", () => { 10 | const token = ref(getToken() || "") 11 | const roles = ref([]) 12 | const username = ref("") 13 | const avatar = ref("/favicon.ico") 14 | const tagsViewStore = useTagsViewStore() 15 | const settingsStore = useSettingsStore() 16 | 17 | // 设置 Token 18 | const setToken = (value: string) => { 19 | _setToken(value) 20 | token.value = value 21 | } 22 | 23 | // 获取用户详情 24 | const getInfo = async () => { 25 | const { data } = await getCurrentUserApi() 26 | username.value = data.username 27 | // 验证返回的 roles 是否为一个非空数组,否则塞入一个没有任何作用的默认角色,防止路由守卫逻辑进入无限循环 28 | roles.value = data.roles?.length > 0 ? data.roles : routerConfig.defaultRoles 29 | } 30 | 31 | // 模拟角色变化 32 | const changeRoles = (role: string) => { 33 | const newToken = `token-${role}` 34 | token.value = newToken 35 | _setToken(newToken) 36 | // 用刷新页面代替重新登录 37 | location.reload() 38 | } 39 | 40 | // 登出 41 | const logout = () => { 42 | removeToken() 43 | token.value = "" 44 | roles.value = [] 45 | resetRouter() 46 | resetTagsView() 47 | } 48 | 49 | // 重置 Token 50 | const resetToken = () => { 51 | removeToken() 52 | token.value = "" 53 | roles.value = [] 54 | } 55 | 56 | // 重置 Visited Views 和 Cached Views 57 | const resetTagsView = () => { 58 | if (!settingsStore.cacheTagsView) { 59 | tagsViewStore.delAllVisitedViews() 60 | tagsViewStore.delAllCachedViews() 61 | } 62 | } 63 | 64 | return { token, roles, username, avatar, setToken, getInfo, changeRoles, logout, resetToken } 65 | }) 66 | 67 | /** 68 | * @description 在 SPA 应用中可用于在 pinia 实例被激活前使用 store 69 | * @description 在 SSR 应用中可用于在 setup 外使用 store 70 | */ 71 | export function useUserStoreOutside() { 72 | return useUserStore(pinia) 73 | } 74 | -------------------------------------------------------------------------------- /web/src/plugins/element-plus-icons.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import * as ElementPlusIconsVue from "@element-plus/icons-vue" 3 | 4 | export function installElementPlusIcons(app: App) { 5 | // 注册所有 Element Plus Icons 6 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 7 | app.component(key, component) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /web/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import { installElementPlusIcons } from "./element-plus-icons" 3 | import { installPermissionDirective } from "./permission-directive" 4 | import { installSvgIcon } from "./svg-icon" 5 | import { installVxeTable } from "./vxe-table" 6 | 7 | export function installPlugins(app: App) { 8 | installElementPlusIcons(app) 9 | installPermissionDirective(app) 10 | installSvgIcon(app) 11 | installVxeTable(app) 12 | } 13 | -------------------------------------------------------------------------------- /web/src/plugins/permission-directive.ts: -------------------------------------------------------------------------------- 1 | import type { App, Directive } from "vue" 2 | import { useUserStore } from "@/pinia/stores/user" 3 | import { isArray } from "@@/utils/validate" 4 | 5 | /** 6 | * @name 权限指令 7 | * @description 和权限判断函数 checkPermission 功能类似 8 | */ 9 | const permission: Directive = { 10 | mounted(el, binding) { 11 | const { value: permissionRoles } = binding 12 | const { roles } = useUserStore() 13 | if (isArray(permissionRoles) && permissionRoles.length > 0) { 14 | const hasPermission = roles.some(role => permissionRoles.includes(role)) 15 | hasPermission || el.parentNode?.removeChild(el) 16 | } else { 17 | throw new Error(`参数必须是一个数组且长度大于 0,参考:v-permission="['admin', 'editor']"`) 18 | } 19 | } 20 | } 21 | 22 | export function installPermissionDirective(app: App) { 23 | app.directive("permission", permission) 24 | } 25 | -------------------------------------------------------------------------------- /web/src/plugins/svg-icon.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import SvgIcon from "~virtual/svg-component" 3 | 4 | export function installSvgIcon(app: App) { 5 | // 注册 SvgIcon 组件 6 | app.component("SvgIcon", SvgIcon) 7 | } 8 | -------------------------------------------------------------------------------- /web/src/plugins/vxe-table.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import VXETable from "vxe-table" // https://vxetable.cn/#/start/install 3 | 4 | // 全局默认参数 5 | VXETable.setConfig({ 6 | // 全局尺寸 7 | size: "medium", 8 | // 全局 zIndex 起始值,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡 9 | zIndex: 9999, 10 | // 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据 11 | version: 0, 12 | // 全局 loading 提示内容,如果为 null 则不显示文本 13 | loadingText: null, 14 | table: { 15 | showHeader: true, 16 | showOverflow: "tooltip", 17 | showHeaderOverflow: "tooltip", 18 | autoResize: true, 19 | // stripe: false, 20 | border: "inner", 21 | // round: false, 22 | emptyText: "暂无数据", 23 | rowConfig: { 24 | isHover: true, 25 | isCurrent: true, 26 | // 行数据的唯一主键字段名 27 | keyField: "_VXE_ID" 28 | }, 29 | columnConfig: { 30 | resizable: false 31 | }, 32 | align: "center", 33 | headerAlign: "center" 34 | }, 35 | pager: { 36 | // size: "medium", 37 | // 配套的样式 38 | perfect: false, 39 | pageSize: 10, 40 | pagerCount: 7, 41 | pageSizes: [10, 20, 50], 42 | layouts: ["Total", "PrevJump", "PrevPage", "Number", "NextPage", "NextJump", "Sizes", "FullJump"] 43 | }, 44 | modal: { 45 | minWidth: 500, 46 | minHeight: 400, 47 | lockView: true, 48 | mask: true, 49 | // duration: 3000, 50 | // marginSize: 20, 51 | dblclickZoom: false, 52 | showTitleOverflow: true, 53 | transfer: true, 54 | draggable: false 55 | } 56 | }) 57 | 58 | export function installVxeTable(app: App) { 59 | // Vxe Table 组件完整引入 60 | app.use(VXETable) 61 | } 62 | -------------------------------------------------------------------------------- /web/src/router/config.ts: -------------------------------------------------------------------------------- 1 | import type { RouterHistory } from "vue-router" 2 | import { createWebHashHistory, createWebHistory } from "vue-router" 3 | 4 | /** 路由配置 */ 5 | interface RouterConfig { 6 | /** 7 | * @name 路由模式 8 | * @description hash 模式和 html5 模式 9 | */ 10 | history: RouterHistory 11 | /** 12 | * @name 是否开启动态路由功能 13 | * @description 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段) 14 | * @description 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 dynamic: false 15 | */ 16 | dynamic: boolean 17 | /** 18 | * @name 默认角色 19 | * @description 当动态路由功能关闭时: 20 | * @description 1. 应该将所有路由都写到常驻路由里面(表明所有登录的用户能访问的页面都是一样的) 21 | * @description 2. 系统自动给当前登录用户赋值一个没有任何作用的默认角色 22 | */ 23 | defaultRoles: Array 24 | /** 25 | * @name 是否开启三级及其以上路由缓存功能 26 | * @description 1. 开启后会进行路由降级(把三级及其以上的路由转化为二级路由) 27 | * @description 2. 由于都会转成二级路由,所以二级及其以上路由有内嵌子路由将会失效 28 | */ 29 | thirdLevelRouteCache: boolean 30 | } 31 | 32 | const VITE_ROUTER_HISTORY = import.meta.env.VITE_ROUTER_HISTORY 33 | 34 | const VITE_PUBLIC_PATH = import.meta.env.VITE_PUBLIC_PATH 35 | 36 | export const routerConfig: RouterConfig = { 37 | history: VITE_ROUTER_HISTORY === "hash" ? createWebHashHistory(VITE_PUBLIC_PATH) : createWebHistory(VITE_PUBLIC_PATH), 38 | dynamic: true, 39 | defaultRoles: ["DEFAULT_ROLE"], 40 | thirdLevelRouteCache: false 41 | } 42 | -------------------------------------------------------------------------------- /web/src/router/guard.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from "vue-router" 2 | import { usePermissionStore } from "@/pinia/stores/permission" 3 | import { useUserStore } from "@/pinia/stores/user" 4 | import { routerConfig } from "@/router/config" 5 | import { isWhiteList } from "@/router/whitelist" 6 | import { setRouteChange } from "@@/composables/useRouteListener" 7 | import { useTitle } from "@@/composables/useTitle" 8 | import { getToken } from "@@/utils/cache/cookies" 9 | import NProgress from "nprogress" 10 | 11 | NProgress.configure({ showSpinner: false }) 12 | 13 | const { setTitle } = useTitle() 14 | 15 | const LOGIN_PATH = "/login" 16 | 17 | export function registerNavigationGuard(router: Router) { 18 | // 全局前置守卫 19 | router.beforeEach(async (to, _from) => { 20 | NProgress.start() 21 | const userStore = useUserStore() 22 | const permissionStore = usePermissionStore() 23 | // 如果没有登录 24 | if (!getToken()) { 25 | // 如果在免登录的白名单中,则直接进入 26 | if (isWhiteList(to)) return true 27 | // 其他没有访问权限的页面将被重定向到登录页面 28 | return LOGIN_PATH 29 | } 30 | // 如果已经登录,并准备进入 Login 页面,则重定向到主页 31 | if (to.path === LOGIN_PATH) return "/" 32 | // 如果用户已经获得其权限角色 33 | if (userStore.roles.length !== 0) return true 34 | // 否则要重新获取权限角色 35 | try { 36 | await userStore.getInfo() 37 | // 注意:角色必须是一个数组! 例如: ["admin"] 或 ["developer", "editor"] 38 | const roles = userStore.roles 39 | // 生成可访问的 Routes 40 | routerConfig.dynamic ? permissionStore.setRoutes(roles) : permissionStore.setAllRoutes() 41 | // 将 "有访问权限的动态路由" 添加到 Router 中 42 | permissionStore.addRoutes.forEach(route => router.addRoute(route)) 43 | // 设置 replace: true, 因此导航将不会留下历史记录 44 | return { ...to, replace: true } 45 | } catch (error) { 46 | // 过程中发生任何错误,都直接重置 Token,并重定向到登录页面 47 | userStore.resetToken() 48 | ElMessage.error((error as Error).message || "路由守卫发生错误") 49 | return LOGIN_PATH 50 | } 51 | }) 52 | 53 | // 全局后置钩子 54 | router.afterEach((to) => { 55 | setRouteChange(to) 56 | setTitle(to.meta.title) 57 | NProgress.done() 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /web/src/router/helper.ts: -------------------------------------------------------------------------------- 1 | import type { Router, RouteRecordNormalized, RouteRecordRaw } from "vue-router" 2 | import { cloneDeep, omit } from "lodash-es" 3 | import { createRouter } from "vue-router" 4 | import { routerConfig } from "./config" 5 | 6 | /** 路由降级(把三级及其以上的路由转化为二级路由) */ 7 | export function flatMultiLevelRoutes(routes: RouteRecordRaw[]) { 8 | const routesMirror = cloneDeep(routes) 9 | routesMirror.forEach((route) => { 10 | // 如果路由是三级及其以上路由,对其进行降级处理 11 | isMultipleRoute(route) && promoteRouteLevel(route) 12 | }) 13 | return routesMirror 14 | } 15 | 16 | /** 判断路由层级是否大于 2 */ 17 | function isMultipleRoute(route: RouteRecordRaw) { 18 | const children = route.children 19 | // 只要有一个子路由的 children 长度大于 0,就说明是三级及其以上路由 20 | if (children?.length) return children.some(child => child.children?.length) 21 | return false 22 | } 23 | 24 | /** 生成二级路由 */ 25 | function promoteRouteLevel(route: RouteRecordRaw) { 26 | // 创建 router 实例是为了获取到当前传入的 route 的所有路由信息 27 | let router: Router | null = createRouter({ 28 | history: routerConfig.history, 29 | routes: [route] 30 | }) 31 | const routes = router.getRoutes() 32 | // 在 addToChildren 函数中使用上面获取到的路由信息来更新 route 的 children 33 | addToChildren(routes, route.children || [], route) 34 | router = null 35 | // 转为二级路由后,去除所有子路由中的 children 36 | route.children = route.children?.map(item => omit(item, "children") as RouteRecordRaw) 37 | } 38 | 39 | /** 将给定的子路由添加到指定的路由模块中 */ 40 | function addToChildren(routes: RouteRecordNormalized[], children: RouteRecordRaw[], routeModule: RouteRecordRaw) { 41 | children.forEach((child) => { 42 | const route = routes.find(item => item.name === child.name) 43 | if (route) { 44 | // 初始化 routeModule 的 children 45 | routeModule.children = routeModule.children || [] 46 | // 如果 routeModule 的 children 属性中不包含该路由,则将其添加进去 47 | if (!routeModule.children.includes(route)) { 48 | routeModule.children.push(route) 49 | } 50 | // 如果该子路由还有自己的子路由,则递归调用此函数将它们也添加进去 51 | if (child.children?.length) { 52 | addToChildren(routes, child.children, routeModule) 53 | } 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /web/src/router/whitelist.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalizedGeneric, RouteRecordNameGeneric } from "vue-router" 2 | 3 | /** 免登录白名单(匹配路由 path) */ 4 | const whiteListByPath: string[] = ["/login"] 5 | 6 | /** 免登录白名单(匹配路由 name) */ 7 | const whiteListByName: RouteRecordNameGeneric[] = [] 8 | 9 | /** 判断是否在白名单 */ 10 | export function isWhiteList(to: RouteLocationNormalizedGeneric) { 11 | // path 和 name 任意一个匹配上即可 12 | return whiteListByPath.includes(to.path) || whiteListByName.includes(to.name) 13 | } 14 | -------------------------------------------------------------------------------- /web/tests/components/Notify.test.ts: -------------------------------------------------------------------------------- 1 | import Notify from "@@/components/Notify/index.vue" 2 | import List from "@@/components/Notify/List.vue" 3 | import { shallowMount } from "@vue/test-utils" 4 | import { describe, expect, it } from "vitest" 5 | 6 | describe("notify", () => { 7 | it("正常渲染", () => { 8 | const wrapper = shallowMount(Notify) 9 | expect(wrapper.classes("notify")).toBe(true) 10 | }) 11 | }) 12 | 13 | describe("list", () => { 14 | it("list 长度为 0", () => { 15 | const wrapper = shallowMount(List, { 16 | props: { 17 | data: [] 18 | } 19 | }) 20 | expect(wrapper.find("el-empty-stub").exists()).toBe(true) 21 | }) 22 | it("list 长度不为 0", () => { 23 | const wrapper = shallowMount(List, { 24 | props: { 25 | data: [ 26 | { 27 | title: "" 28 | } 29 | ] 30 | } 31 | }) 32 | expect(wrapper.find("el-empty-stub").exists()).toBe(false) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /web/tests/demo.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 该文件所有示例均是为了向你演示 Vitest 最基本的用法 3 | * @link https://cn.vitest.dev/api 4 | * @api describe: 形成一个作用域 5 | * @api test/it: 定义了一组关于测试期望的方法,它接收测试名称和一个含有测试期望的函数 6 | * @api expect: 用来创建断言 7 | * @api toBe: 可以用于断言原始类型是否相等,或者对象是否共享相同的引用 8 | * @api toEqual: 断言实际值是否等于接收到的值或具有相同的结构(如果是对象,则递归比较它们) 9 | */ 10 | 11 | import { describe, expect, it } from "vitest" 12 | 13 | const author1 = { 14 | name: "pany", 15 | email: "939630029@qq.com", 16 | url: "https://github.com/pany-ang" 17 | } 18 | 19 | const author2 = { 20 | name: "pany", 21 | email: "939630029@qq.com", 22 | url: "https://github.com/pany-ang" 23 | } 24 | 25 | describe("这里填写作用域名称", () => { 26 | it("测试基础数据类型", () => { 27 | expect(1 + 1).toBe(2) 28 | }) 29 | it("测试引用类型", () => { 30 | expect(author1).toEqual(author2) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /web/tests/utils/validate.test.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from "@@/utils/validate" 2 | import { describe, expect, it } from "vitest" 3 | 4 | describe("isArray", () => { 5 | it("string", () => { 6 | expect(isArray("")).toBe(false) 7 | }) 8 | it("number", () => { 9 | expect(isArray(1)).toBe(false) 10 | }) 11 | it("boolean", () => { 12 | expect(isArray(true)).toBe(false) 13 | }) 14 | it("null", () => { 15 | expect(isArray(null)).toBe(false) 16 | }) 17 | it("undefined", () => { 18 | expect(isArray(undefined)).toBe(false) 19 | }) 20 | it("symbol", () => { 21 | expect(isArray(Symbol())).toBe(false) 22 | }) 23 | it("bigInt", () => { 24 | expect(isArray(BigInt(1))).toBe(false) 25 | }) 26 | it("object", () => { 27 | expect(isArray({})).toBe(false) 28 | }) 29 | it("array object", () => { 30 | expect(isArray([])).toBe(true) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | /** 2 | * @link https://www.typescriptlang.org/tsconfig 3 | * @link https://cn.vuejs.org/guide/typescript/overview#configuring-tsconfig-json 4 | * @link https://cn.vite.dev/guide/features#typescript-compiler-options 5 | */ 6 | 7 | { 8 | "compilerOptions": { 9 | "target": "esnext", 10 | "jsx": "preserve", 11 | "jsxImportSource": "vue", 12 | "lib": ["esnext", "dom"], 13 | "useDefineForClassFields": true, 14 | "experimentalDecorators": true, 15 | // baseUrl 用来告诉编译器到哪里去查找模块,使用非相对模块时必须配置此项 16 | "baseUrl": ".", 17 | "module": "esnext", 18 | "moduleResolution": "bundler", 19 | // 非相对模块导入的路径映射配置,根据 baseUrl 配置进行路径计算,与 vite.config 中 alias 配置同步 20 | "paths": { 21 | "@/*": ["src/*"], 22 | "@@/*": ["src/common/*"] 23 | }, 24 | "resolveJsonModule": true, 25 | "types": ["vite/client", "element-plus/global"], 26 | // 允许导入 .ts .mts .tsx 拓展名的文件 27 | "allowImportingTsExtensions": true, 28 | // 允许 JS 29 | "allowJs": true, 30 | // TS 严格模式 31 | "strict": true, 32 | "importHelpers": true, 33 | // 不输出任何编译后的文件,只进行类型检查 34 | "noEmit": true, 35 | "sourceMap": true, 36 | "allowSyntheticDefaultImports": true, 37 | "esModuleInterop": true, 38 | "isolatedModules": true, 39 | "skipLibCheck": true 40 | }, 41 | // 需要被编译的文件列表 42 | "include": ["**/*.ts", "**/*.tsx", "**/*.vue", "**/*.d.ts"], 43 | // 从编译中排除的文件列表 44 | "exclude": ["node_modules", "dist"] 45 | } 46 | -------------------------------------------------------------------------------- /web/types/api.d.ts: -------------------------------------------------------------------------------- 1 | /** 所有 api 接口的响应数据都应该准守该格式 */ 2 | interface ApiResponseData { 3 | code: number 4 | data: T 5 | message: string 6 | } 7 | -------------------------------------------------------------------------------- /web/types/auto/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | // biome-ignore lint: disable 6 | export {} 7 | 8 | /* prettier-ignore */ 9 | declare module 'vue' { 10 | export interface GlobalComponents { 11 | ElAlert: typeof import('element-plus/es')['ElAlert'] 12 | ElAside: typeof import('element-plus/es')['ElAside'] 13 | ElAvatar: typeof import('element-plus/es')['ElAvatar'] 14 | ElBacktop: typeof import('element-plus/es')['ElBacktop'] 15 | ElBadge: typeof import('element-plus/es')['ElBadge'] 16 | ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] 17 | ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] 18 | ElButton: typeof import('element-plus/es')['ElButton'] 19 | ElCard: typeof import('element-plus/es')['ElCard'] 20 | ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] 21 | ElContainer: typeof import('element-plus/es')['ElContainer'] 22 | ElDialog: typeof import('element-plus/es')['ElDialog'] 23 | ElDivider: typeof import('element-plus/es')['ElDivider'] 24 | ElDrawer: typeof import('element-plus/es')['ElDrawer'] 25 | ElDropdown: typeof import('element-plus/es')['ElDropdown'] 26 | ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] 27 | ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 28 | ElEmpty: typeof import('element-plus/es')['ElEmpty'] 29 | ElForm: typeof import('element-plus/es')['ElForm'] 30 | ElFormItem: typeof import('element-plus/es')['ElFormItem'] 31 | ElHeader: typeof import('element-plus/es')['ElHeader'] 32 | ElIcon: typeof import('element-plus/es')['ElIcon'] 33 | ElInput: typeof import('element-plus/es')['ElInput'] 34 | ElMain: typeof import('element-plus/es')['ElMain'] 35 | ElMenu: typeof import('element-plus/es')['ElMenu'] 36 | ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] 37 | ElOption: typeof import('element-plus/es')['ElOption'] 38 | ElPagination: typeof import('element-plus/es')['ElPagination'] 39 | ElPopover: typeof import('element-plus/es')['ElPopover'] 40 | ElProgress: typeof import('element-plus/es')['ElProgress'] 41 | ElRadio: typeof import('element-plus/es')['ElRadio'] 42 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 43 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] 44 | ElSelect: typeof import('element-plus/es')['ElSelect'] 45 | ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] 46 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 47 | ElTable: typeof import('element-plus/es')['ElTable'] 48 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 49 | ElTabPane: typeof import('element-plus/es')['ElTabPane'] 50 | ElTabs: typeof import('element-plus/es')['ElTabs'] 51 | ElTag: typeof import('element-plus/es')['ElTag'] 52 | ElTooltip: typeof import('element-plus/es')['ElTooltip'] 53 | ElUpload: typeof import('element-plus/es')['ElUpload'] 54 | RouterLink: typeof import('vue-router')['RouterLink'] 55 | RouterView: typeof import('vue-router')['RouterView'] 56 | } 57 | export interface ComponentCustomProperties { 58 | vLoading: typeof import('element-plus/es')['ElLoadingDirective'] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /web/types/auto/svg-component-global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // biome-ignore format: off 4 | // biome-ignore lint: off 5 | // @ts-nocheck 6 | // Generated by unplugin-svg-component 7 | import 'vue' 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | SvgIcon: import("vue").DefineComponent<{ 11 | name: { 12 | type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">; 13 | default: string; 14 | required: true; 15 | }; 16 | }, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly; 19 | default: string; 20 | required: true; 21 | }; 22 | }>>, { 23 | name: "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management"; 24 | }>; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/types/auto/svg-component.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // biome-ignore format: off 4 | // biome-ignore lint: off 5 | // @ts-nocheck 6 | // Generated by unplugin-svg-component 7 | declare module '~virtual/svg-component' { 8 | const SvgIcon: import("vue").DefineComponent<{ 9 | name: { 10 | type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">; 11 | default: string; 12 | required: true; 13 | }; 14 | }, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly; 17 | default: string; 18 | required: true; 19 | }; 20 | }>>, { 21 | name: "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management"; 22 | }>; 23 | export const svgNames: ["dashboard", "file", "fullscreen-exit", "fullscreen", "kb", "keyboard-down", "keyboard-enter", "keyboard-esc", "keyboard-up", "search", "team-management", "user-config", "user-management"]; 24 | export type SvgName = "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management"; 25 | export default SvgIcon; 26 | } 27 | -------------------------------------------------------------------------------- /web/types/directives.d.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from "vue" 2 | 3 | export {} 4 | 5 | // 由 app.directive 全局注册的自定义指令需要在这里声明 TS 类型才能获得类型提示 6 | declare module "vue" { 7 | export interface ComponentCustomProperties { 8 | vPermission: Directive 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web/types/env.d.ts: -------------------------------------------------------------------------------- 1 | /** 声明 vite 环境变量的类型(如果未声明则默认是 any) */ 2 | interface ImportMetaEnv { 3 | readonly VITE_APP_TITLE: string 4 | readonly VITE_BASE_URL: string 5 | readonly VITE_ROUTER_HISTORY: "hash" | "html5" 6 | readonly VITE_PUBLIC_PATH: string 7 | } 8 | 9 | interface ImportMeta { 10 | readonly env: ImportMetaEnv 11 | } 12 | -------------------------------------------------------------------------------- /web/types/vue-router.d.ts: -------------------------------------------------------------------------------- 1 | import type * as ElementPlusIconsVue from "@element-plus/icons-vue" 2 | import type { SvgName } from "~virtual/svg-component" 3 | import "vue-router" 4 | 5 | export {} 6 | 7 | type ElementPlusIconsName = keyof typeof ElementPlusIconsVue 8 | 9 | declare module "vue-router" { 10 | interface RouteMeta { 11 | /** 12 | * @description 设置该路由在侧边栏和面包屑中展示的名字 13 | */ 14 | title?: string 15 | /** 16 | * @description 设置该路由的图标,记得将 svg 导入 src/common/assets/icons 17 | */ 18 | svgIcon?: SvgName 19 | /** 20 | * @description 设置该路由的图标,直接使用 Element Plus 的 Icon(与 svgIcon 同时设置时,svgIcon 将优先生效) 21 | */ 22 | elIcon?: ElementPlusIconsName 23 | /** 24 | * @description 默认 false,设置 true 的时候该路由不会在侧边栏出现 25 | */ 26 | hidden?: boolean 27 | /** 28 | * @description 设置能进入该路由的角色,支持多个角色叠加 29 | */ 30 | roles?: string[] 31 | /** 32 | * @description 默认 true,如果设置为 false,则不会在面包屑中显示 33 | */ 34 | breadcrumb?: boolean 35 | /** 36 | * @description 默认 false,如果设置为 true,它则会固定在 tags-view 中 37 | */ 38 | affix?: boolean 39 | /** 40 | * @description 当一个路由的 children 属性中声明的非隐藏子路由只有 1 个且该子路由为叶子节点时,会将这个子路由当做父路由显示在侧边栏 41 | * @description 当大于 1 个时,会恢复成嵌套模式 42 | * @description 如果想不管个数总是显示父路由,可以在父路由上设置 alwaysShow: true 43 | */ 44 | alwaysShow?: boolean 45 | /** 46 | * @description 示例: activeMenu: "/xxx/xxx", 47 | * @description 当设置了该属性进入路由时,则会高亮 activeMenu 属性对应的侧边栏 48 | * @description 该属性适合使用在有 hidden: true 属性的路由上 49 | */ 50 | activeMenu?: string 51 | /** 52 | * @description 是否缓存该路由页面 53 | * @description 默认为 false,为 true 时代表需要缓存,此时该路由和该页面都需要设置一致的 Name 54 | */ 55 | keepAlive?: boolean 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /web/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetAttributify, presetWind3 } from "unocss" 2 | 3 | export default defineConfig({ 4 | // 预设 5 | presets: [ 6 | // 属性化模式 & 无值的属性模式 7 | presetAttributify({ 8 | prefix: "un-", 9 | prefixedOnly: false 10 | }), 11 | // 默认预设 12 | presetWind3({ 13 | important: "#app" 14 | }) 15 | ], 16 | // 自定义规则 17 | rules: [], 18 | // 自定义快捷方式 19 | shortcuts: { 20 | "wh-full": "w-full h-full", 21 | "flex-center": "flex justify-center items-center", 22 | "flex-x-center": "flex justify-center", 23 | "flex-y-center": "flex items-center" 24 | } 25 | }) 26 | --------------------------------------------------------------------------------