├── public └── favicon.ico ├── .env.production ├── src ├── stores │ ├── index.ts │ ├── requestOldRoutes.ts │ ├── tagsViewRoutes.ts │ ├── routesList.ts │ ├── keepAliveNames.ts │ ├── userInfo.ts │ └── themeConfig.ts ├── utils │ ├── mitt.ts │ ├── authFunction.ts │ ├── setIconfont.ts │ ├── loading.ts │ ├── watermark.ts │ ├── storage.ts │ ├── arrayOperation.ts │ ├── theme.ts │ ├── commonFunction.ts │ ├── request.ts │ └── getStyleSheets.ts ├── theme │ ├── index.scss │ ├── media │ │ ├── cityLinkage.scss │ │ ├── tagsView.scss │ │ ├── dialog.scss │ │ ├── pagination.scss │ │ ├── personal.scss │ │ ├── index.scss │ │ ├── media.scss │ │ ├── home.scss │ │ ├── date.scss │ │ ├── form.scss │ │ ├── error.scss │ │ ├── layout.scss │ │ ├── scrollbar.scss │ │ ├── login.scss │ │ └── chart.scss │ ├── tableTool.scss │ ├── iconSelector.scss │ ├── mixins │ │ └── index.scss │ ├── other.scss │ ├── loading.scss │ ├── common │ │ └── transition.scss │ └── waves.scss ├── i18n │ ├── pages │ │ ├── formI18n │ │ │ ├── zh-cn.ts │ │ │ ├── zh-tw.ts │ │ │ └── en.ts │ │ └── login │ │ │ ├── zh-cn.ts │ │ │ ├── zh-tw.ts │ │ │ └── en.ts │ └── index.ts ├── types │ ├── axios.d.ts │ ├── mitt.d.ts │ ├── layout.d.ts │ ├── pinia.d.ts │ └── global.d.ts ├── directive │ ├── index.ts │ └── authDirective.ts ├── layout │ ├── footer │ │ └── index.vue │ ├── component │ │ ├── header.vue │ │ └── main.vue │ ├── navBars │ │ ├── index.vue │ │ ├── breadcrumb │ │ │ ├── closeFull.vue │ │ │ ├── userNews.vue │ │ │ ├── search.vue │ │ │ └── index.vue │ │ └── tagsView │ │ │ └── contextmenu.vue │ ├── navMenu │ │ ├── subItem.vue │ │ └── vertical.vue │ ├── main │ │ ├── transverse.vue │ │ ├── defaults.vue │ │ ├── classic.vue │ │ └── columns.vue │ ├── index.vue │ ├── logo │ │ └── index.vue │ ├── routerView │ │ ├── link.vue │ │ ├── iframes.vue │ │ └── parent.vue │ └── upgrade │ │ └── index.vue ├── api │ └── login │ │ └── index.ts ├── components │ ├── auth │ │ ├── auth.vue │ │ ├── authAll.vue │ │ └── auths.vue │ ├── svgIcon │ │ └── index.vue │ ├── iconSelector │ │ └── list.vue │ └── Crontab │ │ ├── month.vue │ │ ├── hour.vue │ │ ├── min.vue │ │ ├── second.vue │ │ ├── year.vue │ │ └── day.vue ├── views │ ├── flowManage │ │ ├── index.vue │ │ ├── api.ts │ │ └── config.ts │ ├── flowDesign │ │ ├── component │ │ │ ├── tool │ │ │ │ ├── help.vue │ │ │ │ └── index.vue │ │ │ ├── drawer │ │ │ │ └── line.vue │ │ │ └── contextmenu │ │ │ │ └── index.vue │ │ ├── api.ts │ │ └── js │ │ │ └── config.ts │ ├── functionManage │ │ ├── index.vue │ │ ├── api.ts │ │ └── config.ts │ ├── appManage │ │ ├── index.vue │ │ ├── api.ts │ │ └── config.ts │ ├── taskManage │ │ ├── api.ts │ │ └── index.vue │ └── error │ │ ├── 404.vue │ │ └── 401.vue ├── assets │ └── logo-mini.svg ├── App.vue └── main.ts ├── .env ├── .env.development ├── .eslintignore ├── .gitignore ├── index.html ├── Dockerfile ├── default.conf ├── README.md ├── LICENSE ├── .prettierrc.js ├── package.json ├── .github └── workflows │ └── docker-publish.yml ├── .eslintrc.js └── vite.config.ts /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kkangert/kspider-ui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 线上环境 2 | ENV = production 3 | 4 | # 线上环境接口地址 5 | VITE_API_URL = http://localhost:8086 6 | 7 | # API基本路径 8 | VITE_API_BASE_URL = '/api' -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | // https://pinia.vuejs.org/ 2 | import { createPinia } from 'pinia'; 3 | 4 | // 创建 5 | const pinia = createPinia(); 6 | 7 | // 导出 8 | export default pinia; 9 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # port 端口号 2 | VITE_PORT = 8888 3 | 4 | # open 运行 npm run dev 时自动打开浏览器 5 | VITE_OPEN = false 6 | 7 | # public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可 8 | VITE_PUBLIC_PATH = / -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 本地环境 2 | ENV = development 3 | 4 | # 本地环境接口地址 5 | VITE_API_URL = http://localhost:8086 6 | # VITE_API_URL = http://192.168.2.130:8086/ 7 | 8 | # API基本路径 9 | VITE_API_BASE_URL = '/api' -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | *.sh 3 | node_modules 4 | lib 5 | *.md 6 | *.scss 7 | *.woff 8 | *.ttf 9 | .vscode 10 | .idea 11 | dist 12 | mock 13 | public 14 | bin 15 | build 16 | config 17 | index.html 18 | src/assets -------------------------------------------------------------------------------- /src/utils/mitt.ts: -------------------------------------------------------------------------------- 1 | // https://www.npmjs.com/package/mitt 2 | import mitt, { Emitter } from 'mitt'; 3 | 4 | // 类型 5 | const emitter: Emitter = mitt(); 6 | 7 | // 导出 8 | export default emitter; 9 | -------------------------------------------------------------------------------- /src/theme/index.scss: -------------------------------------------------------------------------------- 1 | @import './app.scss'; 2 | @import 'common/transition.scss'; 3 | @import './other.scss'; 4 | @import './element.scss'; 5 | @import './media/media.scss'; 6 | @import './waves.scss'; 7 | @import './dark.scss'; 8 | -------------------------------------------------------------------------------- /src/theme/media/cityLinkage.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | .el-cascader__dropdown.el-popper { 7 | overflow: auto; 8 | max-width: 100%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/theme/media/tagsView.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .tags-view-form { 7 | .tags-view-form-col { 8 | margin-bottom: 20px; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/i18n/pages/formI18n/zh-cn.ts: -------------------------------------------------------------------------------- 1 | // 定义内容 2 | export default { 3 | formI18nLabel: { 4 | name: '姓名', 5 | email: '用户归属部门', 6 | autograph: '登陆账户名', 7 | }, 8 | formI18nPlaceholder: { 9 | name: '请输入姓名', 10 | email: '请输入用户归属部门', 11 | autograph: '请输入登陆账户名', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/i18n/pages/formI18n/zh-tw.ts: -------------------------------------------------------------------------------- 1 | // 定义内容 2 | export default { 3 | formI18nLabel: { 4 | name: '姓名', 5 | email: '用戶歸屬部門', 6 | autograph: '登入帳戶名', 7 | }, 8 | formI18nPlaceholder: { 9 | name: '請輸入姓名', 10 | email: '請輸入用戶歸屬部門', 11 | autograph: '請輸入登入帳戶名', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/theme/media/dialog.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于800px 4 | ------------------------------- */ 5 | @media screen and (max-width: 800px) { 6 | .el-dialog { 7 | width: 90% !important; 8 | } 9 | .el-dialog.is-fullscreen { 10 | width: 100% !important; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/types/axios.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as axios from 'axios'; 3 | 4 | // 扩展 axios 数据返回类型,可自行扩展 5 | declare module 'axios' { 6 | export interface AxiosResponse { 7 | code: number; 8 | data: T; 9 | message: string; 10 | type?: string; 11 | [key: string]: T; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/i18n/pages/formI18n/en.ts: -------------------------------------------------------------------------------- 1 | // 定义内容 2 | export default { 3 | formI18nLabel: { 4 | name: 'name', 5 | email: 'email', 6 | autograph: 'autograph', 7 | }, 8 | formI18nPlaceholder: { 9 | name: 'Please enter your name', 10 | email: 'Please enter the users Department', 11 | autograph: 'Please enter the login account name', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/theme/media/pagination.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | .el-pager, 7 | .el-pagination__jump { 8 | display: none !important; 9 | } 10 | // 默认居中对齐 11 | .el-pagination, 12 | .table-footer { 13 | justify-content: center !important; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/theme/media/personal.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .personal-info { 7 | padding-left: 0 !important; 8 | margin-top: 15px; 9 | } 10 | .personal-recommend-col { 11 | margin-bottom: 15px; 12 | &:last-of-type { 13 | margin-bottom: 0; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/theme/media/index.scss: -------------------------------------------------------------------------------- 1 | /* 栅格布局(媒体查询变量) 2 | * https://developer.mozilla.org/zh-CN/docs/Learn/CSS/CSS_layout/Media_queries 3 | * $us ≥376px 响应式栅格 4 | * $xs ≥576px 响应式栅格 5 | * $sm ≥768px 响应式栅格 6 | * $md ≥992px 响应式栅格 7 | * $lg ≥1200px 响应式栅格 8 | * $xl ≥1920px 响应式栅格 9 | ------------------------------- */ 10 | $us: 376px; 11 | $xs: 576px; 12 | $sm: 768px; 13 | $md: 992px; 14 | $lg: 1200px; 15 | $xl: 1920px; 16 | -------------------------------------------------------------------------------- /src/theme/media/media.scss: -------------------------------------------------------------------------------- 1 | @import './login.scss'; 2 | @import './error.scss'; 3 | @import './layout.scss'; 4 | @import './personal.scss'; 5 | @import './tagsView.scss'; 6 | @import './home.scss'; 7 | @import './chart.scss'; 8 | @import './form.scss'; 9 | @import './scrollbar.scss'; 10 | @import './pagination.scss'; 11 | @import './dialog.scss'; 12 | @import './cityLinkage.scss'; 13 | @import './date.scss'; 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Kspider 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/stores/requestOldRoutes.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | /** 4 | * 后端返回原始路由(未处理时) 5 | * @methods setCacheKeepAlive 设置接口原始路由数据 6 | */ 7 | export const useRequestOldRoutes = defineStore('requestOldRoutes', { 8 | state: (): RequestOldRoutesState => ({ 9 | requestOldRoutes: [], 10 | }), 11 | actions: { 12 | async setRequestOldRoutes(routes: Array) { 13 | this.requestOldRoutes = routes; 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/theme/media/home.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .home-media, 7 | .home-media-sm { 8 | margin-top: 15px; 9 | } 10 | } 11 | 12 | /* 页面宽度小于1200px 13 | ------------------------------- */ 14 | @media screen and (max-width: $lg) { 15 | .home-media-lg { 16 | margin-top: 15px; 17 | } 18 | .home-monitor { 19 | .flex-warp-item { 20 | width: 33.33% !important; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/directive/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { authDirective } from '/@/directive/authDirective'; 3 | import { wavesDirective, dragDirective } from '/@/directive/customDirective'; 4 | 5 | /** 6 | * 导出指令方法:v-xxx 7 | * @methods authDirective 用户权限指令,用法:v-auth 8 | * @methods wavesDirective 按钮波浪指令,用法:v-waves 9 | * @methods dragDirective 自定义拖动指令,用法:v-drag 10 | */ 11 | export function directive(app: App) { 12 | // 用户权限指令 13 | authDirective(app); 14 | // 按钮波浪指令 15 | wavesDirective(app); 16 | // 自定义拖动指令 17 | dragDirective(app); 18 | } 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # step1 2 | FROM node:20.15.1-slim as builds 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | COPY . /app/ 7 | 8 | # 编译 9 | RUN npm i -g pnpm && pnpm i && pnpm run build 10 | 11 | # step2 12 | FROM nginx:1.18.0-alpine 13 | 14 | # 作者 15 | LABEL org.opencontainers.image.authors=kangert 16 | 17 | # 添加时区环境变量,亚洲,上海 18 | ENV TimeZone=Asia/Shanghai 19 | 20 | # 将前端dist文件中的内容复制到nginx目录 21 | COPY --from=builds /app/dist/ /usr/share/nginx/html/ 22 | 23 | # nginx默认配置文件 24 | COPY --from=builds /app/default.conf /etc/nginx/conf.d/ 25 | 26 | # 暴露端口 27 | EXPOSE 80 28 | -------------------------------------------------------------------------------- /src/layout/footer/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 26 | -------------------------------------------------------------------------------- /src/api/login/index.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * (不建议写成 request.post(xxx),因为这样 post 时,无法 params 与 data 同时传参) 5 | * 6 | * 登录api接口集合 7 | * @method signIn 用户登录 8 | * @method signOut 用户退出登录 9 | */ 10 | export function useLoginApi() { 11 | return { 12 | signIn: (data: object) => { 13 | return request({ 14 | url: '/auth/login', 15 | method: 'post', 16 | data, 17 | }); 18 | }, 19 | signOut: (data: object) => { 20 | return request({ 21 | url: '/auth/signOut', 22 | method: 'post', 23 | data, 24 | }); 25 | }, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/theme/media/date.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | // 时间选择器适配 7 | .el-date-range-picker { 8 | width: 100vw; 9 | .el-picker-panel__body { 10 | min-width: 100%; 11 | .el-date-range-picker__content { 12 | .el-date-range-picker__header div { 13 | margin-left: 22px; 14 | margin-right: 0px; 15 | } 16 | & + .el-date-range-picker__content { 17 | .el-date-range-picker__header div { 18 | margin-left: 0px; 19 | margin-right: 22px; 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/i18n/pages/login/zh-cn.ts: -------------------------------------------------------------------------------- 1 | // 定义内容 2 | export default { 3 | label: { 4 | one1: '用户名登录', 5 | two2: '手机号登录', 6 | }, 7 | link: { 8 | one3: '第三方登录', 9 | two4: '友情链接', 10 | }, 11 | account: { 12 | accountPlaceholder1: '用户名', 13 | accountPlaceholder2: '密码', 14 | accountPlaceholder3: '请输入验证码', 15 | accountBtnText: '登 录', 16 | }, 17 | mobile: { 18 | placeholder1: '请输入手机号', 19 | placeholder2: '请输入验证码', 20 | codeText: '获取验证码', 21 | btnText: '登 录', 22 | msgText: '* 温馨提示:建议使用谷歌、Microsoft Edge,版本 79.0.1072.62 及以上浏览器,360浏览器请使用极速模式', 23 | }, 24 | scan: { 25 | text: '打开手机扫一扫,快速登录/注册', 26 | }, 27 | signInText: '欢迎回来!', 28 | }; 29 | -------------------------------------------------------------------------------- /src/layout/component/header.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /src/i18n/pages/login/zh-tw.ts: -------------------------------------------------------------------------------- 1 | // 定义内容 2 | export default { 3 | label: { 4 | one1: '用戶名登入', 5 | two2: '手機號登入', 6 | }, 7 | link: { 8 | one3: '協力廠商登入', 9 | two4: '友情連結', 10 | }, 11 | account: { 12 | accountPlaceholder1: '用戶名admin或不輸均為common', 13 | accountPlaceholder2: '密碼:123456', 14 | accountPlaceholder3: '請輸入驗證碼', 15 | accountBtnText: '登入', 16 | }, 17 | mobile: { 18 | placeholder1: '請輸入手機號', 19 | placeholder2: '請輸入驗證碼', 20 | codeText: '獲取驗證碼', 21 | btnText: '登入', 22 | msgText: '* 溫馨提示:建議使用穀歌、Microsoft Edge,版本79.0.1072.62及以上瀏覽器,360瀏覽器請使用極速模式', 23 | }, 24 | scan: { 25 | text: '打開手機掃一掃,快速登錄/注册', 26 | }, 27 | signInText: '歡迎回來!', 28 | }; 29 | -------------------------------------------------------------------------------- /src/theme/tableTool.scss: -------------------------------------------------------------------------------- 1 | .table-tool-popper { 2 | padding: 0 !important; 3 | .tool-box { 4 | display: flex; 5 | border-bottom: 1px solid var(--el-border-color-lighter); 6 | box-sizing: border-box; 7 | color: var(--el-text-color-primary); 8 | height: 40px; 9 | align-items: center; 10 | } 11 | .tool-sortable { 12 | max-height: 303px; 13 | .tool-sortable-item { 14 | display: flex; 15 | box-sizing: border-box; 16 | color: var(--el-text-color-primary); 17 | align-items: center; 18 | padding: 0 12px; 19 | &:hover { 20 | background: var(--el-fill-color-lighter); 21 | } 22 | i { 23 | opacity: 0.7; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/auth/auth.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/views/flowManage/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/stores/tagsViewRoutes.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { Session } from '/@/utils/storage'; 3 | 4 | /** 5 | * TagsView 路由列表 6 | * @methods setTagsViewRoutes 设置 TagsView 路由列表 7 | * @methods setCurrenFullscreen 设置开启/关闭全屏时的 boolean 状态 8 | */ 9 | export const useTagsViewRoutes = defineStore('tagsViewRoutes', { 10 | state: (): TagsViewRoutesState => ({ 11 | tagsViewRoutes: [], 12 | isTagsViewCurrenFull: false, 13 | }), 14 | actions: { 15 | async setTagsViewRoutes(data: Array) { 16 | this.tagsViewRoutes = data; 17 | }, 18 | setCurrenFullscreen(bool: Boolean) { 19 | Session.set('isTagsViewCurrenFull', bool); 20 | this.isTagsViewCurrenFull = bool; 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/auth/authAll.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | -------------------------------------------------------------------------------- /src/stores/routesList.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | /** 4 | * 路由列表 5 | * @methods setRoutesList 设置路由数据 6 | * @methods setColumnsMenuHover 设置分栏布局菜单鼠标移入 boolean 7 | * @methods setColumnsNavHover 设置分栏布局最左侧导航鼠标移入 boolean 8 | */ 9 | export const useRoutesList = defineStore('routesList', { 10 | state: (): RoutesListState => ({ 11 | routesList: [], 12 | isColumnsMenuHover: false, 13 | isColumnsNavHover: false, 14 | }), 15 | actions: { 16 | async setRoutesList(data: Array) { 17 | this.routesList = data; 18 | }, 19 | async setColumnsMenuHover(bool: Boolean) { 20 | this.isColumnsMenuHover = bool; 21 | }, 22 | async setColumnsNavHover(bool: Boolean) { 23 | this.isColumnsNavHover = bool; 24 | }, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /src/theme/media/form.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | .el-form-item__label { 7 | width: 100% !important; 8 | text-align: left !important; 9 | // 移动端 label 右对齐问题 10 | justify-content: flex-start !important; 11 | } 12 | .el-form-item__content { 13 | margin-left: 0 !important; 14 | } 15 | .el-form-item { 16 | // 响应式表单时,登录页需要重新处理 17 | display: unset !important; 18 | } 19 | // 表格演示中的表单筛选 20 | .table-form-btn { 21 | display: flex !important; 22 | .el-form-item__label { 23 | width: auto !important; 24 | } 25 | } 26 | // 表格演示中的表单筛选最大高度,适配移动端 27 | .table-search-container { 28 | max-height: 160px; 29 | overflow: auto; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/theme/iconSelector.scss: -------------------------------------------------------------------------------- 1 | /* Popover 弹出框(图标选择器) 2 | ------------------------------- */ 3 | .icon-selector-popper { 4 | padding: 0 !important; 5 | .icon-selector-warp { 6 | height: 260px; 7 | overflow: hidden; 8 | position: relative; 9 | .icon-selector-warp-title { 10 | position: absolute; 11 | height: 40px; 12 | line-height: 40px; 13 | left: 15px; 14 | } 15 | .el-tabs__header { 16 | display: flex; 17 | justify-content: flex-end; 18 | padding: 0 15px; 19 | border-bottom: 1px solid var(--el-border-color-light); 20 | margin: 0 !important; 21 | .el-tabs__nav-wrap { 22 | &::after { 23 | height: 0 !important; 24 | } 25 | .el-tabs__item { 26 | padding: 0 5px !important; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/auth/auths.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /src/i18n/pages/login/en.ts: -------------------------------------------------------------------------------- 1 | // 定义内容 2 | export default { 3 | label: { 4 | one1: 'User name login', 5 | two2: 'Mobile number', 6 | }, 7 | link: { 8 | one3: 'Third party login', 9 | two4: 'Links', 10 | }, 11 | account: { 12 | accountPlaceholder1: 'The user name admin or not is common', 13 | accountPlaceholder2: 'Password: 123456', 14 | accountPlaceholder3: 'Please enter the verification code', 15 | accountBtnText: 'Sign in', 16 | }, 17 | mobile: { 18 | placeholder1: 'Please input mobile phone number', 19 | placeholder2: 'Please enter the verification code', 20 | codeText: 'Get code', 21 | btnText: 'Sign in', 22 | msgText: 23 | 'Warm tip: it is recommended to use Google, Microsoft edge, version 79.0.1072.62 and above browsers, and 360 browser, please use speed mode', 24 | }, 25 | scan: { 26 | text: 'Open the mobile phone to scan and quickly log in / register', 27 | }, 28 | signInText: 'welcome back!', 29 | }; 30 | -------------------------------------------------------------------------------- /src/theme/media/error.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .error { 7 | .error-flex { 8 | flex-direction: column-reverse !important; 9 | height: auto !important; 10 | width: 100% !important; 11 | } 12 | .right, 13 | .left { 14 | flex: unset !important; 15 | display: flex !important; 16 | } 17 | .left-item { 18 | margin: auto !important; 19 | } 20 | .right img { 21 | max-width: 450px !important; 22 | @extend .left-item; 23 | } 24 | } 25 | } 26 | 27 | /* 页面宽度大于768px小于992px 28 | ------------------------------- */ 29 | @media screen and (min-width: $sm) and (max-width: $md) { 30 | .error { 31 | .error-flex { 32 | padding-left: 30px !important; 33 | } 34 | } 35 | } 36 | 37 | /* 页面宽度小于1200px 38 | ------------------------------- */ 39 | @media screen and (max-width: $lg) { 40 | .error { 41 | .error-flex { 42 | padding: 0 30px; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | map $http_upgrade $connection_upgrade { 2 | default Upgrade; 3 | '' close; 4 | } 5 | 6 | server { 7 | listen 80; 8 | listen [::]:80; 9 | server_name localhost; 10 | 11 | #access_log /var/log/nginx/host.access.log main; 12 | 13 | location / { 14 | alias /usr/share/nginx/html/; 15 | index index.html index.htm index.php; 16 | try_files $uri $uri/ /index.html; 17 | } 18 | 19 | location /api/ { 20 | proxy_http_version 1.1; 21 | proxy_set_header Host $host; 22 | proxy_set_header X-Real-IP $remote_addr; 23 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 24 | proxy_pass http://kspider:8086/; 25 | } 26 | 27 | location /ws { 28 | proxy_http_version 1.1; 29 | proxy_set_header Host $host; 30 | proxy_set_header Upgrade $http_upgrade; 31 | proxy_set_header Connection $connection_upgrade; 32 | proxy_pass http://kspider:8086; 33 | } 34 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | 3 | [![Kspider Docker Image CI](https://github.com/kkangert/kspider-ui/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/kkangert/kspider-ui/actions/workflows/docker-publish.yml) 4 | 5 | 一个可视化的爬虫平台。以流程图的方式配置爬虫,基本上无需编写代码即可完成工作。 6 | 提供了常用的功能,当然使用者也可自定义扩展。[Kspider 后端](https://github.com/kkangert/kspider)更方便、更快捷、更好用。Kspider 不仅限爬虫,也可用于 WEB 自动化测试,更多功能等你探索。 7 | 8 | ## 特性 9 | 10 | - 支持在线任务管理 11 | - 支持在线任务调试 12 | - 支持在线脚本表达式编写 13 | - 支持任务日志 14 | - 支持爬虫可视化调试 15 | - 支持产物下载 16 | 17 | 新增的特性: 18 | 19 | - 采用 Vue3、vite、Typescript、[fast-crud](https://github.com/fast-crud/fast-crud)构建,感受最新的技术,提升开发效率。 20 | - 支持表达式提示(TODO) 21 | - 支持函数在线编辑 22 | 23 | ## 免责声明 24 | 25 | 请勿使用本项目进行任何可能会违反法律规定和道德约束的工作。如您选择使用本项目,即代表您遵守此声明,作者不承担由于您违反此声明所带来的任何法律风险和损失。 26 | 27 | ## 作者 28 | 29 | - WeChat: kangert 30 | - Email: kangert@qq.com 31 | 32 | ## 🌟 Star History 33 | 34 | [![Star History Chart](https://api.star-history.com/svg?repos=kkangert/kspider-ui&type=Date)](https://star-history.com/#kkangert/kspider-ui&Date) 35 | -------------------------------------------------------------------------------- /src/utils/authFunction.ts: -------------------------------------------------------------------------------- 1 | import { useUserInfo } from '/@/stores/userInfo'; 2 | import { judementSameArr } from '/@/utils/arrayOperation'; 3 | 4 | /** 5 | * 单个权限验证 6 | * @param value 权限值 7 | * @returns 有权限,返回 `true`,反之则反 8 | */ 9 | export function auth(value: string): boolean { 10 | const stores = useUserInfo(); 11 | return stores.userInfos.authBtnList.some((v: string) => v === value); 12 | } 13 | 14 | /** 15 | * 多个权限验证,满足一个则为 true 16 | * @param value 权限值 17 | * @returns 有权限,返回 `true`,反之则反 18 | */ 19 | export function auths(value: Array): boolean { 20 | let flag = false; 21 | const stores = useUserInfo(); 22 | stores.userInfos.authBtnList.map((val: string) => { 23 | value.map((v: string) => { 24 | if (val === v) flag = true; 25 | }); 26 | }); 27 | return flag; 28 | } 29 | 30 | /** 31 | * 多个权限验证,全部满足则为 true 32 | * @param value 权限值 33 | * @returns 有权限,返回 `true`,反之则反 34 | */ 35 | export function authAll(value: Array): boolean { 36 | const stores = useUserInfo(); 37 | return judementSameArr(value, stores.userInfos.authBtnList); 38 | } 39 | -------------------------------------------------------------------------------- /src/views/flowDesign/component/tool/help.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 37 | -------------------------------------------------------------------------------- /src/layout/navBars/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | 28 | 36 | -------------------------------------------------------------------------------- /src/views/functionManage/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/views/flowManage/api.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * App应用管理API接口集合 5 | */ 6 | export function useSpiderApi() { 7 | return { 8 | /** 9 | * 新增 10 | * @param params 11 | * @returns 12 | */ 13 | add: (params: object) => { 14 | return request({ 15 | url: '/kspider/add', 16 | method: 'post', 17 | data: params, 18 | }); 19 | }, 20 | 21 | /** 22 | * 删除 23 | * @param params 24 | * @returns 25 | */ 26 | delete: (params: object) => { 27 | return request({ 28 | url: '/kspider/delete', 29 | method: 'post', 30 | data: params, 31 | }); 32 | }, 33 | 34 | /** 35 | * 修改 36 | * @param params 37 | * @returns 38 | */ 39 | edit: (params: object) => { 40 | return request({ 41 | url: '/kspider/edit', 42 | method: 'post', 43 | data: params, 44 | }); 45 | }, 46 | 47 | /** 48 | * 查询 49 | * @param params 50 | * @returns 51 | */ 52 | query: (params: object) => { 53 | return request({ 54 | url: '/kspider/query', 55 | method: 'post', 56 | data: params, 57 | }); 58 | }, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/views/functionManage/api.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * App应用管理API接口集合 5 | */ 6 | export function useFunctionApi() { 7 | return { 8 | /** 9 | * 新增 10 | * @param params 11 | * @returns 12 | */ 13 | add: (params: object) => { 14 | return request({ 15 | url: '/function/add', 16 | method: 'post', 17 | data: params, 18 | }); 19 | }, 20 | 21 | /** 22 | * 删除 23 | * @param params 24 | * @returns 25 | */ 26 | delete: (params: object) => { 27 | return request({ 28 | url: '/function/delete', 29 | method: 'post', 30 | data: params, 31 | }); 32 | }, 33 | 34 | /** 35 | * 分页查询 36 | * @param params 37 | * @returns 38 | */ 39 | query: (params: object) => { 40 | return request({ 41 | url: '/function/query', 42 | method: 'post', 43 | data: params, 44 | }); 45 | }, 46 | 47 | /** 48 | * 编辑 49 | * @param params 50 | * @returns 51 | */ 52 | edit: (params: object) => { 53 | return request({ 54 | url: '/function/edit', 55 | method: 'post', 56 | data: params, 57 | }); 58 | }, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 kangert@qq.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 一行最多多少个字符 3 | printWidth: 150, 4 | // 指定每个缩进级别的空格数 5 | tabWidth: 2, 6 | // 使用制表符而不是空格缩进行 7 | useTabs: true, 8 | // 在语句末尾打印分号 9 | semi: true, 10 | // 使用单引号而不是双引号 11 | singleQuote: true, 12 | // 更改引用对象属性的时间 可选值"" 13 | quoteProps: 'as-needed', 14 | // 在JSX中使用单引号而不是双引号 15 | jsxSingleQuote: false, 16 | // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"",默认none 17 | trailingComma: 'es5', 18 | // 在对象文字中的括号之间打印空格 19 | bracketSpacing: true, 20 | // jsx 标签的反尖括号需要换行 21 | jsxBracketSameLine: false, 22 | // 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x 23 | arrowParens: 'always', 24 | // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码 25 | rangeStart: 0, 26 | rangeEnd: Infinity, 27 | // 指定要使用的解析器,不需要写文件开头的 @prettier 28 | requirePragma: false, 29 | // 不需要自动在文件开头插入 @prettier 30 | insertPragma: false, 31 | // 使用默认的折行标准 always\never\preserve 32 | proseWrap: 'preserve', 33 | // 指定HTML文件的全局空格敏感度 css\strict\ignore 34 | htmlWhitespaceSensitivity: 'css', 35 | // Vue文件脚本和样式标签缩进 36 | vueIndentScriptAndStyle: false, 37 | // 换行符使用 lf 结尾是 可选值"" 38 | endOfLine: 'lf', 39 | }; 40 | -------------------------------------------------------------------------------- /src/stores/keepAliveNames.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | /** 4 | * 路由缓存列表 5 | * @methods setCacheKeepAlive 设置要缓存的路由 names(开启 Tagsview) 6 | * @methods addCachedView 添加要缓存的路由 names(关闭 Tagsview) 7 | * @methods delCachedView 删除要缓存的路由 names(关闭 Tagsview) 8 | * @methods delOthersCachedViews 右键菜单`关闭其它`,删除要缓存的路由 names(关闭 Tagsview) 9 | * @methods delAllCachedViews 右键菜单`全部关闭`,删除要缓存的路由 names(关闭 Tagsview) 10 | */ 11 | export const useKeepALiveNames = defineStore('keepALiveNames', { 12 | state: (): KeepAliveNamesState => ({ 13 | keepAliveNames: [], 14 | cachedViews: [], 15 | }), 16 | actions: { 17 | async setCacheKeepAlive(data: Array) { 18 | this.keepAliveNames = data; 19 | }, 20 | async addCachedView(view: any) { 21 | if (view.meta.isKeepAlive) this.cachedViews?.push(view.name); 22 | }, 23 | async delCachedView(view: any) { 24 | const index = this.cachedViews.indexOf(view.name); 25 | index > -1 && this.cachedViews.splice(index, 1); 26 | }, 27 | async delOthersCachedViews(view: any) { 28 | if (view.meta.isKeepAlive) this.cachedViews = [view.name]; 29 | else this.cachedViews = []; 30 | }, 31 | async delAllCachedViews() { 32 | this.cachedViews = []; 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/theme/media/layout.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于576px 4 | ------------------------------- */ 5 | @media screen and (max-width: $xs) { 6 | // MessageBox 弹框 7 | .el-message-box { 8 | width: 80% !important; 9 | } 10 | } 11 | 12 | /* 页面宽度小于768px 13 | ------------------------------- */ 14 | @media screen and (max-width: $sm) { 15 | // Breadcrumb 面包屑 16 | .layout-navbars-breadcrumb-hide { 17 | display: none; 18 | } 19 | // 外链视图 20 | .layout-view-link { 21 | a { 22 | max-width: 80%; 23 | text-align: center; 24 | } 25 | } 26 | // 菜单搜索 27 | .layout-search-dialog { 28 | .el-autocomplete { 29 | width: 80% !important; 30 | } 31 | } 32 | } 33 | 34 | /* 页面宽度小于1000px 35 | ------------------------------- */ 36 | @media screen and (max-width: 1000px) { 37 | // 布局配置 38 | .layout-drawer-content-flex { 39 | position: relative; 40 | &::after { 41 | content: '手机版不支持切换布局'; 42 | position: absolute; 43 | top: 0; 44 | right: 0; 45 | bottom: 0; 46 | left: 0; 47 | z-index: 1; 48 | text-align: center; 49 | height: 140px; 50 | line-height: 140px; 51 | background: rgba(255, 255, 255, 0.9); 52 | color: #666666; 53 | } 54 | } 55 | // pagination 分页中的工具栏 56 | .table-footer-tool { 57 | display: none !important; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/types/mitt.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * mitt 事件类型定义 3 | * 4 | * @method openSetingsDrawer 打开布局设置弹窗 5 | * @method restoreDefault 分栏布局,鼠标移入、移出数据显示 6 | * @method setSendColumnsChildren 分栏布局,鼠标移入、移出菜单数据传入到 navMenu 下的菜单中 7 | * @method setSendClassicChildren 经典布局,开启切割菜单时,菜单数据传入到 navMenu 下的菜单中 8 | * @method getBreadcrumbIndexSetFilterRoutes 布局设置弹窗,开启切割菜单时,菜单数据传入到 navMenu 下的菜单中 9 | * @method layoutMobileResize 浏览器窗口改变时,用于适配移动端界面显示 10 | * @method openOrCloseSortable 布局设置弹窗,开启 TagsView 拖拽 11 | * @method openShareTagsView 布局设置弹窗,开启 TagsView 共用 12 | * @method onTagsViewRefreshRouterView tagsview 刷新界面 13 | * @method onCurrentContextmenuClick tagsview 右键菜单每项点击时 14 | */ 15 | declare type MittType = { 16 | openSetingsDrawer?: string; 17 | restoreDefault?: string; 18 | setSendColumnsChildren: T; 19 | setSendClassicChildren: T; 20 | getBreadcrumbIndexSetFilterRoutes?: string; 21 | layoutMobileResize: T; 22 | openOrCloseSortable?: string; 23 | openShareTagsView?: string; 24 | onTagsViewRefreshRouterView?: T; 25 | onCurrentContextmenuClick?: T; 26 | }; 27 | 28 | // mitt 参数类型定义 29 | declare type LayoutMobileResize = { 30 | layout: string; 31 | clientWidth: number; 32 | }; 33 | 34 | // mitt 参数菜单类型 35 | declare type MittMenu = { 36 | children: RouteRecordRaw[]; 37 | item?: RouteItem; 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/setIconfont.ts: -------------------------------------------------------------------------------- 1 | // 字体图标 url 2 | const cssCdnUrlList: Array = [ 3 | '//at.alicdn.com/t/c/font_2298093_rnp72ifj3ba.css', 4 | '//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css', 5 | 6 | // Kspider执行器节点ICON 7 | '//at.alicdn.com/t/c/font_3968407_rcq8r0tfjzc.css', 8 | ]; 9 | // 第三方 js url 10 | const jsCdnUrlList: Array = []; 11 | 12 | // 动态批量设置字体图标 13 | export function setCssCdn() { 14 | if (cssCdnUrlList.length <= 0) return false; 15 | cssCdnUrlList.map((v) => { 16 | let link = document.createElement('link'); 17 | link.rel = 'stylesheet'; 18 | link.href = v; 19 | link.crossOrigin = 'anonymous'; 20 | document.getElementsByTagName('head')[0].appendChild(link); 21 | }); 22 | } 23 | 24 | // 动态批量设置第三方js 25 | export function setJsCdn() { 26 | if (jsCdnUrlList.length <= 0) return false; 27 | jsCdnUrlList.map((v) => { 28 | let link = document.createElement('script'); 29 | link.src = v; 30 | document.body.appendChild(link); 31 | }); 32 | } 33 | 34 | /** 35 | * 批量设置字体图标、动态js 36 | * @method cssCdn 动态批量设置字体图标 37 | * @method jsCdn 动态批量设置第三方js 38 | */ 39 | const setIntroduction = { 40 | // 设置css 41 | cssCdn: () => { 42 | setCssCdn(); 43 | }, 44 | // 设置js 45 | jsCdn: () => { 46 | setJsCdn(); 47 | }, 48 | }; 49 | 50 | // 导出函数方法 51 | export default setIntroduction; 52 | -------------------------------------------------------------------------------- /src/theme/mixins/index.scss: -------------------------------------------------------------------------------- 1 | /* 第三方图标字体间距/大小设置 2 | ------------------------------- */ 3 | @mixin generalIcon { 4 | font-size: 14px !important; 5 | display: inline-block; 6 | vertical-align: middle; 7 | margin-right: 5px; 8 | width: 24px; 9 | text-align: center; 10 | justify-content: center; 11 | } 12 | 13 | /* 文本不换行 14 | ------------------------------- */ 15 | @mixin text-no-wrap() { 16 | text-overflow: ellipsis; 17 | overflow: hidden; 18 | white-space: nowrap; 19 | } 20 | 21 | /* 多行文本溢出 22 | ------------------------------- */ 23 | @mixin text-ellipsis($line: 2) { 24 | overflow: hidden; 25 | word-break: break-all; 26 | text-overflow: ellipsis; 27 | display: -webkit-box; 28 | -webkit-line-clamp: $line; 29 | -webkit-box-orient: vertical; 30 | } 31 | 32 | /* 滚动条(页面未使用) div 中使用: 33 | ------------------------------- */ 34 | // .test { 35 | // @include scrollBar; 36 | // } 37 | @mixin scrollBar { 38 | // 滚动条凹槽的颜色,还可以设置边框属性 39 | &::-webkit-scrollbar-track-piece { 40 | background-color: #f8f8f8; 41 | } 42 | // 滚动条的宽度 43 | &::-webkit-scrollbar { 44 | width: 9px; 45 | height: 9px; 46 | } 47 | // 滚动条的设置 48 | &::-webkit-scrollbar-thumb { 49 | background-color: #dddddd; 50 | background-clip: padding-box; 51 | min-height: 28px; 52 | } 53 | &::-webkit-scrollbar-thumb:hover { 54 | background-color: #bbb; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/directive/authDirective.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { useUserInfo } from '/@/stores/userInfo'; 3 | import { judementSameArr } from '/@/utils/arrayOperation'; 4 | 5 | /** 6 | * 用户权限指令 7 | * @directive 单个权限验证(v-auth="xxx") 8 | * @directive 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]") 9 | * @directive 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]") 10 | */ 11 | export function authDirective(app: App) { 12 | // 单个权限验证(v-auth="xxx") 13 | app.directive('auth', { 14 | mounted(el, binding) { 15 | const stores = useUserInfo(); 16 | if (!stores.userInfos.authBtnList.some((v: string) => v === binding.value)) el.parentNode.removeChild(el); 17 | }, 18 | }); 19 | // 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]") 20 | app.directive('auths', { 21 | mounted(el, binding) { 22 | let flag = false; 23 | const stores = useUserInfo(); 24 | stores.userInfos.authBtnList.map((val: string) => { 25 | binding.value.map((v: string) => { 26 | if (val === v) flag = true; 27 | }); 28 | }); 29 | if (!flag) el.parentNode.removeChild(el); 30 | }, 31 | }); 32 | // 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]") 33 | app.directive('auth-all', { 34 | mounted(el, binding) { 35 | const stores = useUserInfo(); 36 | const flag = judementSameArr(binding.value, stores.userInfos.authBtnList); 37 | if (!flag) el.parentNode.removeChild(el); 38 | }, 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/types/layout.d.ts: -------------------------------------------------------------------------------- 1 | // aside 2 | declare type AsideState = { 3 | menuList: RouteRecordRaw[]; 4 | clientWidth: number; 5 | }; 6 | 7 | // columnsAside 8 | declare type ColumnsAsideState = { 9 | columnsAsideList: T[]; 10 | liIndex: number; 11 | liOldIndex: null | number; 12 | liHoverIndex: null | number; 13 | liOldPath: null | string; 14 | difference: number; 15 | routeSplit: string[]; 16 | }; 17 | 18 | // navBars breadcrumb 19 | declare type BreadcrumbState = { 20 | breadcrumbList: T[]; 21 | routeSplit: string[]; 22 | routeSplitFirst: string; 23 | routeSplitIndex: number; 24 | }; 25 | 26 | // navBars search 27 | declare type SearchState = { 28 | isShowSearch: boolean; 29 | menuQuery: string; 30 | tagsViewList: T[]; 31 | }; 32 | 33 | // navBars tagsView 34 | declare type TagsViewState = { 35 | routeActive: string | T; 36 | routePath: string | unknown; 37 | dropdown: { 38 | x: string | number; 39 | y: string | number; 40 | }; 41 | sortable: T; 42 | tagsRefsIndex: number; 43 | tagsViewList: T[]; 44 | tagsViewRoutesList: T[]; 45 | }; 46 | 47 | // navBars parent 48 | declare type ParentViewState = { 49 | refreshRouterViewKey: string; 50 | iframeRefreshKey: string; 51 | keepAliveNameList: string[]; 52 | iframeList: T[]; 53 | }; 54 | 55 | // navBars link 56 | declare type LinkViewState = { 57 | title: string; 58 | isLink: string; 59 | }; 60 | -------------------------------------------------------------------------------- /src/utils/loading.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue'; 2 | import '/@/theme/loading.scss'; 3 | 4 | /** 5 | * 页面全局 Loading 6 | * @method start 创建 loading 7 | * @method done 移除 loading 8 | */ 9 | export const NextLoading = { 10 | // 创建 loading 11 | start: () => { 12 | const bodys: Element = document.body; 13 | const div = document.createElement('div'); 14 | div.setAttribute('class', 'loading-next'); 15 | const htmls = ` 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | `; 30 | div.innerHTML = htmls; 31 | bodys.insertBefore(div, bodys.childNodes[0]); 32 | window.nextLoading = true; 33 | }, 34 | // 移除 loading 35 | done: (time: number = 0) => { 36 | nextTick(() => { 37 | setTimeout(() => { 38 | window.nextLoading = false; 39 | const el = document.querySelector('.loading-next'); 40 | el?.parentNode?.removeChild(el); 41 | }, time); 42 | }); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/layout/navBars/breadcrumb/closeFull.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | 54 | -------------------------------------------------------------------------------- /src/theme/other.scss: -------------------------------------------------------------------------------- 1 | /* wangeditor 富文本编辑器 2 | ------------------------------- */ 3 | .editor-container { 4 | z-index: 10; // 用于 wangeditor 点击全屏时 5 | .w-e-toolbar { 6 | border: 1px solid var(--el-border-color-light, #ebeef5) !important; 7 | border-bottom: 1px solid var(--el-border-color-light, #ebeef5) !important; 8 | border-top-left-radius: 3px; 9 | border-top-right-radius: 3px; 10 | z-index: 2 !important; 11 | } 12 | .w-e-text-container { 13 | border: 1px solid var(--el-border-color-light, #ebeef5) !important; 14 | border-top: none !important; 15 | border-bottom-left-radius: 3px; 16 | border-bottom-right-radius: 3px; 17 | z-index: 1 !important; 18 | } 19 | } 20 | 21 | [data-theme='dark'] { 22 | // textarea - css vars 23 | --w-e-textarea-bg-color: var(--el-color-white) !important; 24 | --w-e-textarea-color: var(--el-text-color-primary) !important; 25 | 26 | // toolbar - css vars 27 | --w-e-toolbar-color: var(--el-text-color-primary) !important; 28 | --w-e-toolbar-bg-color: var(--el-color-white) !important; 29 | --w-e-toolbar-active-color: var(--el-text-color-primary) !important; 30 | --w-e-toolbar-active-bg-color: var(--next-color-menu-hover) !important; 31 | --w-e-toolbar-border-color: var(--el-border-color-light, #ebeef5) !important; 32 | 33 | // modal - css vars 34 | --w-e-modal-button-bg-color: var(--el-color-primary) !important; 35 | --w-e-modal-button-border-color: var(--el-color-primary) !important; 36 | } 37 | -------------------------------------------------------------------------------- /src/views/appManage/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/layout/navMenu/subItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 50 | -------------------------------------------------------------------------------- /src/theme/media/scrollbar.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | // 滚动条的宽度 7 | ::-webkit-scrollbar { 8 | width: 3px !important; 9 | height: 3px !important; 10 | } 11 | ::-webkit-scrollbar-track-piece { 12 | background-color: var(--next-bg-main-color); 13 | } 14 | // 滚动条的设置 15 | ::-webkit-scrollbar-thumb { 16 | background-color: rgba(144, 147, 153, 0.3); 17 | background-clip: padding-box; 18 | min-height: 28px; 19 | border-radius: 5px; 20 | transition: 0.3s background-color; 21 | } 22 | ::-webkit-scrollbar-thumb:hover { 23 | background-color: rgba(144, 147, 153, 0.5); 24 | } 25 | // element plus scrollbar 26 | .el-scrollbar__bar.is-vertical { 27 | width: 2px !important; 28 | } 29 | .el-scrollbar__bar.is-horizontal { 30 | height: 2px !important; 31 | } 32 | } 33 | 34 | /* 页面宽度大于768px 35 | ------------------------------- */ 36 | @media screen and (min-width: 769px) { 37 | // 滚动条的宽度 38 | ::-webkit-scrollbar { 39 | width: 7px; 40 | height: 7px; 41 | } 42 | ::-webkit-scrollbar-track-piece { 43 | background-color: var(--next-bg-main-color); 44 | } 45 | // 滚动条的设置 46 | ::-webkit-scrollbar-thumb { 47 | background-color: rgba(144, 147, 153, 0.3); 48 | background-clip: padding-box; 49 | min-height: 28px; 50 | border-radius: 5px; 51 | transition: 0.3s background-color; 52 | } 53 | ::-webkit-scrollbar-thumb:hover { 54 | background-color: rgba(144, 147, 153, 0.5); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/theme/loading.scss: -------------------------------------------------------------------------------- 1 | .loading-next { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | .loading-next .loading-next-box { 6 | position: absolute; 7 | top: 50%; 8 | left: 50%; 9 | transform: translate(-50%, -50%); 10 | } 11 | .loading-next .loading-next-box-warp { 12 | width: 80px; 13 | height: 80px; 14 | } 15 | .loading-next .loading-next-box-warp .loading-next-box-item { 16 | width: 33.333333%; 17 | height: 33.333333%; 18 | background: var(--el-color-primary); 19 | float: left; 20 | animation: loading-next-animation 1.2s infinite ease; 21 | border-radius: 1px; 22 | } 23 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(7) { 24 | animation-delay: 0s; 25 | } 26 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(4), 27 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(8) { 28 | animation-delay: 0.1s; 29 | } 30 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(1), 31 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(5), 32 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(9) { 33 | animation-delay: 0.2s; 34 | } 35 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(2), 36 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(6) { 37 | animation-delay: 0.3s; 38 | } 39 | .loading-next .loading-next-box-warp .loading-next-box-item:nth-child(3) { 40 | animation-delay: 0.4s; 41 | } 42 | @keyframes loading-next-animation { 43 | 0%, 44 | 70%, 45 | 100% { 46 | transform: scale3D(1, 1, 1); 47 | } 48 | 35% { 49 | transform: scale3D(0, 0, 1); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/watermark.ts: -------------------------------------------------------------------------------- 1 | // 页面添加水印效果 2 | const setWatermark = (str: string) => { 3 | const id = '1.23452384164.123412416'; 4 | if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id)); 5 | const can = document.createElement('canvas'); 6 | can.width = 200; 7 | can.height = 130; 8 | const cans = can.getContext('2d'); 9 | cans.rotate((-20 * Math.PI) / 180); 10 | cans.font = '12px Vedana'; 11 | cans.fillStyle = 'rgba(200, 200, 200, 0.30)'; 12 | cans.textBaseline = 'middle'; 13 | cans.fillText(str, can.width / 10, can.height / 2); 14 | const div = document.createElement('div'); 15 | div.id = id; 16 | div.style.pointerEvents = 'none'; 17 | div.style.top = '0px'; 18 | div.style.left = '0px'; 19 | div.style.position = 'fixed'; 20 | div.style.zIndex = '10000000'; 21 | div.style.width = `${document.documentElement.clientWidth}px`; 22 | div.style.height = `${document.documentElement.clientHeight}px`; 23 | div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`; 24 | document.body.appendChild(div); 25 | return id; 26 | }; 27 | 28 | /** 29 | * 页面添加水印效果 30 | * @method set 设置水印 31 | * @method del 删除水印 32 | */ 33 | const watermark = { 34 | // 设置水印 35 | set: (str: string) => { 36 | let id = setWatermark(str); 37 | if (document.getElementById(id) === null) id = setWatermark(str); 38 | }, 39 | // 删除水印 40 | del: () => { 41 | let id = '1.23452384164.123412416'; 42 | if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id)); 43 | }, 44 | }; 45 | 46 | // 导出方法 47 | export default watermark; 48 | -------------------------------------------------------------------------------- /src/views/flowDesign/api.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * 可视化爬虫\WEB自动化测试工具设计界面 5 | */ 6 | export function useSpiderDesignApi() { 7 | return { 8 | /** 9 | * 新增 10 | * @param params 11 | * @returns 12 | */ 13 | add: (params: object) => { 14 | return request({ 15 | url: '/kspider/add', 16 | method: 'post', 17 | data: params, 18 | }); 19 | }, 20 | 21 | /** 22 | * 删除 23 | * @param params 24 | * @returns 25 | */ 26 | delete: (params: object) => { 27 | return request({ 28 | url: '/function/delete', 29 | method: 'post', 30 | data: params, 31 | }); 32 | }, 33 | 34 | /** 35 | * 查询所有节点执行器 36 | * @param params 37 | * @returns 38 | */ 39 | queryNodeList: () => { 40 | return request({ 41 | url: '/kspider/nodeList', 42 | method: 'post', 43 | }); 44 | }, 45 | 46 | /** 47 | * 查询指定节点的配置项 48 | * @param params 49 | * @returns 50 | */ 51 | queryNodeConfigItem: (params: object) => { 52 | return request({ 53 | url: '/kspider/nodeConfigItem', 54 | method: 'post', 55 | data: params, 56 | }); 57 | }, 58 | 59 | /** 60 | * 获取当前工作流json数据(用于前端渲染流程图) 61 | * @param flowId 流程ID 62 | * @returns 63 | */ 64 | querySpiderFlowJson: (flowId: number) => { 65 | return request({ 66 | url: '/kspider/flowJson/' + flowId, 67 | method: 'get', 68 | }); 69 | }, 70 | 71 | /** 72 | * 编辑 73 | * @param params 74 | * @returns 75 | */ 76 | edit: (params: object) => { 77 | return request({ 78 | url: '/kspider/edit', 79 | method: 'post', 80 | data: params, 81 | }); 82 | }, 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /src/layout/main/transverse.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 59 | -------------------------------------------------------------------------------- /src/theme/media/login.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于1200px 4 | ------------------------------- */ 5 | @media screen and (max-width: $lg) and (min-width: $xs) { 6 | .login-container { 7 | .login-left { 8 | .login-left-img { 9 | top: 90% !important; 10 | left: 12% !important; 11 | width: 30% !important; 12 | height: 18% !important; 13 | } 14 | } 15 | .login-right { 16 | position: absolute; 17 | top: 50%; 18 | left: 50%; 19 | transform: translate(-50%, -50%); 20 | } 21 | } 22 | } 23 | 24 | /* 页面宽度小于576px 25 | ------------------------------- */ 26 | @media screen and (max-width: $xs) { 27 | .login-container { 28 | .login-left { 29 | display: none; 30 | } 31 | .login-right { 32 | width: 100% !important; 33 | .login-right-warp { 34 | width: 100% !important; 35 | height: 100% !important; 36 | border: none !important; 37 | .login-right-warp-mian { 38 | .el-form-item { 39 | display: flex !important; 40 | } 41 | .login-right-warp-main-title { 42 | font-size: 20px !important; 43 | } 44 | } 45 | .login-right-warp-one { 46 | &::after { 47 | right: 0 !important; 48 | } 49 | } 50 | .login-right-warp-two { 51 | &::before { 52 | bottom: 1px !important; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | /* 页面宽度小于375px 61 | ------------------------------- */ 62 | @media screen and (max-width: $us) { 63 | .login-container { 64 | .login-right { 65 | .login-right-warp { 66 | .login-right-warp-mian { 67 | .login-right-warp-main-title { 68 | font-size: 18px !important; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 51 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | 3 | /** 4 | * window.localStorage 浏览器永久缓存 5 | * @method set 设置永久缓存 6 | * @method get 获取永久缓存 7 | * @method remove 移除永久缓存 8 | * @method clear 移除全部永久缓存 9 | */ 10 | export const Local = { 11 | // 查看 v2.4.3版本更新日志 12 | setKey(key: string) { 13 | // @ts-ignore 14 | return `${__NEXT_NAME__}:${key}`; 15 | }, 16 | // 设置永久缓存 17 | set(key: string, val: T) { 18 | window.localStorage.setItem(Local.setKey(key), JSON.stringify(val)); 19 | }, 20 | // 获取永久缓存 21 | get(key: string) { 22 | let json = window.localStorage.getItem(Local.setKey(key)); 23 | return JSON.parse(json); 24 | }, 25 | // 移除永久缓存 26 | remove(key: string) { 27 | window.localStorage.removeItem(Local.setKey(key)); 28 | }, 29 | // 移除全部永久缓存 30 | clear() { 31 | window.localStorage.clear(); 32 | }, 33 | }; 34 | 35 | /** 36 | * window.sessionStorage 浏览器临时缓存 37 | * @method set 设置临时缓存 38 | * @method get 获取临时缓存 39 | * @method remove 移除临时缓存 40 | * @method clear 移除全部临时缓存 41 | */ 42 | export const Session = { 43 | // 设置临时缓存 44 | set(key: string, val: T) { 45 | if (key === 'token') return Cookies.set(key, val); 46 | window.sessionStorage.setItem(Local.setKey(key), JSON.stringify(val)); 47 | }, 48 | // 获取临时缓存 49 | get(key: string) { 50 | if (key === 'token') return Cookies.get(key); 51 | let json = window.sessionStorage.getItem(Local.setKey(key)); 52 | return JSON.parse(json); 53 | }, 54 | // 移除临时缓存 55 | remove(key: string) { 56 | if (key === 'token') return Cookies.remove(key); 57 | window.sessionStorage.removeItem(Local.setKey(key)); 58 | }, 59 | // 移除全部临时缓存 60 | clear() { 61 | Cookies.remove('token'); 62 | window.sessionStorage.clear(); 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/utils/arrayOperation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断两数组字符串是否相同(用于按钮权限验证),数组字符串中存在相同时会自动去重(按钮权限标识不会重复) 3 | * @param news 新数据 4 | * @param old 源数据 5 | * @returns 两数组相同返回 `true`,反之则反 6 | */ 7 | export function judementSameArr(newArr: unknown[] | string[], oldArr: string[]): boolean { 8 | const news = removeDuplicate(newArr); 9 | const olds = removeDuplicate(oldArr); 10 | let count = 0; 11 | const leng = news.length; 12 | for (let i in olds) { 13 | for (let j in news) { 14 | if (olds[i] === news[j]) count++; 15 | } 16 | } 17 | return count === leng ? true : false; 18 | } 19 | 20 | /** 21 | * 判断两个对象是否相同 22 | * @param a 要比较的对象一 23 | * @param b 要比较的对象二 24 | * @returns 相同返回 true,反之则反 25 | */ 26 | export function isObjectValueEqual(a: T, b: T): boolean { 27 | if (!a || !b) return false; 28 | let aProps = Object.getOwnPropertyNames(a); 29 | let bProps = Object.getOwnPropertyNames(b); 30 | if (aProps.length != bProps.length) return false; 31 | for (let i = 0; i < aProps.length; i++) { 32 | let propName = aProps[i]; 33 | let propA = a[propName]; 34 | let propB = b[propName]; 35 | if (!b.hasOwnProperty(propName)) return false; 36 | if (propA instanceof Object) { 37 | if (!isObjectValueEqual(propA, propB)) return false; 38 | } else if (propA !== propB) { 39 | return false; 40 | } 41 | } 42 | return true; 43 | } 44 | 45 | /** 46 | * 数组、数组对象去重 47 | * @param arr 数组内容 48 | * @param attr 需要去重的键值(数组对象) 49 | * @returns 50 | */ 51 | export function removeDuplicate(arr: EmptyArrayType, attr?: string) { 52 | if (!Object.keys(arr).length) { 53 | return arr; 54 | } else { 55 | if (attr) { 56 | const obj: EmptyObjectType = {}; 57 | return arr.reduce((cur: EmptyArrayType[], item: EmptyArrayType) => { 58 | obj[item[attr]] ? '' : (obj[item[attr]] = true && item[attr] && cur.push(item)); 59 | return cur; 60 | }, []); 61 | } else { 62 | return [...new Set(arr)]; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/views/taskManage/api.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | export function useSpiderTaskApi() { 4 | return { 5 | /** 6 | * 新增 7 | * @param params 8 | * @returns 9 | */ 10 | add: (params: object) => { 11 | return request({ 12 | url: '/task/add', 13 | method: 'post', 14 | data: params, 15 | }); 16 | }, 17 | 18 | /** 19 | * 删除 20 | * @param params 21 | * @returns 22 | */ 23 | delete: (params: object) => { 24 | return request({ 25 | url: '/task/delete', 26 | method: 'post', 27 | data: params, 28 | }); 29 | }, 30 | 31 | /** 32 | * 分页查询 33 | * @param params 34 | * @returns 35 | */ 36 | query: (params: object) => { 37 | return request({ 38 | url: '/task/query', 39 | method: 'post', 40 | data: params, 41 | }); 42 | }, 43 | 44 | /** 45 | * 编辑 46 | * @param params 47 | * @returns 48 | */ 49 | edit: (params: object) => { 50 | return request({ 51 | url: '/task/edit', 52 | method: 'post', 53 | data: params, 54 | }); 55 | }, 56 | download: (params: object) => { 57 | request({ 58 | url: '/task/download', 59 | method: 'post', 60 | data: params, 61 | responseType: 'blob', 62 | }).then((res: any) => { 63 | const filename = decodeURI(res.headers['content-disposition'].split(';')[1].split('filename=')[1]); 64 | let blob = new Blob([res]); 65 | let url = window.URL.createObjectURL(blob); 66 | let a = document.createElement('a'); 67 | a.href = url; 68 | a.download = filename; 69 | a.click(); 70 | window.URL.revokeObjectURL(url); 71 | }); 72 | return null; 73 | }, 74 | 75 | runTask: (params: object) => { 76 | return request({ 77 | url: '/task/run', 78 | method: 'post', 79 | data: params, 80 | }); 81 | }, 82 | 83 | stopTask: (params: object) => { 84 | return request({ 85 | url: '/task/stop', 86 | method: 'post', 87 | data: params, 88 | }); 89 | }, 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kspider", 3 | "version": "0.0.6", 4 | "description": "Kspider前端项目,为爬虫、WEB自动化测试工具,提供可视化配置方式", 5 | "author": "kangert", 6 | "private": true, 7 | "scripts": { 8 | "dev": "vite --force", 9 | "build": "vite build", 10 | "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" 11 | }, 12 | "dependencies": { 13 | "@element-plus/icons-vue": "^2.0.10", 14 | "@fast-crud/fast-crud": "^1.21.1", 15 | "@fast-crud/fast-extends": "^1.21.1", 16 | "@fast-crud/ui-element": "^1.21.1", 17 | "@fast-crud/ui-interface": "^1.21.1", 18 | "@iconify/vue": "^4.1.2", 19 | "axios": "^1.3.3", 20 | "element-plus": "^2.2.32", 21 | "js-cookie": "^3.0.1", 22 | "jsplumb": "^2.15.6", 23 | "mitt": "^3.0.0", 24 | "nprogress": "^0.2.0", 25 | "pinia": "^2.0.32", 26 | "qs": "^6.11.0", 27 | "screenfull": "^6.0.2", 28 | "sortablejs": "^1.15.0", 29 | "vue": "^3.2.47", 30 | "vue-clipboard3": "^2.0.0", 31 | "vue-i18n": "^9.2.2", 32 | "vue-router": "^4.1.6", 33 | "uuid": "^8.3.2" 34 | }, 35 | "devDependencies": { 36 | "@iconify/iconify": "^2.0.1", 37 | "@iconify/json": "^1.1.376", 38 | "@purge-icons/generated": "^0.7.0", 39 | "@types/node": "^18.15.11", 40 | "@types/nprogress": "^0.2.0", 41 | "@types/sortablejs": "^1.15.1", 42 | "@typescript-eslint/eslint-plugin": "^5.58.0", 43 | "@typescript-eslint/parser": "^5.58.0", 44 | "@vitejs/plugin-vue": "^4.1.0", 45 | "@vue/compiler-sfc": "^3.2.47", 46 | "eslint": "^8.38.0", 47 | "eslint-plugin-vue": "^9.10.0", 48 | "prettier": "^2.8.7", 49 | "sass": "^1.61.0", 50 | "typescript": "^5.0.4", 51 | "vite": "^5.3.4", 52 | "vite-plugin-cdn-import": "^0.3.5", 53 | "vite-plugin-compression": "^0.5.1", 54 | "vite-plugin-purge-icons": "^0.7.0", 55 | "vite-plugin-vue-setup-extend-plus": "^0.1.0", 56 | "vue-eslint-parser": "^9.1.1" 57 | }, 58 | "browserslist": [ 59 | "> 1%", 60 | "last 2 versions", 61 | "not dead" 62 | ], 63 | "engines": { 64 | "node": ">=16.0.0", 65 | "npm": ">=7.0.0", 66 | "pnpm": ">=9.7.1" 67 | }, 68 | "packageManager": "pnpm@9.7.1" 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus'; 2 | 3 | /** 4 | * 颜色转换函数 5 | * @method hexToRgb hex 颜色转 rgb 颜色 6 | * @method rgbToHex rgb 颜色转 Hex 颜色 7 | * @method getDarkColor 加深颜色值 8 | * @method getLightColor 变浅颜色值 9 | */ 10 | export function useChangeColor() { 11 | // str 颜色值字符串 12 | const hexToRgb = (str: string): any => { 13 | let hexs: any = ''; 14 | let reg = /^\#?[0-9A-Fa-f]{6}$/; 15 | if (!reg.test(str)) { 16 | ElMessage.warning('输入错误的hex'); 17 | return ''; 18 | } 19 | str = str.replace('#', ''); 20 | hexs = str.match(/../g); 21 | for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16); 22 | return hexs; 23 | }; 24 | // r 代表红色 | g 代表绿色 | b 代表蓝色 25 | const rgbToHex = (r: any, g: any, b: any): string => { 26 | let reg = /^\d{1,3}$/; 27 | if (!reg.test(r) || !reg.test(g) || !reg.test(b)) { 28 | ElMessage.warning('输入错误的rgb颜色值'); 29 | return ''; 30 | } 31 | let hexs = [r.toString(16), g.toString(16), b.toString(16)]; 32 | for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`; 33 | return `#${hexs.join('')}`; 34 | }; 35 | // color 颜色值字符串 | level 变浅的程度,限0-1之间 36 | const getDarkColor = (color: string, level: number): string => { 37 | let reg = /^\#?[0-9A-Fa-f]{6}$/; 38 | if (!reg.test(color)) { 39 | ElMessage.warning('输入错误的hex颜色值'); 40 | return ''; 41 | } 42 | let rgb = useChangeColor().hexToRgb(color); 43 | for (let i = 0; i < 3; i++) rgb[i] = Math.floor(rgb[i] * (1 - level)); 44 | return useChangeColor().rgbToHex(rgb[0], rgb[1], rgb[2]); 45 | }; 46 | // color 颜色值字符串 | level 加深的程度,限0-1之间 47 | const getLightColor = (color: string, level: number): string => { 48 | let reg = /^\#?[0-9A-Fa-f]{6}$/; 49 | if (!reg.test(color)) { 50 | ElMessage.warning('输入错误的hex颜色值'); 51 | return ''; 52 | } 53 | let rgb = useChangeColor().hexToRgb(color); 54 | for (let i = 0; i < 3; i++) rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]); 55 | return useChangeColor().rgbToHex(rgb[0], rgb[1], rgb[2]); 56 | }; 57 | return { 58 | hexToRgb, 59 | rgbToHex, 60 | getDarkColor, 61 | getLightColor, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/commonFunction.ts: -------------------------------------------------------------------------------- 1 | // 通用函数 2 | import useClipboard from 'vue-clipboard3'; 3 | import { ElMessage } from 'element-plus'; 4 | import { formatDate } from '/@/utils/formatTime'; 5 | import { useI18n } from 'vue-i18n'; 6 | 7 | export default function () { 8 | const { t } = useI18n(); 9 | const { toClipboard } = useClipboard(); 10 | 11 | // 百分比格式化 12 | const percentFormat = (row: EmptyArrayType, column: number, cellValue: string) => { 13 | return cellValue ? `${cellValue}%` : '-'; 14 | }; 15 | // 列表日期时间格式化 16 | const dateFormatYMD = (row: EmptyArrayType, column: number, cellValue: string) => { 17 | if (!cellValue) return '-'; 18 | return formatDate(new Date(cellValue), 'YYYY-mm-dd'); 19 | }; 20 | // 列表日期时间格式化 21 | const dateFormatYMDHMS = (row: EmptyArrayType, column: number, cellValue: string) => { 22 | if (!cellValue) return '-'; 23 | return formatDate(new Date(cellValue), 'YYYY-mm-dd HH:MM:SS'); 24 | }; 25 | // 列表日期时间格式化 26 | const dateFormatHMS = (row: EmptyArrayType, column: number, cellValue: string) => { 27 | if (!cellValue) return '-'; 28 | let time = 0; 29 | if (typeof row === 'number') time = row; 30 | if (typeof cellValue === 'number') time = cellValue; 31 | return formatDate(new Date(time * 1000), 'HH:MM:SS'); 32 | }; 33 | // 小数格式化 34 | const scaleFormat = (value: string = '0', scale: number = 4) => { 35 | return Number.parseFloat(value).toFixed(scale); 36 | }; 37 | // 小数格式化 38 | const scale2Format = (value: string = '0') => { 39 | return Number.parseFloat(value).toFixed(2); 40 | }; 41 | // 点击复制文本 42 | const copyText = (text: string) => { 43 | return new Promise((resolve, reject) => { 44 | try { 45 | //复制 46 | toClipboard(text); 47 | //下面可以设置复制成功的提示框等操作 48 | ElMessage.success(t('message.layout.copyTextSuccess')); 49 | resolve(text); 50 | } catch (e) { 51 | //复制失败 52 | ElMessage.error(t('message.layout.copyTextError')); 53 | reject(e); 54 | } 55 | }); 56 | }; 57 | return { 58 | percentFormat, 59 | dateFormatYMD, 60 | dateFormatYMDHMS, 61 | dateFormatHMS, 62 | scaleFormat, 63 | scale2Format, 64 | copyText, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /src/layout/logo/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | 33 | 76 | -------------------------------------------------------------------------------- /src/views/error/401.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | 76 | -------------------------------------------------------------------------------- /src/components/svgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 68 | -------------------------------------------------------------------------------- /src/layout/main/defaults.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 72 | -------------------------------------------------------------------------------- /src/views/taskManage/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/components/iconSelector/list.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 44 | 45 | 85 | -------------------------------------------------------------------------------- /src/layout/main/classic.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 72 | -------------------------------------------------------------------------------- /src/layout/main/columns.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 72 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Kspider Docker Image CI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | packages: write 9 | contents: read 10 | attestations: write 11 | id-token: write 12 | 13 | jobs: 14 | build_push_dockerhub: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v3 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v3 26 | 27 | - name: Login to Docker Hub 28 | uses: docker/login-action@v3 29 | with: 30 | username: ${{ secrets.DOCKER_USERNAME }} 31 | password: ${{ secrets.DOCKER_PASSWORD }} 32 | 33 | - name: Login to GitHub Container Registry 34 | uses: docker/login-action@v3 35 | with: 36 | registry: ghcr.io 37 | username: ${{ secrets.DOCKER_USERNAME }} 38 | password: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | - name: Login to Aliyun Container Registry 41 | uses: docker/login-action@v3 42 | with: 43 | registry: registry.cn-beijing.aliyuncs.com 44 | username: ${{ secrets.ALIYUN_USERNAME }} 45 | password: ${{ secrets.ALIYUN_PASSWORD }} 46 | 47 | - name: Build and push Docker image 48 | uses: docker/build-push-action@v6 49 | id: build_push_dockerhub 50 | with: 51 | file: Dockerfile 52 | push: true 53 | platforms: linux/amd64,linux/arm64 54 | tags: | 55 | ${{ github.repository }}:latest 56 | ${{ github.repository }}:${{ github.event.release.tag_name }} 57 | 58 | ghcr.io/${{ github.repository }}:latest 59 | ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} 60 | 61 | registry.cn-beijing.aliyuncs.com/${{ github.repository }}:latest 62 | registry.cn-beijing.aliyuncs.com/${{ github.repository }}:${{ github.event.release.tag_name }} 63 | 64 | - name: Generate artifact attestation 65 | uses: actions/attest-build-provenance@v1 66 | with: 67 | subject-name: ghcr.io/${{ github.repository }} 68 | subject-digest: ${{ steps.build_push_dockerhub.outputs.digest }} 69 | push-to-registry: true -------------------------------------------------------------------------------- /src/types/pinia.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * pinia 类型定义 3 | */ 4 | 5 | // 用户信息 6 | declare interface UserInfosState { 7 | userInfos: { 8 | authBtnList: string[]; 9 | photo: string; 10 | roles: string[]; 11 | time: number; 12 | userName: string; 13 | [key: string]: T; 14 | }; 15 | } 16 | 17 | // 路由缓存列表 18 | declare interface KeepAliveNamesState { 19 | keepAliveNames: string[]; 20 | cachedViews: string[]; 21 | } 22 | 23 | // 后端返回原始路由(未处理时) 24 | declare interface RequestOldRoutesState { 25 | requestOldRoutes: string[]; 26 | } 27 | 28 | // TagsView 路由列表 29 | declare interface TagsViewRoutesState { 30 | tagsViewRoutes: T[]; 31 | isTagsViewCurrenFull: Boolean; 32 | } 33 | 34 | // 路由列表 35 | declare interface RoutesListState { 36 | routesList: T[]; 37 | isColumnsMenuHover: Boolean; 38 | isColumnsNavHover: Boolean; 39 | } 40 | 41 | // 布局配置 42 | declare interface ThemeConfigState { 43 | themeConfig: { 44 | isDrawer: boolean; 45 | primary: string; 46 | topBar: string; 47 | topBarColor: string; 48 | isTopBarColorGradual: boolean; 49 | menuBar: string; 50 | menuBarColor: string; 51 | menuBarActiveColor: string; 52 | isMenuBarColorGradual: boolean; 53 | columnsMenuBar: string; 54 | columnsMenuBarColor: string; 55 | isColumnsMenuBarColorGradual: boolean; 56 | isColumnsMenuHoverPreload: boolean; 57 | isCollapse: boolean; 58 | isUniqueOpened: boolean; 59 | isFixedHeader: boolean; 60 | isFixedHeaderChange: boolean; 61 | isClassicSplitMenu: boolean; 62 | isLockScreen: boolean; 63 | lockScreenTime: number; 64 | isShowLogo: boolean; 65 | isShowLogoChange: boolean; 66 | isBreadcrumb: boolean; 67 | isTagsview: boolean; 68 | isBreadcrumbIcon: boolean; 69 | isTagsviewIcon: boolean; 70 | isCacheTagsView: boolean; 71 | isSortableTagsView: boolean; 72 | isShareTagsView: boolean; 73 | isFooter: boolean; 74 | isGrayscale: boolean; 75 | isInvert: boolean; 76 | isIsDark: boolean; 77 | isWartermark: boolean; 78 | wartermarkText: string; 79 | tagsStyle: string; 80 | animation: string; 81 | columnsAsideStyle: string; 82 | columnsAsideLayout: string; 83 | layout: string; 84 | isRequestRoutes: boolean; 85 | globalTitle: string; 86 | globalViceTitle: string; 87 | globalViceTitleMsg: string; 88 | globalI18n: string; 89 | globalComponentSize: string; 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/stores/userInfo.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import Cookies from 'js-cookie'; 3 | import { Session } from '/@/utils/storage'; 4 | 5 | /** 6 | * 用户信息 7 | * @methods setUserInfos 设置用户信息 8 | */ 9 | export const useUserInfo = defineStore('userInfo', { 10 | state: (): UserInfosState => ({ 11 | userInfos: { 12 | userName: '', 13 | photo: '', 14 | time: 0, 15 | roles: [], 16 | authBtnList: [], 17 | }, 18 | }), 19 | actions: { 20 | async setUserInfos() { 21 | // 存储用户信息到浏览器缓存 22 | if (Session.get('userInfo')) { 23 | this.userInfos = Session.get('userInfo'); 24 | } else { 25 | const userInfos: any = await this.getApiUserInfo(); 26 | this.userInfos = userInfos; 27 | } 28 | }, 29 | // 模拟接口数据 30 | // https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP 31 | async getApiUserInfo() { 32 | return new Promise((resolve) => { 33 | setTimeout(() => { 34 | // 模拟数据,请求接口时,记得删除多余代码及对应依赖的引入 35 | const userName = Cookies.get('userName'); 36 | // 模拟数据 37 | let defaultRoles: Array = []; 38 | let defaultAuthBtnList: Array = []; 39 | // admin 页面权限标识,对应路由 meta.roles,用于控制路由的显示/隐藏 40 | let adminRoles: Array = ['admin']; 41 | // admin 按钮权限标识 42 | let adminAuthBtnList: Array = ['btn.add', 'btn.del', 'btn.edit', 'btn.link']; 43 | // test 页面权限标识,对应路由 meta.roles,用于控制路由的显示/隐藏 44 | let testRoles: Array = ['common']; 45 | // test 按钮权限标识 46 | let testAuthBtnList: Array = ['btn.add', 'btn.link']; 47 | // 不同用户模拟不同的用户权限 48 | if (userName === 'admin') { 49 | defaultRoles = adminRoles; 50 | defaultAuthBtnList = adminAuthBtnList; 51 | } else { 52 | defaultRoles = testRoles; 53 | defaultAuthBtnList = testAuthBtnList; 54 | } 55 | // 用户信息模拟数据 56 | const userInfos = { 57 | userName: userName, 58 | photo: 59 | userName === 'admin' 60 | ? 'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500' 61 | : 'https://img2.baidu.com/it/u=2370931438,70387529&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', 62 | time: new Date().getTime(), 63 | roles: defaultRoles, 64 | authBtnList: defaultAuthBtnList, 65 | }; 66 | resolve(userInfos); 67 | }, 0); 68 | }); 69 | }, 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /src/theme/media/chart.scss: -------------------------------------------------------------------------------- 1 | @import './index.scss'; 2 | 3 | /* 页面宽度小于768px 4 | ------------------------------- */ 5 | @media screen and (max-width: $sm) { 6 | .big-data-down-left { 7 | width: 100% !important; 8 | flex-direction: unset !important; 9 | flex-wrap: wrap; 10 | .flex-warp-item { 11 | min-height: 196.24px; 12 | padding: 0 7.5px 15px 15px !important; 13 | .flex-warp-item-box { 14 | border: none !important; 15 | border-bottom: 1px solid #ebeef5 !important; 16 | } 17 | } 18 | } 19 | .big-data-down-center { 20 | width: 100% !important; 21 | .big-data-down-center-one, 22 | .big-data-down-center-two { 23 | min-height: 196.24px; 24 | padding-left: 15px !important; 25 | .big-data-down-center-one-content { 26 | border: none !important; 27 | border-bottom: 1px solid #ebeef5 !important; 28 | } 29 | .flex-warp-item-box { 30 | @extend .big-data-down-center-one-content; 31 | } 32 | } 33 | } 34 | .big-data-down-right { 35 | .flex-warp-item { 36 | .flex-warp-item-box { 37 | border: none !important; 38 | border-bottom: 1px solid #ebeef5 !important; 39 | } 40 | &:nth-of-type(2) { 41 | padding-left: 15px !important; 42 | } 43 | &:last-of-type { 44 | .flex-warp-item-box { 45 | border: none !important; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | /* 页面宽度大于768px小于1200px 53 | ------------------------------- */ 54 | @media screen and (min-width: $sm) and (max-width: $lg) { 55 | .chart-warp-bottom { 56 | .big-data-down-left { 57 | width: 50% !important; 58 | } 59 | .big-data-down-center { 60 | width: 50% !important; 61 | } 62 | .big-data-down-right { 63 | .flex-warp-item { 64 | width: 50% !important; 65 | &:nth-of-type(2) { 66 | padding-left: 7.5px !important; 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | /* 页面宽度小于1200px 74 | ------------------------------- */ 75 | @media screen and (max-width: $lg) { 76 | .chart-warp-top { 77 | .up-left { 78 | display: none; 79 | } 80 | } 81 | .chart-warp-bottom { 82 | overflow-y: auto !important; 83 | flex-wrap: wrap; 84 | .big-data-down-right { 85 | width: 100% !important; 86 | flex-direction: unset !important; 87 | flex-wrap: wrap; 88 | .flex-warp-item { 89 | min-height: 196.24px; 90 | padding: 0 7.5px 15px 15px !important; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/views/appManage/api.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request'; 2 | 3 | /** 4 | * App应用管理API接口集合 5 | */ 6 | export function useAppApi() { 7 | return { 8 | /** 9 | * 新增APP 10 | * @param params 11 | * @returns 12 | */ 13 | addApp: (params: object) => { 14 | return request({ 15 | url: '/app/add', 16 | method: 'post', 17 | data: params, 18 | }); 19 | }, 20 | 21 | /** 22 | * 修改APP 23 | * @param params 24 | * @returns 25 | */ 26 | editApp: (params: object) => { 27 | return request({ 28 | url: '/app/edit', 29 | method: 'post', 30 | data: params, 31 | }); 32 | }, 33 | 34 | /** 35 | * 禁用\启用APP 36 | * @param params 37 | * @returns 38 | */ 39 | deleteApp: (params: object) => { 40 | return request({ 41 | url: '/app/delete', 42 | method: 'post', 43 | data: params, 44 | }); 45 | }, 46 | 47 | /** 48 | * 分页查询应用列表 49 | * @param params 50 | * @returns 51 | */ 52 | queryApp: (params: object) => { 53 | return request({ 54 | url: '/app/query', 55 | method: 'post', 56 | data: params, 57 | }); 58 | }, 59 | 60 | /** 61 | * 模型绑定 62 | * @param params 63 | * @returns 64 | */ 65 | binModel: (params: object) => { 66 | return request({ 67 | url: '/app/bindModel', 68 | method: 'post', 69 | data: params, 70 | }); 71 | }, 72 | 73 | /** 74 | * 模型解绑 75 | * @param params 76 | * @returns 77 | */ 78 | unbindModel: (params: object) => { 79 | return request({ 80 | url: '/app/unbindModel', 81 | method: 'post', 82 | data: params, 83 | }); 84 | }, 85 | 86 | /** 87 | * 查询绑定关系 88 | * @param params 89 | */ 90 | queryBindInfo: (params: object) => { 91 | return request({ 92 | url: '/app/queryBindInfo', 93 | method: 'post', 94 | data: params, 95 | }); 96 | }, 97 | 98 | /** 99 | * 绑定回调地址 100 | * @param params 绑定所需参数 101 | * @returns 102 | */ 103 | bindCallbackUrl: (params: Array) => { 104 | return request({ 105 | url: '/app/addCallbackUrl', 106 | method: 'post', 107 | data: params, 108 | }); 109 | }, 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /src/layout/component/main.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 66 | -------------------------------------------------------------------------------- /src/views/flowDesign/component/drawer/line.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 77 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | import pinia from '/@/stores/index'; 3 | import { storeToRefs } from 'pinia'; 4 | import { useThemeConfig } from '/@/stores/themeConfig'; 5 | 6 | // 定义语言国际化内容 7 | 8 | /** 9 | * 说明: 10 | * 须在 pages 下新建文件夹(建议 `要国际化界面目录` 与 `i18n 目录` 相同,方便查找), 11 | * 注意国际化定义的字段,不要与原有的定义字段相同。 12 | * 1、/src/i18n/lang 下的 ts 为框架的国际化内容 13 | * 2、/src/i18n/pages 下的 ts 为各界面的国际化内容 14 | */ 15 | 16 | // element plus 自带国际化 17 | import enLocale from 'element-plus/lib/locale/lang/en'; 18 | import zhcnLocale from 'element-plus/lib/locale/lang/zh-cn'; 19 | import zhtwLocale from 'element-plus/lib/locale/lang/zh-tw'; 20 | 21 | // 定义变量内容 22 | const messages = {}; 23 | const element = { en: enLocale, 'zh-cn': zhcnLocale, 'zh-tw': zhtwLocale }; 24 | const itemize = { en: [], 'zh-cn': [], 'zh-tw': [] }; 25 | const modules: Record = import.meta.glob('./**/*.ts', { eager: true }); 26 | 27 | // 对自动引入的 modules 进行分类 en、zh-cn、zh-tw 28 | // https://vitejs.cn/vite3-cn/guide/features.html#glob-import 29 | for (const path in modules) { 30 | const key = path.match(/(\S+)\/(\S+).ts/); 31 | if (itemize[key![2]]) itemize[key![2]].push(modules[path].default); 32 | else itemize[key![2]] = modules[path]; 33 | } 34 | 35 | // 合并数组对象(非标准数组对象,数组中对象的每项 key、value 都不同) 36 | function mergeArrObj(list: T, key: string) { 37 | let obj = {}; 38 | list[key].forEach((i: EmptyObjectType) => { 39 | obj = Object.assign({}, obj, i); 40 | }); 41 | return obj; 42 | } 43 | 44 | // 处理最终格式 45 | for (const key in itemize) { 46 | messages[key] = { 47 | name: key, 48 | el: element[key].el, 49 | message: mergeArrObj(itemize, key), 50 | }; 51 | } 52 | 53 | // 导入fast-crud国际化配置 54 | // @ts-ignore 55 | import enFsLocale from '@fast-crud/fast-crud/dist/locale/lang/en'; 56 | messages[enLocale.name]['fs'] = enFsLocale.fs; 57 | // @ts-ignore 58 | import zhFsLocale from '@fast-crud/fast-crud/dist/locale/lang/zh-cn'; 59 | messages[zhcnLocale.name]['fs'] = zhFsLocale.fs; 60 | 61 | // 读取 pinia 默认语言 62 | const stores = useThemeConfig(pinia); 63 | const { themeConfig } = storeToRefs(stores); 64 | 65 | // 导出语言国际化 66 | // https://vue-i18n.intlify.dev/guide/essentials/fallback.html#explicit-fallback-with-one-locale 67 | export const i18n = createI18n({ 68 | legacy: false, 69 | silentTranslationWarn: true, 70 | missingWarn: false, 71 | silentFallbackWarn: true, 72 | fallbackWarn: false, 73 | locale: themeConfig.value.globalI18n, 74 | fallbackLocale: zhcnLocale.name, 75 | messages, 76 | }); 77 | -------------------------------------------------------------------------------- /src/views/flowDesign/component/tool/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 49 | 50 | 80 | -------------------------------------------------------------------------------- /src/layout/routerView/link.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 46 | 47 | 94 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from 'axios'; 2 | import { ElMessage, ElMessageBox } from 'element-plus'; 3 | import { Session } from '/@/utils/storage'; 4 | import qs from 'qs'; 5 | 6 | // 配置新建一个 axios 实例 7 | const service: AxiosInstance = axios.create({ 8 | baseURL: import.meta.env.VITE_API_BASE_URL as any, 9 | timeout: 50000, 10 | headers: { 'Content-Type': 'application/json' }, 11 | paramsSerializer: { 12 | serialize(params) { 13 | return qs.stringify(params, { allowDots: true }); 14 | }, 15 | }, 16 | }); 17 | 18 | // 添加请求拦截器 19 | service.interceptors.request.use( 20 | (config) => { 21 | // 在发送请求之前做些什么 token 22 | if (Session.get('token')) { 23 | config.headers!['Authorization'] = `${Session.get('token')}`; 24 | } 25 | return config; 26 | }, 27 | (error) => { 28 | // 对请求错误做些什么 29 | return Promise.reject(error); 30 | } 31 | ); 32 | 33 | // 添加响应拦截器 34 | service.interceptors.response.use( 35 | (response) => { 36 | 37 | // 对响应数据做点什么 38 | const res = response.data; 39 | 40 | // 二进制 41 | if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') { 42 | if (res.type === 'application/json') { 43 | const reader = new FileReader(); 44 | reader.readAsText(res); 45 | reader.onload = () => { 46 | // TODO 错误处理 47 | ElMessage.error({ message: JSON.parse(reader.result).message }); 48 | }; 49 | return Promise.reject(service.interceptors.response); 50 | } else { 51 | // 暴露响应头 52 | Object.assign(response.data, { headers: response.headers }); 53 | return response.data; 54 | } 55 | } else { 56 | // 结构化数据 57 | if (res.code && res.code !== 0) { 58 | // token过期、缓存异常 59 | if (res.code === 5 || res.code === 6 || res.code == 12 || res.code === 13) { 60 | Session.clear(); // 清除浏览器全部临时缓存 61 | ElMessageBox.alert(res.message, '提示', {}) 62 | .then(() => { 63 | window.location.href = '/'; // 去登录页 64 | }) 65 | .catch(() => {}); 66 | } else { 67 | ElMessage.error({ message: res.message }); 68 | } 69 | return Promise.reject(service.interceptors.response); 70 | } else { 71 | // 暴露响应头 72 | Object.assign(response.data, { headers: response.headers }); 73 | return response.data; 74 | } 75 | } 76 | }, 77 | (error) => { 78 | // 对响应错误做点什么 79 | if (error.message.indexOf('timeout') != -1) { 80 | ElMessage.error('网络超时'); 81 | } else if (error.message == 'Network Error') { 82 | ElMessage.error('网络连接错误'); 83 | } else { 84 | if (error.response.data) ElMessage.error(error.response.statusText); 85 | else ElMessage.error('接口路径找不到'); 86 | } 87 | return Promise.reject(error); 88 | } 89 | ); 90 | 91 | // 导出 axios 实例 92 | export default service; 93 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | parser: 'vue-eslint-parser', 9 | parserOptions: { 10 | ecmaVersion: 12, 11 | parser: '@typescript-eslint/parser', 12 | sourceType: 'module', 13 | }, 14 | extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'], 15 | plugins: ['vue', '@typescript-eslint'], 16 | overrides: [ 17 | { 18 | files: ['*.ts', '*.tsx', '*.vue'], 19 | rules: { 20 | 'no-undef': 'off', 21 | }, 22 | }, 23 | ], 24 | rules: { 25 | // http://eslint.cn/docs/rules/ 26 | // https://eslint.vuejs.org/rules/ 27 | // https://typescript-eslint.io/rules/no-unused-vars/ 28 | '@typescript-eslint/ban-ts-ignore': 'off', 29 | '@typescript-eslint/explicit-function-return-type': 'off', 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | '@typescript-eslint/no-var-requires': 'off', 32 | '@typescript-eslint/no-empty-function': 'off', 33 | '@typescript-eslint/no-use-before-define': 'off', 34 | '@typescript-eslint/ban-ts-comment': 'off', 35 | '@typescript-eslint/ban-types': 'off', 36 | '@typescript-eslint/no-non-null-assertion': 'off', 37 | '@typescript-eslint/explicit-module-boundary-types': 'off', 38 | '@typescript-eslint/no-redeclare': 'error', 39 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', 40 | '@typescript-eslint/no-unused-vars': [2], 41 | 'vue/custom-event-name-casing': 'off', 42 | 'vue/attributes-order': 'off', 43 | 'vue/one-component-per-file': 'off', 44 | 'vue/html-closing-bracket-newline': 'off', 45 | 'vue/max-attributes-per-line': 'off', 46 | 'vue/multiline-html-element-content-newline': 'off', 47 | 'vue/singleline-html-element-content-newline': 'off', 48 | 'vue/attribute-hyphenation': 'off', 49 | 'vue/html-self-closing': 'off', 50 | 'vue/no-multiple-template-root': 'off', 51 | 'vue/require-default-prop': 'off', 52 | 'vue/no-v-model-argument': 'off', 53 | 'vue/no-arrow-functions-in-watch': 'off', 54 | 'vue/no-template-key': 'off', 55 | 'vue/no-v-html': 'off', 56 | 'vue/comment-directive': 'off', 57 | 'vue/no-parsing-error': 'off', 58 | 'vue/no-deprecated-v-on-native-modifier': 'off', 59 | 'vue/multi-word-component-names': 'off', 60 | 'no-useless-escape': 'off', 61 | 'no-sparse-arrays': 'off', 62 | 'no-prototype-builtins': 'off', 63 | 'no-constant-condition': 'off', 64 | 'no-use-before-define': 'off', 65 | 'no-restricted-globals': 'off', 66 | 'no-restricted-syntax': 'off', 67 | 'generator-star-spacing': 'off', 68 | 'no-unreachable': 'off', 69 | 'no-multiple-template-root': 'off', 70 | 'no-unused-vars': 'error', 71 | 'no-v-model-argument': 'off', 72 | 'no-case-declarations': 'off', 73 | 'no-console': 'error', 74 | 'no-redeclare': 'off', 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /src/views/flowDesign/js/config.ts: -------------------------------------------------------------------------------- 1 | // jsplumb 默认配置 2 | export const jsplumbDefaults = { 3 | // 多个锚点 [源锚点,目标锚点] 4 | Anchors: [ 5 | 'Top', 6 | 'TopCenter', 7 | 'TopRight', 8 | 'TopLeft', 9 | 'Right', 10 | 'RightMiddle', 11 | 'Bottom', 12 | 'BottomCenter', 13 | 'BottomRight', 14 | 'BottomLeft', 15 | 'Left', 16 | 'LeftMiddle', 17 | ], 18 | // 连线的容器id 19 | Container: 'workflow-right', 20 | // 设置链接线的形状,如直线或者曲线之类的。anchor可以去设置锚点的位置。可选值"" 21 | Connector: ['Bezier', { curviness: 100 }], 22 | // 节点是否可以用鼠标拖动使其断开,默认为true。即用鼠标链接上的连线,也可以使用鼠标拖动让其断开。设置成false,可以让其拖动也不会自动断开 23 | ConnectionsDetachable: false, 24 | // 删除线的时候节点不删除 25 | DeleteEndpointsOnDetach: false, 26 | // 每当添加或以其他方式创建 Endpoint 并且 jsPlumb 尚未给出任何明确的 Endpoint 定义时将使用 27 | Endpoint: ['Blank', { Overlays: '' }], 28 | // 连接中源和目标端点的默认外观 29 | EndpointStyle: { fill: '#1879ffa1', outlineWidth: 1 }, 30 | // jsPlumb 的内部日志记录是否打开 31 | LogEnabled: true, 32 | // 连接器的默认外观 33 | PaintStyle: { 34 | stroke: '#E0E3E7', 35 | strokeWidth: 1, 36 | outlineStroke: 'transparent', 37 | outlineWidth: 10, 38 | }, 39 | // 用于配置任何可拖动元素的默认选项jsPlumb.draggable 40 | DragOptions: { cursor: 'pointer', zIndex: 2000 }, 41 | // 添加到连接器和端点的默认叠加层。已弃用:从 4.x 开始,将不支持此功能。并非所有叠加层都可以连接到连接器和端点。 42 | Overlays: [ 43 | [ 44 | 'Arrow', 45 | { 46 | width: 10, // 箭头尾部的宽度 47 | length: 8, // 从箭头的尾部到头部的距离 48 | location: 1, // 位置,建议使用0~1之间 49 | direction: 1, // 方向,默认值为1(表示向前),可选-1(表示向后) 50 | foldback: 0.623, // 折回,也就是尾翼的角度,默认0.623,当为1时,为正三角 51 | }, 52 | ], 53 | [ 54 | 'Label', 55 | { 56 | label: '', 57 | location: 0.5, 58 | cssClass: 'aLabel', 59 | }, 60 | ], 61 | ], 62 | // 默认渲染模式 svg、canvas 63 | RenderMode: 'svg', 64 | // 悬停状态下连接的默认外观 65 | HoverPaintStyle: { stroke: '#b0b2b5', strokeWidth: 1 }, 66 | // 悬停状态下端点的默认外观 67 | EndpointHoverStyle: { fill: 'red' }, 68 | // 端点和连接的默认范围。范围提供了对哪些端点可以连接到哪些其他端点的基本控制 69 | Scope: 'jsPlumb_DefaultScope', 70 | }; 71 | 72 | // 整个节点作为source或者target 73 | export const jsplumbMakeSource = { 74 | // 设置可以拖拽的类名,只要鼠标移动到该类名上的DOM,就可以拖拽连线 75 | filter: '.workflow-icon-drag', 76 | filterExclude: false, 77 | anchor: 'Continuous', 78 | // 是否允许自己连接自己 79 | allowLoopback: true, 80 | maxConnections: -1, 81 | }; 82 | 83 | // 整个节点作为source或者target 84 | export const jsplumbMakeTarget = { 85 | filter: '.workflow-icon-drag', 86 | filterExclude: false, 87 | // 是否允许自己连接自己 88 | anchor: 'Continuous', 89 | allowLoopback: false, 90 | dropOptions: { hoverClass: 'ef-drop-hover' }, 91 | }; 92 | 93 | // 连线参数 94 | export const jsplumbConnect = { 95 | isSource: true, 96 | isTarget: true, 97 | // 动态锚点、提供了4个方向 Continuous、AutoDefault 98 | anchor: 'Continuous', 99 | }; 100 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // 申明外部 npm 插件模块 2 | declare module 'vue-grid-layout'; 3 | declare module 'qrcodejs2-fixes'; 4 | declare module 'splitpanes'; 5 | declare module 'js-cookie'; 6 | declare module '@wangeditor/editor-for-vue'; 7 | declare module 'js-table2excel'; 8 | declare module 'qs'; 9 | 10 | // 声明一个模块,防止引入文件时报错 11 | declare module '*.json'; 12 | declare module '*.png'; 13 | declare module '*.jpg'; 14 | declare module '*.scss'; 15 | declare module '*.ts'; 16 | declare module '*.js'; 17 | 18 | // 声明文件,*.vue 后缀的文件交给 vue 模块来处理 19 | declare module '*.vue' { 20 | import type { DefineComponent } from 'vue'; 21 | const component: DefineComponent<{}, {}, any>; 22 | export default component; 23 | } 24 | 25 | // 声明文件,定义全局变量 26 | /* eslint-disable */ 27 | declare interface Window { 28 | nextLoading: boolean; 29 | } 30 | 31 | // 声明路由当前项类型 32 | declare type RouteItem = { 33 | path: string; 34 | name?: string | symbol | undefined | null; 35 | redirect?: string; 36 | k?: T; 37 | meta?: { 38 | title?: string; 39 | isLink?: string; 40 | isHide?: boolean; 41 | isKeepAlive?: boolean; 42 | isAffix?: boolean; 43 | isIframe?: boolean; 44 | roles?: string[]; 45 | icon?: string; 46 | isDynamic?: boolean; 47 | isDynamicPath?: string; 48 | isIframeOpen?: string; 49 | loading?: boolean; 50 | }; 51 | children: T[]; 52 | query?: { [key: string]: T }; 53 | params?: { [key: string]: T }; 54 | contextMenuClickId?: string | number; 55 | commonUrl?: string; 56 | isFnClick?: boolean; 57 | url?: string; 58 | transUrl?: string; 59 | title?: string; 60 | id?: string | number; 61 | }; 62 | 63 | // 声明路由 to from 64 | declare interface RouteToFrom extends RouteItem { 65 | path?: string; 66 | children?: T[]; 67 | } 68 | 69 | // 声明路由当前项类型集合 70 | declare type RouteItems = T[]; 71 | 72 | // 声明 ref 73 | declare type RefType = T | null; 74 | 75 | // 声明 HTMLElement 76 | declare type HtmlType = HTMLElement | string | undefined | null; 77 | 78 | // 申明 children 可选 79 | declare type ChilType = { 80 | children?: T[]; 81 | }; 82 | 83 | // 申明 数组 84 | declare type EmptyArrayType = T[]; 85 | 86 | // 申明 对象 87 | declare type EmptyObjectType = { 88 | [key: string]: T; 89 | }; 90 | 91 | // 申明 select option 92 | declare type SelectOptionType = { 93 | value: string | number; 94 | label: string | number; 95 | }; 96 | 97 | // 鼠标滚轮滚动类型 98 | declare interface WheelEventType extends WheelEvent { 99 | wheelDelta: number; 100 | } 101 | 102 | // table 数据格式公共类型 103 | declare interface TableType { 104 | total: number; 105 | loading: boolean; 106 | param: { 107 | pageNum: number; 108 | pageSize: number; 109 | [key: string]: T; 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue'; 2 | import { resolve } from 'path'; 3 | import { defineConfig, loadEnv, ConfigEnv } from 'vite'; 4 | import vueSetupExtend from 'vite-plugin-vue-setup-extend-plus'; 5 | // 使用fast-crud中的iconify图标 6 | import PurgeIcons from 'vite-plugin-purge-icons'; 7 | 8 | const pathResolve = (dir: string) => { 9 | return resolve(__dirname, '.', dir); 10 | }; 11 | 12 | const alias: Record = { 13 | '/@': pathResolve('./src/'), 14 | 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js', 15 | }; 16 | 17 | const viteConfig = defineConfig((mode: ConfigEnv) => { 18 | const env = loadEnv(mode.mode, process.cwd()); 19 | return { 20 | plugins: [vue(), vueSetupExtend(), PurgeIcons()], 21 | root: process.cwd(), 22 | resolve: { alias }, 23 | base: mode.command === 'serve' ? './' : env.VITE_PUBLIC_PATH, 24 | optimizeDeps: { 25 | include: ['element-plus/lib/locale/lang/zh-cn', 'element-plus/lib/locale/lang/en', 'element-plus/lib/locale/lang/zh-tw'], 26 | }, 27 | server: { 28 | host: '0.0.0.0', 29 | port: env.VITE_PORT as unknown as number, 30 | open: JSON.parse(env.VITE_OPEN), 31 | hmr: true, 32 | proxy: { 33 | '/api': { 34 | target: env.VITE_API_URL, 35 | changeOrigin: true, 36 | rewrite: (path) => path.replace(/^\/api/, ''), 37 | }, 38 | '/ws': { 39 | target: env.VITE_API_URL, 40 | changeOrigin: true, 41 | ws: true, 42 | }, 43 | }, 44 | }, 45 | build: { 46 | outDir: 'dist', 47 | chunkSizeWarningLimit: 1500, 48 | rollupOptions: { 49 | output: { 50 | entryFileNames: `assets/[name].[hash].js`, 51 | chunkFileNames: `assets/[name].[hash].js`, 52 | assetFileNames: `assets/[name].[hash].[ext]`, 53 | compact: true, 54 | manualChunks: { 55 | vuecore: ['vue'], 56 | pinia: ['pinia'], 57 | vuerouter: ['vue-router'], 58 | nprogress: ['nprogress'], 59 | axios: ['axios'], 60 | mitt: ['mitt'], 61 | qs: ['qs'], 62 | screenfull: ['screenfull'], 63 | vueclipboard3: ['vue-clipboard3'], 64 | sortablejs: ['sortablejs'], 65 | jsplumb: ['jsplumb'], 66 | uicore: ['@fast-crud/fast-crud'], 67 | uibridging: ['@fast-crud/ui-element'], 68 | uiextends: ['@fast-crud/fast-extends'], 69 | uiinterface: ['@fast-crud/ui-interface'], 70 | }, 71 | }, 72 | }, 73 | }, 74 | css: { preprocessorOptions: { css: { charset: false } } }, 75 | define: { 76 | __VUE_I18N_LEGACY_API__: JSON.stringify(false), 77 | __VUE_I18N_FULL_INSTALL__: JSON.stringify(false), 78 | __INTLIFY_PROD_DEVTOOLS__: JSON.stringify(false), 79 | __NEXT_VERSION__: JSON.stringify(process.env.npm_package_version), 80 | __NEXT_NAME__: JSON.stringify(process.env.npm_package_name), 81 | }, 82 | }; 83 | }); 84 | 85 | export default viteConfig; 86 | -------------------------------------------------------------------------------- /src/theme/common/transition.scss: -------------------------------------------------------------------------------- 1 | /* 页面切换动画 2 | ------------------------------- */ 3 | .slide-right-enter-active, 4 | .slide-right-leave-active, 5 | .slide-left-enter-active, 6 | .slide-left-leave-active { 7 | will-change: transform; 8 | transition: all 0.3s ease; 9 | } 10 | // slide-right 11 | .slide-right-enter-from { 12 | opacity: 0; 13 | transform: translateX(-20px); 14 | } 15 | .slide-right-leave-to { 16 | opacity: 0; 17 | transform: translateX(20px); 18 | } 19 | // slide-left 20 | .slide-left-enter-from { 21 | @extend .slide-right-leave-to; 22 | } 23 | .slide-left-leave-to { 24 | @extend .slide-right-enter-from; 25 | } 26 | // opacitys 27 | .opacitys-enter-active, 28 | .opacitys-leave-active { 29 | will-change: transform; 30 | transition: all 0.3s ease; 31 | } 32 | .opacitys-enter-from, 33 | .opacitys-leave-to { 34 | opacity: 0; 35 | } 36 | 37 | /* Breadcrumb 面包屑过渡动画 38 | ------------------------------- */ 39 | .breadcrumb-enter-active, 40 | .breadcrumb-leave-active { 41 | transition: all 0.5s ease; 42 | } 43 | .breadcrumb-enter-from, 44 | .breadcrumb-leave-active { 45 | opacity: 0; 46 | transform: translateX(20px); 47 | } 48 | .breadcrumb-leave-active { 49 | position: absolute; 50 | z-index: -1; 51 | } 52 | 53 | /* logo 过渡动画 54 | ------------------------------- */ 55 | @keyframes logoAnimation { 56 | 0% { 57 | transform: scale(0); 58 | } 59 | 80% { 60 | transform: scale(1.2); 61 | } 62 | 100% { 63 | transform: scale(1); 64 | } 65 | } 66 | 67 | /* 404、401 过渡动画 68 | ------------------------------- */ 69 | @keyframes error-num { 70 | 0% { 71 | transform: translateY(60px); 72 | opacity: 0; 73 | } 74 | 100% { 75 | transform: translateY(0); 76 | opacity: 1; 77 | } 78 | } 79 | @keyframes error-img { 80 | 0% { 81 | opacity: 0; 82 | } 83 | 100% { 84 | opacity: 1; 85 | } 86 | } 87 | @keyframes error-img-two { 88 | 0% { 89 | opacity: 1; 90 | } 91 | 100% { 92 | opacity: 0; 93 | } 94 | } 95 | 96 | /* 登录页动画 97 | ------------------------------- */ 98 | @keyframes loginLeft { 99 | 0% { 100 | left: -100%; 101 | } 102 | 50%, 103 | 100% { 104 | left: 100%; 105 | } 106 | } 107 | @keyframes loginTop { 108 | 0% { 109 | top: -100%; 110 | } 111 | 50%, 112 | 100% { 113 | top: 100%; 114 | } 115 | } 116 | @keyframes loginRight { 117 | 0% { 118 | right: -100%; 119 | } 120 | 50%, 121 | 100% { 122 | right: 100%; 123 | } 124 | } 125 | @keyframes loginBottom { 126 | 0% { 127 | bottom: -100%; 128 | } 129 | 50%, 130 | 100% { 131 | bottom: 100%; 132 | } 133 | } 134 | 135 | /* 左右左 link.vue 136 | ------------------------------- */ 137 | @keyframes toRight { 138 | 0% { 139 | left: -5px; 140 | } 141 | 50% { 142 | left: 100%; 143 | } 144 | 100% { 145 | left: -5px; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/layout/navBars/breadcrumb/userNews.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 46 | 47 | 103 | -------------------------------------------------------------------------------- /src/layout/routerView/iframes.vue: -------------------------------------------------------------------------------- 1 |