Repository: sunhao1256/lulu-admin Branch: main Commit: 830724491894 Files: 238 Total size: 341.5 KB Directory structure: gitextract_avnt2zqn/ ├── .browserslistrc ├── .editorconfig ├── .env-config.ts ├── .gitignore ├── Makefile ├── README.md ├── build/ │ ├── index.ts │ ├── plugins/ │ │ ├── index.ts │ │ ├── mock.ts │ │ ├── unplugin.ts │ │ └── vuetify.ts │ └── utils/ │ └── index.ts ├── docker/ │ ├── .dockerignore │ ├── Dockerfile │ └── nginx.conf ├── index.html ├── mock/ │ ├── api/ │ │ ├── auth.ts │ │ ├── chat.ts │ │ ├── index.ts │ │ ├── management.ts │ │ └── route.ts │ ├── index.ts │ └── model/ │ ├── auth.ts │ ├── index.ts │ └── route.ts ├── package.json ├── src/ │ ├── App.vue │ ├── assets/ │ │ └── scss/ │ │ ├── settings.css │ │ ├── settings.scss │ │ ├── theme.css │ │ ├── theme.scss │ │ └── vuetify/ │ │ ├── overrides.scss │ │ └── variables/ │ │ ├── _elevations.scss │ │ ├── _font.scss │ │ ├── _global.scss │ │ └── _index.scss │ ├── components/ │ │ ├── common/ │ │ │ ├── Breadcrumb.vue │ │ │ ├── CopyLabel.vue │ │ │ ├── FlagIcon.vue │ │ │ ├── SideConfigMenu.vue │ │ │ ├── SvgIcon.vue │ │ │ └── TrendPercent.vue │ │ ├── dashboard/ │ │ │ ├── ActivityCard.vue │ │ │ ├── SalesCard.vue │ │ │ ├── SourcesCard.vue │ │ │ ├── TableCard.vue │ │ │ ├── TodoCard.vue │ │ │ └── TrackCard.vue │ │ ├── navigation/ │ │ │ ├── MainMenu.vue │ │ │ ├── NavMenu.vue │ │ │ └── NavMenuItem.vue │ │ ├── provider/ │ │ │ ├── DialogProvider.tsx │ │ │ ├── LoadingOverlyProvider.tsx │ │ │ ├── LoadingProgressLine.tsx │ │ │ ├── SnackbarProvider.tsx │ │ │ ├── VuetifyProvider.vue │ │ │ └── index.ts │ │ └── toolbar/ │ │ ├── ToolbarLanguage.vue │ │ ├── ToolbarNotifications.vue │ │ └── ToolbarUser.vue │ ├── composables/ │ │ ├── events.ts │ │ ├── index.ts │ │ ├── router.ts │ │ └── system.ts │ ├── configs/ │ │ ├── currencies.ts │ │ ├── index.ts │ │ ├── locales.ts │ │ ├── service.ts │ │ └── theme.ts │ ├── constants/ │ │ ├── business.ts │ │ └── index.ts │ ├── enum/ │ │ ├── business.ts │ │ ├── common.ts │ │ ├── index.ts │ │ └── system.ts │ ├── filters/ │ │ ├── formatCurrency.ts │ │ └── index.ts │ ├── hooks/ │ │ ├── common/ │ │ │ ├── index.ts │ │ │ ├── useBoolean.ts │ │ │ ├── useBreadcrumb.ts │ │ │ ├── useContext.ts │ │ │ ├── useLoading.ts │ │ │ ├── useLoadingEmpty.ts │ │ │ └── useReload.ts │ │ └── index.ts │ ├── layouts/ │ │ ├── AuthLayout.vue │ │ ├── BlankLayout/ │ │ │ └── index.vue │ │ ├── DefaultLayout.vue │ │ ├── ErrorLayout.vue │ │ └── index.ts │ ├── main.ts │ ├── plugins/ │ │ ├── animate.ts │ │ ├── clipboard.ts │ │ ├── index.ts │ │ ├── vue-i18n.ts │ │ └── vuetify.ts │ ├── router/ │ │ ├── guard/ │ │ │ ├── dynamic.ts │ │ │ ├── index.ts │ │ │ └── permission.ts │ │ ├── helpers/ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── modules/ │ │ │ ├── dashboard.ts │ │ │ ├── index.ts │ │ │ ├── management.ts │ │ │ └── pages.ts │ │ └── routes/ │ │ └── index.ts │ ├── service/ │ │ ├── api/ │ │ │ ├── auth.ts │ │ │ ├── chat.ts │ │ │ ├── index.ts │ │ │ ├── management.adapter.ts │ │ │ └── management.ts │ │ ├── index.ts │ │ └── request/ │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── instance.ts │ │ └── request.ts │ ├── store/ │ │ ├── auth/ │ │ │ ├── helpers.ts │ │ │ └── index.ts │ │ ├── flow/ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── route/ │ │ │ └── index.ts │ │ ├── subscribe/ │ │ │ ├── index.ts │ │ │ └── theme.ts │ │ └── theme/ │ │ ├── helpers.ts │ │ └── index.ts │ ├── translations/ │ │ ├── en.ts │ │ └── zh.ts │ ├── typings/ │ │ ├── api.d.ts │ │ ├── business.d.ts │ │ ├── camunda.d.ts │ │ ├── config.d.ts │ │ ├── env.d.ts │ │ ├── filter.d.ts │ │ ├── global.d.ts │ │ ├── page-route.d.ts │ │ ├── route.d.ts │ │ ├── router.d.ts │ │ ├── storage.d.ts │ │ ├── system.d.ts │ │ ├── utils.d.ts │ │ └── vuetify.d.ts │ ├── utils/ │ │ ├── common/ │ │ │ ├── index.ts │ │ │ ├── pattern.ts │ │ │ └── typeof.ts │ │ ├── crypto/ │ │ │ └── index.ts │ │ ├── flow/ │ │ │ ├── EventEmitter.ts │ │ │ └── tools.ts │ │ ├── index.ts │ │ ├── router/ │ │ │ ├── auth.ts │ │ │ ├── breadcrumb.ts │ │ │ ├── cache.ts │ │ │ ├── component.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── menu.ts │ │ │ ├── module.ts │ │ │ ├── regexp.ts │ │ │ └── transform.ts │ │ ├── service/ │ │ │ ├── error.ts │ │ │ ├── handler.ts │ │ │ ├── index.ts │ │ │ ├── msg.ts │ │ │ └── transform.ts │ │ ├── storage/ │ │ │ ├── index.ts │ │ │ ├── local.ts │ │ │ └── session.ts │ │ └── vue/ │ │ └── index.ts │ └── views/ │ ├── _builtin/ │ │ ├── auth/ │ │ │ ├── components/ │ │ │ │ ├── ForgotPage.vue │ │ │ │ ├── ResetPage.vue │ │ │ │ ├── SigninPage.vue │ │ │ │ ├── SignupPage.vue │ │ │ │ ├── VerifyEmailPage.vue │ │ │ │ └── index.ts │ │ │ └── index.vue │ │ └── error/ │ │ ├── NotFoundPage.vue │ │ └── UnexpectedPage.vue │ ├── board/ │ │ ├── components/ │ │ │ └── BoardCard.vue │ │ ├── pages/ │ │ │ └── BoardPage.vue │ │ └── types.ts │ ├── chart/ │ │ ├── ChannelMessage.vue │ │ ├── ChatChannel.vue │ │ └── ChatPage.vue │ ├── dashboard/ │ │ └── index.vue │ ├── flowable/ │ │ ├── bo-utils/ │ │ │ ├── conditionalUtil.ts │ │ │ ├── documentationUtil.ts │ │ │ ├── idUtil.ts │ │ │ ├── nameUtil.ts │ │ │ ├── processUtil.ts │ │ │ └── userTaskUtil.ts │ │ ├── design/ │ │ │ ├── customTranslate.ts │ │ │ ├── demo3.bpmn │ │ │ ├── design.tsx │ │ │ ├── index.vue │ │ │ ├── initModeler.ts │ │ │ ├── propertiesPanel/ │ │ │ │ ├── components/ │ │ │ │ │ ├── actions.vue │ │ │ │ │ ├── condition.vue │ │ │ │ │ ├── documentation.vue │ │ │ │ │ ├── form.vue │ │ │ │ │ ├── general.vue │ │ │ │ │ └── userAssigne.vue │ │ │ │ └── index.vue │ │ │ ├── provider/ │ │ │ │ ├── index.js │ │ │ │ ├── parts/ │ │ │ │ │ ├── FormProps.js │ │ │ │ │ └── SpellProps.js │ │ │ │ └── selfProvider.js │ │ │ └── translations.ts │ │ ├── index.vue │ │ └── utils/ │ │ └── BpmnValidator.ts │ ├── form/ │ │ ├── design/ │ │ │ ├── components/ │ │ │ │ ├── formitem.tsx │ │ │ │ ├── formitemEdit.tsx │ │ │ │ └── rightpanel.vue │ │ │ ├── demofom.json │ │ │ ├── form.d.ts │ │ │ ├── formComponents.ts │ │ │ ├── index.vue │ │ │ └── preview.tsx │ │ └── list.vue │ ├── index.ts │ ├── menulevels/ │ │ ├── lv2.1.vue │ │ ├── lv3.1.vue │ │ └── lv3.2.vue │ ├── todo/ │ │ ├── TodoLayout.vue │ │ ├── components/ │ │ │ ├── TodoCompose.vue │ │ │ ├── TodoList.vue │ │ │ └── TodoMenu.vue │ │ ├── pages/ │ │ │ ├── CompletedPage.vue │ │ │ ├── LabelPage.vue │ │ │ └── TasksPage.vue │ │ ├── store/ │ │ │ ├── content.ts │ │ │ └── index.ts │ │ └── typs/ │ │ └── index.d.ts │ └── users/ │ ├── EditUser/ │ │ ├── AccountTab.vue │ │ └── InformationTab.vue │ ├── EditUserPage.vue │ ├── UsersPage.vue │ └── content/ │ └── user.ts ├── tsconfig.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .browserslistrc ================================================ > 1% last 2 versions not dead not ie 11 ================================================ FILE: .editorconfig ================================================ [*.{js,jsx,ts,tsx,vue}] indent_style = space indent_size = 2 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: .env-config.ts ================================================ /** 请求服务的环境配置 */ type ServiceEnv = Record; /** 不同请求服务的环境配置 */ const serviceEnv: ServiceEnv = { dev: { url: 'http://localhost:8080', urlPattern: '/url-pattern', secondUrl: 'http://localhost:8081', secondUrlPattern: '/second-url-pattern' }, test: { url: 'http://localhost:8080', urlPattern: '/url-pattern', secondUrl: 'http://localhost:8081', secondUrlPattern: '/second-url-pattern' }, prod: { url: 'http://localhost:8080', urlPattern: '/url-pattern', secondUrl: 'http://localhost:8081', secondUrlPattern: '/second-url-pattern' } }; /** * 获取当前环境模式下的请求服务的配置 * @param env 环境 */ export function getServiceEnvConfig(env: ImportMetaEnv) { const { VITE_SERVICE_ENV = 'dev' } = env; const config = serviceEnv[VITE_SERVICE_ENV]; return config; } ================================================ FILE: .gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? src/typings/components.d.ts src/typings/auto-imports.d.ts /yarn.lock ================================================ FILE: Makefile ================================================ ImageTag ?=v1.1.0 ImageName ?= sunhao1256/lulu-admin-frontend:$(ImageTag) Platform ?=linux/amd64 VERSION=$(shell git rev-parse --short HEAD) all: compile docker-build docker-push compile: yarn build-no-typecheck docker-build: docker build --platform=$(Platform) --build-arg version=$(VERSION) -t ${ImageName} -f docker/Dockerfile . docker-push: docker push ${ImageName} ================================================ FILE: README.md ================================================ # Lulu Admin elegant admin template, based on Vue3 , TypeScript, Vuetify3, Axios form-generator , bpmn-js-camunda ## Lulu Lulu is my arrogant cat ## Feature - bpmn-js-camunda - form-generator 😄 - chat [Vue3](https://vuejs.org/guide/quick-start.html#creating-a-vue-application) [Vuetify3](https://next.vuetifyjs.com/en/getting-started/installation/) - Dialog Provider - SnackBar Provider - LoadingOverly Provider Typescript Tsx Axios - full Axios Request Stack Example - thanks to [SoybeanAdmin](https://github.com/honghuangdc/soybean-admin) Perfect Project 👍 [Pinia](https://pinia.vuejs.org/) ## Caution! [vite-plugin-mock](https://github.com/vbenjs/vite-plugin-mock) instead of server api actually ## Reference infrastructure [SoybeanAdmin](https://github.com/honghuangdc/soybean-admin) **ui plagiarize** [luxAdminPro](https://lux-admin-pro.indielayer.com/dashboard/analytics) - if you want more features, please support genuine [native-ui](https://github.com/tusen-ai/naive-ui) [vben](https://github.com/vbenjs) ## ScreenShot ![](https://i.imgur.com/RQinDIf.png) ![](https://i.imgur.com/wmODutf.png) ![](https://i.imgur.com/S5HeYO2.png) ![](https://i.imgur.com/MgHU7Av.png) ![](https://i.imgur.com/Xr5gqgE.png) ![](https://i.imgur.com/OVjed1u.png) ![](https://i.imgur.com/bNXRsiv.png) ## Project setup ``` # yarn yarn # npm npm install # pnpm pnpm install ``` ### Compiles and hot-reloads for development ``` # yarn yarn dev # npm npm run dev # pnpm pnpm dev ``` ### Compiles and minifies for production ``` # yarn yarn build # npm npm run build # pnpm pnpm build ``` ### Customize configuration See [Configuration Reference](https://vitejs.dev/config/). ================================================ FILE: build/index.ts ================================================ export * from './plugins'; export * from './utils'; ================================================ FILE: build/plugins/index.ts ================================================ import unplugin from "./unplugin"; import vuetify from "./vuetify"; import vue from '@vitejs/plugin-vue' import mock from './mock'; import type {PluginOption} from 'vite'; export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] { return [vue(), vuetify(), ...unplugin(viteEnv), mock(viteEnv)]; } ================================================ FILE: build/plugins/mock.ts ================================================ import {viteMockServe} from 'vite-plugin-mock'; export default (env: ImportMetaEnv) => { const prodMock = true const {VITE_SERVICE_ENV = 'dev'} = env; return viteMockServe({ mockPath: 'mock', localEnabled: VITE_SERVICE_ENV === 'dev', prodEnabled: VITE_SERVICE_ENV === 'prod' && prodMock, injectCode: ` import { setupMockServer } from '../mock'; setupMockServer(); ` }); } ================================================ FILE: build/plugins/unplugin.ts ================================================ import Components from 'unplugin-vue-components/vite'; import AutoImport from 'unplugin-auto-import/vite'; import Icons from 'unplugin-icons/vite' import {getSrcPath} from '../utils'; import {FileSystemIconLoader} from "unplugin-icons/loaders"; import IconsResolver from 'unplugin-icons/resolver' import {createSvgIconsPlugin} from 'vite-plugin-svg-icons'; import vueJsx from '@vitejs/plugin-vue-jsx' export default function unplugin(viteEnv: ImportMetaEnv) { const {VITE_ICON_PREFFIX} = viteEnv; const srcPath = getSrcPath(); const localIconPath = `${srcPath}/assets/images/svgs`; /** 本地svg图标集合名称 */ const collectionName = 'local'; return [ AutoImport({ imports: ['vue', { 'vuetify': ['useTheme'], 'pinia': ['storeToRefs'], }], dirs: [`${srcPath}/plugins`, `${srcPath}/filters`, `${srcPath}/store/**`, `${srcPath}/router`], resolvers: [ IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFFIX, enabledCollections: [collectionName], }), ], dts: "src/typings/auto-imports.d.ts", }), Icons( { compiler: 'vue3', customCollections: { [collectionName]: FileSystemIconLoader(localIconPath) } } ), Components({ dts: 'src/typings/components.d.ts', dirs: ['src/components'], extensions: ['vue'], resolvers: [ IconsResolver( { customCollections: [collectionName], prefix: VITE_ICON_PREFFIX } ), (componentName) => { // where `componentName` is always CapitalCase if (componentName.startsWith('V')) return {name: componentName, from: 'vuetify/lib/labs/components'} }, (componentName) => { // where `componentName` is always CapitalCase if (componentName.endsWith('Provider')) return {name: componentName, from: '@/components/provider'} } ] }), createSvgIconsPlugin({ iconDirs: [localIconPath], symbolId: `${VITE_ICON_PREFFIX}-[dir]-[name]`, inject: 'body-last', customDomId: '__SVG_ICON_LOCAL__' }), vueJsx({optimize: false, enableObjectSlots: true}), ]; } ================================================ FILE: build/plugins/vuetify.ts ================================================ import vuetify from 'vite-plugin-vuetify' export default function vuetifyPlugin() { return vuetify({ autoImport: true, styles: { configFile: 'src/assets/scss/settings.scss' } }) } ================================================ FILE: build/utils/index.ts ================================================ import path from 'path'; /** * 获取项目根路径 * @descrition 末尾不带斜杠 */ export function getRootPath() { return path.resolve(process.cwd()); } /** * 获取项目src路径 * @param srcName - src目录名称(默认: "src") * @descrition 末尾不带斜杠 */ export function getSrcPath(srcName = 'src') { const rootPath = getRootPath(); return `${rootPath}/${srcName}`; } ================================================ FILE: docker/.dockerignore ================================================ node_modules .DS_Store dist .npmrc .cache tests/server/static tests/server/static/upload .local # local env files .env.local .env.*.local .eslintcache # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea # .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? yarn.lock pnpm-lock.yaml /vite-profile.cpuprofile ================================================ FILE: docker/Dockerfile ================================================ FROM nginx:alpine as prod ENV WORKDIR=/lulu-admin WORKDIR $WORKDIR ARG version ENV COMMITID=$version COPY /dist /lulu-admin COPY /docker/nginx.conf /etc/nginx/nginx.conf EXPOSE 80 ================================================ FILE: docker/nginx.conf ================================================ user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; server { listen 80; server_name localhost; location / { # 不缓存html,防止程序更新后缓存继续生效 if ($request_filename ~* .*\.(?:htm|html)$) { add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate"; access_log on; } root /lulu-admin/; index index.html index.htm; try_files $uri $uri/ /index.html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } } ================================================ FILE: index.html ================================================ Lulu Admin
================================================ FILE: mock/api/auth.ts ================================================ import type {MockMethod} from 'vite-plugin-mock'; import {userModel} from '../model'; import {mock} from "mockjs"; const apis: MockMethod[] = [ { url: '/mock/getSmsCode', method: 'post', response: (): Service.MockServiceResult => { return { code: 200, message: 'ok', data: true }; } }, { url: '/mock/login', method: 'post', timeout: 500, response: (options: Service.MockOption): Service.MockServiceResult => { const {userName = undefined, password = undefined} = options.body; if (!userName || !password) { return { code: 400, message: "request parameter invalid", data: null }; } const findItem = userModel.find(item => item.userName === userName && item.password === password); if (findItem) { return { code: 200, message: 'ok', data: { token: findItem.token, refreshToken: findItem.refreshToken } }; } return { code: 1000, message: '用户名或密码错误!', data: null }; } }, { url: '/mock/getUserInfo', method: 'get', response: (options: Service.MockOption): Service.MockServiceResult => { const {authorization = ''} = options.headers; const REFRESH_TOKEN_CODE = 66666; if (!authorization) { return { code: REFRESH_TOKEN_CODE, message: '用户已失效或不存在!', data: null }; } const userInfo: Auth.UserInfo = { userId: '', userName: '', userRole: 'user', userAvatar: 'avatar' + mock({ "number|1-20": 2 })['number'] }; const isInUser = userModel.some(item => { const flag = item.token === authorization; if (flag) { const {userId: itemUserId, userName, userRole} = item; Object.assign(userInfo, {userId: itemUserId, userName, userRole}); } return flag; }); if (isInUser) { return { code: 200, message: 'ok', data: userInfo }; } return { code: REFRESH_TOKEN_CODE, message: '用户信息异常!', data: null }; } }, { url: '/mock/updateToken', method: 'post', response: (options: Service.MockOption): Service.MockServiceResult => { const {refreshToken = ''} = options.body; const findItem = userModel.find(item => item.refreshToken === refreshToken); if (findItem) { return { code: 200, message: 'ok', data: { token: findItem.token, refreshToken: findItem.refreshToken } }; } return { code: 3000, message: '用户已失效或不存在!', data: null }; } } ]; export default apis; ================================================ FILE: mock/api/chat.ts ================================================ import {mock} from 'mockjs'; import type {MockMethod} from 'vite-plugin-mock'; const apis: MockMethod[] = [ { url: '/mock/getMessage', method: 'post', response: (): Service.MockServiceResult => { const data = mock({ id: '@id', text: '@sentence', timestamp: '@datetime', user: { id: '@id', avatar: () => { return 'avatar' + mock({ "number|1-20": 2 })['number'] } } }) return { code: 200, message: 'ok', data }; } }, ]; export default apis; ================================================ FILE: mock/api/index.ts ================================================ import auth from './auth'; import chat from './chat'; import route from './route'; import management from './management'; export default [...management, ...auth, ...route, ...chat]; ================================================ FILE: mock/api/management.ts ================================================ import {mock} from 'mockjs'; import type {MockMethod} from 'vite-plugin-mock'; const apis: MockMethod[] = [ { url: '/mock/getAllUserList', method: 'post', timeout: 1000, response: (): Service.MockServiceResult> => { const data = mock({ pageNo: 1, pageSize: 10, total: 20, 'list|10': [ { id: '@id', name: '@name', 'role|1': ['user', 'admin'], 'age|18-56': 56, 'gender|1': ['0', '1', null], "verified|1-2": true, 'created': mock('@datetime()'), 'lastSignIn': mock('@datetime()'), phone: /^[1](([3][0-9])|([4][01456789])|([5][012356789])|([6][2567])|([7][0-8])|([8][0-9])|([9][012356789]))[0-9]{8}$/, 'email|1': ['@email("gmail.com")'], 'userStatus|1': ['1', '2', '3', '4', null], 'birthDay': mock('@date()'), avatar: () => { return 'avatar' + mock({ "number|1-20": 2 })['number'] } } ], }); return { code: 200, message: 'ok', data: data }; } }, { url: '/mock/getUser/:id?', method: 'post', timeout: 1500, response: (): Service.MockServiceResult => { const data = mock({ id: '@id', name: '@name', 'role|1': ['user', 'admin'], 'age|18-56': 56, 'gender|1': ['0', '1', null], "verified|1-2": true, 'created': mock('@datetime()'), 'lastSignIn': mock('@datetime()'), phone: /^[1](([3][0-9])|([4][01456789])|([5][012356789])|([6][2567])|([7][0-8])|([8][0-9])|([9][012356789]))[0-9]{8}$/, 'email|1': ['@email("qq.com")'], 'userStatus|1': ['1', '2', '3', '4', null], 'birthDay': mock('@date()'), avatar: () => { return 'avatar' + mock({ "number|1-20": 2 })['number'] } }); return { code: 200, message: 'ok', data: data }; } }, { url: '/mock/getAllFormList', method: 'post', timeout: 1000, response: (): Service.MockServiceResult> => { const data = mock({ pageNo: 1, pageSize: 10, total: 20, 'list|10': [ { id: '@id', name: '@name', 'created': mock('@datetime()'), 'status|1': ['1', '0'], } ], }); return { code: 200, message: 'ok', data: data }; } }, ]; export default apis; ================================================ FILE: mock/api/route.ts ================================================ import type {MockMethod} from 'vite-plugin-mock'; import {routeModel, userModel} from '../model'; const apis: MockMethod[] = [ { url: '/mock/getUserRoutes', method: 'post', response: (options: Service.MockOption): Service.MockServiceResult => { const {userId = undefined} = options.body; const routeHomeName: AuthRoute.LastDegreeRouteKey = 'dashboard_analytics'; const role = userModel.find(item => item.userId === userId)?.userRole || 'user'; const filterRoutes = routeModel[role]; return { code: 200, message: 'ok', data: { routes: filterRoutes, home: routeHomeName } }; } } ]; export default apis; ================================================ FILE: mock/index.ts ================================================ import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'; import api from './api'; export function setupMockServer() { createProdMockServer(api); } ================================================ FILE: mock/model/auth.ts ================================================ interface UserModel extends Auth.UserInfo { token: string; refreshToken: string; password: string; } export const userModel: UserModel[] = [ { token: '__TOKEN_ADMIN__', refreshToken: '__REFRESH_TOKEN_ADMIN__', userId: '2', userName: 'admin', userRole: 'admin', password: 'admin123' }, { token: '__TOKEN_USER01__', refreshToken: '__REFRESH_TOKEN_USER01__', userId: '3', userName: 'user01', userRole: 'user', password: 'user01123' } ]; ================================================ FILE: mock/model/index.ts ================================================ export * from './auth'; export * from './route'; ================================================ FILE: mock/model/route.ts ================================================ export const routeModel: Record = { admin: [ { name: 'dashboard', path: '/dashboard', component: 'basic', children: [ { name: 'dashboard_analytics', path: '/dashboard/analytics', component: 'self', meta: { icon: 'mdi-view-dashboard-outline', title: 'menu.dashboard', requiresAuth: true } } ], meta: { title: 'menu.dashboard', icon: 'mdi-view-dashboard-outline', order: 1, requiresAuth: true } }, { name: 'apps', path: '/apps', component: 'basic', children: [ { name: 'apps_manager-user', path: '/apps/manager-user', component: 'blank', children: [ { name: 'apps_manager-user_list', path: '/apps/manager-user/list', component: 'self', meta: { title: 'menu.usersList', requiresAuth: true } }, { name: 'apps_manager-user_edit', path: '/apps/manager-user/edit', component: 'self', meta: { title: 'menu.usersEdit', dynamicPath: '/apps/manager-user/edit/:id?', requiresAuth: true } } ], meta: { title: 'menu.users', icon: 'mdi-account-multiple-outline', requiresAuth: true, order: 2, } }, { name: 'apps_board', path: '/apps/board', component: 'self', meta: { title: 'menu.board', icon: 'mdi-view-column-outline', order: 1, requiresAuth: true, } }, { name: 'apps_todo', path: '/apps/todo', component: 'todo', children: [ { name: 'apps_todo_tasks', path: '/apps/todo/tasks', component: 'self', meta: { title: 'todo.tasks', requiresAuth: true, hide: true, } }, { name: 'apps_todo_completed', path: '/apps/todo/completed', component: 'self', meta: { title: 'todo.completed', requiresAuth: true, hide: true, } }, { name: 'apps_todo_label', path: '/apps/todo/label', component: 'self', meta: { title: 'todo.labels', hide: true, requiresAuth: true, dynamicPath: '/apps/todo/label/:id' } } ], meta: { title: 'menu.todo', icon: 'mdi-format-list-checkbox', requiresAuth: true, order: 1 } }, { name: "apps_chat", path: "/apps/chat", component: "chat", children: [ { name: 'apps_chat-channel', path: '/apps/chat-channel', component: 'self', meta: { title: 'menu.chat-channel', dynamicPath: '/apps/chat-channel/:id?', hide: true } } ], meta: { title: "menu.chat", order: 1, icon: "mdi-forum-outline" } } ], meta: { title: 'menu.apps', requiresAuth: true, } }, { name: 'other', path: '/other', component: 'basic', meta: { title: 'menu.others' }, children: [ { name: 'blank-page', path: '/blank-page', component: 'blank', meta: { title: 'menu.blank', icon: 'mdi-file-outline', } }, { name: 'other_menu-levels', component: 'blank', path: '/other/menu-levels', meta: { title: 'menu.levels' }, children: [ { name: 'other_menu-levels-2-1', path: '/other/menu-levels-2-1', component: 'self', meta: { title: 'menu.levels2-1' } }, { name: 'other_menu-levels-2-2', path: '/other/menu-levels-2-2', component: 'blank', meta: { title: 'menu.levels2-2', }, children: [ { name: 'other_menu-levels-3-1', path: '/other/menu-levels-3-1', component: 'self', meta: { title: 'menu.levels3-1' } }, { name: 'other_menu-levels-3-2', path: '/other/menu-levels-3-2', component: 'self', meta: { title: 'menu.levels3-2' } } ] } ] }, ] }, { name: "flowable", path: "/flowable", component: 'basic', children: [ { name: 'flowable_design', path: '/flowable/design', component: 'self', meta: { title: 'menu.flowable-design', icon: "mdi-pencil" } }, { name: 'form_list', path: '/form/list', component: 'self', meta: { title: 'menu.form-list', } }, { name: 'form_design', path: '/form/design', component: 'self', meta: { title: 'menu.form-design', } }, ], meta: { title: "menu.flowable", icon: "mdi-waves-arrow-right" } }, ], user: [ { name: 'dashboard', path: '/dashboard', component: 'basic', children: [ { name: 'dashboard_analytics', path: '/dashboard/analytics', component: 'self', meta: { icon: 'mdi-view-dashboard-outline', title: 'menu.dashboard', requiresAuth: true } } ], meta: { title: 'menu.dashboard', icon: 'mdi-view-dashboard-outline', order: 1, requiresAuth: true } }, ] }; ================================================ FILE: package.json ================================================ { "name": "lulu-admin", "version": "1.1.0", "homepage": "https://github.com/sunhao1256/lulu-admin", "repository": { "url": "https://github.com/sunhao1256/lulu-admin.git" }, "scripts": { "dev": "cross-env VITE_SERVICE_ENV=dev vite", "build": "npm run typecheck && cross-env VITE_SERVICE_ENV=prod vite build ", "build-no-typecheck": "cross-env VITE_SERVICE_ENV=prod vite build ", "preview": "vite preview", "typecheck": "vue-tsc --noEmit --skipLibCheck" }, "dependencies": { "@bpmn-io/properties-panel": "^1.4.0", "@mdi/font": "7.0.96", "@vueuse/core": "^9.12.0", "animate.css": "^4.1.1", "apexcharts": "^3.36.3", "axios": "0.27.2", "bpmn-js": "^11.5.0", "bpmn-js-properties-panel": "^1.19.1", "camunda-bpmn-moddle": "^7.0.1", "codemirror": "^6.0.1", "crypto-js": "^4.1.1", "flag-icons": "^6.6.6", "lodash-es": "^4.17.21", "moment": "^2.29.4", "pinia": "^2.0.28", "qs": "^6.11.0", "seemly": "^0.3.6", "ua-parser-js": "^1.0.33", "vooks": "^0.2.12", "vue": "^3.2.47", "vue-codemirror": "^6.1.1", "vue-i18n": "^9.2.2", "vue-router": "^4.1.6", "vue3-apexcharts": "^1.4.1", "vuedraggable": "^4.1.0", "vuetify": "3.1.7", "webfontloader": "^1.0" }, "devDependencies": { "@types/crypto-js": "^4.1.1", "@types/lodash-es": "^4.17.6", "@types/node": "^18.11.9", "@types/qs": "^6.9.7", "@types/ua-parser-js": "^0.7.36", "@types/webfontloader": "^1.6.35", "@vitejs/plugin-vue": "^3.0.3", "@vitejs/plugin-vue-jsx": "^3.0.0", "@vue/babel-plugin-jsx": "^1.1.1", "cross-env": "^7.0.3", "mockjs": "^1.1.0", "sass": "^1.58.3", "sass-loader": "^13.2.0", "typescript": "^4.0.0", "unplugin-auto-import": "^0.12.1", "unplugin-icons": "^0.15.1", "unplugin-vue-components": "^0.22.12", "vite": "^3.0.9", "vite-plugin-mock": "^2.9.6", "vite-plugin-svg-icons": "^2.0.1", "vite-plugin-vuetify": "^1.0.0-alpha.12", "vue-tsc": "^1.0.9" } } ================================================ FILE: src/App.vue ================================================ ================================================ FILE: src/assets/scss/settings.css ================================================ /* Error: @use rules must be written before any other rules. * , * 2 | @use "vuetify/settings"; * | ^^^^^^^^^^^^^^^^^^^^^^^ * ' * settings.scss 2:1 root stylesheet */ body::before { font-family: "Source Code Pro", "SF Mono", Monaco, Inconsolata, "Fira Mono", "Droid Sans Mono", monospace, monospace; white-space: pre; display: block; padding: 1em; margin-bottom: 1em; border-bottom: 2px solid black; content: 'Error: @use rules must be written before any other rules.\a \2577 \a 2 \2502 @use "vuetify/settings";\a \2502 ^^^^^^^^^^^^^^^^^^^^^^^\a \2575 \a settings.scss 2:1 root stylesheet'; } ================================================ FILE: src/assets/scss/settings.scss ================================================ @use "vuetify/variables"; @use "vuetify/settings" with ( $spacer: variables.$spacer, $body-font-family: variables.$body-font-family, $font-size-root: variables.$font-size-root, $font-weights: variables.$font-weights, $container-padding-x: 24px, //nav $list-item-nav-title-font-size:0.875rem, $list-item-nav-title-font-weight:600, $list-item-nav-margin-top:0, //icon $icon-sizes: ( 'default': 1.6em, ), $button-icon-density: ( 'default': 1.5, 'comfortable': 0, 'compact': -1 ), //elevations $shadow-key-umbra-opacity:variables.$shadow-key-umbra-opacity, $shadow-key-penumbra-opacity:variables.$shadow-key-penumbra-opacity, $shadow-key-ambient-opacity:variables.$shadow-key-ambient-opacity, $shadow-key-umbra:variables.$shadow-key-umbra, $shadow-key-penumbra:variables.$shadow-key-penumbra, $shadow-key-ambient:variables.$shadow-key-ambient, //card $card-title-padding: variables.$spacer * 2 variables.$spacer * 2, $border-radius-root: 6px, //timeline $timeline-dot-size: 34px, //table $table-row-height: 48px, $typography: ( 'button': ( 'text-transform': none, 'weight': 700, ), ), $list-item-title-font-weight: 600, $field-font-size: 14px ) ; @forward "vuetify/settings"; ================================================ FILE: src/assets/scss/theme.css ================================================ /** * Vuetify Styles Overrides */ .v-application .d-flex { min-width: 0; } .v-application p { margin-bottom: 20px; } .v-application .title { font-size: 1.16rem; font-weight: 800; } .v-application .overline { font-size: 0.625rem !important; font-weight: 400; letter-spacing: 0.1666666667em !important; line-height: 1rem; text-transform: uppercase; } .v-application .text-number { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji !important; } .v-list-item__title { font-size: 0.975rem; font-weight: 600; } .v-list-item__action:first-child, .v-list-item__icon:first-child { margin-right: 14px !important; } .v-application--is-rtl .v-list-item__action:first-child, .v-application--is-rtl .v-list-item__icon:first-child { margin-right: 0 !important; margin-left: 14px !important; } .v-list-item__action:first-child, .v-list-item__icon:first-child { margin-right: 14px !important; } .v-list-group__header__append-icon .v-icon { font-size: 1rem; } .v-list-group__header .v-list-item__icon.v-list-group__header__append-icon { min-width: 0 !important; } .v-list-item__icon { margin: auto; justify-content: center; } .v-list-group--sub-group .v-list-group__header { padding-left: 8px !important; } .v-navigation-drawer__content { flex: 1 1 auto; } .v-navigation-drawer__content .v-list-item__prepend > .v-icon { margin-inline-end: 14px; } .theme--dark .v-navigation-drawer__content { background: none; } .v-table table { padding: 4px; padding-bottom: 8px; } .v-table table th { text-transform: uppercase; white-space: nowrap; } .v-table table td { border-bottom: 0 !important; } .v-table table tbody tr { transition: box-shadow 0.2s, transform 0.2s; } .v-table table tbody tr:not(.v-data-table__selected):hover { box-shadow: 0 3px 15px -2px rgba(0, 0, 0, 0.12); transform: translateY(-4px); } .v-tabs-items { background-color: transparent !important; } .theme--dark.v-btn:not(.v-btn--flat):not(.v-btn--text):not(.v-btn--outlined) { background-color: #273743; } .row { margin-top: 0 !important; margin-bottom: 0 !important; } .v-app-bar .v-field--variant-solo { box-shadow: none; } /*# sourceMappingURL=theme.css.map */ ================================================ FILE: src/assets/scss/theme.scss ================================================ @use "./vuetify/overrides"; @font-face { font-family: 'Quicksand'; src: url(../fonts/Quicksand-VariableFont_wght.ttf); } .cursor-pointer { cursor: pointer; } .cursor-move { cursor: move; } .v-field-flat div { box-shadow: none; } .top-z-index { z-index: 1006 !important; } ================================================ FILE: src/assets/scss/vuetify/overrides.scss ================================================ @use "./variables" as *; @use "vuetify/tools" as *; .v-application { .title { font-size: map-deep-get($headings, 'h6', 'size'); font-weight: map-deep-get($headings, 'h6', 'weight'); line-height: map-deep-get($headings, 'h6', 'line-height'); letter-spacing: map-deep-get($headings, 'h6', 'letter-spacing'); font-family: map-deep-get($headings, 'h6', 'font-family'); } } .v-application .overline { font-size: 0.625rem !important; font-weight: 400; letter-spacing: 0.1666666667em !important; line-height: 1rem; text-transform: uppercase; } .v-navigation-drawer__content { .v-list-item__prepend > .v-icon { margin-inline-end: 14px; } } .v-card-title { display: flex; } .v-data-table { .v-table__wrapper > table { & > thead, & > tbody, { & > tr { & > th { font-weight: bold; color: rgba(var(--v-theme-on-surface)); } transition: box-shadow 0.2s, transform 0.2s; &:not(.v-data-table__selected):hover { box-shadow: 0 3px 15px -2px rgba(0, 0, 0, 0.12); transform: translateY(-4px); } font-size: $data-table-font-size; & > td { border-bottom: 0 !important; } } padding: 4px 4px 8px; } } } .v-text-field-rounded { & > .v-input__control { border-radius: inherit; & > .v-field { border-radius: inherit; } & div { &::before, &::after { display: none; } } } border-radius: 28px; } ================================================ FILE: src/assets/scss/vuetify/variables/_elevations.scss ================================================ $shadow-key-umbra-opacity: rgba(85, 85, 85, 0.08) !default; $shadow-key-penumbra-opacity: rgba(85, 85, 85, 0.06) !default; $shadow-key-ambient-opacity: rgba(85, 85, 85, 0.03) !default; $shadow-key-umbra: ( 0: (0px 0px 0px 0px $shadow-key-umbra-opacity), 1: (0px 2px 10px -1px $shadow-key-umbra-opacity), 2: (0px 3px 10px -2px $shadow-key-umbra-opacity), 3: (0px 3px 30px -2px $shadow-key-umbra-opacity), 4: (0px 2px 30px -1px $shadow-key-umbra-opacity), 5: (0px 3px 30px -1px $shadow-key-umbra-opacity), 6: (0px 3px 30px -1px $shadow-key-umbra-opacity), 7: (0px 4px 30px -2px $shadow-key-umbra-opacity), 8: (0px 5px 30px -3px $shadow-key-umbra-opacity), 9: (0px 5px 30px -3px $shadow-key-umbra-opacity), 10: (0px 6px 30px -3px $shadow-key-umbra-opacity), 11: (0px 6px 30px -4px $shadow-key-umbra-opacity), 12: (0px 7px 30px -4px $shadow-key-umbra-opacity), 13: (0px 7px 30px -4px $shadow-key-umbra-opacity), 14: (0px 7px 30px -4px $shadow-key-umbra-opacity), 15: (0px 8px 30px -5px $shadow-key-umbra-opacity), 16: (0px 8px 30px -5px $shadow-key-umbra-opacity), 17: (0px 8px 30px -5px $shadow-key-umbra-opacity), 18: (0px 9px 30px -5px $shadow-key-umbra-opacity), 19: (0px 9px 30px -6px $shadow-key-umbra-opacity), 20: (0px 10px 30px -6px $shadow-key-umbra-opacity), 21: (0px 10px 30px -6px $shadow-key-umbra-opacity), 22: (0px 10px 30px -6px $shadow-key-umbra-opacity), 23: (0px 11px 30px -7px $shadow-key-umbra-opacity), 24: (0px 11px 30px -7px $shadow-key-umbra-opacity) ); $shadow-key-penumbra: ( 0: (0px 0px 0px 0px $shadow-key-penumbra-opacity), 1: (0px 1px 10px 0px $shadow-key-penumbra-opacity), 2: (0px 2px 20px 0px $shadow-key-penumbra-opacity), 3: (0px 3px 40px 0px $shadow-key-penumbra-opacity), 4: (0px 4px 30px 0px $shadow-key-penumbra-opacity), 5: (0px 5px 30px 0px $shadow-key-penumbra-opacity), 6: (0px 6px 30px 0px $shadow-key-penumbra-opacity), 7: (0px 7px 30px 1px $shadow-key-penumbra-opacity), 8: (0px 8px 30px 1px $shadow-key-penumbra-opacity), 9: (0px 9px 30px 1px $shadow-key-penumbra-opacity), 10: (0px 10px 30px 1px $shadow-key-penumbra-opacity), 11: (0px 11px 30px 1px $shadow-key-penumbra-opacity), 12: (0px 12px 30px 2px $shadow-key-penumbra-opacity), 13: (0px 13px 30px 2px $shadow-key-penumbra-opacity), 14: (0px 14px 30px 2px $shadow-key-penumbra-opacity), 15: (0px 15px 30px 2px $shadow-key-penumbra-opacity), 16: (0px 16px 30px 2px $shadow-key-penumbra-opacity), 17: (0px 17px 30px 2px $shadow-key-penumbra-opacity), 18: (0px 18px 30px 2px $shadow-key-penumbra-opacity), 19: (0px 19px 30px 2px $shadow-key-penumbra-opacity), 20: (0px 20px 30px 3px $shadow-key-penumbra-opacity), 21: (0px 21px 30px 3px $shadow-key-penumbra-opacity), 22: (0px 22px 30px 3px $shadow-key-penumbra-opacity), 23: (0px 23px 30px 3px $shadow-key-penumbra-opacity), 24: (0px 24px 30px 3px $shadow-key-penumbra-opacity) ); $shadow-key-ambient: ( 0: (0px 0px 0px 0px $shadow-key-ambient-opacity), 1: (0px 1px 30px 0px $shadow-key-ambient-opacity), 2: (0px 1px 30px 0px $shadow-key-ambient-opacity), 3: (0px 1px 30px 0px $shadow-key-ambient-opacity), 4: (0px 1px 30px 0px $shadow-key-ambient-opacity), 5: (0px 1px 30px 0px $shadow-key-ambient-opacity), 6: (0px 1px 30px 0px $shadow-key-ambient-opacity), 7: (0px 2px 30px 1px $shadow-key-ambient-opacity), 8: (0px 3px 30px 2px $shadow-key-ambient-opacity), 9: (0px 3px 30px 2px $shadow-key-ambient-opacity), 10: (0px 4px 30px 3px $shadow-key-ambient-opacity), 11: (0px 4px 30px 3px $shadow-key-ambient-opacity), 12: (0px 5px 30px 4px $shadow-key-ambient-opacity), 13: (0px 5px 30px 4px $shadow-key-ambient-opacity), 14: (0px 5px 30px 4px $shadow-key-ambient-opacity), 15: (0px 6px 30px 5px $shadow-key-ambient-opacity), 16: (0px 6px 30px 5px $shadow-key-ambient-opacity), 17: (0px 6px 30px 5px $shadow-key-ambient-opacity), 18: (0px 7px 30px 6px $shadow-key-ambient-opacity), 19: (0px 7px 30px 6px $shadow-key-ambient-opacity), 20: (0px 8px 30px 7px $shadow-key-ambient-opacity), 21: (0px 8px 40px 7px $shadow-key-ambient-opacity), 22: (0px 8px 40px 7px $shadow-key-ambient-opacity), 23: (0px 9px 40px 8px $shadow-key-ambient-opacity), 24: (0px 9px 40px 8px $shadow-key-ambient-opacity) ); ================================================ FILE: src/assets/scss/vuetify/variables/_font.scss ================================================ $body-font-family: 'Quicksand', sans-serif; $font-size-root: 15px; $line-height-root: 1.5; $font-weights: ( 'thin': 300, 'light': 300, 'regular': 400, 'medium': 500, 'bold': 600, 'black': 700 ); $heading-font-family: $body-font-family; $headings: ( 'h6': ( 'size': 1.16rem, 'weight': 800, 'line-height': 2rem, 'letter-spacing': .0125em, 'font-family': $heading-font-family ), 'subtitle-1': ( 'size': 1rem, 'weight': 600, 'line-height': 1.75rem, 'letter-spacing': .009375em, 'font-family': $body-font-family ), 'caption': ( 'size': .75rem, 'weight': 400, 'letter-spacing': .0333333333em, 'line-height': 1.25rem, 'font-family': $body-font-family ), 'overline': ( 'size': .625rem, 'weight': 400, 'letter-spacing': .1666666667em, 'line-height': 1rem, 'font-family': $body-font-family ) ); ================================================ FILE: src/assets/scss/vuetify/variables/_global.scss ================================================ // Global border radius $border-radius-root: 6px; // Spacing $spacer: 8px; $grid-breakpoints: ( 'xs': 0, 'sm': 600px, 'md': 960px, 'lg': 1280px - 16px, 'xl': 1920px - 16px ); $display-breakpoints: ( 'print-only': 'only print', 'screen-only': 'only screen', 'xs-only': 'only screen and (max-width: #{map-get($grid-breakpoints, 'sm') - 1})', 'sm-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'sm')}) and (max-width: #{map-get($grid-breakpoints, 'md') - 1})', 'sm-and-down': 'only screen and (max-width: #{map-get($grid-breakpoints, 'md') - 1})', 'sm-and-up': 'only screen and (min-width: #{map-get($grid-breakpoints, 'sm')})', 'md-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'md')}) and (max-width: #{map-get($grid-breakpoints, 'lg') - 1})', 'md-and-down': 'only screen and (max-width: #{map-get($grid-breakpoints, 'lg') - 1})', 'md-and-up': 'only screen and (min-width: #{map-get($grid-breakpoints, 'md')})', 'lg-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'lg')}) and (max-width: #{map-get($grid-breakpoints, 'xl') - 1})', 'lg-and-down': 'only screen and (max-width: #{map-get($grid-breakpoints, 'xl') - 1})', 'lg-and-up': 'only screen and (min-width: #{map-get($grid-breakpoints, 'lg')})', 'xl-only': 'only screen and (min-width: #{map-get($grid-breakpoints, 'xl')})' ); // Transitions $transition: ( 'fast-out-slow-in': cubic-bezier(0.4, 0, 0.2, 1), 'linear-out-slow-in': cubic-bezier(0, 0, 0.2, 1), 'fast-out-linear-in': cubic-bezier(0.4, 0, 1, 1), 'ease-in-out': cubic-bezier(0.4, 0, 0.6, 1), 'fast-in-fast-out': cubic-bezier(0.25, 0.8, 0.25, 1), 'swing': cubic-bezier(0.25, 0.8, 0.5, 1) ); // Inputs and labels $label-font-size: 14px; $input-font-size: 14px; $input-top-spacing: 16px; $text-field-active-label-height: 12px; // Button $btn-font-weight: 700; $btn-text-transform: none; $btn-letter-spacing: normal; // List $list-item-dense-title-font-size: 0.875rem; $list-item-dense-title-font-weight: 600; // Table $data-table-expanded-content-box-shadow: inset 0px 4px 10px -5px rgba(50, 50, 50, 0.1), inset 0px -4px 6px -5px rgba(50, 50, 50, 0.1); $data-table-font-size: 0.875em; // Tabs $tab-font-weight: 700; // Toolbar $toolbar-transition: 0.2s map-get($transition, 'fast-out-slow-in') transform, 0.2s map-get($transition, 'fast-out-slow-in') left, 0.2s map-get($transition, 'fast-out-slow-in') right, 280ms map-get($transition, 'fast-out-slow-in') box-shadow, 0.25s map-get($transition, 'fast-out-slow-in') max-width, 0.25s map-get($transition, 'fast-out-slow-in') width; ================================================ FILE: src/assets/scss/vuetify/variables/_index.scss ================================================ @forward './_global'; @forward './_font'; @forward './_elevations'; ================================================ FILE: src/components/common/Breadcrumb.vue ================================================ ================================================ FILE: src/components/common/CopyLabel.vue ================================================ ================================================ FILE: src/components/common/FlagIcon.vue ================================================ ================================================ FILE: src/components/common/SideConfigMenu.vue ================================================ ================================================ FILE: src/components/common/SvgIcon.vue ================================================ ================================================ FILE: src/components/common/TrendPercent.vue ================================================ ================================================ FILE: src/components/dashboard/ActivityCard.vue ================================================ ================================================ FILE: src/components/dashboard/SalesCard.vue ================================================ ================================================ FILE: src/components/dashboard/SourcesCard.vue ================================================ ================================================ FILE: src/components/dashboard/TableCard.vue ================================================ ================================================ FILE: src/components/dashboard/TodoCard.vue ================================================ ================================================ FILE: src/components/dashboard/TrackCard.vue ================================================ ================================================ FILE: src/components/navigation/MainMenu.vue ================================================ ================================================ FILE: src/components/navigation/NavMenu.vue ================================================ ================================================ FILE: src/components/navigation/NavMenuItem.vue ================================================ ================================================ FILE: src/components/provider/DialogProvider.tsx ================================================ import {defineComponent, provide, VNodeChild, ref, reactive, h} from 'vue' import {createId} from 'seemly' import {VCard, VDialog, VCardText, VCardActions, VCardTitle, VSpacer, VBtn} from 'vuetify/components' import {render} from '@/utils' interface ContentType { title?: string, cancelText?: string, persistent?: boolean confirmText?: string, confirmColor?: string, main: string | (() => VNodeChild) cancel?: () => void, confirm?: () => void, } export const DialogInjectKey = "DialogInjectKey " export interface DialogReactive { key: string, content: ContentType _modelValue: boolean, _onClose: (key: string) => void close: () => void confirmLoading: (loading: boolean) => void _confirmLoading: boolean } export interface DialogApiInjection { show: (content: ContentType) => DialogReactive closeAll: () => void } export default defineComponent({ name: "dialogProvider", setup() { const dialogListRef = ref([]) const dialogRefs = ref>({}) const api: DialogApiInjection = { show(content: ContentType): DialogReactive { return create(content) }, closeAll() { dialogListRef.value = [] dialogRefs.value = {} } } provide(DialogInjectKey, api) function create(content: ContentType): DialogReactive { const dialogReactive = reactive({ key: createId(), content: { persistent: true, ...content }, _modelValue: true, _confirmLoading: false, _onClose: (key: string) => { dialogListRef.value.splice( dialogListRef.value.findIndex((message) => message.key === key), 1 ) delete dialogRefs.value[key] }, confirmLoading(loading) { this._confirmLoading = loading }, close() { this._modelValue = false setTimeout(() => { this._onClose(this.key) }, 1200) } }) dialogListRef.value.push(dialogReactive) // @ts-ignore return dialogReactive } return Object.assign( { dialogRefs, dialogList: dialogListRef, }, api ) }, render() { return ( <> {this.$slots.default?.()} {this.dialogList.length > 0 ? ( <> {this.dialogList.map((msg: DialogReactive) => { return ( { if (inst) { this.dialogRefs[msg.key] = inst } }) as () => void } v-model={msg._modelValue} maxWidth={290} persistent={!!msg.content.persistent} > {msg.content.title ?? this.$t('common.title')} {render(msg.content.main)} { msg.close() msg.content.cancel?.() } }} >{msg.content.cancelText ?? this.$t('common.cancel')} { msg.content.confirm ? msg.content.confirm() : msg.close() } }} >{msg.content.confirmText ?? this.$t('common.confirm')} ) })} ) : null} ) } }) export function useDialog(): DialogApiInjection { const api = inject(DialogInjectKey, null) if (api === null) { throw new Error('not outer found') } return api } ================================================ FILE: src/components/provider/LoadingOverlyProvider.tsx ================================================ import {defineComponent, ref} from 'vue' import {VOverlay, VProgressCircular} from 'vuetify/components' export const LoadingOverlyInjectKey = "LoadingOverlyInjectKey " export interface LoadingOverlyApiInjection { show: () => void hide: () => void } export default defineComponent({ name: "loadingOverlyProvider", setup() { const model = ref(false) const api: LoadingOverlyApiInjection = { show: () => { model.value = true }, hide: () => { model.value = false } } provide(LoadingOverlyInjectKey, api) return { model } }, render() { return ( <> {this.$slots.default?.()} ) } }) export function useLoadingOverly(): LoadingOverlyApiInjection { const api = inject(LoadingOverlyInjectKey, null) if (api === null) { throw new Error('not outer found') } return api } ================================================ FILE: src/components/provider/LoadingProgressLine.tsx ================================================ import {defineComponent, ref} from 'vue' import {VProgressLinear} from 'vuetify/components' export const LoadingProgressLineInjectKey = "LoadingProgressLineInjectKey " export interface LoadingProgressLineApiInjection { show: () => void hide: () => void } export default defineComponent({ name: "loadingProgressProvider", setup() { const model = ref(false) const api: LoadingProgressLineApiInjection = { show: () => { model.value = true }, hide: () => { model.value = false } } const theme = useThemeStore() provide(LoadingProgressLineInjectKey, api) return { model, theme } }, render() { return ( <> {this.$slots.default?.()} ) } }) export function useLoadingProgressLine(): LoadingProgressLineApiInjection { const api = inject(LoadingProgressLineInjectKey, null) if (api === null) { throw new Error('not outer found') } return api } ================================================ FILE: src/components/provider/SnackbarProvider.tsx ================================================ import {defineComponent, provide, VNodeChild, ref, reactive, h} from 'vue' import {VSnackbar} from 'vuetify/components' import {createId} from 'seemly' import {render} from '@/utils' type ContentType = string | (() => VNodeChild) export const SnackBarInjectKey = "SnackBarInjectKey " interface SnackBarReactive { key: string, content: ContentType modelValue: boolean, options?: Snackbar, onClose: (key: string) => void } export interface SnackBarApiInjection { info: (content: ContentType, options?: Snackbar) => SnackBarReactive success: (content: ContentType, options?: Snackbar) => SnackBarReactive warning: (content: ContentType, options?: Snackbar) => SnackBarReactive error: (content: ContentType, options?: Snackbar) => SnackBarReactive } export default defineComponent({ name: "snackbarProvider", setup() { const snackBarListRef = ref([]) const snackBarRefs = ref>({}) const api: SnackBarApiInjection = { info(content: ContentType, options?: Snackbar): SnackBarReactive { return create(content, {...options, color: "info", timeout: 1000}) }, success(content: ContentType, options?: Snackbar): SnackBarReactive { return create(content, {...options, color: "success", timeout: 1000}) }, error(content: ContentType, options?: Snackbar): SnackBarReactive { return create(content, {...options, color: "error", timeout: 2000}) }, warning(content: ContentType, options?: Snackbar): SnackBarReactive { return create(content, {...options, color: "warning", timeout: 1000}) }, } provide(SnackBarInjectKey, api) function create(content: ContentType, options?: Snackbar): SnackBarReactive { const snackBarReactive = reactive({ key: createId(), content, options: { timeout: 2000, ...options, }, modelValue: true, onClose: (key: string) => { snackBarListRef.value.splice( snackBarListRef.value.findIndex((message) => message.key === key), 1 ) delete snackBarRefs.value[key] } }) snackBarListRef.value.push(snackBarReactive) // @ts-ignore return snackBarReactive } return Object.assign( { snackBarRefs, snackBarList: snackBarListRef, }, api ) }, render() { return ( <> {this.$slots.default?.()} {this.snackBarList.length > 0 ? ( <> {this.snackBarList.map((msg: SnackBarReactive) => { // @ts-ignore return ( { if (inst) { this.snackBarRefs[msg.key] = inst } }) as () => void } v-model={msg.modelValue} {...msg.options} {...{ 'onUpdate:modelValue': (val: boolean) => { if (!val) { setTimeout(() => { msg.onClose(msg.key) }, msg.options ? Number(msg.options.timeout) + 200 : 1000) } } }} > {render(msg.content)} ) })} ) : null} ) } }) export function useSnackBar(): SnackBarApiInjection { const api = inject(SnackBarInjectKey, null) if (api === null) { throw new Error('not outer found') } return api } ================================================ FILE: src/components/provider/VuetifyProvider.vue ================================================ ================================================ FILE: src/components/provider/index.ts ================================================ export * from './SnackbarProvider' export * from './LoadingOverlyProvider' export * from './LoadingProgressLine' export * from './DialogProvider' ================================================ FILE: src/components/toolbar/ToolbarLanguage.vue ================================================ ================================================ FILE: src/components/toolbar/ToolbarNotifications.vue ================================================ ================================================ FILE: src/components/toolbar/ToolbarUser.vue ================================================ ================================================ FILE: src/composables/events.ts ================================================ import { useEventListener } from '@vueuse/core'; import {useThemeStore} from "@/store"; export function useGlobalEvents() { const appConifg= useThemeStore(); useEventListener(window, 'beforeunload', () => { appConifg.cacheThemeSettings(); }); } ================================================ FILE: src/composables/index.ts ================================================ export * from './router'; export * from './system'; ================================================ FILE: src/composables/router.ts ================================================ import { useRouter } from 'vue-router'; import type { RouteLocationRaw } from 'vue-router'; import { router as globalRouter, routeName } from '@/router'; /** * 路由跳转 * @param inSetup - 是否在vue页面/组件的setup里面调用,在axios里面无法使用useRouter和useRoute */ export function useRouterPush(inSetup = true) { const router = inSetup ? useRouter() : globalRouter; const route = globalRouter.currentRoute; /** * 路由跳转 * @param to - 需要跳转的路由 * @param newTab - 是否在新的浏览器Tab标签打开 */ function routerPush(to: RouteLocationRaw, newTab = false) { if (newTab) { const routerData = router.resolve(to); window.open(routerData.href, '_blank'); } else { router.push(to); } } /** 返回上一级路由 */ function routerBack() { router.go(-1); } /** * 跳转首页 * @param newTab - 在新的浏览器标签打开 */ function toHome(newTab = false) { routerPush({ name: routeName('root') }, newTab); } function toLogin(loginModule?: EnumType.LoginModuleKey, redirectUrl?: string) { const module: EnumType.LoginModuleKey = loginModule || 'sign-in'; const routeLocation: RouteLocationRaw = { name: routeName('login'), params: { module } }; const redirect = redirectUrl || route.value.fullPath; Object.assign(routeLocation, { query: { redirect } }); routerPush(routeLocation); } function toLoginModule(module: EnumType.LoginModuleKey) { const { query } = route.value; routerPush({ name: routeName('login'), params: { module }, query}); } function toLoginRedirect() { const { query } = route.value; if (query?.redirect) { routerPush(query.redirect as string); } else { toHome(); } } return { routerPush, routerBack, toHome, toLogin, toLoginModule, toLoginRedirect }; } ================================================ FILE: src/composables/system.ts ================================================ import UAParser from 'ua-parser-js'; import {useAuthStore} from '@/store'; import {isArray, isString} from '@/utils'; import pkg from '~/package.json'; interface AppInfo { name: string; title: string; desc: string; version: string, } interface Package { name: string; version: string; } const pkgWithType = pkg as Package; export function useAppInfo(): AppInfo { const {VITE_APP_NAME: name, VITE_APP_TITLE: title, VITE_APP_DESC: desc} = import.meta.env; return { name, title, desc, version: pkgWithType.version }; } /** 获取设备信息 */ export function useDeviceInfo() { const parser = new UAParser(); const result = parser.getResult(); return result; } /** 权限判断 */ export function usePermission() { const auth = useAuthStore(); function hasPermission(permission: Auth.RoleType | Auth.RoleType[]) { const {userRole} = auth.userInfo; let has = userRole === 'admin'; if (!has) { if (isArray(permission)) { has = (permission as Auth.RoleType[]).includes(userRole); } if (isString(permission)) { has = (permission as Auth.RoleType) === userRole; } } return has; } return { hasPermission }; } ================================================ FILE: src/configs/currencies.ts ================================================ const config: CurrencyConfig.Config = { currency: { label: 'USD', decimalDigits: 2, decimalSeparator: '.', thousandsSeparator: ',', currencySymbol: '$', currencySymbolNumberOfSpaces: 0, currencySymbolPosition: 'left' }, availableCurrencies: [{ label: 'USD', decimalDigits: 2, decimalSeparator: '.', thousandsSeparator: ',', currencySymbol: '$', currencySymbolNumberOfSpaces: 0, currencySymbolPosition: 'left' }, { label: 'EUR', decimalDigits: 2, decimalSeparator: '.', thousandsSeparator: ',', currencySymbol: '€', currencySymbolNumberOfSpaces: 1, currencySymbolPosition: 'right' }] } export default config ================================================ FILE: src/configs/index.ts ================================================ import locales from './locales' import theme from './theme' import currency from './currencies' const config: Config = { theme, locales, currency } export default config ================================================ FILE: src/configs/locales.ts ================================================ import en from '../translations/en' import zh from '../translations/zh' const supported = ['en', 'zh'] let locale = 'en' try { const {0: browserLang} = navigator.language.split('-') if (supported.includes(browserLang)) locale = browserLang } catch (e) { console.log(e) } export default { locale, fallbackLocale: 'en', availableLocales: [{ code: 'en', flag: 'us', label: 'English', messages: en }, { code: 'zh', flag: 'cn', label: '中文', messages: zh }] } ================================================ FILE: src/configs/service.ts ================================================ export const REQUEST_TIMEOUT = 60 * 1000; export const ERROR_MSG_DURATION = 3 * 1000; export const DEFAULT_REQUEST_ERROR_CODE = 'DEFAULT'; export const DEFAULT_REQUEST_ERROR_MSG = '请求错误~'; export const REQUEST_TIMEOUT_CODE = 'ECONNABORTED'; export const REQUEST_TIMEOUT_MSG = '请求超时~'; export const NETWORK_ERROR_CODE = 'NETWORK_ERROR'; export const NETWORK_ERROR_MSG = '网络不可用~'; export const ERROR_STATUS = { 400: '400: 请求出现语法错误~', 401: '401: 用户未授权~', 403: '403: 服务器拒绝访问~', 404: '404: 请求的资源不存在~', 405: '405: 请求方法未允许~', 408: '408: 网络请求超时~', 500: '500: 服务器内部错误~', 501: '501: 服务器未实现请求功能~', 502: '502: 错误网关~', 503: '503: 服务不可用~', 504: '504: 网关超时~', 505: '505: http版本不支持该请求~', [DEFAULT_REQUEST_ERROR_CODE]: DEFAULT_REQUEST_ERROR_MSG }; export const NO_ERROR_MSG_CODE: (string | number)[] = []; export const REFRESH_TOKEN_CODE: (string | number)[] = [66666]; ================================================ FILE: src/configs/theme.ts ================================================ const themeConfig: ThemeConfig.Config = { primary: '#0096c7', followOs: true, globalTheme: 'light', // light | dark menuTheme: 'global', // global | light | dark toolbarTheme: 'global', // global | light | dark isToolbarDetached: false, isContentBoxed: false, isRTL: false, dark: { dark: true, colors: { background: '#111b27', surface: '#05090c', primary: '#0096c7', secondary: '#829099', accent: '#82B1FF', error: '#FF5252', info: '#2196F3', success: '#4CAF50', warning: '#FFC107', } }, // light theme colors light: { dark: false, variables: { "high-emphasis-opacity": 1, "border-opacity": 0.05, }, colors: { background: '#f2f5f8', surface: '#ffffff', primary: '#0096c7', secondary: '#a0b9c8', accent: '#048ba8', error: '#ef476f', info: '#2196F3', success: '#06d6a0', "on-success": '#ffffff', warning: '#ffd166', } } } export default themeConfig ================================================ FILE: src/constants/business.ts ================================================ /** 用户性别 */ export const genderLabels: Record = { 0: 'female', 1: 'male', 2: 'unknown' }; export const genderOptions: { value: UserManagement.GenderKey; label: string }[] = [ {value: '0', label: genderLabels['0']}, {value: '1', label: genderLabels['1']}, {value: '2', label: genderLabels['2']}, ]; /** 用户状态 */ export const userStatusLabels: Record = { 1: 'active', 2: 'disabled', 4: 'deleted' }; export const userStatusOptions: { value: UserManagement.UserStatusKey; label: string }[] = [ {value: '1', label: userStatusLabels['1']}, {value: '2', label: userStatusLabels['2']}, {value: '4', label: userStatusLabels['4']} ]; /** 用户状态 */ export const formStatusLabels: Record = { 1: 'active', 0: 'disabled', }; export const formStatusOptions: { value: FormManagement.FormStatusKey; label: string }[] = [ {value: '1', label: formStatusLabels['1']}, {value: '0', label: formStatusLabels['0']}, ]; ================================================ FILE: src/constants/index.ts ================================================ export * from './business'; ================================================ FILE: src/enum/business.ts ================================================ export enum EnumUserRole { admin = 'admin', user = 'commonUser' } export enum EnumLoginModule { 'sign-in' = 'siginIn', 'sign-up' = 'signUp', 'reset' = 'reset', 'verify-email' = 'verifyEmail', 'forgot' = 'forgotPass', } ================================================ FILE: src/enum/common.ts ================================================ export enum EnumContentType { json = 'application/json', formUrlencoded = 'application/x-www-form-urlencoded', formData = 'multipart/form-data' } export enum EnumDataType { number = '[object Number]', string = '[object String]', boolean = '[object Boolean]', null = '[object Null]', undefined = '[object Undefined]', object = '[object Object]', array = '[object Array]', function = '[object Function]', date = '[object Date]', regexp = '[object RegExp]', promise = '[object Promise]', set = '[object Set]', map = '[object Map]', file = '[object File]' } ================================================ FILE: src/enum/index.ts ================================================ export * from './common'; export * from './system'; export * from './business'; ================================================ FILE: src/enum/system.ts ================================================ export enum EnumLayoutComponentName { basic = 'basic-layout', blank = 'blank-layout', auth = 'auth-layout', error = 'error-layout', todo = 'todo-layout', chat = 'chat-layout' } export enum EnumThemeLayoutMode { 'vertical' = '左侧菜单模式', 'horizontal' = '顶部菜单模式', 'vertical-mix' = '左侧菜单混合模式', 'horizontal-mix' = '顶部菜单混合模式' } export enum EnumThemeTabMode { 'chrome' = '谷歌风格', 'button' = '按钮风格' } export enum EnumThemeHorizontalMenuPosition { 'flex-start' = '居左', 'center' = '居中', 'flex-end' = '居右' } export enum EnumThemeAnimateMode { 'zoom-fade' = '渐变', 'zoom-out' = '闪现', 'fade-slide' = '滑动', 'fade' = '消退', 'fade-bottom' = '底部消退', 'fade-scale' = '缩放消退' } ================================================ FILE: src/filters/formatCurrency.ts ================================================ import configs from "@/configs"; export function formatCurrency(value: number, currency?: CurrencyConfig.Currency): number | string { const {currency: currencyConfig} = configs let c: CurrencyConfig.Currency c = currency || currencyConfig.currency return formatPrice(value, c) } export function formatPrice(price: number, currency: CurrencyConfig.Currency) { try { const numberFormatted = numberFormat( price, currency.decimalDigits, currency.decimalSeparator, currency.thousandsSeparator ) if (currency.currencySymbol) { const priceSeparator = currency.currencySymbolNumberOfSpaces > 0 ? ' '.repeat(currency.currencySymbolNumberOfSpaces) : '' let priceParts = [numberFormatted, priceSeparator, currency.currencySymbol] if (currency.currencySymbolPosition === 'left') { priceParts = priceParts.reverse() } return priceParts.join('') } else { return numberFormatted } } catch (e) { return price } } export function numberFormat(number: number, decimals: number, dec_point: string, thousands_sep: string) { if (isNaN(number)) { return number } const negative = number < 0 if (negative) number = number * -1 const str = number.toFixed(decimals ? decimals : 0).toString().split('.') const parts = [] for (let i = str[0].length; i > 0; i -= 3) { parts.unshift(str[0].substring(Math.max(0, i - 3), i)) } str[0] = parts.join(thousands_sep ? thousands_sep : ',') return (negative ? '-' : '') + str.join(dec_point ? dec_point : '.') } ================================================ FILE: src/filters/index.ts ================================================ import {App} from "vue"; export function registerFilters(app: App) { app.config.globalProperties.$filters = { formatCurrency: formatCurrency } } ================================================ FILE: src/hooks/common/index.ts ================================================ import useContext from './useContext'; import useBoolean from './useBoolean'; import useLoading from './useLoading'; import useLoadingEmpty from './useLoadingEmpty'; import useReload from './useReload'; export { useContext, useBoolean, useLoading, useLoadingEmpty, useReload }; ================================================ FILE: src/hooks/common/useBoolean.ts ================================================ import { ref } from 'vue'; export default function useBoolean(initValue = false) { const bool = ref(initValue); function setBool(value: boolean) { bool.value = value; } function setTrue() { setBool(true); } function setFalse() { setBool(false); } function toggle() { setBool(!bool.value); } return { bool, setBool, setTrue, setFalse, toggle }; } ================================================ FILE: src/hooks/common/useBreadcrumb.ts ================================================ import {computed} from 'vue'; import {useRoute} from 'vue-router'; import {routePath} from '@/router'; import {useRouteStore} from '@/store'; import {getBreadcrumbsByPredicate} from '@/utils'; export default function useBreadcrumb(rootPath: Exclude = 'root') { const route = useRoute(); const routeStore = useRouteStore(); const breadcrumbs = computed(() => getBreadcrumbsByPredicate(menu => { return !!route.matched.find(m => m.path == menu.routePath) }, routeStore.menus as App.GlobalMenuOption[], routePath(rootPath)) ); return { breadcrumbs }; } ================================================ FILE: src/hooks/common/useContext.ts ================================================ import { inject, provide } from 'vue'; import type { InjectionKey } from 'vue'; export default function useContext(contextName = 'context') { const injectKey: InjectionKey = Symbol(contextName); function useProvide(context: T) { provide(injectKey, context); return context; } function useInject() { return inject(injectKey) as T; } return { useProvide, useInject }; } ================================================ FILE: src/hooks/common/useLoading.ts ================================================ import useBoolean from './useBoolean'; export default function useLoading(initValue = false) { const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue); return { loading, startLoading, endLoading }; } ================================================ FILE: src/hooks/common/useLoadingEmpty.ts ================================================ import useBoolean from './useBoolean'; export default function useLoadingEmpty(initLoading = false, initEmpty = false) { const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initLoading); const { bool: empty, setBool: setEmpty } = useBoolean(initEmpty); return { loading, startLoading, endLoading, empty, setEmpty }; } ================================================ FILE: src/hooks/common/useReload.ts ================================================ import { nextTick } from 'vue'; import useBoolean from './useBoolean'; export default function useReload() { // 重载的标志 const { bool: reloadFlag, setTrue, setFalse } = useBoolean(true); async function handleReload(duration = 0) { setFalse(); await nextTick(); if (duration > 0) { setTimeout(() => { setTrue(); }, duration); } } return { reloadFlag, handleReload }; } ================================================ FILE: src/hooks/index.ts ================================================ export * from './common'; ================================================ FILE: src/layouts/AuthLayout.vue ================================================ ================================================ FILE: src/layouts/BlankLayout/index.vue ================================================ ================================================ FILE: src/layouts/DefaultLayout.vue ================================================ ================================================ FILE: src/layouts/ErrorLayout.vue ================================================ ================================================ FILE: src/layouts/index.ts ================================================ const BasicLayout = () => import('./DefaultLayout.vue'); const BlankLayout = () => import('./BlankLayout/index.vue'); const AuthLayout = () => import('./AuthLayout.vue'); const ErrorLayout = () => import('./ErrorLayout.vue'); const TodoLayout= () => import('../views/todo/TodoLayout.vue'); const ChatLayout= () => import('../views/chart/ChatPage.vue'); export {BasicLayout, BlankLayout, AuthLayout, ErrorLayout, TodoLayout , ChatLayout}; ================================================ FILE: src/main.ts ================================================ /** * main.ts * * Bootstraps Vuetify and other plugins then mounts the App` */ // Components import App from './App.vue' // Composables import "@/assets/scss/theme.scss" import "animate.css/animate.min.css" async function setupApp() { const app = createApp(App) setupStore(app) await setupRouter(app) registerFilters(app) registerPlugins(app) app.mount('#app') } setupApp() ================================================ FILE: src/plugins/animate.ts ================================================ export function animate(node: HTMLElement, animationName: string, callBack?: () => void) { node.classList.add('animate__animated', `animate__${animationName}`) function handleAnimationEnd() { node.classList.remove('animate__animated', `animate__${animationName}`) node.removeEventListener('animationend', handleAnimationEnd) if (callBack) { callBack() } } node.addEventListener('animationend', handleAnimationEnd) } ================================================ FILE: src/plugins/clipboard.ts ================================================ export function clipboard(text: string, toastText = 'Copied to Clipboard') { try { navigator.clipboard.writeText(text).then(() => { window.$snackBar?.info(toastText) }) }catch (e) { // In Production navigator clipboard will return undefined due to unsafe http protocol, https will fine window.$snackBar?.error("Copied to Clipboard failed!") } } ================================================ FILE: src/plugins/index.ts ================================================ /** * plugins/user.d.ts * * Automatically included in `./src/main.ts` */ // Plugins import VueApexCharts from 'vue3-apexcharts' import 'virtual:svg-icons-register'; // Types import type {App} from 'vue' export function registerPlugins(app: App) { app.use(vuetify) app.use(vueI18n) app.component('apexchart', VueApexCharts) } ================================================ FILE: src/plugins/vue-i18n.ts ================================================ import {createI18n} from 'vue-i18n' import config from '../configs' const {locale, availableLocales, fallbackLocale} = config.locales const messages = {} availableLocales.forEach((l: any) => { // @ts-ignore messages[l.code] = l.messages }) const i18n = createI18n({ locale, fallbackLocale, messages, legacy: false, globalInjection:true }) export default i18n ================================================ FILE: src/plugins/vuetify.ts ================================================ /** * plugins/vuetify.ts * * Framework documentation: https://vuetifyjs.com` */ // Styles import '@mdi/font/css/materialdesignicons.css' import 'vuetify/styles' // Composables import {createVuetify} from 'vuetify' import configs from "@/configs"; import i18n from "@/plugins/vue-i18n"; export default createVuetify({ defaults: { VTextField: { variant: 'underlined', 'clearIcon': 'mdi-close' }, VAutocomplete: { 'clearIcon': 'mdi-close', 'noDataText': i18n.global.t('$vuetify.dataIterator.noResultsText') }, VSelect: { 'clearIcon': 'mdi-close' }, VBtn: { variant: 'elevated', }, }, theme: { themes: { light: configs.theme.light, dark: configs.theme.dark, }, variations: { colors: ["primary"], lighten: 5, darken: 5 }, }, }) ================================================ FILE: src/router/guard/dynamic.ts ================================================ import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'; import { routeName } from '@/router'; import { useRouteStore } from '@/store'; import { localStg } from '@/utils'; export async function createDynamicRouteGuard( to: RouteLocationNormalized, _from: RouteLocationNormalized, next: NavigationGuardNext ) { const route = useRouteStore(); const isLogin = Boolean(localStg.get('token')); if (!route.isInitAuthRoute) { if (!isLogin) { const toName = to.name as AuthRoute.AllRouteKey; if (route.isValidConstantRoute(toName) && !to.meta.requiresAuth) { next(); } else { const redirect = to.fullPath; next({ name: routeName('login'), query: { redirect } }); } return false; } await route.initAuthRoute(); if (to.name === routeName('not-found')) { // 动态路由没有加载导致被not-found路由捕获,等待权限路由加载好了,回到之前的路由 // 若路由是从根路由重定向过来的,重新回到根路由 const ROOT_ROUTE_NAME: AuthRoute.AllRouteKey = 'root'; const path = to.redirectedFrom?.name === ROOT_ROUTE_NAME ? '/' : to.fullPath; next({ path, replace: true, query: to.query, hash: to.hash }); return false; } } // 权限路由已经加载,仍然未找到,重定向到404 if (to.name === routeName('not-found')) { next({ name: routeName('404'), replace: true }); return false; } return true; } ================================================ FILE: src/router/guard/index.ts ================================================ import type {Router} from 'vue-router'; import {useTitle} from '@vueuse/core'; import {createPermissionGuard} from './permission'; import i18n from '@/plugins/vue-i18n' export function createRouterGuard(router: Router) { router.beforeEach(async (to, from, next) => { await createPermissionGuard(to, from, next); }); router.afterEach(to => { //set document title useTitle(i18n.global.t(to.meta.title)); }); } ================================================ FILE: src/router/guard/permission.ts ================================================ import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'; import { routeName } from '@/router'; import { useAuthStore } from '@/store'; import { exeStrategyActions, localStg } from '@/utils'; import { createDynamicRouteGuard } from './dynamic'; export async function createPermissionGuard( to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext ) { const permission = await createDynamicRouteGuard(to, from, next); if (!permission) return; if (to.meta.href) { window.open(to.meta.href); next({ path: from.fullPath, replace: true, query: from.query }); return; } const auth = useAuthStore(); const isLogin = Boolean(localStg.get('token')); const permissions = to.meta.permissions || []; const needLogin = Boolean(to.meta?.requiresAuth) || Boolean(permissions.length); const hasPermission = !permissions.length || permissions.includes(auth.userInfo.userRole); const actions: Common.StrategyAction[] = [ // 已登录状态跳转登录页,跳转至首页 [ isLogin && to.name === routeName('login'), () => { next({ name: routeName('root') }); } ], // 不需要登录权限的页面直接通行 [ !needLogin, () => { next(); } ], // 未登录状态进入需要登录权限的页面 [ !isLogin && needLogin, () => { const redirect = to.fullPath; next({ name: routeName('login'), query: { redirect } }); } ], // 登录状态进入需要登录权限的页面,有权限直接通行 [ isLogin && needLogin && hasPermission, () => { next(); } ], [ // 登录状态进入需要登录权限的页面,无权限,重定向到无权限页面 isLogin && needLogin && !hasPermission, () => { next({ name: routeName('403') }); } ] ]; exeStrategyActions(actions); } ================================================ FILE: src/router/helpers/index.ts ================================================ ================================================ FILE: src/router/index.ts ================================================ import type {App} from "vue" import { transformAuthRouteToVueRoutes } from '@/utils/router/transform'; import { transformRouteNameToRoutePath } from '@/utils'; import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'; import { constantRoutes } from './routes'; import { createRouterGuard } from './guard'; const { VITE_HASH_ROUTE = 'N', VITE_BASE_URL } = import.meta.env; export const router = createRouter({ history: VITE_HASH_ROUTE === 'Y' ? createWebHashHistory(VITE_BASE_URL) : createWebHistory(VITE_BASE_URL), routes: transformAuthRouteToVueRoutes(constantRoutes), }); export const routeName = (key: AuthRoute.AllRouteKey) => key; export const routePath = (key: Exclude) => transformRouteNameToRoutePath(key); export async function setupRouter(app: App) { app.use(router); createRouterGuard(router); await router.isReady(); } export * from './routes'; export * from './modules'; ================================================ FILE: src/router/modules/dashboard.ts ================================================ const dashboard: AuthRoute.Route = { name: 'dashboard', path: '/dashboard', component: 'basic', children: [ { name: 'dashboard_analytics', path: '/dashboard/analytics', component: 'self', meta: { icon: 'mdi-view-dashboard-outline', title: 'menu.dashboard', requiresAuth: true } } ], meta: { title: 'menu.dashboard', icon: 'mdi-view-dashboard-outline', order: 1, requiresAuth: true } }; export default dashboard; ================================================ FILE: src/router/modules/index.ts ================================================ import { handleModuleRoutes } from '@/utils'; const modules = import.meta.glob('./**/*.ts', { eager: true }) as AuthRoute.RouteModule; export const routes = handleModuleRoutes(modules); ================================================ FILE: src/router/modules/management.ts ================================================ const management: AuthRoute.Route = { name: 'apps', path: '/apps', component: 'basic', children: [ { name: 'apps_manager-user', path: '/apps/manager-user', component: 'blank', children: [ { name: 'apps_manager-user_list', path: '/apps/manager-user/list', component: 'self', meta: { title: 'menu.usersList', requiresAuth: true } }, { name: 'apps_manager-user_edit', path: '/apps/manager-user/edit', component: 'self', meta: { title: 'menu.usersEdit', dynamicPath: '/apps/manager-user/edit/:id?', requiresAuth: true } } ], meta: { title: 'menu.users', icon: 'mdi-account-multiple-outline', requiresAuth: true, order: 2, } }, { name: 'apps_board', path: '/apps/board', component: 'self', meta: { title: 'menu.board', icon: 'mdi-view-column-outline', order: 1, requiresAuth: true, } }, { name: 'apps_todo', path: '/apps/todo', component: 'todo', children: [ { name: 'apps_todo_tasks', path: '/apps/todo/tasks', component: 'self', meta: { title: 'todo.tasks', requiresAuth: true, hide: true, } }, { name: 'apps_todo_completed', path: '/apps/todo/completed', component: 'self', meta: { title: 'todo.completed', requiresAuth: true, hide: true, } }, { name: 'apps_todo_label', path: '/apps/todo/label', component: 'self', meta: { title: 'todo.labels', hide: true, requiresAuth: true, dynamicPath: '/apps/todo/label/:id' } } ], meta: { title: 'menu.todo', icon: 'mdi-format-list-checkbox', requiresAuth: true, order: 1 } } ], meta: { title: 'menu.apps', requiresAuth: true, } } ; export default management; ================================================ FILE: src/router/modules/pages.ts ================================================ const pages: AuthRoute.Route = { name: 'pages', path: '/pages', component: 'error', children: [ { name: 'pages_error', path: '/pages/error', meta: { title: 'menu.errorPages', icon: 'mdi-file-cancel-outline', order: 1, requiresAuth: true }, children: [ { name: 'pages_error_notfound', path: '/pages/error/notfound', component: 'self', meta: { icon: 'mdi-file-outline', title: 'menu.errorNotFound', }, }, { name: 'pages_error_unexpected', path: '/pages/error/unexpected', component: 'self', meta: { icon: 'mdi-file-outline', title: 'menu.errorUnexpected', }, } ] }, ], meta: { title: 'menu.pages', order: 1, } }; export default pages; ================================================ FILE: src/router/routes/index.ts ================================================ import {getLoginModuleRegExp} from '@/utils'; export const ROOT_ROUTE: AuthRoute.Route = { name: 'root', path: '/', redirect: import.meta.env.VITE_ROUTE_HOME_PATH, meta: { title: 'Root' } }; export const constantRoutes: AuthRoute.Route[] = [ ROOT_ROUTE, { name: 'login', path: '/login', component: 'self', props: route => { const moduleType = (route.params.module as EnumType.LoginModuleKey) || 'sign-in'; return { module: moduleType }; }, meta: { title: 'login.title', dynamicPath: `/login/:module(${getLoginModuleRegExp()})?`, singleLayout: 'auth' } }, { name: '403', path: '/403', component: 'self', meta: { title: 'error.forbidden', singleLayout: 'blank' } }, { name: '404', path: '/404', component: 'self', meta: { title: 'error.notfound', singleLayout: 'error' } }, { name: '500', path: '/500', component: 'self', meta: { title: 'error.other', singleLayout: 'error' } }, { name: 'not-found', path: '/:pathMatch(.*)*', component: 'blank', meta: { title: 'error.notfound', singleLayout: 'blank' } } ]; ================================================ FILE: src/service/api/auth.ts ================================================ import { mockRequest } from '../request'; export function fetchSmsCode(phone: string) { return mockRequest.post('/getSmsCode', { phone }); } export function fetchLogin(userName: string, password: string) { return mockRequest.post('/login', { userName, password }); } export function fetchUserInfo() { return mockRequest.get('/getUserInfo'); } export function fetchUserRoutes(userId: string) { return mockRequest.post('/getUserRoutes', { userId }); } export function fetchUpdateToken(refreshToken: string) { return mockRequest.post('/updateToken', { refreshToken }); } ================================================ FILE: src/service/api/chat.ts ================================================ import {mockRequest} from '../request'; export function fetchMessage() { return mockRequest.post("/getMessage"); } ================================================ FILE: src/service/api/index.ts ================================================ export * from './auth'; export * from './chat'; export * from './management'; ================================================ FILE: src/service/api/management.adapter.ts ================================================ export function adapterOfFetchUserList(data: ApiCommon.PageResult | null): ApiCommon.PageResult { if (!data) return { pageNo: 1, pageSize: 20, list: [], total: 0, }; return { total: data.total, pageNo: data.pageNo, pageSize: data.pageSize, list: data.list.map((item, index) => { const user: UserManagement.User = { role: 'user', ...item }; return user; }) } } export function adapterOfFetchUser(data: ApiUserManagement.User | null): UserManagement.User | null { if (!data) return null; const user: UserManagement.User = { role: 'user', ...data }; return user } export function deriveFetchListAdapter(transfer: (t: T) => Y) { return (data: ApiCommon.PageResult | null): ApiCommon.PageResult => { if (!data) return { pageNo: 1, pageSize: 20, list: [], total: 0, }; return { total: data.total, pageNo: data.pageNo, pageSize: data.pageSize, list: data.list.map((item, index) => { const user: Y = transfer(item) return user; }) } } } ================================================ FILE: src/service/api/management.ts ================================================ import {adapter} from '@/utils'; import {mockRequest} from '../request'; import {adapterOfFetchUserList, adapterOfFetchUser, deriveFetchListAdapter} from './management.adapter'; export const fetchUserList = async () => { const data = await mockRequest.post | null>('/getAllUserList'); return adapter(adapterOfFetchUserList, data); }; export const fetchUser = async (id: string) => { const data = await mockRequest.post(`/getUser/${id}`); return adapter(adapterOfFetchUser, data); }; export const fetchFormList = async () => { const data = await mockRequest.post | null>(`/getAllFormList`); return adapter(deriveFetchListAdapter(apiFrom => { const form: FormManagement.Form = { ...apiFrom } return form; }), data); }; ================================================ FILE: src/service/index.ts ================================================ export * from './api'; ================================================ FILE: src/service/request/helpers.ts ================================================ import type { AxiosRequestConfig } from 'axios'; import { useAuthStore } from '@/store'; import { localStg } from '@/utils'; import { fetchUpdateToken } from '../api'; /** * 刷新token * @param axiosConfig - token失效时的请求配置 */ export async function handleRefreshToken(axiosConfig: AxiosRequestConfig) { const { resetAuthStore } = useAuthStore(); const refreshToken = localStg.get('refreshToken') || ''; const { data } = await fetchUpdateToken(refreshToken); if (data) { localStg.set('token', data.token); localStg.set('refreshToken', data.refreshToken); const config = { ...axiosConfig }; if (config.headers) { config.headers.Authorization = data.token; } return config; } resetAuthStore(); return null; } ================================================ FILE: src/service/request/index.ts ================================================ import { getServiceEnvConfig } from '~/.env-config'; import { createRequest } from './request'; const { url, urlPattern, secondUrl, secondUrlPattern } = getServiceEnvConfig(import.meta.env); const isHttpProxy = import.meta.env.VITE_HTTP_PROXY === 'Y'; export const request = createRequest({ baseURL: isHttpProxy ? urlPattern : url }); export const secondRequest = createRequest({ baseURL: isHttpProxy ? secondUrlPattern : secondUrl }); export const mockRequest = createRequest({ baseURL: '/mock' }); ================================================ FILE: src/service/request/instance.ts ================================================ import axios from 'axios'; import type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'; import { REFRESH_TOKEN_CODE } from '@/configs/service'; import { localStg, handleAxiosError, handleBackendError, handleResponseError, handleServiceResult, transformRequestData } from '@/utils'; import { handleRefreshToken } from './helpers'; export default class CustomAxiosInstance { instance: AxiosInstance; backendConfig: Service.BackendResultConfig; constructor( axiosConfig: AxiosRequestConfig, backendConfig: Service.BackendResultConfig = { codeKey: 'code', dataKey: 'data', msgKey: 'message', successCode: 200 } ) { this.backendConfig = backendConfig; this.instance = axios.create(axiosConfig); this.setInterceptor(); } setInterceptor() { this.instance.interceptors.request.use( async config => { const handleConfig = { ...config }; if (handleConfig.headers) { const contentType = handleConfig.headers['Content-Type'] as string; handleConfig.data = await transformRequestData(handleConfig.data, contentType); handleConfig.headers.Authorization = 'Bearer ' +localStg.get('token') || ''; } return handleConfig; }, (axiosError: AxiosError) => { const error = handleAxiosError(axiosError); return handleServiceResult(error, null); } ); this.instance.interceptors.response.use( async response => { const { status } = response; if (status === 200 || status < 300 || status === 304) { const backend = response.data; const { codeKey, dataKey, successCode } = this.backendConfig; if (backend[codeKey] === successCode) { return handleServiceResult(null, backend[dataKey]); } if (REFRESH_TOKEN_CODE.includes(backend[codeKey])) { const config = await handleRefreshToken(response.config); if (config) { return this.instance.request(config); } } const error = handleBackendError(backend, this.backendConfig); return handleServiceResult(error, null); } const error = handleResponseError(response); return handleServiceResult(error, null); }, (axiosError: AxiosError) => { const error = handleAxiosError(axiosError); return handleServiceResult(error, null); } ); } } ================================================ FILE: src/service/request/request.ts ================================================ import {ref} from 'vue'; import type {Ref} from 'vue'; import type {AxiosInstance, AxiosRequestConfig} from 'axios'; import {useBoolean, useLoading} from '@/hooks'; import CustomAxiosInstance from './instance'; type RequestMethod = 'get' | 'post' | 'put' | 'delete'; interface RequestParam { url: string; method?: RequestMethod; data?: any; axiosConfig?: AxiosRequestConfig; } export function createRequest(axiosConfig: AxiosRequestConfig, backendConfig?: Service.BackendResultConfig) { const customInstance = new CustomAxiosInstance(axiosConfig, backendConfig); async function asyncRequest(param: RequestParam): Promise> { const {url} = param; const method = param.method || 'get'; const {instance} = customInstance; const res = (await getRequestResponse({ instance, method, url, data: param.data, config: param.axiosConfig })) as Service.RequestResult; return res; } function get(url: string, config?: AxiosRequestConfig) { return asyncRequest({url, method: 'get', axiosConfig: config}); } function post(url: string, data?: any, config?: AxiosRequestConfig) { return asyncRequest({url, method: 'post', data, axiosConfig: config}); } function put(url: string, data?: any, config?: AxiosRequestConfig) { return asyncRequest({url, method: 'put', data, axiosConfig: config}); } function handleDelete(url: string, config: AxiosRequestConfig) { return asyncRequest({url, method: 'delete', axiosConfig: config}); } return { get, post, put, delete: handleDelete }; } interface RequestResultHook { data: Ref; error: Ref; loading: Ref; network: Ref; } export function createHookRequest(axiosConfig: AxiosRequestConfig, backendConfig?: Service.BackendResultConfig) { const customInstance = new CustomAxiosInstance(axiosConfig, backendConfig); function useRequest(param: RequestParam): RequestResultHook { const {loading, startLoading, endLoading} = useLoading(); const {bool: network, setBool: setNetwork} = useBoolean(window.navigator.onLine); startLoading(); const data = ref(null) as Ref; const error = ref(null); function handleRequestResult(response: any) { const res = response as Service.RequestResult; data.value = res.data; error.value = res.error; endLoading(); setNetwork(window.navigator.onLine); } const {url} = param; const method = param.method || 'get'; const {instance} = customInstance; getRequestResponse({instance, method, url, data: param.data, config: param.axiosConfig}).then( handleRequestResult ); return { data, error, loading, network }; } function get(url: string, config?: AxiosRequestConfig) { return useRequest({url, method: 'get', axiosConfig: config}); } function post(url: string, data?: any, config?: AxiosRequestConfig) { return useRequest({url, method: 'post', data, axiosConfig: config}); } function put(url: string, data?: any, config?: AxiosRequestConfig) { return useRequest({url, method: 'put', data, axiosConfig: config}); } function handleDelete(url: string, config: AxiosRequestConfig) { return useRequest({url, method: 'delete', axiosConfig: config}); } return { get, post, put, delete: handleDelete }; } async function getRequestResponse(params: { instance: AxiosInstance; method: RequestMethod; url: string; data?: any; config?: AxiosRequestConfig; }) { const {instance, method, url, data, config} = params; let res: any; if (method === 'get' || method === 'delete') { res = await instance[method](url, config); } else { res = await instance[method](url, data, config); } return res; } ================================================ FILE: src/store/auth/helpers.ts ================================================ import { localStg } from '@/utils'; export function getToken() { return localStg.get('token') || ''; } export function getUserInfo() { const emptyInfo: Auth.UserInfo = { userId: '', userName: '', userRole: 'user' }; const userInfo: Auth.UserInfo = localStg.get('userInfo') || emptyInfo; return userInfo; } export function clearAuthStorage() { localStg.remove('token'); localStg.remove('refreshToken'); localStg.remove('userInfo'); } ================================================ FILE: src/store/auth/index.ts ================================================ import {unref, nextTick} from 'vue'; import {defineStore} from 'pinia'; import {router} from '@/router'; import {fetchLogin, fetchUserInfo} from '@/service'; import {useRouterPush} from '@/composables'; import {localStg} from '@/utils'; import {useRouteStore} from '@/store'; import {getToken, getUserInfo, clearAuthStorage} from './helpers'; import i18n from '@/plugins/vue-i18n' interface AuthState { userInfo: Auth.UserInfo; token: string; loginLoading: boolean; } export const useAuthStore = defineStore('auth-store', { state: (): AuthState => ({ userInfo: getUserInfo(), token: getToken(), loginLoading: false }), getters: { isLogin(state) { return Boolean(state.token); } }, actions: { resetAuthStore() { const {toLogin} = useRouterPush(false); const {resetRouteStore} = useRouteStore(); const route = unref(router.currentRoute); clearAuthStorage(); this.$reset(); window?.$loadingOverly?.hide() window?.$dialog?.closeAll() if (route.meta.requiresAuth) { toLogin(); } nextTick(() => { resetRouteStore(); }); }, async handleActionAfterLogin(backendToken: ApiAuth.Token) { const route = useRouteStore(); const {toLoginRedirect} = useRouterPush(false); const loginSuccess = await this.loginByToken(backendToken); if (loginSuccess) { await route.initAuthRoute(); toLoginRedirect(); if (route.isInitAuthRoute) { window.$snackBar?.success(`${i18n.global.t("welcomeBack")},${this.userInfo.userName}!`); } return; } this.resetAuthStore(); }, async loginByToken(backendToken: ApiAuth.Token) { let successFlag = false; const {token, refreshToken} = backendToken; localStg.set('token', token); localStg.set('refreshToken', refreshToken); const {data} = await fetchUserInfo(); if (data) { localStg.set('userInfo', data); this.userInfo = data; this.token = token; successFlag = true; } return successFlag; }, async login(userName: string, password: string) { this.loginLoading = true; const {data} = await fetchLogin(userName, password); if (data) { await this.handleActionAfterLogin(data); } this.loginLoading = false; }, async updateUserRole(userRole: Auth.RoleType) { const {resetRouteStore, initAuthRoute} = useRouteStore(); const accounts: Record = { admin: { userName: 'Admin', password: 'admin123' }, user: { userName: 'User01', password: 'user01123' } }; const {userName, password} = accounts[userRole]; const {data} = await fetchLogin(userName, password); if (data) { await this.loginByToken(data); resetRouteStore(); await initAuthRoute(); } } } }); ================================================ FILE: src/store/flow/index.ts ================================================ import {defineStore} from 'pinia' import {Base} from 'diagram-js/lib/model' import Canvas from 'diagram-js/lib/core/Canvas' import ElementRegistry from 'diagram-js/lib/core/ElementRegistry' type ModelerStore = { activeElement: any activeElementId: any modeler: any moddle: any modeling: any commandStack: any, canvas: Canvas | null elementRegistry: ElementRegistry | null } const defaultState: ModelerStore = { activeElement: undefined, activeElementId: undefined, modeler: null, moddle: null, modeling: null, canvas: null, elementRegistry: null, commandStack: null, } export const useModelStore = defineStore('modeler', { state: () => defaultState, getters: { getActive: (state) => state.activeElement, getActiveId: (state) => state.activeElementId, getModeler: (state) => state.modeler, getModdle: (state) => state.moddle, getModeling: (state) => state.modeling, getCommandStack: (state) => state.commandStack, getCanvas: (state) => state.canvas, getElRegistry: (state) => state.elementRegistry }, actions: { setModeler(modeler: any) { this.modeler = modeler }, setModules(key: K, module: any) { this[key] = module }, setElement(element: Base, id: string) { this.activeElement = element this.activeElementId = id } } }) ================================================ FILE: src/store/index.ts ================================================ import {createPinia} from 'pinia' import type {App} from "vue"; export function setupStore(app: App) { const store = createPinia() app.use(store) } export * from './theme' export * from './route' export * from './auth' export * from './subscribe' export * from './flow' ================================================ FILE: src/store/route/index.ts ================================================ import {defineStore} from 'pinia'; import {ROOT_ROUTE, constantRoutes, router, routes as staticRoutes} from '@/router'; import {fetchUserRoutes} from '@/service'; import { localStg, filterAuthRoutesByUserPermission, getCacheRoutes, getConstantRouteNames, transformAuthRouteToVueRoutes, transformAuthRouteToVueRoute, transformAuthRouteToMenu, transformAuthRouteToSearchMenus, transformRouteNameToRoutePath, transformRoutePathToRouteName, sortRoutes } from '@/utils'; import {useAuthStore} from '../auth'; interface RouteState { authRouteMode: ImportMetaEnv['VITE_AUTH_ROUTE_MODE']; isInitAuthRoute: boolean; routeHomeName: AuthRoute.AllRouteKey; menus: App.GlobalMenuOption[]; searchMenus: AuthRoute.Route[]; cacheRoutes: string[]; } export const useRouteStore = defineStore('route-store', { state: (): RouteState => ({ authRouteMode: import.meta.env.VITE_AUTH_ROUTE_MODE, isInitAuthRoute: false, routeHomeName: transformRoutePathToRouteName(import.meta.env.VITE_ROUTE_HOME_PATH), menus: [], searchMenus: [], cacheRoutes: [] }), actions: { resetRouteStore() { this.resetRoutes(); this.$reset(); }, resetRoutes() { const routes = router.getRoutes(); routes.forEach(route => { const name = (route.name || 'root') as AuthRoute.AllRouteKey; if (!this.isConstantRoute(name)) { router.removeRoute(name); } }); }, isConstantRoute(name: AuthRoute.AllRouteKey) { const constantRouteNames = getConstantRouteNames(constantRoutes); return constantRouteNames.includes(name); }, isValidConstantRoute(name: AuthRoute.AllRouteKey) { const NOT_FOUND_PAGE_NAME: AuthRoute.NotFoundRouteKey = 'not-found'; const constantRouteNames = getConstantRouteNames(constantRoutes); return constantRouteNames.includes(name) && name !== NOT_FOUND_PAGE_NAME; }, handleAuthRoute(routes: AuthRoute.Route[]) { (this.menus as App.GlobalMenuOption[]) = transformAuthRouteToMenu(routes); this.searchMenus = transformAuthRouteToSearchMenus(routes); const vueRoutes = transformAuthRouteToVueRoutes(routes); vueRoutes.forEach(route => { router.addRoute(route); }); this.cacheRoutes = getCacheRoutes(vueRoutes); }, handleUpdateRootRedirect(routeKey: AuthRoute.AllRouteKey) { if (routeKey === 'root' || routeKey === 'not-found') { throw new Error('root or not-found should not be routeKey'); } const rootRoute: AuthRoute.Route = {...ROOT_ROUTE, redirect: transformRouteNameToRoutePath(routeKey)}; const rootRouteName: AuthRoute.AllRouteKey = 'root'; router.removeRoute(rootRouteName); const rootVueRoute = transformAuthRouteToVueRoute(rootRoute)[0]; router.addRoute(rootVueRoute); }, async initDynamicRoute() { const {userId} = localStg.get('userInfo') || {}; if (!userId) { throw new Error('userId is mandatory '); } const {error, data} = await fetchUserRoutes(userId); if (!error) { this.routeHomeName = data.home; this.handleUpdateRootRedirect(data.home); this.handleAuthRoute(sortRoutes(data.routes)); this.isInitAuthRoute = true; } }, async initStaticRoute() { const auth = useAuthStore(); const routes = filterAuthRoutesByUserPermission(staticRoutes, auth.userInfo.userRole); this.handleAuthRoute(routes); this.isInitAuthRoute = true; }, async initAuthRoute() { if (this.authRouteMode === 'dynamic') { await this.initDynamicRoute(); } else { await this.initStaticRoute(); } } } }); ================================================ FILE: src/store/subscribe/index.ts ================================================ import subscribeThemeStore from './theme'; export function subscribeStore() { subscribeThemeStore(); } ================================================ FILE: src/store/subscribe/theme.ts ================================================ import {effectScope, onScopeDispose, watch} from 'vue'; import {useOsTheme} from 'vooks'; import {useThemeStore} from '@/store'; export default function subscribeThemeStore() { const theme = useThemeStore(); const osTheme = useOsTheme(); const scope = effectScope(); const {themes, global} = useTheme() scope.run(() => { // themeconfig watch( () => theme, (n) => { themes.value["dark"].colors.primary = n.primary themes.value["light"].colors.primary = n.primary global.name.value = n.globalTheme theme.cacheThemeSettings() }, {immediate: true, deep: true} ); // watch os theme watch( osTheme, newValue => { if (theme.followOs) { global.name.value = newValue || 'light' theme.cacheThemeSettings() } }, {immediate: true} ); }); onScopeDispose(() => { scope.stop(); }); } ================================================ FILE: src/store/theme/helpers.ts ================================================ import {localStg} from '@/utils'; import configs from "@/configs"; export function initThemeSettings() { const storageSettings = localStg.get('themeSettings'); if (storageSettings) { return storageSettings; } return configs.theme; } ================================================ FILE: src/store/theme/index.ts ================================================ import {defineStore} from 'pinia' import {localStg} from "@/utils"; import {initThemeSettings} from "@/store/theme/helpers"; export const useThemeStore = defineStore("theme", { state: () => { return { ...initThemeSettings(), } }, actions: { cacheThemeSettings() { localStg.set('themeSettings', this.$state); }, } }) ================================================ FILE: src/translations/en.ts ================================================ export default { 'welcomeBack': 'welcome back', common: { add: 'Add', cancel: 'Cancel', confirm: 'confirm', description: 'Description', delete: 'Delete', title: 'Title', save: 'Save', faq: 'FAQ', contact: 'Contact Us', tos: 'Terms of Service', policy: 'Privacy Policy' }, board: { titlePlaceholder: 'Enter a title for this card', deleteDescription: 'Are you sure you want to delete this card?', editCard: 'Edit Card', deleteCard: 'Delete Card', state: { TODO: 'TO DO', INPROGRESS: 'INPROGRESS', TESTING: 'TESTING', DONE: 'DONE' } }, chat: { online: 'Users Online ({count})', addChannel: 'Add Channel', channel: 'Channel | Channels', message: 'Message' }, email: { compose: 'Compose Email', send: 'Send', subject: 'Subject', labels: 'Labels', emptyList: 'Empty email list', inbox: 'Inbox', sent: 'Sent', drafts: 'Drafts', starred: 'Starred', trash: 'Trash', work: 'Work', invoice: 'Invoice' }, todo: { addTask: 'Add Task', tasks: 'Tasks', completed: 'Completed', labels: 'Labels' }, dashboard: { activity: 'Activity', weeklySales: 'Weekly Sales', sales: 'Sales', recentOrders: 'Recent Orders', sources: 'Traffic Sources', lastweek: 'vs last week', orders: 'Orders', customers: 'Customers', tickets: 'Support Tickets', viewReport: 'View Report' }, usermenu: { profile: 'Profile', signin: 'Sign In', dashboard: 'Dashboard', signout: 'Sign Out', notSignin: 'Not Sign in', }, error: { notfound: 'Page Not Found', other: 'An Error Ocurred' }, check: { title: 'Set New Password', backtosign: 'Back to Sign In', newpassword: 'New Password', button: 'Set new password and Sign in', error: 'The action link is invalid', verifylink: 'Verifying link...', verifyemail: 'Verifying email address...', emailverified: 'Email verified! Redirecting...' }, forgot: { title: 'Forgot Password?', subtitle: 'Enter your account email address and we will send you a link to reset your password.', email: 'Email', button: 'Request Password Reset', backtosign: 'Back to Sign In' }, login: { title: 'Sign In', email: 'Email', password: 'Password', button: 'Sign In', orsign: 'Or sign in with', forgot: 'Forgot password?', noaccount: 'Don\'t have an account?', create: 'Create one here', error: 'The email / password combination is invalid' }, register: { title: 'Create Account', name: 'Full name', email: 'Email', password: 'Password', button: 'Create Account', orsign: 'Or sign up with', agree: 'By signing up, you agree to the', account: 'Already have an account?', signin: 'Sign In' }, utility: { maintenance: 'In Maintenance' }, faq: { call: 'Have other questions? Please reach out ' }, ecommerce: { products: 'Products', filters: 'Filters', collections: 'Collections', priceRange: 'Price Range', customerReviews: 'Customer Reviews', up: 'and up', brand: 'Brand', search: 'Search for product', results: 'Results ( {0} of {1} )', orders: 'Orders', shipping: 'Shipping', freeShipping: 'Free Shipping', inStock: 'In Stock', quantity: 'Quantity', addToCart: 'Add To Cart', buyNow: 'Buy Now', price: 'Price', about: 'About this item', description: 'Description', reviews: 'Reviews', details: 'Product Details', cart: 'Cart', summary: 'Order Summary', total: 'Total', discount: 'Discount', subtotal: 'Subtotal', continue: 'Continue Shopping', checkout: 'Checkout' }, menu: { apps: 'apps', search: 'Search (press "ctrl + /" to focus)', dashboard: 'Dashboard', logout: 'Logout', profile: 'Profile', blank: 'Blank Page', pages: 'Pages', others: 'Others', email: 'Email', chat: 'Chat', 'chat-channel': 'Chat Channel', todo: 'To Do', board: 'Task Board', users: 'Users', usersList: 'List', usersEdit: 'Edit', ecommerce: 'Ecommerce', ecommerceList: 'Products', ecommerceProductDetails: 'Product Details', ecommerceOrders: 'Orders', ecommerceCart: 'Cart', auth: 'Auth Pages', authLogin: 'Signin / Login', authRegister: 'Signup / Register', authVerify: 'Verify Email', authForgot: 'Forgot Password', authReset: 'Reset Password', errorPages: 'Error Pages', errorNotFound: 'Not Found / 404', errorUnexpected: 'Unexpected / 500', utilityPages: 'Utility Pages', utilityMaintenance: 'Maintenance', utilitySoon: 'Coming Soon', utilityHelp: 'FAQs / Help', levels: 'Menu Levels', 'levels2-1': 'levels2-1', 'levels2-2': 'levels2-2', 'levels3-1': 'levels3-1', 'levels3-2': 'levels3-2', disabled: 'Menu Disabled', docs: 'Documentation', feedback: 'Feedback', support: 'Support', landingPage: 'Landing Page', pricingPage: 'Pricing Page', 'flowable': 'Flowable', 'flowable-design': 'Flowable Design', 'form-list': 'Forms', 'form-design': 'Form Design' }, // Vuetify components translations $vuetify: { badge: 'Badge', close: 'Close', dataIterator: { noResultsText: 'No matching records found', loadingText: 'Loading items...' }, dataTable: { itemsPerPageText: 'Rows per page:', ariaLabel: { sortDescending: 'Sorted descending.', sortAscending: 'Sorted ascending.', sortNone: 'Not sorted.', activateNone: 'Activate to remove sorting.', activateDescending: 'Activate to sort descending.', activateAscending: 'Activate to sort ascending.' }, sortBy: 'Sort by' }, dataFooter: { itemsPerPageText: 'Items per page:', itemsPerPageAll: 'All', nextPage: 'Next page', prevPage: 'Previous page', firstPage: 'First page', lastPage: 'Last page', pageText: '{0}-{1} of {2}' }, datePicker: { itemsSelected: '{0} selected', nextMonthAriaLabel: 'Next month', nextYearAriaLabel: 'Next year', prevMonthAriaLabel: 'Previous month', prevYearAriaLabel: 'Previous year' }, noDataText: 'No data available', carousel: { prev: 'Previous visual', next: 'Next visual', ariaLabel: { delimiter: 'Carousel slide {0} of {1}' } }, calendar: { moreEvents: '{0} more' }, fileInput: { counter: '{0} files', counterSize: '{0} files ({1} in total)' }, timePicker: { am: 'AM', pm: 'PM' }, pagination: { ariaLabel: { wrapper: 'Pagination Navigation', next: 'Next page', previous: 'Previous page', page: 'Goto Page {0}', currentPage: 'Current Page, Page {0}' } } } } ================================================ FILE: src/translations/zh.ts ================================================ export default { 'welcomeBack': '欢迎回来', 'common': { 'add': '加', 'confirm': '确认', 'cancel': '取消', 'description': '描述', 'delete': '删除', 'title': '标题', 'save': '保存', 'faq': '常问问题', 'contact': '联系我们', 'tos': '服务条款', 'policy': '隐私政策' }, 'board': { 'titlePlaceholder': '输入此卡的标题', 'deleteDescription': '您确定要删除此卡吗?', 'editCard': '编辑卡', 'deleteCard': '删除卡', 'state': { 'TODO': '去做', 'INPROGRESS': '进行中', 'TESTING': '测试中', 'DONE': '完成' } }, 'chat': { 'online': '在线用户({count})', 'addChannel': '添加频道', 'channel': '频道|频道', 'message': '信息' }, 'email': { 'compose': '编写邮件', 'send': '发送', 'subject': '学科', 'labels': '标签', 'emptyList': '空的电子邮件清单', 'inbox': '收件箱', 'sent': '已发送', 'drafts': '草稿', 'starred': '已加星标', 'trash': '垃圾', 'work': '工作', 'invoice': '发票' }, 'todo': { 'addTask': '添加任务', 'tasks': '任务', 'completed': '已完成', 'labels': '标签' }, 'dashboard': { 'activity': '活动', 'weeklySales': '每周销售', 'sales': '营业额', 'recentOrders': '最近的订单', 'sources': '流量来源', 'lastweek': '与上周', 'orders': '订单', 'customers': '顾客', 'tickets': '支持票', 'viewReport': '查看报告' }, 'usermenu': { 'profile': '个人资料', 'signin': '登入', 'dashboard': '仪表板', 'signout': '登出', 'notSignin': '未登录', }, 'error': { 'notfound': '网页未找到', 'other': '发生错误' }, 'check': { 'title': '设置新密码', 'backtosign': '返回登录', 'newpassword': '新密码', 'button': '设置新密码并登录', 'error': '动作链接无效', 'verifylink': '正在验证链接...', 'verifyemail': '正在验证电子邮件地址...', 'emailverified': '电子邮件已验证!重定向中...' }, 'forgot': { 'title': '忘记密码?', 'subtitle': '输入您的帐户电子邮件地址,我们将向您发送一个链接以重置密码。', 'email': '电子邮件', 'button': '要求重设密码', 'backtosign': '返回登录' }, 'login': { 'title': '登入', 'email': '电子邮件', 'password': '密码', 'button': '登入', 'orsign': '或使用登录', 'forgot': '忘记密码?', 'noaccount': '还没有帐号?', 'create': '在此处创建一个', 'error': '电子邮件/密码组合无效' }, 'register': { 'title': '创建帐号', 'name': '全名', 'email': '电子邮件', 'password': '密码', 'button': '创建帐号', 'orsign': '或注册', 'agree': '签署即表示您同意', 'account': '已经有帐号了?', 'signin': '登入' }, 'utility': { 'maintenance': '维护中' }, 'faq': { 'call': '还有其他问题吗?请伸出手' }, 'ecommerce': { 'products': '产品展示', 'filters': '筛选器', 'collections': '馆藏', 'priceRange': '价格范围', 'customerReviews': '顾客评论', 'up': '及以上', 'brand': '牌', 'search': '搜索产品', 'results': '结果({0},共{1})', 'orders': '订单', 'shipping': '运输', 'freeShipping': '免费送货', 'inStock': '有现货', 'quantity': '数量', 'addToCart': '添加到购物车', 'buyNow': '立即购买', 'price': '价钱', 'about': '关于这个项目', 'description': '描述', 'reviews': '评论', 'details': '产品详情', 'cart': '大车', 'summary': '订单摘要', 'total': '总', 'discount': '折扣', 'subtotal': '小计', 'continue': '继续购物', 'checkout': '查看' }, 'menu': { 'apps': 'apps', 'search': '搜索(按“ Ctrl + /”进行聚焦)', 'dashboard': '仪表板', 'logout': '登出', 'profile': '个人资料', 'blank': '空白页', 'pages': '页数', 'others': '其他', 'email': '电子邮件', 'chat': '聊天室', 'chat-channel': '聊天频道', 'todo': '去做', 'board': '任务板', 'users': '用户数', 'usersList': '清单', 'usersEdit': '编辑', 'ecommerce': '电子商务', 'ecommerceList': '产品展示', 'ecommerceProductDetails': '产品详情', 'ecommerceOrders': '订单', 'ecommerceCart': '大车', 'auth': '验证页面', 'authLogin': '登录/登录', 'authRegister': '注册/注册', 'authVerify': '验证邮件', 'authForgot': '忘记密码', 'authReset': '重设密码', 'errorPages': '错误页面', 'errorNotFound': '找不到/ 404', 'errorUnexpected': '意想不到的/ 500', 'utilityPages': '实用页面', 'utilityMaintenance': '保养', 'utilitySoon': '快来了', 'utilityHelp': '常见问题/帮助', 'levels': '菜单级别', 'levels2-1': '级别2.1', 'levels2-2': '级别2.2', 'levels3-1': '级别3.1', 'levels3-2': '级别3.2', 'disabled': '菜单已禁用', 'docs': '文献资料', 'feedback': '反馈', 'support': '支持', 'landingPage': '登陆页面', 'pricingPage': '定价页面', 'flowable': '流程管理', 'flowable-design': '流程设计', 'form-list':'表单', 'form-design':'表单设计' }, '$vuetify': { 'badge': '徽章', 'close': '关', 'dataIterator': { 'noResultsText': '未找到匹配的记录', 'loadingText': '正在载入项目...' }, 'dataTable': { 'itemsPerPageText': '每页行数:', 'ariaLabel': { 'sortDescending': '降序排列。', 'sortAscending': '升序排列。', 'sortNone': '未排序。', 'activateNone': '激活以删除排序。', 'activateDescending': '激活以降序排列。', 'activateAscending': '激活以升序排序。' }, 'sortBy': '排序方式' }, 'dataFooter': { 'itemsPerPageText': '每页项目:', 'itemsPerPageAll': '所有', 'nextPage': '下一页', 'prevPage': '上一页', 'firstPage': '第一页', 'lastPage': '最后一页', 'pageText': '{2}中的{0}-{1}' }, 'datePicker': { 'itemsSelected': '已选择{0}', 'nextMonthAriaLabel': '下个月', 'nextYearAriaLabel': '明年', 'prevMonthAriaLabel': '前一个月', 'prevYearAriaLabel': '前一年' }, 'noDataText': '无可用数据', 'carousel': { 'prev': '以前的视觉', 'next': '下一个视觉', 'ariaLabel': { 'delimiter': '{1}的轮播幻灯片{0}' } }, 'calendar': { 'moreEvents': '还有{0}个' }, 'fileInput': { 'counter': '{0}个文件', 'counterSize': '{0}个文件(共{1}个)' }, 'timePicker': { 'am': 'AM', 'pm': 'PM' }, 'pagination': { 'ariaLabel': { 'wrapper': '分页导航', 'next': '下一页', 'previous': '上一页', 'page': '转到页面{0}', 'currentPage': '当前页,第{0}页' } } } } ================================================ FILE: src/typings/api.d.ts ================================================ declare namespace ApiCommon { interface PageResult { pageNo: number, pageSize: number, list: T, total: number, } } declare namespace ApiAuth { interface Token { token: string; refreshToken: string; } type UserInfo = Auth.UserInfo; } declare namespace ApiRoute { interface Route { routes: AuthRoute.Route[]; home: AuthRoute.AllRouteKey; } } declare namespace ApiForm { interface Form { config: string; id: string; name: string; status: '1' | '0'; created: string; } } declare namespace ApiUserManagement { interface Address { city?: string, state?: string, country?: string, zipCode?: string, detail?: string, } interface User { id: string, name: string, verified: boolean, created: string, lastSignIn: string, birthDay: string, gender: '0' | '1' | '2'; phone: string; email: string; userStatus: '1' | '2' | '4'; avatar: string address: Address } } declare namespace ApiChatManagement { interface message { id: string, text: string, timestamp: string, image?: string, user: { avatar: string, id: string } } } declare namespace ApiChatManagement { interface message { id: string, text: string, timestamp: string, image?: string, user: { avatar: string, id: string } } } declare namespace ApiChatManagement { interface message { id: string, text: string, timestamp: string, image?: string, user: { avatar: string, id: string } } } ================================================ FILE: src/typings/business.d.ts ================================================ declare namespace Auth { type RoleType = keyof typeof import('@/enum').EnumUserRole; interface UserInfo { userId: string; userName: string; userRole: RoleType; userAvatar?: string; } } declare namespace UserManagement { interface User extends ApiUserManagement.User { role: Auth.RoleType } /** * 用户性别 * - 0: 女 * - 1: 男 */ type GenderKey = NonNullable; /** * 用户状态 * - 1: 启用 * - 2: 禁用 * - 3: 冻结 * - 4: 软删除 */ type UserStatusKey = NonNullable; } declare namespace FormManagement { interface Form extends ApiForm.Form { } type FormStatusKey = NonNullable; } ================================================ FILE: src/typings/camunda.d.ts ================================================ declare module "bpmn-js/*" declare module "diagram-js/*" declare module "@bpmn-io/*" declare module "bpmn-js-properties-panel" declare module "camunda-bpmn-moddle/*" declare module "*.bpmn" { const value: any; // Add better type definitions here if desired. export default value; } ================================================ FILE: src/typings/config.d.ts ================================================ interface Config { theme: ThemeConfig.Config, locales: any currency: CurrencyConfig.Config, } declare namespace CurrencyConfig { interface Currency { label: string, decimalDigits: number, decimalSeparator: string thousandsSeparator: string, currencySymbol: string, currencySymbolNumberOfSpaces: number, currencySymbolPosition: string } interface Config { currency: Currency, availableCurrencies: Currency[], } } declare namespace ThemeConfig { interface Config { //primary color primary: string, //follow OS theme followOs: boolean, // global theme for the theme globalTheme: string, // side menu theme, use global theme or custom menuTheme: string, // toolbar theme, use global theme or custom toolbarTheme: string, // show toolbar detached from top isToolbarDetached: boolean, // wrap pages content with a max-width isContentBoxed: boolean, // application is right to left isRTL: boolean, // dark theme colors dark: import('vuetify').ThemeDefinition, // light theme colors light: import('vuetify').ThemeDefinition, } } declare namespace NavigationConfig { interface Menu { icon?: string key?: string, text?: string, link?: string, regex?: RegExp, disabled?: boolean, items?: NonNullable } interface Config { menu: Menu[] footer: Footer[] } interface Footer { text?: string, key: string, href?: string, target?: string } } ================================================ FILE: src/typings/env.d.ts ================================================ type ServiceEnvType = 'dev' | 'test' | 'prod'; interface ServiceEnvConfig { url: string; urlPattern: '/url-pattern'; secondUrl: string; secondUrlPattern: '/second-url-pattern'; } interface ImportMetaEnv { readonly VITE_BASE_URL: string; readonly VITE_APP_NAME: string; readonly VITE_APP_TITLE: string; readonly VITE_APP_DESC: string; readonly VITE_AUTH_ROUTE_MODE: 'static' | 'dynamic'; readonly VITE_ROUTE_HOME_PATH: AuthRoute.RoutePath; readonly VITE_ICON_PREFFIX: string; readonly VITE_ICON_LOCAL_PREFFIX: string; readonly VITE_SERVICE_ENV?: ServiceEnvType; readonly VITE_HTTP_PROXY?: 'Y' | 'N'; readonly VITE_VISUALIZER?: 'Y' | 'N'; readonly VITE_COMPRESS?: 'Y' | 'N'; readonly VITE_COMPRESS_TYPE?: 'gzip' | 'brotliCompress' | 'deflate' | 'deflateRaw'; readonly VITE_PWA?: 'Y' | 'N'; readonly VITE_HASH_ROUTE?: 'Y' | 'N'; readonly VITE_VERCEL?: 'Y' | 'N'; } interface ImportMeta { readonly env: ImportMetaEnv; } ================================================ FILE: src/typings/filter.d.ts ================================================ declare namespace filters { type formatCurrency = (value: number, currency?: CurrencyConfig.Currency) => number | string; } interface filtersConfig { formatCurrency: filters.formatCurrency } declare module '@vue/runtime-core' { export interface ComponentCustomProperties { $filters: filtersConfig } } export {} ================================================ FILE: src/typings/global.d.ts ================================================ interface Window { $snackBar?: import('@/components/provider').SnackBarApiInjection, $loadingOverly?: import('@/components/provider').LoadingOverlyApiInjection, $dialog?: import("@/components/provider").DialogApiInjection } declare namespace Common { type StrategyAction = [boolean, () => void]; } declare const PROJECT_BUILD_TIME: string; ================================================ FILE: src/typings/page-route.d.ts ================================================ declare namespace PageRoute { /** * the root route key * @translate 根路由 */ type RootRouteKey = 'root'; /** * the not found route, which catch the invalid route path * @translate 未找到路由(捕获无效路径的路由) */ type NotFoundRouteKey = 'not-found'; /** * the route key * @translate 页面路由 */ type RouteKey = | '403' | '404' | '500' | 'login' | 'not-found' | 'dashboard' | 'dashboard_analytics' | 'apps' | 'apps_manager-user' | 'apps_manager-user_list' | 'apps_manager-user_edit' | 'apps_board' | 'apps_todo' | 'apps_todo_tasks' | 'apps_todo_completed' | 'apps_todo_label' | 'apps_chat' | 'apps_chat-channel' | 'pages' | 'pages_error' | 'pages_error_notfound' | 'pages_error_unexpected' | 'other' | 'blank-page' | 'other_menu-levels' | 'other_menu-levels-2-1' | 'other_menu-levels-2-2' | 'other_menu-levels-3-1' | 'other_menu-levels-3-2' | 'flowable' | 'flowable_design' | 'form' | 'form_list' | 'form_design' ; /** * last degree route key, which has the page file * @translate 最后一级路由(该级路有对应的页面文件) */ type LastDegreeRouteKey = Extract; } ================================================ FILE: src/typings/route.d.ts ================================================ declare namespace AuthRoute { type RootRoutePath = '/'; type NotFoundRoutePath = '/:pathMatch(.*)*'; type RootRouteKey = PageRoute.RootRouteKey; type NotFoundRouteKey = PageRoute.NotFoundRouteKey; type RouteKey = PageRoute.RouteKey; type LastDegreeRouteKey = PageRoute.LastDegreeRouteKey; type AllRouteKey = RouteKey | RootRouteKey | NotFoundRouteKey; type RoutePath = AuthRouteUtils.GetRoutePath; type RouteComponentType = 'basic' | 'blank' | 'self' | 'auth' | 'error' | 'todo' | 'chat'; interface RouteMeta { title: string; dynamicPath?: AuthRouteUtils.GetDynamicPath; singleLayout?: Extract; requiresAuth?: boolean; permissions?: Auth.RoleType[]; keepAlive?: boolean; icon?: string; hide?: boolean; href?: string; multiTab?: boolean; order?: number; activeMenu?: RouteKey; multi?: boolean; affix?: boolean; } type Route = K extends AllRouteKey ? { name: K; path: AuthRouteUtils.GetRoutePath; redirect?: AuthRouteUtils.GetRoutePath; /** * 路由组件 * - basic: 基础布局,具有公共部分的布局 * - blank: 空白布局 * - multi: 多级路由布局(三级路由或三级以上时,除第一级路由和最后一级路由,其余的采用该布局) * - self: 作为子路由,使用自身的布局(作为最后一级路由,没有子路由) */ component?: RouteComponentType; children?: Route[]; meta: RouteMeta>; } & Omit : never; type RouteModule = Record; } declare namespace AuthRouteUtils { type RouteKeySplitMark = '_'; type RoutePathSplitMark = '/'; type BlankString = ''; /** key转换成path */ type KeyToPath = K extends `${infer _Left}${RouteKeySplitMark}${RouteKeySplitMark}${infer _Right}` ? never : K extends `${infer Left}${RouteKeySplitMark}${infer Right}` ? Left extends BlankString ? never : Right extends BlankString ? never : KeyToPath<`${Left}${RoutePathSplitMark}${Right}`> : `${RoutePathSplitMark}${K}`; /** 根据路由key获取路由路径 */ type GetRoutePath = K extends AuthRoute.AllRouteKey ? K extends AuthRoute.RootRouteKey ? AuthRoute.RootRoutePath : K extends AuthRoute.NotFoundRouteKey ? AuthRoute.NotFoundRoutePath : KeyToPath : never; /** 获取一级路由(有子路由的一级路由和没有子路由的路由) */ type GetFirstDegreeRouteKey = K extends `${infer _Left}${RouteKeySplitMark}${infer _Right}` ? never : K; /** 获取有子路由的一级路由 */ type GetFirstDegreeRouteKeyWithChildren = K extends `${infer Left}${RouteKeySplitMark}${infer _Right}` ? Left : never; /** 单级路由的key (单级路由需要添加一个父级路由用于应用布局组件) */ type SingleRouteKey = Exclude; /** 单独路由父级路由key */ type SingleRouteParentKey = `${SingleRouteKey}-parent`; /** 单独路由父级路由path */ type SingleRouteParentPath = KeyToPath; /** 获取路由动态路径 */ type GetDynamicPath

= | `${P}/:${string}` | `${P}/:${string}(${string})` | `${P}/:${string}(${string})?`; } ================================================ FILE: src/typings/router.d.ts ================================================ import 'vue-router'; declare module 'vue-router' { interface RouteMeta extends AuthRoute.RouteMeta {} } ================================================ FILE: src/typings/storage.d.ts ================================================ declare namespace StorageInterface { /** localStorage的存储数据的类型 */ interface Session { demoKey: string; } /** localStorage的存储数据的类型 */ interface Local { token: string; refreshToken: string; userInfo: Auth.UserInfo; themeSettings: ThemeConfig.Config; } } ================================================ FILE: src/typings/system.d.ts ================================================ /** 枚举的key类型 */ declare namespace EnumType { /** 布局组件名称 */ type LayoutComponentName = keyof typeof import('@/enum').EnumLayoutComponentName; /** 布局模式 */ type ThemeLayoutMode = keyof typeof import('@/enum').EnumThemeLayoutMode; /** 多页签风格 */ type ThemeTabMode = keyof typeof import('@/enum').EnumThemeTabMode; /** 水平模式的菜单位置 */ type ThemeHorizontalMenuPosition = keyof typeof import('@/enum').EnumThemeHorizontalMenuPosition; /** 过渡动画 */ type ThemeAnimateMode = keyof typeof import('@/enum').EnumThemeAnimateMode; /** 登录模块 */ type LoginModuleKey = keyof typeof import('@/enum').EnumLoginModule; } /** 请求的相关类型 */ declare namespace Service { /** * 请求的错误类型: * - axios: axios错误:网络错误, 请求超时, 默认的兜底错误 * - http: 请求成功,响应的http状态码非200的错误 * - backend: 请求成功,响应的http状态码为200,由后端定义的业务错误 */ type RequestErrorType = 'axios' | 'http' | 'backend'; /** 请求错误 */ interface RequestError { /** 请求服务的错误类型 */ type: RequestErrorType; /** 错误码 */ code: string | number; /** 错误信息 */ msg: string; } /** 后端接口返回的数据结构配置 */ interface BackendResultConfig { /** 表示后端请求状态码的属性字段 */ codeKey: string; /** 表示后端请求数据的属性字段 */ dataKey: string; /** 表示后端消息的属性字段 */ msgKey: string; /** 后端业务上定义的成功请求的状态 */ successCode: number | string; } /** 自定义的请求成功结果 */ interface SuccessResult { /** 请求错误 */ error: null; /** 请求数据 */ data: T; } /** 自定义的请求失败结果 */ interface FailedResult { /** 请求错误 */ error: RequestError; /** 请求数据 */ data: null; } /** 自定义的请求结果 */ type RequestResult = SuccessResult | FailedResult; /** 多个请求数据结果 */ type MultiRequestResult = T extends [infer First, ...infer Rest] ? [First] extends [any] ? Rest extends any[] ? [Service.RequestResult, ...MultiRequestResult] : [Service.RequestResult] : Rest extends any[] ? MultiRequestResult : [] : []; /** 请求结果的适配器函数 */ type ServiceAdapter = (...args: A) => T; /** mock示例接口类型:后端接口返回的数据的类型 */ interface MockServiceResult { /** 状态码 */ code: string | number; /** 接口数据 */ data: T; /** 接口消息 */ message: string; } /** mock的响应option */ interface MockOption { url: Record; body: Record; query: Record; headers: Record; } } /** 主题相关类型 */ declare namespace Theme { /** 布局样式 */ interface Layout { /** 最小宽度 */ minWidth: number; /** 布局模式 */ mode: EnumType.ThemeLayoutMode; /** 布局模式列表 */ modeList: LayoutModeList[]; } interface LayoutModeList { value: EnumType.ThemeLayoutMode; label: import('@/enum').EnumThemeLayoutMode; } /** 其他主题颜色 */ interface OtherColor { /** 信息 */ info: string; /** 成功 */ success: string; /** 警告 */ warning: string; /** 错误 */ error: string; } /** 头部样式 */ interface Header { /** 头部反转色 */ inverted: boolean; /** 头部高度 */ height: number; /** 面包屑样式 */ crumb: Crumb; } /** 面包屑样式 */ interface Crumb { /** 面包屑可见 */ visible: boolean; /** 显示图标 */ showIcon: boolean; } /** 标多页签样式 */ export interface Tab { /** 多页签可见 */ visible: boolean; /** 多页签高度 */ height: number; /** 多页签风格 */ mode: EnumType.ThemeTabMode; /** 多页签风格列表 */ modeList: ThemeTabModeList[]; /** 开启多页签缓存 */ isCache: boolean; } /** 多页签风格列表 */ interface ThemeTabModeList { value: EnumType.ThemeTabMode; label: import('@/enum').EnumThemeTabMode; } /** 侧边栏样式 */ interface Sider { /** 侧边栏反转色 */ inverted: boolean; /** 侧边栏宽度 */ width: number; /** 侧边栏折叠时的宽度 */ collapsedWidth: number; /** vertical-mix模式下侧边栏宽度 */ mixWidth: number; /** vertical-mix模式下侧边栏折叠时的宽度 */ mixCollapsedWidth: number; /** vertical-mix模式下侧边栏的子菜单的宽度 */ mixChildMenuWidth: number; } /** 菜单样式 */ interface Menu { /** 水平模式的菜单的位置 */ horizontalPosition: EnumType.ThemeHorizontalMenuPosition; /** 水平模式的菜单的位置列表 */ horizontalPositionList: HorizontalMenuPositionList[]; } /** 水平模式的菜单的位置列表 */ interface HorizontalMenuPositionList { value: EnumType.ThemeHorizontalMenuPosition; label: import('@/enum').EnumThemeHorizontalMenuPosition; } /** 底部样式 */ interface Footer { /** 是否固定底部 */ fixed: boolean; /** 底部高度 */ height: number; /* 底部是否可见 */ visible: boolean; } /** 页面样式 */ interface Page { /** 页面是否开启动画 */ animate: boolean; /** 动画类型 */ animateMode: EnumType.ThemeAnimateMode; /** 动画类型列表 */ animateModeList: AnimateModeList[]; } /** 动画类型列表 */ interface AnimateModeList { value: EnumType.ThemeAnimateMode; label: import('@/enum').EnumThemeAnimateMode; } } declare namespace App { /** 全局头部属性 */ interface GlobalHeaderProps { /** 显示logo */ showLogo: boolean; /** 显示头部菜单 */ showHeaderMenu: boolean; /** 显示菜单折叠按钮 */ showMenuCollapse: boolean; } type GlobalMenuOption = { key: string; label: string; routeName: string; routePath: string; icon?: string; children?: GlobalMenuOption[]; }; /** 面包屑 */ type GlobalBreadcrumb = BreadcrumbItem /** 多页签Tab的路由 */ interface GlobalTabRoute extends Pick { /** 滚动的位置 */ scrollPosition: { left: number; top: number; }; } } declare namespace I18nType { interface Schema { system: { title: string; }; routes: { dashboard: { dashboard: string; analysis: string; workbench: string; }; about: { about: string; }; }; } } ================================================ FILE: src/typings/utils.d.ts ================================================ declare namespace TypeUtil { type Noop = (...args: any) => any; type UnionInclude = K extends keyof T ? true : false; type GetFunArgs = F extends (...args: infer P) => any ? P : never; type Writable = { [K in keyof T]: T[K] }; type FirstOfArray = T extends [infer First, ...infer _Rest] ? First : never; type LastOfArray = T extends [...infer _Rest, infer Last] ? Last : never; // union to tuple type Union2IntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer R) => void ? R : never; type GetUnionLast = Union2IntersectionFn extends () => infer I ? I : never; type UnionToTuple = [T] extends [never] ? R : UnionToTuple>, [GetUnionLast, ...R]>; } ================================================ FILE: src/typings/vuetify.d.ts ================================================ // export vuetify types.ts for ts type BreadcrumbItem = import('vuetify/components').VBreadcrumbsItem['$props'] type DataTableHeader = import('vuetify/labs/VDataTable').VDataTable['headers'] type Snackbar = import('vuetify/components').VSnackbar['$props'] type Dialog = import('vuetify/components').VDialog['$props'] type VCardTitle = import('vuetify/components').VCardTitle type VTextField = import('vuetify/components').VTextField type VSelect = import('vuetify/components').VSelect type VRadio= import('vuetify/components').VRadio type VCheckbox= import('vuetify/components').VCheckbox type VBtn = import('vuetify/components').VBtn type VCol= import('vuetify/components').VCol ================================================ FILE: src/utils/common/index.ts ================================================ export * from './typeof'; export * from './pattern'; ================================================ FILE: src/utils/common/pattern.ts ================================================ export function exeStrategyActions(actions: Common.StrategyAction[]) { actions.some(item => { const [flag, action] = item; if (flag) { action(); } return flag; }); } ================================================ FILE: src/utils/common/typeof.ts ================================================ import { EnumDataType } from '@/enum'; export function isNumber(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.number; } export function isString(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.string; } export function isBoolean(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.boolean; } export function isNull(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.null; } export function isUndefined(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.undefined; } export function isObject>(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.object; } export function isArray(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.array; } export function isFunction any | void | never>(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.function; } export function isDate(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.date; } export function isRegExp(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.regexp; } export function isPromise>(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.promise; } export function isSet>(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.set; } export function isMap>(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.map; } export function isFile(data: T | unknown): data is T { return Object.prototype.toString.call(data) === EnumDataType.file; } ================================================ FILE: src/utils/crypto/index.ts ================================================ import CryptoJS from 'crypto-js'; const CryptoSecret = '__CryptoJS_Secret__'; /** * 加密数据 * @param data - 数据 */ export function encrypto(data: any) { const newData = JSON.stringify(data); return CryptoJS.AES.encrypt(newData, CryptoSecret).toString(); } /** * 解密数据 * @param cipherText - 密文 */ export function decrypto(cipherText: string) { const bytes = CryptoJS.AES.decrypt(cipherText, CryptoSecret); const originalText = bytes.toString(CryptoJS.enc.Utf8); if (originalText) { return JSON.parse(originalText); } return null; } ================================================ FILE: src/utils/flow/EventEmitter.ts ================================================ import {getRawType, notNull} from './tools' const isArray = (obj: any) => getRawType(obj) === 'array' const isNullOrUndefined = (obj: any) => !notNull(obj) class EventEmitter { static _events: { [key: string]: Function[] } = {} constructor() { } static _addListener(type: string, fn: any, context?: any, once?: any) { if (typeof fn !== 'function') { throw new TypeError('fn must be a function') } fn.context = context fn.once = !!once const event = EventEmitter._events[type] // only one, let `this._events[type]` to be a function if (isNullOrUndefined(event)) { EventEmitter._events[type] = fn } else if (typeof event === 'function') { // already has one function, `this._events[type]` must be a function before EventEmitter._events[type] = [event, fn] } else if (isArray(event)) { // already has more than one function, just push EventEmitter._events[type].push(fn) } return EventEmitter } static addListener(type: string, fn: any, context?: any) { return EventEmitter._addListener(type, fn, context) } static on(type: string, fn: any, context?: any) { return EventEmitter.addListener(type, fn, context) } static once(type: string, fn: any, context?: any) { return EventEmitter._addListener(type, fn, context, true) } static emit(type: any, ...rest: any[]) { if (isNullOrUndefined(type)) { throw new Error('emit must receive at lease one argument') } const event: any = EventEmitter._events[type] if (isNullOrUndefined(event)) return false if (typeof event === 'function') { event.call(event.context || null, ...rest) if (event.once) { EventEmitter.removeListener(type, event) } } else if (isArray(event)) { event.map((e: any) => { e.call(e.context || null, ...rest) if (e.once) { EventEmitter.removeListener(type, e) } }) } return true } static removeListener(type: any, fn: any) { if (isNullOrUndefined(EventEmitter._events)) return EventEmitter // if type is undefined or null, nothing to do, just return this if (isNullOrUndefined(type)) return EventEmitter if (typeof fn !== 'function') { throw new Error('fn must be a function') } const events = EventEmitter._events[type] if (typeof events === 'function') { events === fn && delete EventEmitter._events[type] } else { const findIndex = events.findIndex((e) => e === fn) if (findIndex === -1) return EventEmitter // match the first one, shift faster than splice if (findIndex === 0) { events.shift() } else { events.splice(findIndex, 1) } // just left one listener, change Array to Function if (events.length === 1) { // @ts-ignore EventEmitter._events[type] = events[0] } } return EventEmitter } static removeAllListeners(type: any) { if (isNullOrUndefined(EventEmitter._events)) return EventEmitter // if not provide type, remove all if (isNullOrUndefined(type)) EventEmitter._events = Object.create(null) const events = EventEmitter._events[type] if (!isNullOrUndefined(events)) { // check if `type` is the last one if (Object.keys(EventEmitter._events).length === 1) { EventEmitter._events = Object.create(null) } else { delete EventEmitter._events[type] } } return EventEmitter } static listeners(type: any) { if (isNullOrUndefined(EventEmitter._events)) return [] const events = EventEmitter._events[type] // use `map` because we need to return a new array return isNullOrUndefined(events) ? [] : typeof events === 'function' ? [events] : events.map((o) => o) } static listenerCount(type: any) { if (isNullOrUndefined(EventEmitter._events)) return 0 const events = EventEmitter._events[type] return isNullOrUndefined(events) ? 0 : typeof events === 'function' ? 1 : events.length } static eventNames() { if (isNullOrUndefined(EventEmitter._events)) return [] return Object.keys(EventEmitter._events) } } export default EventEmitter ================================================ FILE: src/utils/flow/tools.ts ================================================ /* 空函数 */ export function noop() { } /** * 校验非空 * @param {*} val * @return boolean */ export function notEmpty(val: any) { if (!notNull(val)) { return false } if (getRawType(val) === 'array') { return val.length } if (getRawType(val) === 'object') { return Reflect.ownKeys(val).length } return true } export function notNull(val: any) { return val !== undefined && val !== null } /** * 返回数据原始类型 * @param value * @return { 'string' | 'array' | 'boolean' | 'number' | 'object' | 'function' } type */ export function getRawType(value: any) { return Object.prototype.toString.call(value).slice(8, -1).toLowerCase() } ================================================ FILE: src/utils/index.ts ================================================ export * from './service'; export * from './storage'; export * from './router'; export * from './common'; export * from './vue'; ================================================ FILE: src/utils/router/auth.ts ================================================ export function filterAuthRoutesByUserPermission(routes: AuthRoute.Route[], permission: Auth.RoleType) { return routes.map(route => filterAuthRouteByUserPermission(route, permission)).flat(1); } function filterAuthRouteByUserPermission(route: AuthRoute.Route, permission: Auth.RoleType): AuthRoute.Route[] { const filterRoute = { ...route }; const hasPermission = !route.meta.permissions || permission === 'admin' || route.meta.permissions.includes(permission); if (filterRoute.children) { const filterChildren = filterRoute.children.map(item => filterAuthRouteByUserPermission(item, permission)).flat(1); Object.assign(filterRoute, { children: filterChildren }); } return hasPermission ? [filterRoute] : []; } ================================================ FILE: src/utils/router/breadcrumb.ts ================================================ /** * 获取面包屑数据 * @param activeKey - 当前页面路由的key * @param menus - 菜单数据 * @param rootPath - 根路由路径 */ export function getBreadcrumbByRouteKey(activeKey: string, menus: App.GlobalMenuOption[], rootPath: string) { const breadcrumbMenu = getBreadcrumbMenu((menu) => activeKey.includes(menu.routeName), menus); const breadcrumb = breadcrumbMenu.map(item => transformBreadcrumbMenuToBreadcrumb(item, rootPath)); return breadcrumb; } export function getBreadcrumbsByPredicate(predicate: (menu: App.GlobalMenuOption) => boolean, menus: App.GlobalMenuOption[], rootPath: string) { const breadcrumbMenu = getBreadcrumbMenu(predicate, menus); const breadcrumb = breadcrumbMenu.map(item => transformBreadcrumbMenuToBreadcrumb(item, rootPath)); return breadcrumb; } /** * 根据菜单数据获取面包屑格式的菜单 * @param predicate * @param menus - 菜单数据 */ function getBreadcrumbMenu(predicate: (menu: App.GlobalMenuOption) => boolean, menus: App.GlobalMenuOption[]) { const breadcrumbMenu: App.GlobalMenuOption[] = []; menus.some(menu => { const flag = predicate(menu); if (flag) { breadcrumbMenu.push(...getBreadcrumbMenuItem(predicate, menu)); } return flag; }); return breadcrumbMenu; } /** * 根据单个菜单数据获取面包屑格式的菜单 * @param predicate * @param menu - 单个菜单数据 */ function getBreadcrumbMenuItem(predicate: (menu: App.GlobalMenuOption) => boolean, menu: App.GlobalMenuOption) { const breadcrumbMenu: App.GlobalMenuOption[] = []; if (predicate(menu)) { breadcrumbMenu.push(menu); } if (predicate(menu) && menu.children && menu.children.length) { breadcrumbMenu.push( ...menu.children.map(item => getBreadcrumbMenuItem(predicate, item as App.GlobalMenuOption)).flat(1) ); } return breadcrumbMenu; } /** * 将面包屑格式的菜单数据转换成面包屑数据 * @param menu - 单个菜单数据 * @param rootPath - 根路由路径 */ function transformBreadcrumbMenuToBreadcrumb(menu: App.GlobalMenuOption, rootPath: string) { const breadcrumb: App.GlobalBreadcrumb = { key: menu.routeName, title: menu.label, to: menu.routePath, disabled: menu.routePath === rootPath, }; return breadcrumb; } ================================================ FILE: src/utils/router/cache.ts ================================================ import type { RouteRecordRaw } from 'vue-router'; /** * 获取缓存的路由对应组件的名称 * @param routes - 转换后的vue路由 */ export function getCacheRoutes(routes: RouteRecordRaw[]) { const cacheNames: string[] = []; routes.forEach(route => { // 只需要获取二级路由的缓存的组件名 if (hasChildren(route)) { (route.children as RouteRecordRaw[]).forEach(item => { if (isKeepAlive(item)) { cacheNames.push(item.name as string); } }); } }); return cacheNames; } /** * 路由是否缓存 * @param route */ function isKeepAlive(route: RouteRecordRaw) { return Boolean(route?.meta?.keepAlive); } /** * 是否有二级路由 * @param route */ function hasChildren(route: RouteRecordRaw) { return Boolean(route.children && route.children.length); } ================================================ FILE: src/utils/router/component.ts ================================================ import type {RouteComponent} from 'vue-router'; import {views} from '@/views'; import {isFunction} from '@/utils'; import {BasicLayout, BlankLayout, AuthLayout, ErrorLayout, TodoLayout, ChatLayout} from '@/layouts'; type Lazy = () => Promise; interface ModuleComponent { default: RouteComponent; } type LayoutComponent = Record>; /** * 获取布局的vue文件(懒加载的方式) * @param layoutType - 布局类型 */ export function getLayoutComponent(layoutType: EnumType.LayoutComponentName) { const layoutComponent: LayoutComponent = { basic: BasicLayout, blank: BlankLayout, auth: AuthLayout, error: ErrorLayout, todo: TodoLayout, chat: ChatLayout, }; return layoutComponent[layoutType]; } /** * 获取页面导入的vue文件 * @param routeKey - 路由key */ export function getViewComponent(routeKey: AuthRoute.LastDegreeRouteKey) { if (!views[routeKey]) { throw new Error(`route “${routeKey}” miss corresponding component file`); } return setViewComponentName(views[routeKey], routeKey); } function setViewComponentName(component: RouteComponent | Lazy, name: string) { if (isAsyncComponent(component)) { return async () => { const result = await component(); Object.assign(result.default, {name}); return result; }; } Object.assign(component, {name}); return component; } function isAsyncComponent(component: RouteComponent | Lazy): component is Lazy { return isFunction(component); } ================================================ FILE: src/utils/router/helpers.ts ================================================ /** * 获取所有固定路由的名称集合 * @param routes - 固定路由 */ export function getConstantRouteNames(routes: AuthRoute.Route[]) { return routes.map(route => getConstantRouteName(route)).flat(1); } /** * 获取所有固定路由的名称集合 * @param route - 固定路由 */ function getConstantRouteName(route: AuthRoute.Route) { const names = [route.name]; if (route.children?.length) { names.push(...route.children!.map(item => getConstantRouteName(item)).flat(1)); } return names; } ================================================ FILE: src/utils/router/index.ts ================================================ export * from './transform'; export * from './breadcrumb'; export * from './component'; export * from './regexp'; export * from './cache'; export * from './helpers'; export * from './auth'; export * from './module'; export * from './menu'; ================================================ FILE: src/utils/router/menu.ts ================================================ /** * 将权限路由转换成菜单 * @param routes - 路由 */ export function transformAuthRouteToMenu(routes: AuthRoute.Route[]): App.GlobalMenuOption[] { const globalMenu: App.GlobalMenuOption[] = []; routes.forEach(route => { const {name, path, meta} = route; const routePath = route.meta.dynamicPath || path const routeName = name as string; let menuChildren: App.GlobalMenuOption[] | undefined; if (route.children) { menuChildren = transformAuthRouteToMenu(route.children); } const menuItem: App.GlobalMenuOption = addPartialProps({ menu: { key: routeName, label: meta.title, routeName, routePath: routePath }, icon: meta.icon, children: menuChildren }); if (!hideInMenu(route)) { globalMenu.push(menuItem); } }); return globalMenu; } /** * 获取当前路由所在菜单数据的paths * @param activeKey - 当前路由的key * @param menus - 菜单数据 */ export function getActiveKeyPathsOfMenus(activeKey: string, menus: App.GlobalMenuOption[]) { const keys = menus.map(menu => getActiveKeyPathsOfMenu(activeKey, menu)).flat(1); return keys; } function getActiveKeyPathsOfMenu(activeKey: string, menu: App.GlobalMenuOption) { const keys: string[] = []; if (activeKey.startsWith(menu.routeName)) { keys.push(menu.routeName); } if (menu.children) { keys.push(...menu.children.map(item => getActiveKeyPathsOfMenu(activeKey, item as App.GlobalMenuOption)).flat(1)); } return keys; } /** 路由不转换菜单 */ function hideInMenu(route: AuthRoute.Route) { return Boolean(route.meta.hide); } /** 给菜单添加可选属性 */ function addPartialProps(config: { menu: App.GlobalMenuOption; icon?: string; children?: App.GlobalMenuOption[]; }) { const item = {...config.menu}; const {icon, children} = config; if (icon) { Object.assign(item, {icon}); } if (children) { Object.assign(item, {children}); } return item; } ================================================ FILE: src/utils/router/module.ts ================================================ /** * 权限路由排序 * @param routes - 权限路由 */ export function sortRoutes(routes: AuthRoute.Route[]) { return routes.sort((next, pre) => { return Number(next.meta?.order) - Number(pre.meta?.order) }).map(i => { if (i.children) sortRoutes(i.children) return i }) } /** * 处理全部导入的路由模块 * @param modules - 路由模块 */ export function handleModuleRoutes(modules: AuthRoute.RouteModule) { const routes: AuthRoute.Route[] = []; Object.keys(modules).forEach(key => { const item = modules[key].default; if (item) { routes.push(item); } else { window.console.error(`路由模块解析出错: key = ${key}`); } }); return sortRoutes(routes); } ================================================ FILE: src/utils/router/regexp.ts ================================================ /** 获取登录页面模块的动态路由的正则 */ export function getLoginModuleRegExp() { const modules: EnumType.LoginModuleKey[] = ['forgot', 'verify-email', 'sign-up', 'reset', 'sign-in']; return modules.join('|'); } ================================================ FILE: src/utils/router/transform.ts ================================================ import type {RouteRecordRaw} from 'vue-router'; import {getLayoutComponent, getViewComponent} from '@/utils'; import i18n from "@/plugins/vue-i18n"; export function transformAuthRouteToVueRoutes(routes: AuthRoute.Route[]) { return routes.map(route => transformAuthRouteToVueRoute(route)).flat(1); } type ComponentAction = Record void>; export function transformAuthRouteToVueRoute(item: AuthRoute.Route) { const resultRoute: RouteRecordRaw[] = []; const itemRoute = {...item} as RouteRecordRaw; if (hasDynamicPath(item)) { Object.assign(itemRoute, {path: item.meta.dynamicPath}); } if (hasHref(item)) { Object.assign(itemRoute, {component: getViewComponent('404')}); } if (hasComponent(item)) { const action: ComponentAction = { basic() { itemRoute.component = getLayoutComponent('basic'); }, blank() { itemRoute.component = getLayoutComponent('blank'); }, auth() { itemRoute.component = getLayoutComponent('auth'); }, error() { itemRoute.component = getLayoutComponent('error'); }, todo() { itemRoute.component = getLayoutComponent('todo'); }, chat() { itemRoute.component = getLayoutComponent('chat'); }, self() { itemRoute.component = getViewComponent(item.name as AuthRoute.LastDegreeRouteKey); } }; try { if (item.component) { action[item.component](); } else { window.console.error('router resolve failed: ', item); } } catch { window.console.error('router resolve failed: ', item); } } if (isSingleRoute(item)) { if (hasChildren(item)) { window.console.error('singleRoute should not has child: ', item); } if (item.name === 'not-found') { itemRoute.children = [ { path: '', name: item.name, component: getViewComponent('not-found') } ]; } else { //prepare parent layout const parentPath = `${itemRoute.path}-parent` as AuthRouteUtils.SingleRouteKey; let layout switch (item.meta.singleLayout) { case 'blank': layout = getLayoutComponent('blank') break case 'auth': layout = getLayoutComponent('auth') break case 'error': layout = getLayoutComponent('error') break case 'basic': layout = getLayoutComponent('basic') break case 'todo': layout = getLayoutComponent('todo') break case 'chat': layout = getLayoutComponent('chat') break default: layout = getLayoutComponent('blank') } const parentRoute: RouteRecordRaw = { path: parentPath, component: layout, redirect: item.path, children: [itemRoute] }; return [parentRoute]; } } if (hasChildren(item)) { const children = (item.children as AuthRoute.Route[]).map(child => transformAuthRouteToVueRoute(child)).flat(); const redirectPath = (children.find(v => !v.meta?.multi)?.path || '/') as AuthRoute.RoutePath; if (redirectPath === '/') { window.console.error('could not found effective child in multiple router ', item); } itemRoute.children = children; itemRoute.redirect = redirectPath; } resultRoute.push(itemRoute); return resultRoute; } export function transformAuthRouteToSearchMenus(routes: AuthRoute.Route[], treeMap: AuthRoute.Route[] = [], parentTitle: string = '') { if (routes && routes.length === 0) return []; return routes.reduce((acc, cur) => { const parent = parentTitle.length > 0 ? parentTitle + "/" : parentTitle const title = parent + i18n.global.t(cur.meta.title) if (!cur.meta?.hide && (!cur.children || cur.children?.length == 0)) { acc.push({ ...cur, meta: { ...cur, title, } }); } if (cur.children && cur.children.length > 0) { transformAuthRouteToSearchMenus(cur.children, treeMap, title); } return acc; }, treeMap); } export function transformRouteNameToRoutePath(name: Exclude): AuthRoute.RoutePath { const rootPath: AuthRoute.RoutePath = '/'; if (name === 'root') return rootPath; const splitMark = '_'; const pathSplitMark = '/'; const path = name.split(splitMark).join(pathSplitMark); return (pathSplitMark + path) as AuthRoute.RoutePath; } export function transformRoutePathToRouteName(path: K) { if (path === '/') return 'root'; const pathSplitMark = '/'; const routeSplitMark = '_'; const name = path.split(pathSplitMark).slice(1).join(routeSplitMark) as AuthRoute.AllRouteKey; return name; } function hasHref(item: AuthRoute.Route) { return Boolean(item.meta.href); } function hasDynamicPath(item: AuthRoute.Route) { return Boolean(item.meta.dynamicPath); } function hasComponent(item: AuthRoute.Route) { return Boolean(item.component); } function hasChildren(item: AuthRoute.Route) { return Boolean(item.children && item.children.length); } function isSingleRoute(item: AuthRoute.Route) { return Boolean(item.meta.singleLayout); } ================================================ FILE: src/utils/service/error.ts ================================================ import type { AxiosError, AxiosResponse } from 'axios'; import { DEFAULT_REQUEST_ERROR_CODE, DEFAULT_REQUEST_ERROR_MSG, ERROR_STATUS, NETWORK_ERROR_CODE, NETWORK_ERROR_MSG, REQUEST_TIMEOUT_CODE, REQUEST_TIMEOUT_MSG } from '@/configs/service'; import { exeStrategyActions } from '../common'; import { showErrorMsg } from './msg'; type ErrorStatus = keyof typeof ERROR_STATUS; /** * 处理axios请求失败的错误 * @param axiosError - 错误 */ export function handleAxiosError(axiosError: AxiosError) { const error: Service.RequestError = { type: 'axios', code: DEFAULT_REQUEST_ERROR_CODE, msg: DEFAULT_REQUEST_ERROR_MSG }; const actions: Common.StrategyAction[] = [ [ // 网路错误 !window.navigator.onLine || axiosError.message === 'Network Error', () => { Object.assign(error, { code: NETWORK_ERROR_CODE, msg: NETWORK_ERROR_MSG }); } ], [ // 超时错误 axiosError.code === REQUEST_TIMEOUT_CODE && axiosError.message.includes('timeout'), () => { Object.assign(error, { code: REQUEST_TIMEOUT_CODE, msg: REQUEST_TIMEOUT_MSG }); } ], [ // 请求不成功的错误 Boolean(axiosError.response), () => { const errorCode: ErrorStatus = (axiosError.response?.status as ErrorStatus) || 'DEFAULT'; const msg = ERROR_STATUS[errorCode]; Object.assign(error, { code: errorCode, msg }); } ] ]; exeStrategyActions(actions); showErrorMsg(error); return error; } /** * 处理请求成功后的错误 * @param response - 请求的响应 */ export function handleResponseError(response: AxiosResponse) { const error: Service.RequestError = { type: 'axios', code: DEFAULT_REQUEST_ERROR_CODE, msg: DEFAULT_REQUEST_ERROR_MSG }; if (!window.navigator.onLine) { // 网路错误 Object.assign(error, { code: NETWORK_ERROR_CODE, msg: NETWORK_ERROR_MSG }); } else { // 请求成功的状态码非200的错误 const errorCode: ErrorStatus = response.status as ErrorStatus; const msg = ERROR_STATUS[errorCode] || DEFAULT_REQUEST_ERROR_MSG; Object.assign(error, { type: 'http', code: errorCode, msg }); } showErrorMsg(error); return error; } /** * 处理后端返回的错误(业务错误) * @param backendResult - 后端接口的响应数据 */ export function handleBackendError(backendResult: Record, config: Service.BackendResultConfig) { const { codeKey, msgKey } = config; const error: Service.RequestError = { type: 'backend', code: backendResult[codeKey], msg: backendResult[msgKey] }; showErrorMsg(error); return error; } ================================================ FILE: src/utils/service/handler.ts ================================================ /** 统一失败和成功的请求结果的数据类型 */ export async function handleServiceResult(error: Service.RequestError | null, data: any) { if (error) { const fail: Service.FailedResult = { error, data: null }; return fail; } const success: Service.SuccessResult = { error: null, data }; return success; } /** 请求结果的适配器:用于接收适配器函数和请求结果 */ export function adapter( adapterFun: T, ...args: Service.MultiRequestResult> ): Service.RequestResult> { let result: Service.RequestResult | undefined; const hasError = args.some(item => { const flag = Boolean(item.error); if (flag) { result = { error: item.error, data: null }; } return flag; }); if (!hasError) { const adapterFunArgs = args.map(item => item.data); result = { error: null, data: adapterFun(...adapterFunArgs) }; } return result!; } ================================================ FILE: src/utils/service/index.ts ================================================ export * from './transform'; export * from './error'; export * from './handler'; ================================================ FILE: src/utils/service/msg.ts ================================================ import { ERROR_MSG_DURATION, NO_ERROR_MSG_CODE } from '@/configs/service'; /** 错误消息栈,防止同一错误同时出现 */ const errorMsgStack = new Map([]); function addErrorMsg(error: Service.RequestError) { errorMsgStack.set(error.code, error.msg); } function removeErrorMsg(error: Service.RequestError) { errorMsgStack.delete(error.code); } function hasErrorMsg(error: Service.RequestError) { return errorMsgStack.has(error.code); } /** * 显示错误信息 * @param error */ export function showErrorMsg(error: Service.RequestError) { if (!error.msg || NO_ERROR_MSG_CODE.includes(error.code) || hasErrorMsg(error)) return; addErrorMsg(error); window.console.warn(error.code, error.msg); window.$snackBar?.error(error.msg, {timeout: ERROR_MSG_DURATION }); setTimeout(() => { removeErrorMsg(error); }, ERROR_MSG_DURATION); } ================================================ FILE: src/utils/service/transform.ts ================================================ import qs from 'qs'; import FormData from 'form-data'; import { EnumContentType } from '@/enum'; import { isArray, isFile } from '../common'; /** * 请求数据的转换 * @param requestData - 请求数据 * @param contentType - 请求头的Content-Type */ export async function transformRequestData(requestData: any, contentType?: string) { // application/json类型不处理 let data = requestData; // form类型转换 if (contentType === EnumContentType.formUrlencoded) { data = qs.stringify(requestData); } // form-data类型转换 if (contentType === EnumContentType.formData) { data = await handleFormData(requestData); } return data; } async function handleFormData(data: Record) { const formData = new FormData(); const entries = Object.entries(data); entries.forEach(async ([key, value]) => { const isFileType = isFile(value) || (isArray(value) && value.length && isFile(value[0])); if (isFileType) { await transformFile(formData, key, value); } else { formData.append(key, value); } }); return formData; } /** * 接口为上传文件的类型时数据转换 * @param key - 文件的属性名 * @param file - 单文件或多文件 */ async function transformFile(formData: FormData, key: string, file: File[] | File) { if (isArray(file)) { // 多文件 await Promise.all( (file as File[]).map(item => { formData.append(key, item); return true; }) ); } else { // 单文件 formData.append(key, file); } } ================================================ FILE: src/utils/storage/index.ts ================================================ export * from './local'; export * from './session'; ================================================ FILE: src/utils/storage/local.ts ================================================ import { decrypto, encrypto } from '../crypto'; interface StorageData { value: T; expire: number | null; } function createLocalStorage() { const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7; function set(key: K, value: T[K], expire: number | null = DEFAULT_CACHE_TIME) { const storageData: StorageData = { value, expire: expire !== null ? new Date().getTime() + expire * 1000 : null }; const json = encrypto(storageData); window.localStorage.setItem(key as string, json); } function get(key: K) { const json = window.localStorage.getItem(key as string); if (json) { let storageData: StorageData | null = null; try { storageData = decrypto(json); } catch { // 防止解析失败 } if (storageData) { const { value, expire } = storageData; // 在有效期内直接返回 if (expire === null || expire >= Date.now()) { return value as T[K]; } } remove(key); return null; } return null; } function remove(key: keyof T) { window.localStorage.removeItem(key as string); } function clear() { window.localStorage.clear(); } return { set, get, remove, clear }; } export const localStg = createLocalStorage(); ================================================ FILE: src/utils/storage/session.ts ================================================ import { decrypto, encrypto } from '../crypto'; export function setSession(key: string, value: unknown) { const json = encrypto(value); sessionStorage.setItem(key, json); } export function getSession(key: string) { const json = sessionStorage.getItem(key); let data: T | null = null; if (json) { try { data = decrypto(json); } catch { // 防止解析失败 } } return data; } export function removeSession(key: string) { window.sessionStorage.removeItem(key); } export function clearSession() { window.sessionStorage.clear(); } function createSessionStorage() { function set(key: K, value: T[K]) { const json = encrypto(value); sessionStorage.setItem(key as string, json); } function get(key: K) { const json = sessionStorage.getItem(key as string); let data: T[K] | null = null; if (json) { try { data = decrypto(json); } catch { // 防止解析失败 } } return data; } function remove(key: keyof T) { window.sessionStorage.removeItem(key as string); } function clear() { window.sessionStorage.clear(); } return { set, get, remove, clear }; } export const sessionStg = createSessionStorage(); ================================================ FILE: src/utils/vue/index.ts ================================================ import { createTextVNode, VNodeChild } from 'vue' export const render = ( r: | string | number | undefined | null | ((...args: [...T]) => VNodeChild) | unknown, ...args: [...T] ): VNodeChild => { if (typeof r === 'function') { return r(...args) } else if (typeof r === 'string') { return createTextVNode(r) } else if (typeof r === 'number') { return createTextVNode(String(r)) } else { return null } } ================================================ FILE: src/views/_builtin/auth/components/ForgotPage.vue ================================================ ================================================ FILE: src/views/_builtin/auth/components/ResetPage.vue ================================================ ================================================ FILE: src/views/_builtin/auth/components/SigninPage.vue ================================================ ================================================ FILE: src/views/_builtin/auth/components/SignupPage.vue ================================================ ================================================ FILE: src/views/_builtin/auth/components/VerifyEmailPage.vue ================================================ ================================================ FILE: src/views/_builtin/auth/components/index.ts ================================================ import ForgotPg from './ForgotPage.vue'; import SigninPg from './SigninPage.vue'; import SignUpPg from './SignupPage.vue'; import ResetPg from './ResetPage.vue'; import VerifyEmailPg from './VerifyEmailPage.vue'; export {ForgotPg,SignUpPg,SigninPg,ResetPg,VerifyEmailPg}; ================================================ FILE: src/views/_builtin/auth/index.vue ================================================ ================================================ FILE: src/views/_builtin/error/NotFoundPage.vue ================================================ ================================================ FILE: src/views/_builtin/error/UnexpectedPage.vue ================================================ ================================================ FILE: src/views/board/components/BoardCard.vue ================================================ ================================================ FILE: src/views/board/pages/BoardPage.vue ================================================ ================================================ FILE: src/views/board/types.ts ================================================ export type state = 'TODO' | 'INPROGRESS' | 'TESTING' | 'DONE' export interface card { id: string | number, title?: string, description: string, order: number, state: state, } ================================================ FILE: src/views/chart/ChannelMessage.vue ================================================ ================================================ FILE: src/views/chart/ChatChannel.vue ================================================ ================================================ FILE: src/views/chart/ChatPage.vue ================================================ ================================================ FILE: src/views/dashboard/index.vue ================================================ ================================================ FILE: src/views/flowable/bo-utils/conditionalUtil.ts ================================================ // helper //////////////////// import {getBusinessObject, is, isAny} from "bpmn-js/lib/util/ModelUtil"; import {find} from "lodash-es"; const CONDITIONAL_SOURCES = [ 'bpmn:Activity', 'bpmn:ExclusiveGateway', 'bpmn:InclusiveGateway', 'bpmn:ComplexGateway' ]; export function isNotConditional(element: any) { return ( !(is(element, 'bpmn:SequenceFlow') && isConditionalSource(element.source)) && !getConditionalEventDefinition(element) ) } function isConditionalSource(element: any) { return isAny(element, CONDITIONAL_SOURCES); } function getConditionalEventDefinition(element: any) { if (!is(element, 'bpmn:Event')) { return false; } return getEventDefinition(element, 'bpmn:ConditionalEventDefinition'); } function getEventDefinition(element: any, eventType: string) { const businessObject = getBusinessObject(element); const eventDefinitions = businessObject.get('eventDefinitions') || []; return find(eventDefinitions, function (definition) { return is(definition, eventType); }); } ================================================ FILE: src/views/flowable/bo-utils/documentationUtil.ts ================================================ import {Base} from 'diagram-js/lib/model' import {useModelStore} from '@/store' import {without} from 'min-dash' export function getDocumentValue(element: Base): string { const businessObject = element?.businessObject const documentation = businessObject && findDocumentation(businessObject.get('documentation')) return documentation && documentation.text } export function setDocumentValue(element: Base, value: string | undefined) { const store = useModelStore() const modeling = store.getModeling const bpmnFactory = store.getModeler?.get('bpmnFactory') const businessObject = element.businessObject const documentation = findDocumentation(businessObject && businessObject.get('documentation')) // (1) 更新或者移除 原有 documentation if (documentation) { if (value) { return modeling.updateModdleProperties(element, documentation, {text: value}) } else { return modeling.updateModdleProperties(element, businessObject, { documentation: without(businessObject.get('documentation'), documentation) }) } } // (2) 创建新的 documentation if (value) { const newDocumentation = bpmnFactory?.create('bpmn:Documentation', { text: value }) return modeling.updateModdleProperties(element, businessObject, { documentation: [...businessObject.get('documentation'), newDocumentation] }) } } //////////// helpers const DOCUMENTATION_TEXT_FORMAT = 'text/plain' function findDocumentation(docs: any) { return docs.find(function (d: any) { return (d.textFormat || DOCUMENTATION_TEXT_FORMAT) === DOCUMENTATION_TEXT_FORMAT }) } ================================================ FILE: src/views/flowable/bo-utils/idUtil.ts ================================================ import {Base} from 'diagram-js/lib/model' import {useModelStore} from '@/store' import {isIdValid} from "@/views/flowable/utils/BpmnValidator"; export function getIdValue(element: Base): string { return element.businessObject.id } export function setIdValue(element: Base, value: string) { const errorMsg = isIdValid(element.businessObject, value) if (errorMsg && errorMsg.length) { return window.$snackBar?.warning(errorMsg) } const store = useModelStore() const modeling = store.getModeling modeling.updateProperties(element, { id: value }) } ================================================ FILE: src/views/flowable/bo-utils/nameUtil.ts ================================================ import {useModelStore} from '@/store' import {Base} from 'diagram-js/lib/model' import {getBusinessObject, is} from 'bpmn-js/lib/util/ModelUtil' import {isAny} from 'bpmn-js/lib/features/modeling/util/ModelingUtil' import {add as collectionAdd} from 'diagram-js/lib/util/Collections' // 根据元素获取 name 属性 export function getNameValue(element: Base): string | undefined { if (isAny(element, ['bpmn:Collaboration', 'bpmn:DataAssociation', 'bpmn:Association'])) { return undefined } if (is(element, 'bpmn:TextAnnotation')) { return element.businessObject.text } if (is(element, 'bpmn:Group')) { const businessObject = getBusinessObject(element), categoryValueRef = businessObject?.categoryValueRef return categoryValueRef?.value } return element?.businessObject.name } // 根据输入内容设置 name 属性 export function setNameValue(element: Base, value: string): void { const store = useModelStore() const modeling = store.getModeling const canvas = store.getCanvas const bpmnFactory = store.getModeler?.get('bpmnFactory') if (isAny(element, ['bpmn:Collaboration', 'bpmn:DataAssociation', 'bpmn:Association'])) { return undefined } if (is(element, 'bpmn:TextAnnotation')) { return modeling?.updateProperties(element, {text: value}) } if (is(element, 'bpmn:Group')) { const businessObject = getBusinessObject(element), categoryValueRef = businessObject.categoryValueRef if (!categoryValueRef) { initializeCategory(businessObject, canvas?.getRootElement(), bpmnFactory) } return modeling?.updateLabel(element, value) } modeling?.updateProperties(element, {name: value}) } //////////////// helpers function createCategoryValue(definitions: any, bpmnFactory: any): any { const categoryValue = bpmnFactory.create('bpmn:CategoryValue') const category = bpmnFactory.create('bpmn:Category', { categoryValue: [categoryValue] }) collectionAdd(definitions.get('rootElements'), category) getBusinessObject(category).$parent = definitions getBusinessObject(categoryValue).$parent = category return categoryValue } function initializeCategory(businessObject: any, rootElement: any, bpmnFactory: any) { const definitions = getBusinessObject(rootElement).$parent businessObject.categoryValueRef = createCategoryValue(definitions, bpmnFactory) } ================================================ FILE: src/views/flowable/bo-utils/processUtil.ts ================================================ import {Base} from 'diagram-js/lib/model' import {useModelStore} from '@/store' const prefix = "camunda" export function getProcessExecutable(element: Base): boolean { return element.businessObject.isExecutable } export function setProcessExecutable(element: Base, value: boolean) { const store = useModelStore() const modeling = store.getModeling modeling.updateProperties(element, { isExecutable: value }) } export function getProcessVersionTag(element: Base): string | undefined { return element.businessObject.get(`${prefix}:versionTag`) } export function setProcessVersionTag(element: Base, value: string) { const modeling = useModelStore().getModeling modeling.updateProperties(element, { [`${prefix}:versionTag`]: value }) } ================================================ FILE: src/views/flowable/bo-utils/userTaskUtil.ts ================================================ import {is} from "bpmn-js/lib/util/ModelUtil"; export function isUserService(element: any): boolean { return is(element, 'bpmn:UserTask') } export function isStartEvent(element: any): boolean { return is(element, 'bpmn:StartEvent') } ================================================ FILE: src/views/flowable/design/customTranslate.ts ================================================ import translations from './translations'; export default function customTranslate(template: any, replacements: any) { replacements = replacements || {}; // Translate template = translations[template] || template; // Replace return template.replace(/{([^}]+)}/g, function (_: any, key: any) { return replacements[key] || '{' + key + '}'; }); } ================================================ FILE: src/views/flowable/design/demo3.bpmn ================================================ Flow_0kydfqm Flow_0kydfqm Flow_0krvqew Flow_162i7sq Flow_0krvqew Flow_1bhkt0w Flow_162i7sq Flow_13lqqfh Flow_13lqqfh Flow_1bhkt0w ================================================ FILE: src/views/flowable/design/design.tsx ================================================ import {defineComponent, ref, toRefs, nextTick} from 'vue' import type {PropType} from 'vue' import {useModelStore} from '@/store' import customTranslate from "./customTranslate"; import initModeler from './initModeler' import camundaModdleDescriptors from 'camunda-bpmn-moddle/resources/camunda'; const Designer = defineComponent({ name: 'Designer', props: { xml: { type: String as PropType, default: undefined } }, emits: ['update:xml', 'command-stack-changed'], setup(props, {emit, slots}) { const {xml} = toRefs(props) const designer = ref(null) const {current} = useTheme() onMounted(async () => { try { await nextTick() const editorSettings = { container: designer.value, bpmnRenderer: { defaultFillColor: current.value.colors.primary, defaultLabelColor: current.value.colors.surface }, keyboard: { bindTo: document, }, additionalModules: [ { translate: ['value', customTranslate] }, ], moddleExtensions: { camunda: camundaModdleDescriptors } } initModeler(editorSettings, emit) await createNewDiagram(xml.value, editorSettings) } catch (e) { console.log(e) } }) return () =>

{slots.right?.()}
} }) export default Designer const createNewDiagram = async function (newXml?: string, settings?: any) { try { const store = useModelStore() const timestamp = Date.now() const {processId, processName, processEngine} = settings || {} const newId: string = processId ? processId : `Process_${timestamp}` const newName: string = processName || `Process_${timestamp}` const xmlString = newXml || emptyXML(newId, newName, processEngine) const modeler = store.getModeler const {warnings} = await modeler!.importXML(xmlString) if (warnings && warnings.length) { warnings.forEach((warn: any) => console.warn(warn)) } } catch (e) { console.error(`[Process Designer Warn]: ${typeof e === 'string' ? e : (e as Error)?.message}`) } } const emptyXML = (key: string, name: string, type?: string): string => { return ` ` } ================================================ FILE: src/views/flowable/design/index.vue ================================================ ================================================ FILE: src/views/flowable/design/initModeler.ts ================================================ import {markRaw} from 'vue' import {useModelStore} from '@/store' import BpmnModeler from "bpmn-js/lib/Modeler"; import EventEmitter from "@/utils/flow/EventEmitter"; export default function ( options: any, emit: (event: any, ...args: any[]) => void ) { const store = useModelStore() store.getModeler && store.getModeler.destroy() store.setModeler(null) const modeler = new BpmnModeler(options) store.setModeler(markRaw(modeler)) store.setModules('moddle', markRaw(modeler.get('moddle'))) store.setModules('modeling', markRaw(modeler.get('modeling'))) store.setModules('canvas', markRaw(modeler.get('canvas'))) store.setModules('commandStack', markRaw(modeler.get('commandStack'))) store.setModules('elementRegistry', markRaw(modeler.get('elementRegistry'))) EventEmitter.emit('modeler-init', modeler) modeler.on('commandStack.changed', async (event: any) => { try { const {xml} = await modeler.saveXML({format: true}) emit('update:xml', xml) emit('command-stack-changed', event) } catch (error) { console.error(error) } }) } ================================================ FILE: src/views/flowable/design/propertiesPanel/components/actions.vue ================================================ ================================================ FILE: src/views/flowable/design/propertiesPanel/components/condition.vue ================================================ ================================================ FILE: src/views/flowable/design/propertiesPanel/components/documentation.vue ================================================ ================================================ FILE: src/views/flowable/design/propertiesPanel/components/form.vue ================================================ ================================================ FILE: src/views/flowable/design/propertiesPanel/components/general.vue ================================================ ================================================ FILE: src/views/flowable/design/propertiesPanel/components/userAssigne.vue ================================================ ================================================ FILE: src/views/flowable/design/propertiesPanel/index.vue ================================================ ================================================ FILE: src/views/flowable/design/provider/index.js ================================================ import SelfProvider from './selfProvider'; export default { __init__: [ 'magicPropertiesProvider' ], magicPropertiesProvider: [ 'type', SelfProvider ] }; ================================================ FILE: src/views/flowable/design/provider/parts/FormProps.js ================================================ import {SelectEntry, isSelectEntryEdited} from '@bpmn-io/properties-panel'; import {useService} from 'bpmn-js-properties-panel' import {getBusinessObject} from 'bpmn-js/lib/util/ModelUtil'; export function FormProps(props) { return [ { id: "form", component: Form, isEdited: isSelectEntryEdited, } ] } function Form(props) { const {element} = props; const translate = useService('translate'); const bpmnFactory = useService('bpmnFactory'); const businessObject = getBusinessObject(element); const commandStack = useService('commandStack'); let extensionElements = businessObject.get('extensionElements'); const getOptions = () => { return [ {value: '', label: translate('')}, {value: 'formRef', label: translate('Camunda Forms')}, {value: 'formKey', label: translate('Embedded or External Task Forms')}, {value: 'formData', label: translate('Generated Task Forms')}, {value: 'fuck', label: translate('Generated Task Forms')} ]; } const setValue = (value) => { console.log(value) } const getValue = () => { return 'fuck' } return SelectEntry({ element, id: 'form', label: translate('FormRef'), getOptions, setValue, getValue }) } ================================================ FILE: src/views/flowable/design/provider/parts/SpellProps.js ================================================ import {SelectEntry, isSelectEntryEdited} from '@bpmn-io/properties-panel'; import {useService} from 'bpmn-js-properties-panel' // import hooks from the vendored preact package import {useEffect, useState} from '@bpmn-io/properties-panel/preact/hooks'; export default function (element) { return [ { id: 'lulu-formType', element, component: FormType, isEdited: isSelectEntryEdited } ]; } function FormType(props) { const {element, id} = props; const modeling = useService('modeling'); const translate = useService('translate'); const debounce = useService('debounceInput'); const getValue = () => { return element.businessObject.spell || ''; } const setValue = value => { return modeling.updateProperties(element, { spell: value }); } const [spells, setSpells] = useState([]); useEffect(() => { function fetchSpells() { fetch('http://localhost:1234/spell') .then(res => res.json()) .then(spellbook => setSpells(spellbook)) .catch(error => console.error(error)); } fetchSpells(); }, [setSpells]); const getOptions = () => { return [ {label: '', value: undefined}, ...spells.map(spell => ({ label: spell, value: spell })) ]; } return SelectEntry( { id, element, description: translate('Apply a black magic spell'), label: translate('FormType'), getValue, setValue, getOptions, debounce } ) } ================================================ FILE: src/views/flowable/design/provider/selfProvider.js ================================================ import spellProps from './parts/SpellProps'; import {is} from 'bpmn-js/lib/util/ModelUtil'; import {FormProps} from "@/views/flowable/design/provider/parts/FormProps"; const LOW_PRIORITY = 500; export default function SelfProvider(propertiesPanel, translate) { this.getGroups = function (element) { return function (groups) { deleteById(groups, "CamundaPlatform__FormData") const form = findGroup(groups, 'CamundaPlatform__Form') if (form) { form.entries = [ ...FormProps({element}) ] } return groups; } }; propertiesPanel.registerProvider(LOW_PRIORITY, this); } SelfProvider.$inject = ['translate']; // Create the custom magic group function createMagicGroup(element, translate) { // create a group called "Magic components". const magicGroup = { id: 'magic', label: translate('Magic components'), entries: spellProps(element) }; return magicGroup } function findGroup(groups, id) { return groups.find(g => g.id === id); } function findEntry(entries, id) { return entries.find(g => g.id === id); } function deleteById(array, id) { const index = array.findIndex(g => g.id === id) if (index > -1) { array.splice(index, 1) } } ================================================ FILE: src/views/flowable/design/translations.ts ================================================ const translations: Record = { "Activate the create/remove space tool": "启动创建/删除空间工具", "Activate the global connect tool": "启动全局连接工具", "Activate the hand tool": "启动手动工具", "Activate the lasso tool": "启动 Lasso 工具", "Ad-hoc": "Ad-hoc子流程", "Add Lane above": "添加到通道之上", "Add Lane below": "添加到通道之下", "Append ConditionIntermediateCatchEvent": "添加中间条件捕获事件", "Append element": "添加元素", "Append EndEvent": "添加结束事件", "Append Gateway": "添加网关", "Append Intermediate/Boundary Event": "添加中间/边界事件", "Append MessageIntermediateCatchEvent": "添加消息中间捕获事件", "Append ReceiveTask": "添加接收任务", "Append SignalIntermediateCatchEvent": "添加信号中间捕获事件", "Append Task": "添加任务", "Append TimerIntermediateCatchEvent": "添加定时器中间捕获事件", "Append compensation activity": "追加补偿活动", "Append {type}": "追加 {type}", "Boundary Event": "边界事件", "Business Rule Task": "规则任务", "Call Activity": "引用流程", "Cancel Boundary Event": "取消边界事件", "Cancel End Event": "取消结束事件", "Change type": "更改类型", "Collapsed Pool": "折叠池", "Collection": "集合", "Compensation Boundary Event": "补偿边界事件", "Compensation End Event": "结束补偿事件", "Compensation Intermediate Throw Event": "中间补偿抛出事件", "Compensation Start Event": "补偿启动事件", "Complex Gateway": "复杂网关", "Conditional Boundary Event": "条件边界事件", "Conditional Boundary Event (non-interrupting)": "条件边界事件 (非中断)", "Conditional Flow": "条件流", "Conditional Intermediate Catch Event": "中间条件捕获事件", "Conditional Start Event": "条件启动事件", "Conditional Start Event (non-interrupting)": "条件启动事件 (非中断)", "Connect using Association": "文本关联", "Connect using DataInputAssociation": "数据关联", "Connect using Sequence/MessageFlow or Association": "消息关联", "Create IntermediateThrowEvent/BoundaryEvent": "创建中间抛出/边界事件", "Create DataObjectReference": "创建数据对象引用", "Create DataStoreReference": "创建数据存储引用", "Create element": "创建元素", "Create EndEvent": "创建结束事件", "Create Gateway": "创建网关", "Create Group": "创建组", "Create Intermediate/Boundary Event": "创建中间/边界事件", "Create Pool/Participant": "创建池/参与者", "Create StartEvent": "创建开始事件", "Create Task": "创建任务", "Create expanded SubProcess": "创建可折叠子流程", "Create {type}": "创建 {type}", "Data": "数据", "Data Object Reference": "数据对象引用", "Data Store Reference": "数据存储引用", "Default Flow": "默认流", "Divide into three Lanes": "分成三条通道", "Divide into two Lanes": "分成两条通道", "Empty Pool": "空泳道", "Empty Pool (removes content)": "清空泳道(删除内容)", "End Event": "结束事件", "Error Boundary Event": "错误边界事件", "Error End Event": "结束错误事件", "Error Start Event": "错误启动事件", "Escalation Boundary Event": "升级边界事件", "Escalation Boundary Event (non-interrupting)": "升级边界事件 (非中断)", "Escalation End Event": "结束升级事件", "Escalation Intermediate Throw Event": "中间升级抛出事件", "Escalation Start Event": "升级启动事件", "Escalation Start Event (non-interrupting)": "升级启动事件 (非中断)", "Events": "事件", "Event Sub Process": "事件子流程", "Event based Gateway": "事件网关", "Exclusive Gateway": "独占网关", "Expanded Pool": "展开泳道", "Gateways": "网关", "Inclusive Gateway": "包容网关", "Intermediate Throw Event": "中间抛出事件", "Link Intermediate Catch Event": "中间链接捕获事件", "Link Intermediate Throw Event": "中间链接抛出事件", "Loop": "循环", "Manual Task": "手动任务", "Message Boundary Event": "消息边界事件", "Message Boundary Event (non-interrupting)": "消息边界事件 (非中断)", "Message End Event": "结束消息事件", "Message Intermediate Catch Event": "中间消息捕获事件", "Message Intermediate Throw Event": "中间消息抛出事件", "Message Start Event": "消息启动事件", "Message Start Event (non-interrupting)": "消息启动事件 (非中断)", "Parallel Gateway": "并行网关", "Parallel Multi Instance": "并行多实例", "Participants": "参与者", "Participant Multiplicity": "参与者多重性", "Receive Task": "接受任务", "Remove": "移除", "Script Task": "脚本任务", "Send Task": "发送任务", "Sequence Flow": "顺序流", "Sequential Multi Instance": "串行多实例", "Service Task": "服务任务", "Signal Boundary Event": "信号边界事件", "Signal Boundary Event (non-interrupting)": "信号边界事件 (非中断)", "Signal End Event": "结束信号事件", "Signal Intermediate Catch Event": "中间信号捕获事件", "Signal Intermediate Throw Event": "中间信号抛出事件", "Signal Start Event": "信号启动事件", "Signal Start Event (non-interrupting)": "信号启动事件 (非中断)", "Start Event": "开始事件", "Sub Process": "子流程", "Sub Processes": "子流程", "Sub Process (collapsed)": "可折叠子流程", "Sub Process (expanded)": "可展开子流程", "Task": "任务", "Tasks": "任务", "Terminate End Event": "终止边界事件", "Timer Boundary Event": "定时边界事件", "Timer Boundary Event (non-interrupting)": "定时边界事件 (非中断)", "Timer Intermediate Catch Event": "中间定时捕获事件", "Timer Start Event": "定时启动事件", "Timer Start Event (non-interrupting)": "定时启动事件 (非中断)", "Transaction": "事务", "User Task": "用户任务", "already rendered {element}": "{element} 已呈现", "diagram not part of bpmn:Definitions": "图表不是 bpmn:Definitions 的一部分", "element required": "需要元素", "correcting missing bpmnElement on {plane} to {rootElement}": "在 {plane} 上更正缺失的 bpmnElement 为 {rootElement}", "element {element} referenced by {referenced}#{property} not yet drawn": "元素 {element} 的引用 {referenced}#{property} 尚未绘制", "failed to import {element}": "{element} 导入失败", "flow elements must be children of pools/participants": "元素必须是池/参与者的子级", "more than {count} child lanes": "超过 {count} 条通道", "missing {semantic}#attachedToRef": "在 {element} 中缺少 {semantic}#attachedToRef", "multiple DI elements defined for {element}": "为 {element} 定义了多个 DI 元素", "no bpmnElement referenced in {element}": "{element} 中没有引用 bpmnElement", "no diagram to display": "没有要显示的图表", "no shape type specified": "未指定形状类型", "no parent for {element} in {parent}": "在 {element} 中没有父元素 {parent}", "no process or collaboration to display": "没有可显示的流程或协作", "out of bounds release": "越界释放", "General": "通用" }; export default translations ================================================ FILE: src/views/flowable/index.vue ================================================ ================================================ FILE: src/views/flowable/utils/BpmnValidator.ts ================================================ const SPACE_REGEX = /\s/ // for QName validation as per http://www.w3.org/TR/REC-xml/#NT-NameChar // | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] const QNAME_REGEX = /^([a-z][\w-.]*:)?[a-z_][\w-.]*$/i // for ID validation as per BPMN Schema (QName - Namespace) const ID_REGEX = /^[a-z_][\w-.]*$/i export function containsSpace(value: string) { return SPACE_REGEX.test(value) } /** * checks whether the id value is valid * * @param {ModdleElement} element * @param {String} idValue * @param {Function} translate * * @return {String} error message */ export function isIdValid(element: any, idValue: any) { const assigned = element.$model.ids.assigned(idValue) const idAlreadyExists = assigned && assigned !== element if (!idValue) { return 'ID required.' } if (idAlreadyExists) { return 'ID must unique' } return validateId(idValue) } export function validateId(idValue: any) { if (containsSpace(idValue)) { return 'ID can not contains space' } if (!ID_REGEX.test(idValue)) { if (QNAME_REGEX.test(idValue)) { return 'ID 不能包含前缀' } return 'ID 必须符合 BPMN 规范' } } ================================================ FILE: src/views/form/design/components/formitem.tsx ================================================ import {defineComponent, toRefs} from 'vue' import { VTextField, VChip, VBtn, VCheckbox, VTextarea, VSwitch, VSelect, VFileInput, VRadioGroup, VRadio, VCol } from 'vuetify/components' import {PropType} from "vue"; import VueDraggable from 'vuedraggable' import Formitem from "@/views/form/design/components/formitem"; import {VForm} from "vuetify/components/VForm"; import {cloneDeep, uniqueId} from "lodash-es"; import {VRow} from "vuetify/components/VGrid"; export default defineComponent({ props: { item: { required: true, type: Object as PropType, default: () => { } }, active: { required: false, type: Object as PropType, default: () => { } }, layers: { required: false, type: Number, default: 0 }, list: { required: false, type: Array as PropType>, default: () => [] }, idPrefix: { required: false, type: String, default: 'formComponent' }, preview: { required: false, type: Boolean, default: false, } }, emits: { 'delete': (e: formComponent) => { return e }, 'selected': (e: formComponent) => { return e }, 'duplicate': (e: formComponent) => { return e }, }, setup(props, {emit}) { const {active, item} = toRefs(props) const zIndexStyle = 'z-index: ' + props.layers * 10 const cloneComponent = (c: formComponent) => { const clone = cloneDeep(c) clone.id = props.idPrefix + '_' + uniqueId() recursiveIdGenerator(clone.config.formChildren) return clone } const recursiveIdGenerator = (list: formComponent[]) => { if (!list) return list.forEach(c => { c.id = props.idPrefix + '_' + uniqueId() if (c.config.formChildren) { recursiveIdGenerator(c.config.formChildren) } }) } const deleteComponent = (list: formComponent[], c: formComponent) => { const index = list.findIndex(i => i.id == c.id) if (index > -1) { list.splice(index, 1) } } const duplicateComponent = (list: formComponent[], c: formComponent) => { const find = list.find(i => i.id == c.id) if (find) { const tmp = cloneComponent(find) list.splice(list.indexOf(find), 0, tmp) } } const emitSelected = (c: formComponent) => { emit('selected', c) } const emitDelete = (c: formComponent) => { emit('delete', c) deleteComponent(props.list, c) } const emitDuplicate = (c: formComponent) => { emit('duplicate', c) duplicateComponent(props.list, c) } const flexRawRender = (item: formComponent) => { // @ts-ignore if (props.preview) { return
{item.config.formChildren.map((c: formComponent) => { return })}
} return {{ default: () => , item: ({element}: { element: formComponent }) => emitDelete(e)} onDuplicate={(e: formComponent) => emitDuplicate(e)} onSelected={(e: formComponent) => emitSelected(e)} layers={props.layers + 1} item={element}> }} } const switchRender = (item: formComponent) => { switch (item.type) { case "button": return {item.name} case "checkbox": return <>{item.config.options.map((o: formOption) => { return })} case "switch": return case "textField": return case "textArea": return case "date": return case "time": return case "number": return case "select": return case "user": return case "role": return case "upload": return case "radio": return {item.config.options.map((o: formOption) => { return })} case "flexRow": return flexRawRender(item) default: return } } return () =>
{ event.stopPropagation() emitSelected(item.value) }}> {active.value?.id === item.value.id && !props.preview &&
emitDuplicate(item.value)}>duplicate emitDelete(item.value)}>delete
} {item.value.type === 'flexRow' && !props.preview && FlexRow } {switchRender(item.value)}
} }) ================================================ FILE: src/views/form/design/components/formitemEdit.tsx ================================================ import {defineComponent} from 'vue' import { VTextField, VBtn, VSwitch, VColorPicker, VIcon, VSlider, VSelect } from 'vuetify/components' import {PropType} from "vue"; export default defineComponent({ props: { 'modelValue': { required: true, type: Object as PropType, default: () => { } }, }, emits: ['update:modelValue'], setup(props, {emit}) { const optionsRender = (item: formComponent) => { return
{item.config.options.map((o: formOption, index: number) => { return
{ item.config.options.splice(index, 1) } }} class={'position-absolute cursor-pointer pa-1'} style={'right:-10px;top:-10px'} size={'small'} color={'error'} icon={"mdi-close-circle-outline"}>
})} { item.config.options.push({}) } }}>New Option
} const variantSelectRender = (item: formComponent) => { return } const densitySelectRender = (item: formComponent) => { return } const switchRender = (item: formComponent) => { switch (item.type) { case "button": return case "checkbox": return optionsRender(item) case "radio": return optionsRender(item) case "switch": return case "textField": return [variantSelectRender(item)] case "textArea": return undefined case "date": return undefined case "time": return undefined case "number": return undefined case "select": return optionsRender(item) case "upload": return default: return undefined } } return () =>
{densitySelectRender(props.modelValue)} {switchRender(props.modelValue)}
} }) ================================================ FILE: src/views/form/design/components/rightpanel.vue ================================================ ================================================ FILE: src/views/form/design/demofom.json ================================================ [ { "name": "Applicant", "value": "user", "type": "user", "config": { "variant": "outlined" }, "id": "Formlgdaa59i_9" }, { "name": "Begin Date", "value": "date", "type": "date", "config": { "variant": "outlined" }, "id": "Formlgdaa59i_10" }, { "name": "End Date", "value": "date", "type": "date", "config": { "variant": "outlined" }, "id": "Formlgdaa59i_11" }, { "name": "Amount of passenger", "value": "number", "type": "number", "config": { "variant": "outlined" }, "id": "Formlgdaa59i_12" }, { "name": "Driver", "value": "user", "type": "user", "config": { "variant": "outlined" }, "id": "Formlgdaa59i_13" }, { "name": "Describe reason", "value": "textarea", "type": "textArea", "config": { "variant": "outlined" }, "id": "Formlgdaa59i_14" } ] ================================================ FILE: src/views/form/design/form.d.ts ================================================ interface formComponentGroup { name: string, children: formComponent[] } interface formComponent { [key: string]: any, id?: string, name: string, value: string, type: 'textField' | 'number' | 'switch' | 'button' | 'textArea' | 'select' | 'date' | 'time' | 'checkbox' | 'upload' | 'user' | 'role' | 'flexRow' | 'radio' config: formComponentConfig } type formComponentConfig = Record | ( VSelect & VTextField & VBtn & VCheckbox & VRadio & VCol & { options: formOption[], formChildren: formComponent[] } ) interface formOption { label?: string, value?: string } type formComponentType = NonNullable interface form { id: string, name: string, } ================================================ FILE: src/views/form/design/formComponents.ts ================================================ const formComponents: (primary: string) => formComponentGroup[] = (primary: string) => [ { name: 'Base', children: [ { name: 'textField', value: 'textField', type: 'textField', config: { variant: 'outlined', } }, { name: 'button', value: 'button', type: 'button', config: { color: primary, } }, { name: 'date', value: 'date', type: 'date', config: { variant: 'outlined', } }, { name: 'time', value: 'time', type: 'time', config: { variant: 'outlined', } }, { name: 'number', value: 'number', type: 'number', config: { variant: 'outlined', } }, { name: 'textarea', value: 'textarea', type: 'textArea', config: { variant: 'outlined', } }, { name: 'upload', value: 'upload', type: 'upload', config: { variant: 'outlined', } }, { name: 'select', value: 'select', type: 'select', config: { variant: 'outlined', options: [ { label: 'option1', value: 'option1' }, { label: 'option2', value: 'option2' } ] } }, { name: 'checkbox', value: 'checkbox', type: 'checkbox', config: { options: [ { label: 'option1', value: 'option1' }, { label: 'option2', value: 'option2' } ] } }, { name: 'radio', value: 'radio', type: 'radio', config: { options: [ { label: 'option1', value: 'option1' }, { label: 'option2', value: 'option2' } ] } } ] }, { name: 'Advanced', children: [ { name: 'user', value: 'user', type: 'user', config: { variant: 'outlined' } }, { name: 'role', value: 'role', type: 'role', config: { variant: 'outlined' } } ] }, { name: 'Layout', children: [ { name: 'flexRow', value: 'flexRow', type: 'flexRow', config: { formChildren: [] } } ] } ] export default formComponents ================================================ FILE: src/views/form/design/index.vue ================================================ ================================================ FILE: src/views/form/design/preview.tsx ================================================ import {defineComponent, computed, ref, PropType} from 'vue' import { VDialog, VCard, VCardText, VCardTitle, VCardActions, VBtn } from "vuetify/components"; import Formitem from "@/views/form/design/components/formitem"; import {VSpacer} from "vuetify/components/VGrid"; export default defineComponent({ props: { 'modelValue': { required: true, type: Boolean, default: () => { } }, form: { required: true, type: Object as PropType
, }, components: { required: true, type: Array as PropType } }, setup(props, {emit}) { const m = ref(false) const model = computed({ set(v: Boolean) { m.value = v emit('update:modelValue', v) }, get() { return props.modelValue } }) return () => {props.form.name || props.form.id} {props.components.map(c => { return })} model.value = false}} variant={'tonal'} color={'error'}>cancel } }) ================================================ FILE: src/views/form/list.vue ================================================ ================================================ FILE: src/views/index.ts ================================================ import type {RouteComponent} from 'vue-router'; export const views: Record Promise<{ default: RouteComponent }>)> = { 404: () => import('./_builtin/error/NotFoundPage.vue'), 403: () => import('./_builtin/error/NotFoundPage.vue'), 500: () => import('./_builtin/error/UnexpectedPage.vue'), login: () => import('./_builtin/auth/index.vue'), 'not-found': () => import('./_builtin/error/NotFoundPage.vue'), 'dashboard_analytics': () => import('./dashboard/index.vue'), "apps_manager-user_list": () => import('./users/UsersPage.vue'), "apps_manager-user_edit": () => import('./users/EditUserPage.vue'), "apps_board": () => import('./board/pages/BoardPage.vue'), "apps_todo_tasks": () => import('@/views/todo/pages/TasksPage.vue'), "apps_todo_completed": () => import('@/views/todo/pages/CompletedPage.vue'), "apps_todo_label": () => import('@/views/todo/pages/LabelPage.vue'), "apps_chat-channel": () => import('./chart/ChatChannel.vue'), "pages_error_notfound": () => import('./_builtin/error/NotFoundPage.vue'), "pages_error_unexpected": () => import('./_builtin/error/UnexpectedPage.vue'), "other_menu-levels-2-1": () => import('./menulevels/lv2.1.vue'), "other_menu-levels-3-1": () => import('./menulevels/lv3.1.vue'), "other_menu-levels-3-2": () => import('./menulevels/lv3.2.vue'), "flowable_design": () => import('./flowable/design/index.vue'), "form_list": () => import('./form/list.vue'), "form_design": () => import('./form/design/index.vue'), }; ================================================ FILE: src/views/menulevels/lv2.1.vue ================================================ ================================================ FILE: src/views/menulevels/lv3.1.vue ================================================ ================================================ FILE: src/views/menulevels/lv3.2.vue ================================================ ================================================ FILE: src/views/todo/TodoLayout.vue ================================================ ================================================ FILE: src/views/todo/components/TodoCompose.vue ================================================ ================================================ FILE: src/views/todo/components/TodoList.vue ================================================ ================================================ FILE: src/views/todo/components/TodoMenu.vue ================================================ ================================================ FILE: src/views/todo/pages/CompletedPage.vue ================================================ ================================================ FILE: src/views/todo/pages/LabelPage.vue ================================================ ================================================ FILE: src/views/todo/pages/TasksPage.vue ================================================ ================================================ FILE: src/views/todo/store/content.ts ================================================ const taskList:Array = [{ id: 1, title: 'find the report on one winged airplanes', description: '✈️ one wing is the way to go', labels: ['work'], completed: false }, { id: 2, title: '🤘 get marshmallows for camping', description: 'we need it for reasons 🤤', labels: ['groceries'], completed: false }, { id: 3, title: '🏄‍♀️ book surf lessons for September', description: '', labels: ['fun'], completed: false }, { id: 4, title: 'Take my car to the shop', description: 'the brakes are broken', labels: [], completed: true }, { id: 5, title: 'Choose a pool 🏊‍♂️ from the catalog', description: 'must fit the whole family', labels: ['fun', 'groceries'], completed: false }] const labels:Array= [{ id: 'work', title: 'Work', color: 'primary' }, { id: 'fun', title: 'Fun', color: 'blue' }, { id: 'groceries', title: 'Groceries', color: 'orange' }] export {labels, taskList} ================================================ FILE: src/views/todo/store/index.ts ================================================ import {defineStore} from "pinia"; import {taskList, labels} from './content' interface TodoState { taskList: Array labels: Array } export const useTodoStore = defineStore('todo-store', { state: (): TodoState => ({ taskList, labels }), getters:{ incompleteTasks({taskList}) { return taskList.filter((t) => !t.completed) }, completeTasks({taskList}) { return taskList.filter((t) => t.completed) } }, actions: { addTask(task: Todo.Task) { this.taskList.push({ ...task, completed: false, id: '_' + Math.random().toString(36).substr(2, 9), }) }, updateTask(task: Todo.Task) { const taskIdx = this.taskList.find((t) => t.id === task.id) if (taskIdx) Object.assign(taskIdx, task) }, taskCompleted(task: Todo.Task) { const taskIdx = this.taskList.findIndex((t) => t.id === task.id) this.taskList[taskIdx].completed = true }, taskIncomplete(task: Todo.Task) { const taskIdx = this.taskList.findIndex((t) => t.id === task.id) this.taskList[taskIdx].completed = false }, deleteTask(task: Todo.Task) { const taskIdx = this.taskList.findIndex((t) => t.id === task.id) if (taskIdx !== -1) this.taskList.splice(taskIdx, 1) } } }) ================================================ FILE: src/views/todo/typs/index.d.ts ================================================ declare namespace Todo { interface Task{ id: number|string, title: string, description: string, labels: Array, completed: boolean } interface Label{ id:string, title:string, color:string } } ================================================ FILE: src/views/users/EditUser/AccountTab.vue ================================================ ================================================ FILE: src/views/users/EditUser/InformationTab.vue ================================================ ================================================ FILE: src/views/users/EditUserPage.vue ================================================ ================================================ FILE: src/views/users/UsersPage.vue ================================================ ================================================ FILE: src/views/users/content/user.ts ================================================ const user: Array = [{ gender: '0', birthDay: '1996-12-16', 'address': {}, phone: '123', 'id': "1", 'email': 'bfitchew0@ezinearticles.com', 'name': 'Bartel Fitchew', 'verified': false, 'created': '2019-08-09T03:14:12Z', 'lastSignIn': '2019-08-14T20:00:53Z', userStatus: "1", 'role': 'user', 'avatar': 'avatar3' }, { gender: '0', 'address': {}, birthDay: '1997-08-01', phone: '123', 'id': "2", 'email': 'bfitchew0@ezinearticles.com', 'name': 'Bartel Fuck', 'verified': true, 'created': '2019-08-09T03:14:12Z', 'lastSignIn': '2019-08-14T20:00:53Z', userStatus: "4", 'role': 'user', 'avatar': 'avatar2' }] export default user ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "module": "ESNext", "target": "ESNext", "lib": ["DOM", "ESNext"], "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "jsx": "preserve", "jsxFactory": "h", "jsxFragmentFactory": "Fragment", "moduleResolution": "node", "resolveJsonModule": true, "noUnusedLocals": true, "strictNullChecks": true, "allowJs": true, "forceConsistentCasingInFileNames": true, "paths": { "~/*": ["./*"], "@/*": ["./src/*"] }, "types": ["vite/client", "node", "unplugin-icons/types/vue" ,"vuetify"] }, "exclude": ["node_modules", "dist"] } ================================================ FILE: vite.config.ts ================================================ import {setupVitePlugins, getSrcPath, getRootPath} from './build'; // Utilities import {defineConfig, loadEnv} from 'vite' // https://vitejs.dev/config/ export default defineConfig(configEnv => { const viteEnv = loadEnv(configEnv.mode, process.cwd()) as unknown as ImportMetaEnv; const rootPath = getRootPath(); const srcPath = getSrcPath(); return { plugins: setupVitePlugins(viteEnv), define: {'process.env': {}}, resolve: { alias: { '~': rootPath, '@': srcPath, }, extensions: [ '.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue', ], }, server: { port: 3322, open: true, host: '0.0.0.0', }, build: { reportCompressedSize: false, sourcemap: false, commonjsOptions: { ignoreTryCatch: false } }, assetsInclude:["**/*.bpmn"] } })