├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.json ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── env.d.ts ├── images ├── image-20240527131428007.png ├── image-20240527131440378.png ├── image-20240527131823450.png ├── image-20240527131840315.png ├── image-20240527131920020.png └── image-20240527131929228.png ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── api │ ├── home │ │ └── index.ts │ ├── index.ts │ ├── poc │ │ └── index.ts │ ├── project │ │ └── index.ts │ ├── tasks │ │ └── index.ts │ └── user │ │ └── index.ts ├── assets │ ├── base.css │ ├── login.png │ ├── logo.svg │ └── main.css ├── components │ ├── Common │ │ ├── Loading │ │ │ ├── LoadingCss.css │ │ │ ├── LoadingView.vue │ │ │ └── index.ts │ │ ├── Tables │ │ │ ├── Config │ │ │ │ └── config.ts │ │ │ ├── Search │ │ │ │ └── index.ts │ │ │ ├── TableColumn.vue │ │ │ ├── TablesView.vue │ │ │ ├── index.ts │ │ │ └── useTableContext.ts │ │ ├── Test │ │ │ └── TestTable.vue │ │ ├── TestVIew.vue │ │ └── WaterMark │ │ │ ├── WaterMark.ts │ │ │ └── index.ts │ ├── HeaDer.vue │ ├── Project │ │ ├── ProjectList.vue │ │ ├── ProjectVulInfoWinodws.vue │ │ └── SideBar.vue │ ├── RouterBreadcrumd.vue │ ├── SideBar.vue │ ├── TagsView.vue │ ├── Tasks │ │ └── CreateTasksWindows.vue │ └── Users │ │ └── CreateUserWindows.vue ├── main.ts ├── plugins │ ├── index.ts │ └── loading │ │ └── index.ts ├── router │ └── index.ts ├── stores │ ├── counter.ts │ ├── sidebar.ts │ ├── tags.ts │ ├── taskinfo.ts │ ├── user.ts │ └── userpermiss.ts ├── type │ └── table │ │ └── index.d.ts ├── utils │ └── request │ │ └── index.ts └── views │ ├── HomeView.vue │ ├── echarts │ ├── CpuEcharts.vue │ ├── MemoryEcharts.vue │ └── NetworkEcharts.vue │ ├── error │ ├── 401View.vue │ └── 404View.vue │ ├── home │ └── DashBoard.vue │ ├── login │ └── LoginView.vue │ ├── poc │ ├── PocListView.vue │ └── ts │ │ └── PocList.ts │ ├── project │ ├── ProJectView.vue │ └── View │ │ ├── ProJectInfo.vue │ │ ├── ProJectList.vue │ │ ├── ProJectVulList.vue │ │ └── ts │ │ ├── ProJectVul.ts │ │ ├── TaskCelery.ts │ │ └── TaskInfo.ts │ ├── tasks │ ├── TasksCeleryView.vue │ ├── TasksReportView.vue │ ├── TasksView.vue │ └── ts │ │ ├── TaskCelery.ts │ │ ├── TaskReport.ts │ │ └── Tasks.ts │ └── user │ ├── ResetPassword.vue │ ├── UserInfo.vue │ ├── UserView.vue │ └── ts │ └── UserList.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vitest.json ├── vite.config.ts ├── vitest.config.ts ├── web-types.json └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier/skip-formatting' 11 | ], 12 | overrides: [ 13 | { 14 | files: [ 15 | 'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}', 16 | 'cypress/support/**/*.{js,ts,jsx,tsx}' 17 | ], 18 | 'extends': [ 19 | 'plugin:cypress/recommended' 20 | ] 21 | } 22 | ], 23 | parserOptions: { 24 | ecmaVersion: 'latest' 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | .vscode 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .idea 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iotscan-web 2 | 3 | 这是一个基于vue3+element-plus+vite4+pinia开发一个资产测绘平台+漏洞扫描的前端项目,提供多种自定义的开发,如果你的扫描器或资产测绘平台不追求UI仅仅是为了快速开发,可以参考此项目。 4 | 5 | ## Project Setup 6 | 7 | ```shell 8 | npm install 9 | ``` 10 | 11 | ### Compile and Hot-Reload for Development 12 | 13 | ```shell 14 | npm run dev 15 | ``` 16 | 17 | ### Type-Check, Compile and Minify for Production 18 | 19 | ```shell 20 | npm run build 21 | ``` 22 | 23 | ### Run Unit Tests with [Vitest](https://vitest.dev/) 24 | 25 | ```shell 26 | npm run test:unit 27 | ``` 28 | 29 | ### Lint with [ESLint](https://eslint.org/) 30 | 31 | ```shell 32 | npm run lint 33 | ``` 34 | 35 | ### Preview使用 36 | 37 | ```shell 38 | # stage environment 39 | pnpm preview:stage 40 | 41 | # prod environment 42 | pnpm preview:prod 43 | ``` 44 | 45 | ## 界面展示 46 | 47 | ![image-20240527131428007](./images/image-20240527131428007.png) 48 | 49 | ![image-20240527131440378](./images/image-20240527131440378.png) 50 | 51 | ![image-20240527131823450](./images/image-20240527131823450.png) 52 | 53 | ![image-20240527131840315](./images/image-20240527131840315.png) 54 | 55 | ![image-20240527131920020](./images/image-20240527131920020.png) 56 | 57 | ![image-20240527131929228](./images/image-20240527131929228.png) 58 | 59 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const ElButton: typeof import('element-plus/es')['ElButton'] 9 | const ElInput: typeof import('element-plus/es')['ElInput'] 10 | const ElMessage: typeof import('element-plus/es')['ElMessage'] 11 | const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] 12 | const ElTag: typeof import('element-plus/es')['ElTag'] 13 | } 14 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | CreateTasksWindows: typeof import('./src/components/Tasks/CreateTasksWindows.vue')['default'] 11 | CreateUserWindows: typeof import('./src/components/Users/CreateUserWindows.vue')['default'] 12 | ElAvatar: typeof import('element-plus/es')['ElAvatar'] 13 | ElButton: typeof import('element-plus/es')['ElButton'] 14 | ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] 15 | ElCard: typeof import('element-plus/es')['ElCard'] 16 | ElCol: typeof import('element-plus/es')['ElCol'] 17 | ElCollapse: typeof import('element-plus/es')['ElCollapse'] 18 | ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] 19 | ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] 20 | ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] 21 | ElDialog: typeof import('element-plus/es')['ElDialog'] 22 | ElDivider: typeof import('element-plus/es')['ElDivider'] 23 | ElDropdown: typeof import('element-plus/es')['ElDropdown'] 24 | ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] 25 | ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 26 | ElEmpty: typeof import('element-plus/es')['ElEmpty'] 27 | ElForm: typeof import('element-plus/es')['ElForm'] 28 | ElFormItem: typeof import('element-plus/es')['ElFormItem'] 29 | ElIcon: typeof import('element-plus/es')['ElIcon'] 30 | ElImage: typeof import('element-plus/es')['ElImage'] 31 | ElInput: typeof import('element-plus/es')['ElInput'] 32 | ElLink: typeof import('element-plus/es')['ElLink'] 33 | ElMenu: typeof import('element-plus/es')['ElMenu'] 34 | ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] 35 | ElOption: typeof import('element-plus/es')['ElOption'] 36 | ElPagination: typeof import('element-plus/es')['ElPagination'] 37 | ElProgress: typeof import('element-plus/es')['ElProgress'] 38 | ElRadio: typeof import('element-plus/es')['ElRadio'] 39 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 40 | ElRow: typeof import('element-plus/es')['ElRow'] 41 | ElSelect: typeof import('element-plus/es')['ElSelect'] 42 | ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] 43 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 44 | ElTable: typeof import('element-plus/es')['ElTable'] 45 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 46 | ElTabPane: typeof import('element-plus/es')['ElTabPane'] 47 | ElTabs: typeof import('element-plus/es')['ElTabs'] 48 | ElTag: typeof import('element-plus/es')['ElTag'] 49 | ElTooltip: typeof import('element-plus/es')['ElTooltip'] 50 | HeaDer: typeof import('./src/components/HeaDer.vue')['default'] 51 | LoadingView: typeof import('./src/components/Common/Loading/LoadingView.vue')['default'] 52 | ProjectList: typeof import('./src/components/Project/ProjectList.vue')['default'] 53 | ProjectVulInfoWinodws: typeof import('./src/components/Project/ProjectVulInfoWinodws.vue')['default'] 54 | RouterBreadcrumd: typeof import('./src/components/RouterBreadcrumd.vue')['default'] 55 | RouterLink: typeof import('vue-router')['RouterLink'] 56 | RouterView: typeof import('vue-router')['RouterView'] 57 | SideBar: typeof import('./src/components/SideBar.vue')['default'] 58 | TableColumn: typeof import('./src/components/Common/Tables/TableColumn.vue')['default'] 59 | TablesView: typeof import('./src/components/Common/Tables/TablesView.vue')['default'] 60 | TagsView: typeof import('./src/components/TagsView.vue')['default'] 61 | TestTable: typeof import('./src/components/Common/Test/TestTable.vue')['default'] 62 | TestVIew: typeof import('./src/components/Common/TestVIew.vue')['default'] 63 | } 64 | export interface ComponentCustomProperties { 65 | vLoading: typeof import('element-plus/es')['ElLoadingDirective'] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module "*.vue" { 3 | // 引入vue模块中ts的方法 4 | import type { DefineComponent } from "vue" 5 | // 定义vue组件以及类型注解 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } -------------------------------------------------------------------------------- /images/image-20240527131428007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/images/image-20240527131428007.png -------------------------------------------------------------------------------- /images/image-20240527131440378.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/images/image-20240527131440378.png -------------------------------------------------------------------------------- /images/image-20240527131823450.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/images/image-20240527131823450.png -------------------------------------------------------------------------------- /images/image-20240527131840315.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/images/image-20240527131840315.png -------------------------------------------------------------------------------- /images/image-20240527131920020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/images/image-20240527131920020.png -------------------------------------------------------------------------------- /images/image-20240527131929228.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/images/image-20240527131929228.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iotscan-web", 3 | "version": "1.0.0", 4 | "private": true, 5 | "web-types": "./web-types.json", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check build-only", 9 | "preview": "vite preview", 10 | "test:unit": "vitest", 11 | "build-only": "vite build", 12 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", 13 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 14 | "format": "prettier --write src/" 15 | }, 16 | "dependencies": { 17 | "@element-plus/icons-vue": "^2.1.0", 18 | "axios": "^1.5.0", 19 | "axios-retry": "^4.0.0", 20 | "echarts": "^5.4.3", 21 | "element-plus": "^2.3.12", 22 | "eslint-config-prettier": "^9.1.0", 23 | "eslint-config-typescript": "^3.0.0", 24 | "eslint-plugin-cypress": "^2.15.1", 25 | "js-base64": "^3.7.7", 26 | "pinia": "^2.1.6", 27 | "pinia-plugin-persistedstate": "^3.2.0", 28 | "vue": "^3.3.4", 29 | "vue-clipboard3": "^2.0.0", 30 | "vue-router": "^4.2.4", 31 | "vue-schart": "^2.0.0", 32 | "vxe-table": "^4.5.12", 33 | "vxe-table-plugin-element": "^3.1.0" 34 | }, 35 | "devDependencies": { 36 | "@rushstack/eslint-patch": "^1.3.2", 37 | "@tsconfig/node18": "^18.2.0", 38 | "@types/jsdom": "^21.1.1", 39 | "@types/node": "^18.18.4", 40 | "@vitejs/plugin-vue": "^4.3.1", 41 | "@vitejs/plugin-vue-jsx": "^3.0.2", 42 | "@vue/cli-plugin-typescript": "~5.0.0", 43 | "@vue/eslint-config-prettier": "^8.0.0", 44 | "@vue/eslint-config-typescript": "^11.0.3", 45 | "@vue/test-utils": "^2.4.1", 46 | "@vue/tsconfig": "^0.4.0", 47 | "eslint-plugin-vue": "^9.23.0", 48 | "jsdom": "^22.1.0", 49 | "less": "^4.2.0", 50 | "npm-run-all": "^4.1.5", 51 | "prettier": "3.0.3", 52 | "typescript": "~5.1.6", 53 | "unplugin-auto-import": "^0.16.6", 54 | "unplugin-vue-components": "^0.25.2", 55 | "vite": "4.4.9", 56 | "vite-plugin-vue-setup-extend": "^0.4.0", 57 | "vitest": "^0.34.2", 58 | "vue-tsc": "^1.8.8" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | 23 | 25 | -------------------------------------------------------------------------------- /src/api/home/index.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export default { 4 | get_home_info() { 5 | return request( 6 | { 7 | url: '/home/list', 8 | method: 'get', 9 | } 10 | ) 11 | }, 12 | get_network_info() { 13 | return request( 14 | { 15 | url: '/home/list/get_device_network_speed', 16 | method: 'get', 17 | } 18 | ) 19 | }, 20 | get_device_network() { 21 | return request( 22 | { 23 | url: '/home/list/get_device_network', 24 | method: 'get', 25 | } 26 | ) 27 | }, 28 | get_device_status() { 29 | return request( 30 | { 31 | url: '/home/list/get_device_status', 32 | method: 'get', 33 | } 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/src/api/index.ts -------------------------------------------------------------------------------- /src/api/poc/index.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export default { 4 | get_poc_list(data: { id: string, page: number, page_size: number }) { 5 | return request( 6 | { 7 | url: '/tools/poc_list', 8 | method: 'get', 9 | params: data, 10 | } 11 | ) 12 | }, 13 | get_poc_list_search(data: { keyword: string, page: number, page_size: number }) { 14 | return request( 15 | { 16 | url: '/tools/poc_list/search', 17 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 18 | method: 'post', 19 | data: data, 20 | } 21 | ) 22 | }, 23 | get_poc_init() { 24 | return request( 25 | { 26 | url: '/tools/poc_list', 27 | method: 'post', 28 | } 29 | ) 30 | }, 31 | delete_poc(data: { poc_id: string }) { 32 | return request({ 33 | url: '/tools/poc_list/delete_poc', 34 | method: 'post', 35 | data: data 36 | }) 37 | } 38 | } -------------------------------------------------------------------------------- /src/api/project/index.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export default { 4 | get_project_list(data: { page: number; page_size: number; tasks_id: string }) { 5 | /** 6 | * 获取资产信息 7 | */ 8 | return request({ 9 | url: '/tasks/result', 10 | method: 'get', 11 | params: data 12 | }) 13 | }, 14 | get_tasks_search(data: { task_ids: any; query: string; page: number; page_size: number }) { 15 | /** 16 | * 搜索任务结果数据 17 | */ 18 | return request({ 19 | url: 'tasks/result/search', 20 | method: 'get', 21 | params: data 22 | }) 23 | }, 24 | export_result(data: { task_ids: any }) { 25 | /** 26 | * 导出资产,输出为xlsx文件的二进制数据流 27 | */ 28 | return request({ 29 | url: '/tools/report/export_xlsx', 30 | method: 'get', 31 | responseType: 'blob', // 设置响应的数据为二进制数据流 32 | params: data 33 | }) 34 | }, 35 | get_tasks_result(data: { 36 | page: number 37 | page_size: number 38 | task_id: string 39 | task_type: number 40 | search: string 41 | }) { 42 | /** 43 | * 获取任务的扫描结果,其中task_type=0为端口扫描结果,task_type=1为poc扫描结果,search为搜索内容 44 | */ 45 | return request({ 46 | url: '/tasks/get_task_result', 47 | method: 'get', 48 | params: data 49 | }) 50 | }, 51 | get_tasks_statistics(data: { task_ids: any; search: string }) { 52 | /** 53 | * 获取任务的扫描结果统计聚合信息 54 | */ 55 | return request({ 56 | url: '/tasks/get_task_result/get_statistics', 57 | method: 'get', 58 | params: data 59 | }) 60 | }, 61 | get_tasks_status_details(data: { task_id: any }) { 62 | /** 63 | * 获取任务的扫描状态 64 | */ 65 | return request({ 66 | url: '/tasks/tasklist/get_tasks_status_details', 67 | method: 'get', 68 | params: data 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/api/tasks/index.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export default { 4 | get_tasks_list(data: { page: number; page_size: number; search: string }) { 5 | return request({ 6 | url: '/tasks/tasklist', 7 | method: 'get', 8 | params: data 9 | }) 10 | }, 11 | create_task(data: { 12 | host: string[] 13 | desc: string 14 | scanning_speed: number 15 | is_use_port_scan: boolean 16 | port_type: string 17 | is_use_proxy: boolean 18 | poc_type: number 19 | poc_warehouse_ids: string[] 20 | }) { 21 | return request({ 22 | url: '/tasks/tasklist/create_task', 23 | method: 'post', 24 | data: data 25 | }) 26 | }, 27 | delete_tasks(data: { tasks_id: string }) { 28 | return request({ 29 | url: '/tasks/tasklist/task_delete', 30 | method: 'post', 31 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 32 | data: data 33 | }) 34 | }, 35 | setting_tasks(data: { task_ids: string; task_status: number }) { 36 | return request({ 37 | url: '/tasks/tasklist/task_setting', 38 | method: 'post', 39 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 40 | data: data 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/api/user/index.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export default { 4 | login(data: { username: string; password: string }) { 5 | return request({ 6 | url: '/users/login', 7 | method: 'post', 8 | data: data 9 | }) 10 | }, 11 | logout(data: { refresh: string }) { 12 | return request({ 13 | url: '/users/logout', 14 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 15 | method: 'post', 16 | data: data 17 | }) 18 | }, 19 | rest_password(data: { new_password: string; old_password: string }) { 20 | return request({ 21 | url: '/users/user_setting/ChangePassword', 22 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 23 | method: 'post', 24 | data: data 25 | }) 26 | }, 27 | rest_api_key() { 28 | return request({ 29 | url: '/users/user_setting/reset_api_key', 30 | method: 'get' 31 | }) 32 | }, 33 | getUserInfo() { 34 | return request({ 35 | url: '/users/getuserinfo', 36 | method: 'get' 37 | }) 38 | }, 39 | getUserList(data: { page: number; page_size: number; search: string }) { 40 | return request({ 41 | url: '/users/getuserinfo/get_user_list', 42 | method: 'get', 43 | params: data 44 | }) 45 | }, 46 | create_user(data: { 47 | username: string 48 | password: string 49 | password_again: string 50 | phone: string 51 | email: string 52 | permissions: number | string 53 | }) { 54 | return request({ 55 | url: '/users/user_setting/create_user', 56 | method: 'post', 57 | data: data 58 | }) 59 | }, 60 | delete_user(data: { user_id: string }) { 61 | return request({ 62 | url: '/users/user_setting/delete_user', 63 | method: 'post', 64 | data: data 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | font-weight: normal; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | color: var(--color-text); 64 | background: var(--color-background); 65 | transition: color 0.5s, background-color 0.5s; 66 | line-height: 1.6; 67 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 68 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 69 | font-size: 15px; 70 | text-rendering: optimizeLegibility; 71 | -webkit-font-smoothing: antialiased; 72 | -moz-osx-font-smoothing: grayscale; 73 | } 74 | -------------------------------------------------------------------------------- /src/assets/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/src/assets/login.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | html, 7 | body, 8 | #app, 9 | .wrapper { 10 | width: 100%; 11 | height: 100%; 12 | overflow: hidden; 13 | } 14 | 15 | body { 16 | font-family: 'PingFang SC', "Helvetica Neue", Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif; 17 | } 18 | 19 | a { 20 | text-decoration: none 21 | } 22 | 23 | 24 | .content-box { 25 | position: absolute; 26 | left: 250px; 27 | right: 0; 28 | top: 70px; 29 | bottom: 0; 30 | padding-bottom: 30px; 31 | -webkit-transition: left .3s ease-in-out; 32 | transition: left .3s ease-in-out; 33 | background: #fff; 34 | } 35 | 36 | .content { 37 | width: auto; 38 | height: 100%; 39 | padding: 15px; 40 | overflow-y: scroll; 41 | box-sizing: border-box; 42 | } 43 | 44 | .content-collapse { 45 | left: 65px; 46 | } 47 | 48 | .container { 49 | padding: 30px; 50 | background: #fff; 51 | border: 1px solid #ddd; 52 | border-radius: 5px; 53 | } 54 | 55 | .crumbs { 56 | margin: 10px 0; 57 | } 58 | 59 | .el-table th { 60 | background-color: #f5f7fa !important; 61 | } 62 | 63 | .pagination { 64 | margin: 20px 0; 65 | text-align: right; 66 | } 67 | 68 | .plugins-tips { 69 | padding: 20px 10px; 70 | margin-bottom: 20px; 71 | } 72 | 73 | .el-button + .el-tooltip { 74 | margin-left: 10px; 75 | } 76 | 77 | .el-table tr:hover { 78 | background: #f6faff; 79 | } 80 | 81 | .mgb20 { 82 | margin-bottom: 20px; 83 | } 84 | 85 | .move-enter-active, 86 | .move-leave-active { 87 | transition: opacity .1s ease; 88 | } 89 | 90 | .move-enter-from, 91 | .move-leave-to { 92 | opacity: 0; 93 | } 94 | 95 | /*BaseForm*/ 96 | 97 | .form-box { 98 | width: 600px; 99 | } 100 | 101 | .form-box .line { 102 | text-align: center; 103 | } 104 | 105 | .el-time-panel__content::after, 106 | .el-time-panel__content::before { 107 | margin-top: -7px; 108 | } 109 | 110 | .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) { 111 | padding-bottom: 0; 112 | } 113 | 114 | 115 | [class*=" el-icon-"], [class^=el-icon-] { 116 | speak: none; 117 | font-style: normal; 118 | font-weight: 400; 119 | font-variant: normal; 120 | text-transform: none; 121 | line-height: 1; 122 | vertical-align: baseline; 123 | display: inline-block; 124 | -webkit-font-smoothing: antialiased; 125 | -moz-osx-font-smoothing: grayscale; 126 | } 127 | 128 | .el-sub-menu [class^=el-icon-] { 129 | vertical-align: middle; 130 | margin-right: 5px; 131 | width: 24px; 132 | text-align: center; 133 | font-size: 18px; 134 | } 135 | 136 | [hidden] { 137 | display: none !important; 138 | } -------------------------------------------------------------------------------- /src/components/Common/Loading/LoadingCss.css: -------------------------------------------------------------------------------- 1 | .easy-loading { 2 | background-color: rgba(0, 0, 0, 0.5); 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | bottom: 0; 7 | right: 0; 8 | z-index: 9999; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | } 13 | 14 | .easy-loading .message { 15 | margin-top: 160px; 16 | color: #f2f2f2; 17 | } 18 | 19 | .easy-loading .loading { 20 | position: absolute; 21 | margin: auto; 22 | top: 0; 23 | bottom: 0; 24 | left: 0; 25 | right: 0; 26 | width: 6.25em; 27 | height: 6.25em; 28 | animation: rotate 2.4s linear infinite; 29 | } 30 | 31 | .easy-loading .loading .white { 32 | top: 0; 33 | bottom: 0; 34 | left: 0; 35 | right: 0; 36 | background: white; 37 | animation: flash 2.4s linear infinite; 38 | opacity: 0; 39 | } 40 | 41 | .easy-loading .loading .dot { 42 | position: absolute; 43 | margin: auto; 44 | width: 2.4em; 45 | height: 2.4em; 46 | border-radius: 100%; 47 | transition: all 1s ease; 48 | } 49 | 50 | .easy-loading .loading .dot:nth-child(2) { 51 | top: 0; 52 | bottom: 0; 53 | left: 0; 54 | background: #ff4444; 55 | animation: dotsY 2.4s linear infinite; 56 | } 57 | 58 | .easy-loading .loading .dot:nth-child(3) { 59 | left: 0; 60 | right: 0; 61 | top: 0; 62 | background: #ffbb33; 63 | animation: dotsX 2.4s linear infinite; 64 | } 65 | 66 | .easy-loading .loading .dot:nth-child(4) { 67 | top: 0; 68 | bottom: 0; 69 | right: 0; 70 | background: #99cc00; 71 | animation: dotsY 2.4s linear infinite; 72 | } 73 | 74 | .easy-loading .loading .dot:nth-child(5) { 75 | left: 0; 76 | right: 0; 77 | bottom: 0; 78 | background: #33b5e5; 79 | animation: dotsX 2.4s linear infinite; 80 | } 81 | 82 | @keyframes rotate { 83 | 0% { 84 | transform: rotate(0); 85 | } 86 | 10% { 87 | width: 6.25em; 88 | height: 6.25em; 89 | } 90 | 66% { 91 | width: 2.4em; 92 | height: 2.4em; 93 | } 94 | 100% { 95 | transform: rotate(360deg); 96 | width: 6.25em; 97 | height: 6.25em; 98 | } 99 | } 100 | 101 | @keyframes dotsY { 102 | 66% { 103 | opacity: 0.1; 104 | width: 2.4em; 105 | } 106 | 77% { 107 | opacity: 1; 108 | width: 0; 109 | } 110 | } 111 | 112 | @keyframes dotsX { 113 | 66% { 114 | opacity: 0.1; 115 | height: 2.4em; 116 | } 117 | 77% { 118 | opacity: 1; 119 | height: 0; 120 | } 121 | } 122 | 123 | @keyframes flash { 124 | 33% { 125 | opacity: 0; 126 | border-radius: 0; 127 | } 128 | 55% { 129 | opacity: 0.6; 130 | border-radius: 100%; 131 | } 132 | 66% { 133 | opacity: 0; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/components/Common/Loading/LoadingView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/Common/Loading/index.ts: -------------------------------------------------------------------------------- 1 | import {createVNode, render} from 'vue' 2 | import type {VNode, App} from 'vue'; 3 | 4 | import Loading from './LoadingView.vue' 5 | 6 | const showLoading = (message?: string) => { 7 | const vNode: VNode = createVNode(Loading, {message: message}) 8 | render(vNode, document.body) 9 | } 10 | const hideLoading = () => { 11 | render(null, document.body) 12 | } 13 | 14 | export default { 15 | install(app: App) { 16 | // Vue 提供的全局配置 可以自定义 17 | app.config.globalProperties.$easyLoading = { 18 | showLoading, 19 | hideLoading 20 | } 21 | }, 22 | showLoading, 23 | hideLoading 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Common/Tables/Config/config.ts: -------------------------------------------------------------------------------- 1 | import {h} from 'vue' 2 | import {ElInput, ElMessage} from "element-plus"; 3 | import useClipboard from "vue-clipboard3"; 4 | import {CopyDocument} from "@element-plus/icons-vue"; 5 | 6 | const {toClipboard} = useClipboard() 7 | // 基本表格配置 8 | export const tableColumn: Table.Column[] = [ 9 | {type: 'selection', width: '50'}, 10 | {type: 'index', width: '50', label: 'No.'}, 11 | {prop: 'name', label: '名字', sortable: true}, 12 | {prop: 'test', label: 'test'}, 13 | {type: 'date', prop: 'date', label: '日期'}, 14 | {prop: 'address', label: '地址', slot: 'address', showOverflowTooltip: true}, 15 | { 16 | width: '240', 17 | label: '操作', 18 | buttons: [ 19 | {name: '编辑', type: 'primary', icon: 'Edit', command: 'edit'}, 20 | {name: '删除', type: 'danger', icon: 'Edit', command: 'delete'} 21 | ] 22 | } 23 | ] 24 | 25 | interface User { 26 | date: number 27 | name: string 28 | address: string 29 | } 30 | 31 | const renderExpandContent = (row: User) => 32 | h('div', {style: 'margin-left: 50px;margin-right: 50px;'}, [ 33 | h('p', `名字:${row.name}`), 34 | h('p', `地址:${row.address}`), 35 | h('p', `日期:${row.date}`), 36 | h('div', { 37 | style: 'display: flex;' 38 | }, [ 39 | h('div', {style: 'flex: 1; position: relative; margin-right: 25px;'}, [ 40 | h(ElInput, { 41 | type: 'textarea', 42 | rows: 16, 43 | modelValue: row.date, 44 | readonly: true, 45 | }), 46 | h(CopyDocument, { 47 | style: 'position: absolute; top: 10px; right:25px;float: right; border: 1px padding: 1px;width:15px;height:15px', 48 | onClick: async () => { 49 | await toClipboard(String(row.date)) 50 | ElMessage.success("复制成功") 51 | } 52 | }) 53 | ]), 54 | h('div', {style: 'flex: 1; position: relative; margin-left: 25px;'}, [ 55 | h(ElInput, { 56 | type: 'textarea', 57 | rows: 16, 58 | modelValue: row.address, 59 | readonly: true, 60 | }), 61 | h(CopyDocument, { 62 | style: 'position: absolute; top: 10px; right:25px;float: right; border: 1px padding: 1px;width:15px;height:15px', 63 | onClick: async () => { 64 | console.log(row.address) 65 | await toClipboard(row.address) 66 | ElMessage.success("复制成功") 67 | } 68 | }) 69 | ]) 70 | ]) 71 | ]) 72 | 73 | 74 | // 带有分页的表格配置 75 | export const tableDemoColumn: Table.Column[] = [ 76 | {type: 'expand', width: '50', render: ({row}) => renderExpandContent(row)}, 77 | {type: 'index', width: '65', label: 'No.', align: 'center'}, 78 | {prop: 'avatar', label: '头像', width: '100', align: 'center'}, 79 | {prop: 'address', label: '地址', slot: 'address', showOverflowTooltip: true, width: '180px'}, 80 | {prop: 'name', label: '姓名', width: '100'}, 81 | {prop: 'age', label: '年龄', width: '90', align: 'center'}, 82 | {prop: 'gender', label: '性别', width: '90', slot: 'gender', align: 'center'}, 83 | {prop: 'mobile', label: '手机号', width: '180'}, 84 | {prop: 'email', label: '邮箱', showOverflowTooltip: true}, 85 | { 86 | width: '220', 87 | label: '操作', 88 | buttons: [ 89 | {name: '编辑', type: 'success', command: 'edit', icon: 'Edit'}, 90 | {name: '删除', type: 'danger', command: 'delete', icon: 'Delete'} 91 | ] 92 | } 93 | ] 94 | -------------------------------------------------------------------------------- /src/components/Common/Tables/Search/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/src/components/Common/Tables/Search/index.ts -------------------------------------------------------------------------------- /src/components/Common/Tables/TableColumn.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 84 | 85 | -------------------------------------------------------------------------------- /src/components/Common/Tables/TablesView.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 156 | 157 | 165 | -------------------------------------------------------------------------------- /src/components/Common/Tables/index.ts: -------------------------------------------------------------------------------- 1 | import type {TableColumnCtx} from 'element-plus/es/components/table/src/table-column/defaults' 2 | import {type TableInstance} from 'element-plus' 3 | 4 | type SortParams = { 5 | column: TableColumnCtx 6 | order: any 7 | prop: string 8 | } 9 | /** 10 | * 获取 element 的事件类型 11 | * type ElTableEmitsType = TableInstance['$emit'] 12 | * type EmitsEvent = { (event: Event, ...args: any[]): void } & ElTableEmitsType 13 | * 但是 vite 加载失败,无法解析 报错如下 14 | * [@vue/compiler-sfc] Unresolvable type reference or unsupported built-in utility type 15 | * 暂时没找到更好的解决办法 16 | * 所以,暂时先手动定义 element 表格的事件类型 17 | */ 18 | // element table 的事件 19 | const elementEvents = [ 20 | 'select', 21 | 'select-all', 22 | 'selection-change', 23 | 'cell-mouse-enter', 24 | 'cell-mouse-leave', 25 | 'cell-contextmenu', 26 | 'cell-click', 27 | 'cell-dblclick', 28 | 'row-click', 29 | 'row-contextmenu', 30 | 'row-dblclick', 31 | 'header-click', 32 | 'header-contextmenu', 33 | 'sort-change', 34 | 'filter-change', 35 | 'current-change', 36 | 'header-dragend', 37 | 'expand-change' 38 | ] as const 39 | type ElTableEmitsType = (typeof elementEvents)[number] 40 | type Event = 41 | | 'command' 42 | | 'size-change' 43 | | 'current-change' 44 | | 'pagination-change' 45 | | 'search' 46 | | 'refresh' 47 | | 'edit-cancel' 48 | | 'edit-end' 49 | | ElTableEmitsType 50 | 51 | type EmitsEvent = (event: Event, ...args: any[]) => void 52 | export type {SortParams, EmitsEvent, TableInstance} 53 | -------------------------------------------------------------------------------- /src/components/Common/Tables/useTableContext.ts: -------------------------------------------------------------------------------- 1 | import {provide, inject} from 'vue'; 2 | import type {EmitsEvent} from './index' 3 | 4 | const key = Symbol('easy-table') 5 | 6 | type Instance = { 7 | options: any 8 | emit: EmitsEvent; 9 | }; 10 | 11 | 12 | export function createTableContext(instance: Instance) { 13 | provide(key, instance) 14 | } 15 | 16 | export function useTableContext(): Instance { 17 | return inject(key) as Instance 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Common/Test/TestTable.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expzhizhuo/iotscan-web/bcc35e77745c2b01baa9432b95587e113e2596e3/src/components/Common/Test/TestTable.vue -------------------------------------------------------------------------------- /src/components/Common/TestVIew.vue: -------------------------------------------------------------------------------- 1 | 135 | 136 | 166 | 167 | -------------------------------------------------------------------------------- /src/components/Common/WaterMark/WaterMark.ts: -------------------------------------------------------------------------------- 1 | // 全局保存 canvas 和 div ,避免重复创建(单例模式) 2 | const globalCanvas = document.createElement('canvas'); 3 | const globalWaterMark = document.createElement('div'); 4 | 5 | // 返回一个包含图片展示的 数据URL 6 | const getDataUrl = (binding: any) => { 7 | const rotate = -20; 8 | const canvas = globalCanvas || document.createElement('canvas'); 9 | const ctx = canvas.getContext('2d'); // 获取canvas画布的绘图环境 10 | canvas.width = 360; // 单个水印大小 11 | canvas.height = 180; 12 | ctx?.rotate((rotate * Math.PI) / 180); // 水印旋转角度 13 | ctx.font = binding.font || '16px Vedana'; 14 | ctx.fillStyle = binding.fillStyle || 'rgba(200, 200, 200, 0.30)'; 15 | ctx.textBaseline = 'middle'; 16 | ctx?.fillText(binding.text || '默认水印文字', canvas.width / 10, canvas.height / 2); 17 | return canvas.toDataURL('image/png'); 18 | }; 19 | 20 | // watermark 样式 21 | let style = ` 22 | width: 100%; 23 | height: 100%; 24 | display: block; 25 | overflow: hidden; 26 | position: absolute; 27 | left: 0px; 28 | top: 0px; 29 | z-index: 100000; 30 | font-size: 12px; 31 | background-repeat: repeat; 32 | background-position: center; 33 | pointer-events: none;`; 34 | 35 | // 定义指令配置项 36 | const directives: any = { 37 | mounted(el: HTMLElement, binding: any) { 38 | // 注意img有onload的方法,如果自定义指令注册在html标签的话,只需要init(el, binding.value) 39 | // el.onload = init.bind(null, el, binding); 40 | init(el, binding.value); 41 | }, 42 | }; 43 | 44 | // 初始化 45 | const init = (el: HTMLElement, binding: any = {}) => { 46 | // 设置水印 47 | setWaterMark(el, binding); 48 | // 启动监控 49 | createObserver(el, binding); 50 | }; 51 | 52 | // 设置水印 53 | const setWaterMark = (el: HTMLElement, binding: any) => { 54 | const {parentElement} = el; 55 | // const {width, height} = parentElement!.getBoundingClientRect(); 56 | // 获取对应的 canvas 画布相关的 base64 url 57 | const url = getDataUrl(binding); 58 | 59 | // 创建 waterMark 父元素 60 | const waterMark = globalWaterMark || document.createElement('div'); 61 | waterMark.className = `pdp-water-mark`; // 方便自定义展示结果 62 | // style = `${style}background-image: url(${url});width:${width}px; height:${height}px`; 63 | style = `${style}background-image: url(${url});`; 64 | waterMark.setAttribute('style', style); 65 | // 如果父元素有自己的stayle 则获取后和自定义的拼接,并避免重复添加 66 | let currStyle = parentElement?.getAttribute('style') ? parentElement?.getAttribute('style') : ''; 67 | currStyle = currStyle?.includes('position: relative') 68 | ? currStyle 69 | : currStyle + 'position: relative;'; 70 | // 将对应图片的父容器作为定位元素 71 | parentElement?.setAttribute('style', currStyle); 72 | 73 | // 将图片元素移动到 waterMark 中 74 | parentElement?.appendChild(waterMark); 75 | }; 76 | 77 | /** 78 | * 监听 DOM 变化 79 | * 用 MutationObserver 对水印元素进行监听,删除时,我们再立即生成一个水印元素就可以了 80 | * @param el 81 | * @param binding 82 | */ 83 | const createObserver = (el: any, binding: any) => { 84 | const waterMarkEl = el.parentElement?.querySelector('.pdp-water-mark'); 85 | const observer = new MutationObserver((mutationsList) => { 86 | // console.log('mutationsList', mutationsList); 87 | if (mutationsList.length) { 88 | const {removedNodes, type, target} = mutationsList[0]; 89 | const currStyle = waterMarkEl?.getAttribute('style'); 90 | 91 | // 证明被删除了 92 | if (removedNodes[0] === waterMarkEl) { 93 | // 停止观察。调用该方法后,DOM 再发生变动,也不会触发观察器 94 | observer.disconnect(); 95 | // 初始化(设置水印,启动监控) 96 | init(el, binding); 97 | } else if (type === 'attributes' && target === waterMarkEl && currStyle !== style) { 98 | waterMarkEl.setAttribute('style', style); 99 | } 100 | } 101 | }); 102 | 103 | observer.observe(el.parentElement, { 104 | childList: true, 105 | attributes: true, 106 | subtree: true, 107 | }); 108 | }; 109 | 110 | export default { 111 | name: 'watermark', 112 | directives, 113 | }; 114 | 115 | -------------------------------------------------------------------------------- /src/components/Common/WaterMark/index.ts: -------------------------------------------------------------------------------- 1 | import type {App} from 'vue'; 2 | import watermark from './WaterMark'; 3 | 4 | export default function installDirective(app: App) { 5 | // 水印指令 6 | app.directive(watermark.name, watermark.directives); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/components/HeaDer.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 90 | 91 | 134 | -------------------------------------------------------------------------------- /src/components/Project/ProjectList.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 265 | 266 | 415 | -------------------------------------------------------------------------------- /src/components/Project/ProjectVulInfoWinodws.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 123 | 124 | -------------------------------------------------------------------------------- /src/components/Project/SideBar.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 70 | 71 | 91 | -------------------------------------------------------------------------------- /src/components/RouterBreadcrumd.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/SideBar.vue: -------------------------------------------------------------------------------- 1 | 132 | 133 | 187 | 188 | 208 | -------------------------------------------------------------------------------- /src/components/TagsView.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 92 | 93 | -------------------------------------------------------------------------------- /src/components/Tasks/CreateTasksWindows.vue: -------------------------------------------------------------------------------- 1 | 136 | 137 | 196 | 197 | 203 | -------------------------------------------------------------------------------- /src/components/Users/CreateUserWindows.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {createApp} from "vue" 2 | import ElementPlus from "element-plus" 3 | import {createPinia} from "pinia" 4 | import * as ElementPlusIconsVue from "@element-plus/icons-vue" 5 | import App from "./App.vue" 6 | import router from "./router" 7 | import {usePermissStore} from "@/stores/userpermiss" 8 | import {useUserStore} from "@/stores/user" 9 | import piniaPluginPersistedstate from "pinia-plugin-persistedstate" 10 | import "element-plus/dist/index.css" 11 | import "./assets/main.css" 12 | 13 | import plugins from '@/plugins' 14 | 15 | import directives from "@/components/Common/WaterMark"; 16 | 17 | 18 | // 执行方法得到实例 19 | const pinia = createPinia() 20 | pinia.use(piniaPluginPersistedstate) 21 | 22 | const app = createApp(App) 23 | app.use(pinia) 24 | app.use(ElementPlus) 25 | app.use(directives) 26 | plugins(app) 27 | 28 | // 自定义权限指令 29 | const permiss = usePermissStore() 30 | app.directive("permiss", { 31 | mounted(el, binding) { 32 | if (!permiss.key.includes(String(binding.value))) { 33 | el["hidden"] = true 34 | } 35 | } 36 | }) 37 | 38 | router.beforeEach((to, from, next) => { 39 | document.title = `${to.meta.title} | iotscan管理系统` 40 | const users = useUserStore() 41 | const permiss = usePermissStore() 42 | if (!router.hasRoute(to.name ?? '')) { 43 | next('/404') 44 | } else if (!users.username && to.path !== "/login") { 45 | next("/login") 46 | } else if (to.meta.permiss && !permiss.key.includes(String(to.meta.permiss))) { 47 | // 如果没有权限,则进入401 48 | next("/401") 49 | } else { 50 | next() 51 | } 52 | }) 53 | // 导入element plus的icon库 54 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 55 | app.component(key, component) 56 | } 57 | app.use(router) 58 | 59 | app.mount("#app") 60 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type {App} from 'vue' 2 | 3 | export default (app: App) => { 4 | register(app, import.meta.glob('./**/index.ts', {eager: true})) 5 | } 6 | 7 | function register(app: App, modules: Record) { 8 | Object.entries(modules).map(([, module]) => { 9 | module.default(app) 10 | }) 11 | } 12 | 13 | type IEasyLoading = { 14 | showLoading: (msg?: string) => void 15 | hideLoading: () => void 16 | } 17 | 18 | // export interface I$EasyMessage extends IEasyMessage { 19 | // config: (options: EasyMessageOption) => EasyMessageResult 20 | // closeMessage: () => void 21 | // isShow: boolean 22 | // } 23 | 24 | // 编写ts 插件声明文件防止报错 和 智能提示 25 | declare module '@vue/runtime-core' { 26 | export interface ComponentCustomProperties { 27 | $easyLoading: IEasyLoading 28 | // $easyMessage: I$EasyMessage 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/plugins/loading/index.ts: -------------------------------------------------------------------------------- 1 | import type {App} from 'vue' 2 | import Loading from '@/components/Common/Loading' 3 | 4 | export default (app: App) => { 5 | app.use(Loading) 6 | } 7 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import Home from '../views/HomeView.vue' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | redirect: '/dashboard' 10 | }, 11 | { 12 | path: '/project', 13 | redirect: '/project/info' 14 | }, 15 | { 16 | path: '/', 17 | name: 'Home', 18 | component: Home, 19 | children: [ 20 | { 21 | path: '/dashboard', 22 | name: 'dashboard', 23 | meta: { 24 | title: '系统首页', 25 | permiss: 1 26 | }, 27 | component: () => import('@/views/home/DashBoard.vue') 28 | }, 29 | { 30 | path: '/Tasks', 31 | name: 'Tasks', 32 | meta: { 33 | title: '任务列表', 34 | permiss: '2' 35 | }, 36 | component: () => import('@/views/tasks/TasksView.vue') 37 | }, 38 | // { 39 | // path: "/celery", 40 | // name: "celery", 41 | // meta: { 42 | // title: "celery任务列表", 43 | // permiss: "2" 44 | // }, 45 | // component: () => import("@/views/tasks/TasksCeleryView.vue") 46 | // }, 47 | { 48 | path: '/report', 49 | name: 'report', 50 | meta: { 51 | title: '报告列表', 52 | permiss: '2' 53 | }, 54 | component: () => import('@/views/tasks/TasksReportView.vue') 55 | }, 56 | { 57 | path: '/vul_list', 58 | name: 'vul_list', 59 | meta: { 60 | title: 'POC列表', 61 | permiss: '2' 62 | }, 63 | component: () => import('@/views/poc/PocListView.vue') 64 | }, 65 | { 66 | path: '/user_list', 67 | name: 'user_list', 68 | meta: { 69 | title: '用户列表', 70 | permiss: '2' 71 | }, 72 | component: () => import('@/views/user/UserView.vue') 73 | }, 74 | { 75 | path: '/user', 76 | name: 'user_list', 77 | meta: { 78 | title: '用户列表', 79 | permiss: '2' 80 | }, 81 | component: () => import('@/views/user/UserView.vue') 82 | }, 83 | { 84 | path: '/user/resetpassword', 85 | name: 'resetpassword', 86 | meta: { 87 | title: '重置密码', 88 | permiss: '1' 89 | }, 90 | component: () => import('@/views/user/ResetPassword.vue') 91 | }, 92 | { 93 | path: '/user/info', 94 | name: 'userinfo', 95 | meta: { 96 | title: '个人中心', 97 | permiss: '1' 98 | }, 99 | component: () => import('@/views/user/UserInfo.vue') 100 | } 101 | ] 102 | }, 103 | { 104 | path: '/project', 105 | name: '任务详情', 106 | meta: { 107 | title: '任务详情', 108 | permiss: '2' 109 | }, 110 | component: () => import('@/views/project/ProJectView.vue'), 111 | children: [ 112 | { 113 | path: '/project/info', 114 | name: '项目信息', 115 | meta: { 116 | title: '项目信息', 117 | permiss: 2 118 | }, 119 | component: () => import('@/views/project/View/ProJectInfo.vue') 120 | }, 121 | { 122 | path: '/project/list', 123 | name: '任务详情', 124 | meta: { 125 | title: '任务详情', 126 | permiss: 2 127 | }, 128 | component: () => import('@/views/project/View/ProJectList.vue') 129 | }, 130 | { 131 | path: '/project/vul_list', 132 | name: '漏洞信息', 133 | meta: { 134 | title: '漏洞信息', 135 | permiss: 2 136 | }, 137 | component: () => import('@/views/project/View/ProJectVulList.vue') 138 | } 139 | ] 140 | }, 141 | { 142 | path: '/login', 143 | name: '登陆', 144 | meta: { 145 | title: '登陆' 146 | }, 147 | component: () => import('@/views/login/LoginView.vue') 148 | }, 149 | { 150 | path: '/404', 151 | name: '404', 152 | meta: { 153 | title: '404' 154 | }, 155 | component: () => import('@/views/error/404View.vue') 156 | }, 157 | { 158 | path: '/401', 159 | name: '401', 160 | meta: { 161 | title: '401' 162 | }, 163 | component: () => import('@/views/error/401View.vue') 164 | }, 165 | { 166 | path: '/echarts', 167 | name: 'echarts', 168 | component: () => import('@/views/echarts/NetworkEcharts.vue') 169 | }, 170 | { 171 | path: '/test', 172 | name: 'test', 173 | meta: { 174 | title: '测试页面' 175 | }, 176 | component: () => import('@/components/Common/TestVIew.vue') 177 | } 178 | ] 179 | }) 180 | 181 | export default router 182 | -------------------------------------------------------------------------------- /src/stores/counter.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useCounterStore = defineStore('counter', () => { 5 | const count = ref(0) 6 | const doubleCount = computed(() => count.value * 2) 7 | function increment() { 8 | count.value++ 9 | } 10 | 11 | return { count, doubleCount, increment } 12 | }) 13 | -------------------------------------------------------------------------------- /src/stores/sidebar.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia" 2 | 3 | export const useSidebarStore = defineStore("sidebar", { 4 | state: () => { 5 | return { 6 | collapse: false 7 | } 8 | }, 9 | getters: {}, 10 | actions: { 11 | handleCollapse() { 12 | this.collapse = !this.collapse 13 | } 14 | } 15 | }) -------------------------------------------------------------------------------- /src/stores/tags.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | 3 | interface ListItem { 4 | name: string; 5 | path: string; 6 | title: string; 7 | } 8 | 9 | export const useTagsStore = defineStore('tags', { 10 | state: () => { 11 | return { 12 | list: [] 13 | }; 14 | }, 15 | getters: { 16 | show: state => { 17 | return state.list.length > 0; 18 | }, 19 | nameList: state => { 20 | return state.list.map(item => item.name); 21 | } 22 | }, 23 | actions: { 24 | delTagsItem(index: number) { 25 | this.list.splice(index, 1); 26 | }, 27 | setTagsItem(data: ListItem) { 28 | this.list.push(data); 29 | }, 30 | clearTags() { 31 | this.list = []; 32 | }, 33 | closeTagsOther(data: ListItem[]) { 34 | this.list = data; 35 | }, 36 | closeCurrentTag(data: any) { 37 | for (let i = 0, len = this.list.length; i < len; i++) { 38 | const item = this.list[i]; 39 | if (item.path === data.$route.fullPath) { 40 | if (i < len - 1) { 41 | data.$router.push(this.list[i + 1].path); 42 | } else if (i > 0) { 43 | data.$router.push(this.list[i - 1].path); 44 | } else { 45 | data.$router.push('/'); 46 | } 47 | this.list.splice(i, 1); 48 | break; 49 | } 50 | } 51 | } 52 | }, 53 | persist: { 54 | key: "tag", 55 | storage: sessionStorage, 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /src/stores/taskinfo.ts: -------------------------------------------------------------------------------- 1 | import {defineStore} from 'pinia'; 2 | import router from '@/router/index' 3 | 4 | export const useTaskStore = defineStore('tasks', { 5 | state: () => ({ 6 | task_ids: '', 7 | }), 8 | actions: { 9 | //储存任务id 10 | setTaskIds(val: string) { 11 | this.task_ids = val 12 | }, 13 | getTaskIds() { 14 | return this.task_ids 15 | }, 16 | clear() { 17 | this.task_ids = '' 18 | router.push('/Tasks') 19 | } 20 | }, 21 | persist: { 22 | key: "task_info", 23 | storage: sessionStorage, 24 | paths: ["task_ids"] 25 | }, 26 | }) -------------------------------------------------------------------------------- /src/stores/user.ts: -------------------------------------------------------------------------------- 1 | // 用户信息pinia配置信息 2 | 3 | import {defineStore} from 'pinia' 4 | import router from '@/router/index' 5 | 6 | export const useUserStore = defineStore('user', { 7 | state: () => ({ 8 | token: '', 9 | roles: '', 10 | username: '', 11 | permiss: '', 12 | refresh: '' 13 | }), 14 | actions: { 15 | // 登录成功后将返回的 token 存起来 16 | setToken(val: string) { 17 | this.token = val 18 | }, 19 | getToken() { 20 | return this.token 21 | }, 22 | setRoles(val: string) { 23 | this.roles = val 24 | }, 25 | getRoles() { 26 | return this.roles 27 | }, 28 | setPermiss(val: string) { 29 | this.permiss = val 30 | }, 31 | getPermiss() { 32 | return this.permiss 33 | }, 34 | setUserName(val: string) { 35 | this.username = val 36 | }, 37 | getUserName() { 38 | return this.username 39 | }, 40 | setRefresh(val: string) { 41 | this.refresh = val 42 | }, 43 | getRefresh() { 44 | return this.refresh 45 | }, 46 | // 清空 token 和角色并跳转到登录页 47 | logout() { 48 | this.token = '' 49 | this.roles = '' 50 | this.username = '' 51 | this.permiss = '' 52 | this.refresh = '' 53 | router.push('/login') 54 | } 55 | }, 56 | persist: { 57 | key: 'token', 58 | storage: localStorage, 59 | paths: ['token', 'roles', 'username', 'permiss', 'refresh'] 60 | } 61 | }) -------------------------------------------------------------------------------- /src/stores/userpermiss.ts: -------------------------------------------------------------------------------- 1 | // noinspection TypeScriptValidateTypes 2 | 3 | import {defineStore} from "pinia" 4 | 5 | interface ObjectList { 6 | [key: string]: string[]; 7 | } 8 | 9 | export const usePermissStore = defineStore("permiss", { 10 | state: () => { 11 | return { 12 | key: [], 13 | defaultList: { 14 | admin: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16"], 15 | user: ["1", "2", "3", "11", "13", "14", "15"] 16 | } 17 | } 18 | }, 19 | 20 | actions: { 21 | handleSet(val: string[]) { 22 | this.key = val 23 | }, 24 | clear() { 25 | this.key = [] 26 | } 27 | }, 28 | persist: { 29 | storage: localStorage, 30 | paths: ["key"], 31 | } 32 | }) -------------------------------------------------------------------------------- /src/type/table/index.d.ts: -------------------------------------------------------------------------------- 1 | // table表格 2 | declare namespace Table { 3 | type VNodeChild = import('vue').VNodeChild 4 | type Type = 'selection' | 'index' | 'expand' | 'image' | 'date' 5 | type Size = 'large' | 'default' | 'small' 6 | type Align = 'center' | 'left' | 'right' 7 | type Command = string | number 8 | type DateFormat = 'YYYY-MM-DD' | 'YYYY-MM-DD HH:mm:ss' | 'YYYY-MM-DD HH:mm' | 'YYYY-MM' 9 | type Order = 'ascending' | 'descending' 10 | 11 | interface ButtonItem { 12 | name: string, 13 | icon?: string, 14 | command: Command, 15 | size?: Size 16 | type?: 'primary' | 'success' | 'warning' | 'danger' | 'info', 17 | } 18 | 19 | interface Sort { 20 | prop: string 21 | order: Order 22 | init?: any 23 | silent?: any 24 | } 25 | 26 | interface Column { 27 | // 对应列的类型。 如果设置了selection则显示多选框; 如果设置了 index 则显示该行的索引(从 1 开始计算); 如果设置了 expand 则显示为一个可展开的按钮 28 | type?: Type, 29 | label?: string, 30 | prop?: string, 31 | slot?: string 32 | width?: string, 33 | icon?: string, 34 | align?: Align, 35 | dateFormat?: DateFormat // 显示在页面中的日期格式,简单列举了几种格式, 可自行配置 36 | showOverflowTooltip?: boolean, //超出固定长度是不是进行缩放展示 37 | buttons?: ButtonItem[], 38 | render?: (row?: any, index?: number) => VNodeChild // 渲染函数,渲染这一列的每一行的单元格 39 | sortable?: boolean | 'custom', // 对应列是否可以排序, 如果设置为 'custom',则代表用户希望远程排序,需要监听 Table 的 sort-change 事件 40 | headerRender?: ({column, index}) => VNodeChild, // 渲染函数,渲染列表头 41 | headerSlot?: string, // 自定义表头插槽名字 42 | children?: Column[] // 配置多级表头的数据集合, 具体用法可参考多级表头使用示例。 43 | } 44 | 45 | interface Options { 46 | height?: string | number, 47 | // Table 的高度, 默认为自动高度。 如果 height 为 number 类型,单位 px;如果 height 为 string 类型,则这个高度会设置为 Table 的 style.height 的值,Table 的高度会受控于外部样式。 48 | stripe?: boolean, // 是否为斑马纹 table 49 | maxHeight?: string | number, // Table 的最大高度。 合法的值为数字或者单位为 px 的高度。 50 | size?: Size // Table 的尺寸 51 | showHeader?: boolean // 是否显示表头, 52 | tooltipEffect?: 'dark' | 'light' // tooltip effect 属性 53 | showPagination?: boolean, // 是否展示分页器 54 | paginationConfig?: Pagination, // 分页器配置项,详情见下方 paginationConfig 属性, 55 | rowStyle?: ({row, rowIndex}) => stirng | object // 行的 style 的回调方法,也可以使用一个固定的 Object 为所有行设置一样的 Style。 56 | headerCellStyle?: import('vue').CSSProperties, // 表头单元格的style样式,是一个object为所有表头单元格设置一样的 Style。注:CSSProperties类型就是一个对象,像正常在style中写css一样 {color: #f00} 57 | defaultSort?: Sort // 默认的排序列的 prop 和顺序。 它的 prop 属性指定默认的排序的列,order 指定默认排序的顺序。 58 | rowKey?: string, // 行数据的 Key,用来优化 Table 的渲染。 59 | } 60 | 61 | interface Pagination { 62 | total?: number, // 总条目数 63 | currentPage: number, // 当前页数,支持 v-model 双向绑定 64 | pageSize: number, // 每页显示条目个数,支持 v-model 双向绑定 65 | pageSizes?: number[], // 每页显示个数选择器的选项设置 66 | layout?: string, // 组件布局,子组件名用逗号分隔 67 | background?: boolean // 是否为分页按钮添加背景色 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/request/index.ts: -------------------------------------------------------------------------------- 1 | /* 对axios的请求封装 */ 2 | import { ElMessage, ElMessageBox } from 'element-plus' 3 | import axios from 'axios' 4 | import { useUserStore } from '@/stores/user' 5 | import router from '@/router' 6 | import axiosRetry from 'axios-retry' 7 | 8 | const service = axios.create({ 9 | baseURL: '/api', 10 | timeout: 10 * 60 * 1000 11 | } as any) 12 | 13 | /** 14 | * 请求重试配置 15 | */ 16 | axiosRetry(service, { 17 | retries: 3, // 设置重试次数为3次 18 | retryDelay: () => 1000, // 设置重试的间隔时间 19 | shouldResetTimeout: true, // 重置请求超时时间 20 | // error.code===ECONNABORTED表示请求超时了 ERR_NETWORK网络出错 21 | retryCondition: (error) => ['ECONNABORTED', 'ERR_NETWORK'].includes(error.code!) // 重试条件 22 | }) 23 | 24 | /** 25 | * 每次请求在 header 中带上 token 26 | */ 27 | service.interceptors.request.use((config: any) => { 28 | const userStore = useUserStore() 29 | if (userStore.getToken()) { 30 | config.headers.Authorization = 'Bearer ' + userStore.getToken() 31 | } 32 | return config 33 | }) 34 | /** 35 | * 拦截每一次响应,判断是否 token 失效 36 | * 如果 token 失效就退出登录并提示信息 37 | */ 38 | service.interceptors.response.use( 39 | (response) => { 40 | const res = response 41 | const userStore = useUserStore() 42 | // token 无效 43 | if (res.data.code === 401 || res.data.code === 402 || res.status === 401) { 44 | ElMessage({ 45 | message: res.data.msg || '页面长时间未使用,请重新登录', 46 | type: 'warning', 47 | duration: 5000 48 | }) 49 | userStore.logout() 50 | handleError('页面长时间未使用,请重新登录') 51 | return Promise.reject(new Error(res.data.msg || 'Error')) 52 | } else if (res.status != 200 || (res.data.code != 200 && 'code' in res.data)) { 53 | let data: string = '' 54 | if (res.data.data && res.data.data != '') { 55 | data = res.data.data 56 | } else if (res.data.msg && res.data.msg != 'error') { 57 | data = res.data.msg 58 | } 59 | ElMessage({ 60 | message: data || '接口异常', 61 | type: 'error', 62 | duration: 5000 63 | }) 64 | return Promise.reject(new Error(data || '接口异常')) 65 | } else { 66 | return res 67 | } 68 | }, 69 | (error) => { 70 | /** 71 | * axios拦截器请求错误统一处理 72 | */ 73 | if (error.response.status == 401 || error.response.data.code == 401) { 74 | ElMessage.error('非法访问') 75 | router.push('/401').then() 76 | } else if (error.response.status == 500) { 77 | ElMessage.error('服务器出错了') 78 | setTimeout(() => { 79 | window.location.href = '/login' 80 | }, 1000) 81 | } else if (error.response.status == 429) { 82 | ElMessage.error(error.response.data.msg) 83 | } else { 84 | handleError('服务器异常') 85 | } 86 | } 87 | ) 88 | 89 | function handleError(msg: string) { 90 | /** 91 | * 统一错误处理 92 | */ 93 | const userStore = useUserStore() 94 | const userName = userStore.getUserName() 95 | if (userName) { 96 | ElMessageBox.confirm(msg, { 97 | confirmButtonText: '再次登录', 98 | cancelButtonText: '取消', 99 | type: 'warning' 100 | }).then(() => { 101 | userStore.logout() 102 | }) 103 | } 104 | } 105 | 106 | export default service 107 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 29 | 47 | 48 | 51 | -------------------------------------------------------------------------------- /src/views/echarts/CpuEcharts.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 114 | 115 | -------------------------------------------------------------------------------- /src/views/echarts/MemoryEcharts.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /src/views/echarts/NetworkEcharts.vue: -------------------------------------------------------------------------------- 1 | 137 | 138 | 143 | 144 | 147 | -------------------------------------------------------------------------------- /src/views/error/401View.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 56 | -------------------------------------------------------------------------------- /src/views/error/404View.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 56 | -------------------------------------------------------------------------------- /src/views/home/DashBoard.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 204 | 205 | 292 | -------------------------------------------------------------------------------- /src/views/login/LoginView.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 110 | 111 | 167 | -------------------------------------------------------------------------------- /src/views/poc/PocListView.vue: -------------------------------------------------------------------------------- 1 | 229 | 230 | 290 | 291 | 298 | -------------------------------------------------------------------------------- /src/views/poc/ts/PocList.ts: -------------------------------------------------------------------------------- 1 | export const PocList_tableColumn: Table.Column[] = [ 2 | {prop: 'poc_name', label: '漏洞名称', showOverflowTooltip: true}, 3 | {prop: 'vul_desc', label: '漏洞描述', showOverflowTooltip: true}, 4 | {prop: 'vul_author', label: '作者', showOverflowTooltip: true}, 5 | {prop: 'vul_leakLevel', label: '漏洞等级', slot: 'vul_leakLevel', sortable: true, width: '120px'}, 6 | {prop: 'vul_name', label: '影响产品', showOverflowTooltip: true}, 7 | {prop: 'vul_range', label: '影响版本', showOverflowTooltip: true}, 8 | {prop: 'has_exp', label: '有无EXP', slot: 'has_exp', width: '80px'}, 9 | {prop: 'vul_vulDate', label: '公布日期',}, 10 | {type: 'date', prop: 'vul_createDate', label: '编写日期',}, 11 | { 12 | width: '180', 13 | label: '操作', 14 | buttons: [], 15 | slot: 'action' 16 | } 17 | ] -------------------------------------------------------------------------------- /src/views/project/ProJectView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 34 | 35 | -------------------------------------------------------------------------------- /src/views/project/View/ProJectInfo.vue: -------------------------------------------------------------------------------- 1 | 125 | 126 | 183 | 184 | 221 | -------------------------------------------------------------------------------- /src/views/project/View/ProJectVulList.vue: -------------------------------------------------------------------------------- 1 | 216 | 217 | 308 | 309 | 322 | -------------------------------------------------------------------------------- /src/views/project/View/ts/ProJectVul.ts: -------------------------------------------------------------------------------- 1 | export const VulReport_tableColumn: Table.Column[] = [ 2 | {prop: 'poc_info', label: '漏洞名称', slot: 'poc_info', showOverflowTooltip: true, width: '200'}, 3 | {prop: 'vul_desc', label: '漏洞描述', slot: 'vul_desc', showOverflowTooltip: true}, 4 | {prop: 'vul_leakLevel', label: '漏洞等级', slot: 'vul_leakLevel', sortable: true, width: '110'}, 5 | {prop: 'vul_name', label: '影响产品', slot: 'vul_name', width: '100', showOverflowTooltip: true}, 6 | {prop: 'vul_range', label: '影响版本', slot: 'vul_range', width: '100', sortable: true,}, 7 | {prop: 'has_exp', label: '有无EXP', slot: 'has_exp', width: '80'}, 8 | {prop: 'vul_vulDate', label: '公布日期', slot: 'vul_vulDate', width: '120'}, 9 | { 10 | width: '180', 11 | label: '操作', 12 | buttons: [], 13 | slot: 'action' 14 | } 15 | ] -------------------------------------------------------------------------------- /src/views/project/View/ts/TaskCelery.ts: -------------------------------------------------------------------------------- 1 | export const TaskCelery_tableColumn: Table.Column[] = [ 2 | {prop: 'poc_name', label: '漏洞名称', showOverflowTooltip: true}, 3 | {prop: 'vul_desc', label: '漏洞描述', showOverflowTooltip: true}, 4 | {prop: 'vul_author', label: '作者'}, 5 | {prop: 'vul_leakLevel', label: '漏洞等级', sortable: true}, 6 | {prop: 'vul_name', label: '影响产品', width: '100px'}, 7 | {prop: 'vul_range', label: '影响版本', width: '100px'}, 8 | {prop: 'has_exp', label: '有无EXP', slot: 'has_exp', width: '80px'}, 9 | {prop: 'vul_vulDate', label: '公布日期',}, 10 | {type: 'date', prop: 'vul_createDate', label: '编写日期',}, 11 | { 12 | width: '180', 13 | label: '操作', 14 | buttons: [], 15 | slot: 'action' 16 | } 17 | ] -------------------------------------------------------------------------------- /src/views/project/View/ts/TaskInfo.ts: -------------------------------------------------------------------------------- 1 | import {h} from "vue"; 2 | import {ElInput, ElMessage} from "element-plus"; 3 | import {CopyDocument} from "@element-plus/icons-vue"; 4 | import useClipboard from "vue-clipboard3"; 5 | 6 | const {toClipboard} = useClipboard() 7 | 8 | interface User { 9 | date: number 10 | poc_name: string 11 | vul_desc: string 12 | vul_vulDate: string 13 | } 14 | 15 | const renderExpandContent = (row: User) => 16 | h('div', {style: 'margin-left: 50px;margin-right: 50px;'}, [ 17 | h('p', `漏洞名称:${row.poc_name}`), 18 | h('p', `漏洞描述:${row.vul_desc}`), 19 | h('p', `日期:${row.vul_vulDate}`), 20 | h('div', { 21 | style: 'display: flex;' 22 | }, [ 23 | h('div', {style: 'flex: 1; position: relative; margin-right: 25px;'}, [ 24 | h(ElInput, { 25 | type: 'textarea', 26 | rows: 16, 27 | modelValue: row.poc_name, 28 | readonly: true, 29 | }), 30 | h(CopyDocument, { 31 | style: 'position: absolute; top: 10px; right:25px;float: right; border: 1px padding: 1px;width:15px;height:15px', 32 | onClick: async () => { 33 | await toClipboard(String(row.poc_name)) 34 | ElMessage.success("复制成功") 35 | } 36 | }) 37 | ]), 38 | h('div', {style: 'flex: 1; position: relative; margin-left: 25px;'}, [ 39 | h(ElInput, { 40 | type: 'textarea', 41 | rows: 16, 42 | modelValue: row.vul_desc, 43 | readonly: true, 44 | }), 45 | h(CopyDocument, { 46 | style: 'position: absolute; top: 10px; right:25px;float: right; border: 1px padding: 1px;width:15px;height:15px', 47 | onClick: async () => { 48 | console.log(row.vul_desc) 49 | await toClipboard(row.vul_desc) 50 | ElMessage.success("复制成功") 51 | } 52 | }) 53 | ]) 54 | ]) 55 | ]) 56 | 57 | export const TaskReport_tableColumn: Table.Column[] = [ 58 | {type: 'expand', width: '50', render: ({row}) => renderExpandContent(row)}, 59 | {prop: 'poc_name', label: '漏洞名称', showOverflowTooltip: true}, 60 | {prop: 'vul_desc', label: '漏洞描述', showOverflowTooltip: true}, 61 | {prop: 'vul_author', label: '作者'}, 62 | {prop: 'vul_leakLevel', label: '漏洞等级', sortable: true}, 63 | {prop: 'vul_name', label: '影响产品', width: '100px'}, 64 | {prop: 'vul_range', label: '影响版本', width: '100px'}, 65 | {prop: 'has_exp', label: '有无EXP', slot: 'has_exp', width: '80px'}, 66 | {prop: 'vul_vulDate', label: '公布日期',}, 67 | {type: 'date', prop: 'vul_createDate', label: '编写日期',}, 68 | { 69 | width: '180', 70 | label: '操作', 71 | buttons: [], 72 | slot: 'action' 73 | } 74 | ] -------------------------------------------------------------------------------- /src/views/tasks/TasksCeleryView.vue: -------------------------------------------------------------------------------- 1 | 196 | 197 | 226 | 227 | -------------------------------------------------------------------------------- /src/views/tasks/TasksReportView.vue: -------------------------------------------------------------------------------- 1 | 196 | 197 | 226 | 227 | -------------------------------------------------------------------------------- /src/views/tasks/ts/TaskCelery.ts: -------------------------------------------------------------------------------- 1 | export const TaskCelery_tableColumn: Table.Column[] = [ 2 | {prop: 'poc_name', label: '漏洞名称', showOverflowTooltip: true}, 3 | {prop: 'vul_desc', label: '漏洞描述', showOverflowTooltip: true}, 4 | {prop: 'vul_author', label: '作者'}, 5 | {prop: 'vul_leakLevel', label: '漏洞等级', sortable: true}, 6 | {prop: 'vul_name', label: '影响产品', width: '100px'}, 7 | {prop: 'vul_range', label: '影响版本', width: '100px'}, 8 | {prop: 'has_exp', label: '有无EXP', slot: 'has_exp', width: '80px'}, 9 | {prop: 'vul_vulDate', label: '公布日期',}, 10 | {type: 'date', prop: 'vul_createDate', label: '编写日期',}, 11 | { 12 | width: '180', 13 | label: '操作', 14 | buttons: [], 15 | slot: 'action' 16 | } 17 | ] -------------------------------------------------------------------------------- /src/views/tasks/ts/TaskReport.ts: -------------------------------------------------------------------------------- 1 | export const TaskReport_tableColumn: Table.Column[] = [ 2 | {prop: 'poc_name', label: '漏洞名称', showOverflowTooltip: true}, 3 | {prop: 'vul_desc', label: '漏洞描述', showOverflowTooltip: true}, 4 | {prop: 'vul_author', label: '作者'}, 5 | {prop: 'vul_leakLevel', label: '漏洞等级', sortable: true}, 6 | {prop: 'vul_name', label: '影响产品', width: '100px'}, 7 | {prop: 'vul_range', label: '影响版本', width: '100px'}, 8 | {prop: 'has_exp', label: '有无EXP', slot: 'has_exp', width: '80px'}, 9 | {prop: 'vul_vulDate', label: '公布日期',}, 10 | {type: 'date', prop: 'vul_createDate', label: '编写日期',}, 11 | { 12 | width: '180', 13 | label: '操作', 14 | buttons: [], 15 | slot: 'action' 16 | } 17 | ] -------------------------------------------------------------------------------- /src/views/tasks/ts/Tasks.ts: -------------------------------------------------------------------------------- 1 | export const Tasks_tableColumn: Table.Column[] = [ 2 | {prop: 'desc', label: '任务描述', showOverflowTooltip: true}, 3 | {prop: 'status', label: '任务状态', slot: "status", sortable: true, showOverflowTooltip: true}, 4 | {prop: 'poc_type', label: 'POC类型', slot: 'poc_type', sortable: true, showOverflowTooltip: true}, 5 | { 6 | prop: 'scanning_speed', 7 | label: '扫描速度', 8 | width: '100px', 9 | slot: 'scanning_speed', 10 | showOverflowTooltip: true 11 | }, 12 | {prop: 'progress', label: '扫描进度', slot: 'progress', sortable: true, showOverflowTooltip: true}, 13 | {prop: 'create_user', label: '创建用户', slot: "create_user"}, 14 | {prop: 'create_time', label: '创建时间', showOverflowTooltip: true}, 15 | { 16 | width: '240', 17 | label: '操作', 18 | buttons: [], 19 | slot: 'action' 20 | } 21 | ] -------------------------------------------------------------------------------- /src/views/user/ResetPassword.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 122 | 123 | -------------------------------------------------------------------------------- /src/views/user/UserInfo.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 107 | 108 | 156 | -------------------------------------------------------------------------------- /src/views/user/UserView.vue: -------------------------------------------------------------------------------- 1 | 161 | 162 | 196 | 197 | -------------------------------------------------------------------------------- /src/views/user/ts/UserList.ts: -------------------------------------------------------------------------------- 1 | export const UserList_tableColumn: Table.Column[] = [ 2 | {prop: 'username', label: '用户名', sortable: true, showOverflowTooltip: true}, 3 | {prop: 'phone', label: '手机号', showOverflowTooltip: true}, 4 | {prop: 'permissions', label: '权限', sortable: true, slot: 'permissions'}, 5 | {prop: 'last_login_ip', label: '最后一次登陆ip', showOverflowTooltip: true}, 6 | {prop: 'last_login', label: '最后一次登陆时间', width: '180px'}, 7 | {type: 'date', prop: 'create_time', label: '创建时间', width: '180px'}, 8 | { 9 | width: '140', 10 | label: '操作', 11 | buttons: [ 12 | {name: '删除', type: 'danger', icon: 'Delete', command: 'delete'} 13 | ] 14 | } 15 | ] -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ], 14 | "compilerOptions": { 15 | // ... 16 | "types": [ 17 | "element-plus/global" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "types": ["node"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": [], 7 | "types": [ 8 | "node", 9 | /** Element Plus 的 Volar 插件支持 */ 10 | "element-plus", 11 | "jsdom" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import VueSetupExtend from 'vite-plugin-vue-setup-extend' 6 | import vueJsx from '@vitejs/plugin-vue-jsx' 7 | import AutoImport from 'unplugin-auto-import/vite' 8 | import Components from 'unplugin-vue-components/vite' 9 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | server: { 14 | /** 是否开启 HTTPS */ 15 | https: false, 16 | /** 设置 host: true 才可以使用 Network 的形式,以 IP 访问项目 */ 17 | host: true, // host: "0.0.0.0" 18 | /** 端口号 */ 19 | port: 3333, 20 | /** 是否自动打开浏览器 */ 21 | open: false, 22 | /** 跨域设置允许 */ 23 | cors: true, 24 | /** 端口被占用时,是否直接退出 */ 25 | strictPort: false, 26 | /* 27 | * 连接后端服务时使用 server.proxy 设置代理 28 | * 当请求路径以 api 开头,说明调用的是真实后端服务 29 | */ 30 | proxy: { 31 | '^/api': { 32 | target: 'http://127.0.0.1:8000/api/v1/', 33 | // target: 'http://172.18.6.232:8000/api/v1', 34 | rewrite: (path) => path.replace(/^\/api/, ''), 35 | ws: true, 36 | /** 是否允许跨域 */ 37 | changeOrigin: true 38 | } 39 | } 40 | }, 41 | /** 打包相关操作 */ 42 | build: { 43 | /** 单个 chunk 文件的大小超过 2048KB 时发出警告 */ 44 | chunkSizeWarningLimit: 2048, 45 | /** 禁用 gzip 压缩大小报告 */ 46 | reportCompressedSize: false, 47 | /** 打包后静态资源目录 */ 48 | assetsDir: 'static', 49 | rollupOptions: { 50 | output: { 51 | /** 52 | * 分块策略 53 | * 1. 注意这些包名必须存在,否则打包会报错 54 | * 2. 如果你不想自定义 chunk 分割策略,可以直接移除这段配置 55 | */ 56 | manualChunks: { 57 | vue: ['vue', 'vue-router', 'pinia'], 58 | element: ['element-plus', '@element-plus/icons-vue'], 59 | vxe: ['vxe-table', 'vxe-table-plugin-element', 'xe-utils'] 60 | } 61 | } 62 | } 63 | }, 64 | /** 混淆器 */ 65 | esbuild: { 66 | /** 打包时移除 console.log */ 67 | pure: ['console.log'], 68 | /** 打包时移除 debugger */ 69 | drop: ['debugger'], 70 | /** 打包时移除所有注释 */ 71 | legalComments: 'none' 72 | }, 73 | plugins: [ 74 | vue(), 75 | vueJsx(), 76 | VueSetupExtend(), 77 | AutoImport({ 78 | resolvers: [ElementPlusResolver()] 79 | }), 80 | Components({ 81 | resolvers: [ElementPlusResolver()] 82 | }) 83 | ], 84 | resolve: { 85 | alias: { 86 | '@': fileURLToPath(new URL('./src', import.meta.url)) 87 | } 88 | } 89 | }) 90 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' 3 | import viteConfig from './vite.config' 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | environment: 'jsdom', 10 | exclude: [...configDefaults.exclude, 'e2e/*'], 11 | root: fileURLToPath(new URL('./', import.meta.url)) 12 | } 13 | }) 14 | ) 15 | -------------------------------------------------------------------------------- /web-types.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/web-types", 3 | "framework": "vue", 4 | "name": "name written in package.json", 5 | "version": "version written in package.json", 6 | "contributions": { 7 | "html": { 8 | "types-syntax": "typescript", 9 | "attributes": [ 10 | { 11 | "name": "v-watermark" 12 | } 13 | ] 14 | } 15 | } 16 | } 17 | --------------------------------------------------------------------------------