Showing preview only (402K chars total). Download the full file or copy to clipboard to get everything.
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<ServiceEnvType, ServiceEnvConfig>;
/** 不同请求服务的环境配置 */
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
<img src="https://i.imgur.com/3e63lxL.jpg" style="zoom:8%;" />
## 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







## 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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap" rel="stylesheet">-->
<title>Lulu Admin</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
================================================
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<boolean> => {
return {
code: 200,
message: 'ok',
data: true
};
}
},
{
url: '/mock/login',
method: 'post',
timeout: 500,
response: (options: Service.MockOption): Service.MockServiceResult<ApiAuth.Token | null> => {
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<ApiAuth.UserInfo | null> => {
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<ApiAuth.Token | null> => {
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<ApiChatManagement.message> => {
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<ApiCommon.PageResult<ApiUserManagement.User[]>> => {
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<ApiUserManagement.User> => {
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<ApiCommon.PageResult<ApiForm.Form[]>> => {
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<Auth.RoleType, AuthRoute.Route[]> = {
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
================================================
<template>
<v-app>
<vuetify-provider>
<router-view/>
</vuetify-provider>
</v-app>
</template>
<script setup lang="ts">
import {useGlobalEvents} from "@/composables/events";
import {subscribeStore} from '@/store';
subscribeStore()
useGlobalEvents();
</script>
================================================
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
================================================
<template>
<v-breadcrumbs class="pa-0 py-2" :items="breadcrumbs" active-color="primary">
<template v-slot:title="{ item}">
{{ $t((item as App.GlobalBreadcrumb ).title || 'unknown') }}
</template>
</v-breadcrumbs>
</template>
<script setup lang="ts">
import {PropType} from 'vue';
import useBreadcrumb from "@/hooks/common/useBreadcrumb";
const props = defineProps({
root: {
type: String as PropType<Exclude<AuthRoute.AllRouteKey, 'not-found'>>,
default: () => 'root'
}
})
const {breadcrumbs} = useBreadcrumb(props.root)
</script>
<style scoped></style>
================================================
FILE: src/components/common/CopyLabel.vue
================================================
<template>
<div ref="copyLabel" class="copylabel animate__faster" @click.stop.prevent="copy">{{ text }}
<v-tooltip location="bottom" activator="parent">
<span>{{ tooltip }}</span>
</v-tooltip>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
// Text to copy to clipboard.ts
text: {
type: String,
default: ''
},
// Text to show on toast
toastText: {
type: String,
default: 'Copied to clipboard.ts!'
},
animation: {
type: String,
default: 'heartBeat'
}
})
const tooltip = ref('copy')
const copyLabel = ref<HTMLElement>()
let timeout: NodeJS.Timeout
onBeforeUnmount(() => {
if (timeout) clearTimeout(timeout)
})
const copy = () => {
if (copyLabel.value) {
animate(copyLabel.value, props.animation)
}
clipboard(props.text, props.toastText)
tooltip.value = 'Copied!'
timeout = setTimeout(() => {
tooltip.value = 'Copy'
}, 2000)
}
</script>
<style scoped>
.copylabel {
cursor: pointer;
display: inline-block;
border-bottom: 1px dashed;
}
</style>
================================================
FILE: src/components/common/FlagIcon.vue
================================================
<template>
<span class="fi" :class="[`fi-${flag}`, { 'flag-round': round }]"></span>
</template>
<script setup lang="ts">
import "flag-icons/css/flag-icons.min.css"
defineProps(
{
flag: {
type: String,
default: 'us'
},
round: {
type: Boolean,
default: false
}
}
)
</script>
<style lang="scss" scoped>
.fi {
height: 22px;
width: 22px;
&.flag-round {
background-size: cover;
border-radius: 100%;
height: 26px;
width: 26px;
}
}
</style>
================================================
FILE: src/components/common/SideConfigMenu.vue
================================================
<template>
<div>
<v-btn
theme="dark"
ref="refButton"
class="drawer-button"
color="#ee44aa"
@click="right = true"
>
<v-icon class="fa-spin">mdi-cog-outline</v-icon>
</v-btn>
<v-navigation-drawer
v-model="right"
location="right"
floating
temporary
order="-10"
width="310"
>
<div class="d-flex align-center pa-2">
<div class="title">Settings</div>
<v-spacer></v-spacer>
<v-btn flat icon @click="right = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<v-divider></v-divider>
<div class="pa-2">
<div class="font-weight-bold my-1">Follow Os Theme</div>
<v-switch v-model="themeConfig.followOs" color="primary"></v-switch>
<div class="font-weight-bold my-1">Global Theme</div>
<v-btn-toggle v-model="themeConfig.globalTheme" color="primary" mandatory variant="outlined" class="mb-2">
<v-btn value="light">Light</v-btn>
<v-btn value="dark">Dark</v-btn>
</v-btn-toggle>
<div class="font-weight-bold my-1">Toolbar Theme</div>
<v-btn-toggle v-model="themeConfig.toolbarTheme" color="primary" mandatory variant="outlined" class="mb-2">
<v-btn value="global">Global</v-btn>
<v-btn value="light">Light</v-btn>
<v-btn value="dark">Dark</v-btn>
</v-btn-toggle>
<div class="font-weight-bold my-1">Toolbar Style</div>
<v-btn-toggle v-model="themeConfig.isToolbarDetached" color="primary" mandatory variant="outlined" class="mb-2">
<v-btn :value="false">Full</v-btn>
<v-btn :value="true">Solo</v-btn>
</v-btn-toggle>
<div class="font-weight-bold my-1">Content Layout</div>
<v-btn-toggle v-model="themeConfig.isContentBoxed" color="primary" mandatory variant="outlined" class="mb-2">
<v-btn :value="false">Fluid</v-btn>
<v-btn :value="true">Boxed</v-btn>
</v-btn-toggle>
<div class="font-weight-bold my-1">Menu Theme</div>
<v-btn-toggle v-model="themeConfig.menuTheme" color="primary" mandatory variant="outlined" class="mb-2">
<v-btn value="global">Global</v-btn>
<v-btn value="light">Light</v-btn>
<v-btn value="dark">Dark</v-btn>
</v-btn-toggle>
<div class="font-weight-bold my-1">Primary Color</div>
<v-color-picker v-model="themeConfig.primary" mode="hexa" :swatches="swatches" show-swatches></v-color-picker>
</div>
<v-divider></v-divider>
</v-navigation-drawer>
</div>
</template>
<script setup lang="ts">
import {ComponentPublicInstance} from "vue";
import {useThemeStore} from "@/store";
const themeConfig = useThemeStore()
const right = ref(false)
let timeout: NodeJS.Timeout
const swatches = reactive([['#0096c7', '#31944f'],
['#EE4f12', '#46BBB1'],
['#ee44aa', '#55BB46']])
const refButton = ref<ComponentPublicInstance>()
const execAnimate = () => {
if (timeout) {
clearTimeout(timeout)
}
const time = (Math.floor(Math.random() * 10 + 1) + 10) * 1000
timeout = setTimeout(() => {
if (refButton.value) {
animate(refButton.value.$el, 'bounce')
}
execAnimate()
}, time)
}
onMounted(() => {
execAnimate()
})
onBeforeUnmount(() => {
if (timeout) clearTimeout(timeout)
})
</script>
<style lang="scss" scoped>
.drawer-button {
position: fixed;
top: 340px;
right: -20px;
z-index: 9999;
box-shadow: 1px 1px 18px #ee44aa;
.v-icon {
margin-left: -18px;
font-size: 1.3rem;
}
.fa-spin {
animation: spin 2s infinite linear;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
</style>
================================================
FILE: src/components/common/SvgIcon.vue
================================================
<template>
<svg aria-hidden="true" v-bind="bindAttrs" class="d-flex flex-column justify-end" >
<use :href="symbolId" fill="currentColor"/>
</svg>
</template>
<script lang="ts" setup>
const props = defineProps({
name: {
type: String,
required: true,
},
})
const attrs = useAttrs();
const bindAttrs = computed<{ class: string; style: string }>(() => ({
class: (attrs.class as string) || '',
style: (attrs.style as string) || ''
}));
const symbolId = computed(() => {
const {VITE_ICON_PREFFIX: prefix} = import.meta.env;
return `#${prefix}-${props.name}`
})
</script>
================================================
FILE: src/components/common/TrendPercent.vue
================================================
<template>
<span>
<span v-if="value === 0">
{{ value }}%
</span>
<span v-else-if="value > 0" class="success--text">
<v-icon small color="success">mdi-arrow-top-right</v-icon> {{ value }}%
</span>
<span v-else class="error--text">
<v-icon small color="error">mdi-arrow-bottom-right</v-icon> {{ Math.abs(value) }}%
</span>
</span>
</template>
<script setup lang="ts">
defineProps({
value: {
type: Number,
default: 0
}
})
</script>
================================================
FILE: src/components/dashboard/ActivityCard.vue
================================================
<template>
<v-card>
<v-card-title>
<div>{{ $t('dashboard.activity') }}</div>
<v-spacer></v-spacer>
<v-menu offset-y left transition="slide-y-transition">
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props" flat>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list density="comfortable">
<v-list-item
v-for="(item,index) in menu"
:key="index"
:to="item.link"
:disabled="item.disabled"
link
:prepend-icon="item.icon"
:title="item.text"
>
</v-list-item>
</v-list>
</v-menu>
</v-card-title>
<v-card-text>
<v-timeline class="pa-0" density="comfortable" align="start" truncate-line="start">
<v-timeline-item v-for="(item,index) in activity" :key="index" :dot-color="item.color" size="small">
<strong>{{ item.what }}</strong>
<div class="text-caption">
<div>{{ item.where }}</div>
<div class="text-grey">{{ item.when }}</div>
</div>
</v-timeline-item>
</v-timeline>
</v-card-text>
</v-card>
</template>
<script lang="ts" setup>
import {reactive} from "vue";
interface _menu {
icon: string,
text: string,
disabled?: boolean,
link?: string
}
const menu = reactive<Array<_menu>>([
{icon: 'mdi-refresh', text: 'Refresh'},
{icon: 'mdi-delete-outline', text: 'Clear'}
])
const activity = reactive([
{
what: 'New Emoji',
where: 'Chat App',
when: '4pm',
color: 'primary'
}, {
what: 'Design Stand Up',
where: 'Chat App',
when: '2pm',
color: 'purple'
}, {
what: 'Lunch Break',
where: '',
when: '11am',
color: 'primary'
}, {
what: 'Answer Emails',
where: 'Work work',
when: '9pm',
color: 'teal lighten-3'
}
])
</script>
================================================
FILE: src/components/dashboard/SalesCard.vue
================================================
<template>
<v-card class="d-flex flex-grow-1 bg-primary-darken-4 " theme="dark">
<!-- loading spinner -->
<div v-if="loading" class="d-flex flex-grow-1 align-center justify-center">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</div>
<!-- information -->
<div v-else class="d-flex flex-column flex-grow-1">
<v-card-title class="d-flex">
<div>{{ $t(label) }}</div>
<v-spacer></v-spacer>
<v-btn variant="text" color="primary" @click="$emit('action-clicked')">{{ actionLabel }}</v-btn>
</v-card-title>
<div class="d-flex flex-column flex-grow-1">
<div class="pa-2">
<div class="text-h4">
{{ $filters.formatCurrency(26358.49) }}
</div>
<div class="text-primary-lighten-1 mt-1">
{{ $filters.formatCurrency(7123.21) }} {{ $t('dashboard.lastweek') }}
</div>
</div>
<v-spacer></v-spacer>
<div class="px-2 pb-2">
<div class="title mb-1">{{ $t('dashboard.weeklySales') }}</div>
<div class="d-flex align-center">
<div class="text-h4">
{{ $filters.formatCurrency(value) }}
</div>
<v-spacer></v-spacer>
<div class="d-flex flex-column text-right">
<div class="font-weight-bold">
<trend-percent :value="percentage"/>
</div>
<div class="text-caption">{{ percentageLabel }}</div>
</div>
</div>
</div>
</div>
<apexchart
type="area"
height="120"
:options="chartOptions"
:series="series"
></apexchart>
</div>
</v-card>
</template>
<script lang="ts" setup>
import moment from 'moment';
const formatDate = (date: string) => {
return date ? moment(date).format('D MMM') : ''
}
const props = defineProps({
value: {
type: Number,
default: 0
},
percentage: {
type: Number,
default: 0
},
percentageLabel: {
type: String,
default: 'vs. last week'
},
series: {
type: Array,
default: () => [{
name: 'Sales',
data: [11, 32, 45, 13]
}]
},
xaxis: {
type: Object,
default: () => ({
type: 'category',
categories: [
'2018-09-19T00:00:00.000Z',
'2018-09-20T00:00:00.000Z',
'2018-09-22T00:00:00.000Z',
'2018-09-23T00:00:00.000Z'
]
// tickAmount: 3
})
},
label: {
type: String,
default: 'dashboard.sales'
},
actionLabel: {
type: String,
default: 'View Report'
},
options: {
type: Object,
default: () => ({})
},
loading: {
type: Boolean,
default: false
}
})
const {themes, current} = useTheme()
const chartOptions = computed(() => {
const primaryColor = current.value.dark
? themes.value["dark"].colors.primary
: themes.value["light"].colors.primary
return {
chart: {
height: 120,
type: 'area',
sparkline: {
enabled: true
},
animations: {
speed: 400
}
},
series: props.series,
colors: [primaryColor],
fill: {
type: 'solid',
colors: [primaryColor],
opacity: 0.15
},
stroke: {
curve: 'smooth',
width: 2
},
xaxis: props.xaxis,
tooltip: {
followCursor: true,
theme: 'dark',
custom: function ({ctx, series, seriesIndex, dataPointIndex, w}: any) {
const seriesName = w.config.series[seriesIndex].name
return `<div class="rounded-lg pa-1 text-caption">
<div class="font-weight-bold">${formatDate(w.globals.categoryLabels[dataPointIndex])}</div>
<div>${series[seriesIndex][dataPointIndex]} ${seriesName}</div>
</div>`
}
},
...props.options
}
})
</script>
================================================
FILE: src/components/dashboard/SourcesCard.vue
================================================
<template>
<v-card class="d-flex flex-column flex-grow-1">
<!-- loading spinner -->
<div v-if="loading" class="d-flex flex-grow-1 align-center justify-center">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</div>
<!-- information -->
<div v-else class="d-flex flex-column flex-grow-1">
<v-card-title>
<div>{{ label }}</div>
<v-spacer></v-spacer>
<div>
<v-select
v-model="selectedInterval"
density="comfortable"
variant="solo"
hide-details
hide-selected
:items="intervals"
></v-select>
</div>
</v-card-title>
<div class="chart-wrap">
<apexchart
type="donut"
width="85%"
:options="chartOptions"
:series="series"
></apexchart>
</div>
</div>
</v-card>
</template>
<script lang="ts" setup>
import {computed, reactive, ref} from "vue";
const props = defineProps({
series: {
type: Array,
default: () => ([])
},
label: {
type: String,
default: ''
},
color: {
type: String,
default: '#333333'
},
value: {
type: Number,
default: 0
},
percentage: {
type: Number,
default: 0
},
percentageLabel: {
type: String,
default: 'vs. last week'
},
options: {
type: Object,
default: () => ({})
},
loading: {
type: Boolean,
default: false
}
})
const selectedInterval = ref('Last 7 days')
const intervals = reactive([
'Last 7 days',
'Last 28 days',
'Last month',
'Last year'
])
const {current} = useTheme()
const chartOptions = computed(() => {
return {
chart: {
type: 'donut',
animations: {
speed: 400
},
background: 'transparent'
},
stroke: {
show: true,
colors: [current.value.dark ? '#333' : '#fff'],
width: 1,
dashArray: 0
},
plotOptions: {
pie: {
expandOnClick: false,
donut: {
size: '74%'
}
}
},
theme: {
mode: current.value.dark ? 'dark' : 'light'
},
labels: ['Referrals', 'Organic Search', 'Social Media', 'Others'],
dataLabels: {
enabled: false
},
colors: ['#2364aa', '#3da5d9', '#73bfb8', '#fec601', '#ea7317'],
fill: {
colors: ['#2364aa', '#3da5d9', '#73bfb8', '#fec601', '#ea7317']
},
legend: {
offsetY: 40,
fontSize: '13px',
fontFamily: 'Quicksand',
fontWeight: 700
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 200
},
legend: {
offsetY: 0,
position: 'bottom'
}
}
}],
...props.options
}
})
</script>
<style lang="scss" scoped>
.chart-wrap {
max-width: 560px;
max-height: 280px;
}
</style>
================================================
FILE: src/components/dashboard/TableCard.vue
================================================
<template>
<v-card>
<v-card-title>{{ label }}</v-card-title>
<v-data-table
:headers="headers"
:items="items"
hide-default-footer
>
<!-- <template v-slot:bottom>-->
<!-- </template>-->
<template v-slot:item.id="{ item:{raw} }">
<div class="font-weight-bold">#
<copy-label :text="raw.id"/>
</div>
</template>
<template v-slot:item.user="{ item:{raw}}">
<div class="d-flex align-center py-1">
<v-avatar size="40" class="elevation-1 grey lighten-3">
<svg-icon :name="raw.user.avatar"></svg-icon>
</v-avatar>
<div class="ml-1">
<div class="font-weight-bold">{{ raw.user.name }}</div>
<div class="text-caption">
<copy-label :text="raw.user.email"/>
</div>
</div>
</div>
</template>
<template v-slot:item.date="{ item:{raw} }">
<div>{{ $filters.formatCurrency(raw.date) }}</div>
</template>
<template v-slot:item.company="{ item:{raw} }">
<copy-label :text="raw.company"/>
</template>
<template v-slot:item.amount="{ item:{raw} }">
{{ $filters.formatCurrency(raw.amount) }}
</template>
<template v-slot:item.status="{ item:{raw} }">
<div class="font-weight-bold d-flex align-center text-no-wrap">
<div v-if="raw.status === 'PENDING'" class="warning--text">
<v-icon small color="warning">mdi-circle-medium</v-icon>
<span>Pending</span>
</div>
<div v-if="raw.status === 'PAID'" class="success--text">
<v-icon small color="success">mdi-circle-medium</v-icon>
<span>Paid</span>
</div>
</div>
</template>
<template v-slot:item.action>
<div class="actions">
<v-btn flat icon >
<v-icon>mdi-open-in-new</v-icon>
</v-btn>
</div>
</template>
</v-data-table>
</v-card>
</template>
<script setup lang="ts">
import {ref} from 'vue'
defineProps
({
label: {
type: String,
default: ''
}
})
const headers = ref<DataTableHeader>([
{title: 'Order Id', align: 'start', key: 'id'},
{
title: 'User',
sortable: false,
key: 'user'
},
{title: 'Date', key: 'date'},
{title: 'Company', key: 'company'},
{title: 'Amount', key: 'amount'},
{title: 'Status', key: 'status'},
{title: '', sortable: false, align: 'end', key: 'action'}
])
const items = ref([
{
id: '2837',
user: {
name: 'John Simon',
email: 'johnsimon@blobhill.com',
avatar: 'avatar1',
},
date: '2020-05-10',
company: 'BlobHill',
amount: 52877,
status: 'PAID'
},
{
id: '2838',
user: {
name: 'Greg Cool J',
email: 'cool@caprimooner.com',
avatar: 'avatar2',
},
date: '2020-05-11',
company: 'Caprimooner',
amount: 2123,
status: 'PENDING'
},
{
id: '2839',
user: {
name: 'Samantha Bush',
email: 'bush@catloveisstilllove.com',
avatar: 'avatar3',
},
date: '2020-05-11',
company: 'CatLovers',
amount: 12313,
status: 'PENDING'
},
{
id: '2840',
user: {
name: 'Ben Howard',
email: 'ben@indiecoolers.com',
avatar: 'avatar4',
},
date: '2020-05-12',
company: 'IndieCoolers',
amount: 9873,
status: 'PAID'
},
{
id: '2841',
user: {
name: 'Jack Candy',
email: 'jack@candylooove.com',
avatar: 'avatar5',
},
date: '2020-05-13',
company: 'CandyLooove',
amount: 29573,
status: 'PAID'
}
])
</script>
================================================
FILE: src/components/dashboard/TodoCard.vue
================================================
<template>
<tasks-page class="todo-card"></tasks-page>
</template>
<script lang="ts" setup>
import TasksPage from '@/views/todo/pages/TasksPage.vue'
</script>
<style lang="scss" scoped>
.todo-card {
height: 100%;
max-height: 360px;
overflow-y: scroll;
}
</style>
================================================
FILE: src/components/dashboard/TrackCard.vue
================================================
<template>
<v-card class="d-flex flex-column flex-grow-1">
<div v-if="loading" class="d-flex flex-grow-1 align-center justify-center">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</div>
<div v-else class="d-flex flex-column flex-grow-1">
<v-card-title>
{{ label }}
</v-card-title>
<div class="d-flex flex-column flex-grow-1">
<div class="px-2 pb-2">
<div class="d-flex align-center">
<div class="text-h4">{{ value }}</div>
<v-spacer></v-spacer>
<div class="d-flex flex-column text-right">
<div class="font-weight-bold">
<trend-percent :value="percentage"/>
</div>
<div class="text-caption">{{ percentageLabel }}</div>
</div>
</div>
</div>
<v-spacer></v-spacer>
<apexchart
type="area"
height="60"
:options="chartOptions"
:series="series"
></apexchart>
</div>
</div>
</v-card>
</template>
<script setup lang="ts">
import moment from 'moment'
const formatDate = (date: string) => {
return date ? moment(date).format('D MMM') : ''
}
const props = defineProps({
series: {
type: Array,
default: () => ([])
},
label: {
type: String,
default: ''
},
color: {
type: String,
default: '#333333'
},
value: {
type: Number,
default: 0
},
percentage: {
type: Number,
default: 0
},
percentageLabel: {
type: String,
default: 'vs. last week'
},
options: {
type: Object,
default: () => ({})
},
loading: {
type: Boolean,
default: false
}
})
const chartOptions = computed(() => {
return {
chart: {
animations: {
speed: 400,
animateGradually: {
enabled: false
}
},
width: '100%',
height: 60,
type: 'area',
sparkline: {
enabled: true
}
},
colors: [props.color],
fill: {
type: 'solid',
colors: [props.color],
opacity: 0.15
},
stroke: {
curve: 'smooth',
width: 2
},
xaxis: {
type: 'datetime'
},
tooltip: {
followCursor: true,
theme: 'dark', //this.$vuetify.theme.isDark ? 'light' : 'dark',
custom: function ({ctx, series, seriesIndex, dataPointIndex, w}: any) {
const seriesName = w.config.series[seriesIndex].name
const dataPoint = w.config.series[seriesIndex].data[dataPointIndex]
return `<div class="rounded-lg pa-1 text-caption">
<div class="font-weight-bold">${formatDate(dataPoint[0])}</div>
<div>${dataPoint[1]} ${seriesName}</div>
</div>`
}
},
...props.options
}
})
</script>
================================================
FILE: src/components/navigation/MainMenu.vue
================================================
<template>
<v-list nav>
<div v-for="(item,index) in menu" :key="index">
<div v-if="item.label" class="pa-1 mt-2 overline">{{ $t(item.label) }}</div>
<nav-menu :menu="item.children"/>
</div>
</v-list>
</template>
<script lang="ts" setup>
import {PropType} from "vue";
defineProps({
menu: {
type: Array as PropType<Array<App.GlobalMenuOption>>,
default: () => []
}
})
</script>
================================================
FILE: src/components/navigation/NavMenu.vue
================================================
<template>
<div>
<nav-menu-item v-for="(level1Item, level1Index) in menu" :key="level1Index" :menu-item="level1Item">
<template v-if="level1Item.children">
<nav-menu-item
v-for="(level2Item, level2Index) in level1Item.children"
:key="level2Index"
:menu-item="level2Item"
subgroup
small
>
<template v-if="level2Item.children">
<nav-menu-item
v-for="(level3Item, level3Index) in level2Item.children"
:key="level3Index"
:menu-item="level3Item"
small
/>
</template>
</nav-menu-item>
</template>
</nav-menu-item>
</div>
</template>
<script lang="ts" setup>
import {PropType} from "vue";
defineProps({
menu: {
type: Array as PropType<Array<App.GlobalMenuOption>>,
default: () => []
}
})
</script>
================================================
FILE: src/components/navigation/NavMenuItem.vue
================================================
<template>
<div>
<v-list-item
v-if="!(menuItem.children && menuItem.children.length>0)"
:to="menuItem.routePath"
density="comfortable"
active-color="primary"
link
>
<template v-slot:prepend>
<v-icon :size="small?'x-small':'default'"
:class="{ 'same-size':small }">
{{ menuItem.icon || 'mdi-circle-medium' }}
</v-icon>
</template>
<v-list-item-title>
{{ $t(menuItem.label) }}
</v-list-item-title>
</v-list-item>
<v-list-group
v-else
active-color="primary"
:subgroup="subgroup"
collapse-icon="mdi-menu-up"
expand-icon="mdi-menu-down"
>
<template v-slot:activator="{ props }">
<v-list-item density="comfortable" v-bind="props"
:title="$t(menuItem.label)">
<template v-slot:prepend>
<v-icon v-if="!subgroup" :size="small ?'x-small':'default'" :class="{'same-size':small }">
{{ menuItem.icon || 'mdi-circle-medium' }}
</v-icon>
</template>
</v-list-item>
</template>
<slot></slot>
</v-list-group>
</div>
</template>
<script lang="ts" setup>
import {PropType} from "vue";
defineProps({
menuItem: {
type: Object as PropType<App.GlobalMenuOption>,
default: () => {
}
},
subgroup: {
type: Boolean,
default: false
},
small: {
type: Boolean,
default: false
}
})
</script>
<style lang="scss" scoped>
@use 'sass:map';
@use '@/assets/scss/settings';
.same-size {
width: calc(var(--v-icon-size-multiplier) * #{map.get(settings.$icon-sizes, 'default')});
}
.v-list-group :deep(.v-list-item ) {
padding-inline-start: settings.$spacer !important;
}
.v-list-group--subgroup :deep(.v-list-group__items .v-list-item ) {
padding-inline-start: settings.$spacer * 5 !important;
}
</style>
================================================
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<DialogReactive[]>([])
const dialogRefs = ref<Record<string, DialogReactive>>({})
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<DialogReactive>({
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 (
<VDialog
ref={
((inst: DialogReactive) => {
if (inst) {
this.dialogRefs[msg.key] = inst
}
}) as () => void
}
v-model={msg._modelValue}
maxWidth={290}
persistent={!!msg.content.persistent}
>
<VCard>
<VCardTitle class={'headline'}>
{msg.content.title ?? this.$t('common.title')}
</VCardTitle>
<VCardText>
{render(msg.content.main)}
</VCardText>
<VCardActions>
<VSpacer></VSpacer>
<VBtn {...{
'onClick': () => {
msg.close()
msg.content.cancel?.()
}
}} >{msg.content.cancelText ?? this.$t('common.cancel')}</VBtn>
<VBtn loading={msg._confirmLoading} color={msg.content?.confirmColor ?? 'error'}{...{
'onClick': () => {
msg.content.confirm ? msg.content.confirm() : msg.close()
}
}} >{msg.content.confirmText ?? this.$t('common.confirm')}</VBtn>
</VCardActions>
</VCard>
</VDialog>
)
})}
</>
) : null}
</>
)
}
})
export function useDialog(): DialogApiInjection {
const api = inject(DialogInjectKey, null)
if (api === null) {
throw new Error('not outer <snackbar-provider> 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?.()}
<VOverlay
v-model={this.model}
class={['align-center', 'justify-center']}
>
<VProgressCircular
color="primary"
indeterminate
size="64"
></VProgressCircular>
</VOverlay>
</>
)
}
})
export function useLoadingOverly(): LoadingOverlyApiInjection {
const api = inject(LoadingOverlyInjectKey, null)
if (api === null) {
throw new Error('not outer <loading-overly-provider> 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?.()}
<VProgressLinear
active={this.model}
indeterminate={true}
absolute={true}
class={[this.theme.isToolbarDetached ? 'mt-3' : null, 'position-fixed']}
style="top: var(--v-layout-top);z-index: 1000"
color="primary-lighten-1"
></VProgressLinear>
</>
)
}
})
export function useLoadingProgressLine(): LoadingProgressLineApiInjection {
const api = inject(LoadingProgressLineInjectKey, null)
if (api === null) {
throw new Error('not outer <loading-progress-provider> 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<SnackBarReactive[]>([])
const snackBarRefs = ref<Record<string, SnackBarReactive>>({})
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<SnackBarReactive>({
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 (
<VSnackbar
ref={
((inst: SnackBarReactive) => {
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)}
</VSnackbar>
)
})}
</>
) : null}
</>
)
}
})
export function useSnackBar(): SnackBarApiInjection {
const api = inject(SnackBarInjectKey, null)
if (api === null) {
throw new Error('not outer <snackbar-provider> found')
}
return api
}
================================================
FILE: src/components/provider/VuetifyProvider.vue
================================================
<template>
<loading-overly-provider>
<snackbar-provider>
<dialog-provider>
<slot/>
<vuetify-provider-content/>
</dialog-provider>
</snackbar-provider>
</loading-overly-provider>
</template>
<script lang="ts" setup>
import {useSnackBar, useDialog} from "@/components/provider";
import {useLoadingOverly} from "@/components/provider";
import SnackbarProvider from "@/components/provider/SnackbarProvider";
import LoadingOverlyProvider from "@/components/provider/LoadingOverlyProvider";
import DialogProvider from "@/components/provider/DialogProvider";
function registerTools() {
window.$snackBar = useSnackBar()
window.$loadingOverly = useLoadingOverly()
window.$dialog = useDialog()
}
const VuetifyProviderContent = defineComponent({
name: 'VuetifyProviderContent',
setup() {
registerTools()
},
render() {
return h('div');
}
});
</script>
<style scoped>
</style>
================================================
FILE: src/components/provider/index.ts
================================================
export * from './SnackbarProvider'
export * from './LoadingOverlyProvider'
export * from './LoadingProgressLine'
export * from './DialogProvider'
================================================
FILE: src/components/toolbar/ToolbarLanguage.vue
================================================
<template>
<v-menu
>
<template v-slot:activator="{ props }">
<v-btn text v-bind="props">
<flag-icon :round="smAndDown" :flag="currentLocale.flag"></flag-icon>
<span v-show="mdAndUp&& showLabel"
class="ml-1"
> {{ currentLocale.label }}</span>
<v-icon v-if="showArrow" right>mdi-chevron-down</v-icon>
</v-btn>
</template>
<v-list nav density="comfortable">
<v-list-item v-for="locale in availableLocales" :key="locale.code" @click="setLocale(locale.code)">
<v-list-item-title class="d-flex justify-center align-center">
<flag-icon class="mr-1" :flag="locale.flag"></flag-icon>
{{ locale.label }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import FlagIcon from '../common/FlagIcon.vue'
import {computed} from "vue";
import {useI18n} from 'vue-i18n'
import configs from "@/configs";
import {useDisplay} from 'vuetify'
defineProps({
showArrow: {
type: Boolean,
default: false
},
// Show the country label
showLabel: {
type: Boolean,
default: true
}
})
const {smAndDown, mdAndUp} = useDisplay()
const {locale} = useI18n()
const {availableLocales: locales} = configs.locales
const currentLocale = computed(() => {
return locales.find((i: any) => i.code === locale.value)
})
const availableLocales = computed(() => {
return locales.filter((i: any) => i.code !== locale.value)
})
const setLocale = (use: string) => {
locale.value = use
}
</script>
================================================
FILE: src/components/toolbar/ToolbarNotifications.vue
================================================
<template>
<v-menu offset-y left transition="slide-y-transition">
<template v-slot:activator="{ props }">
<v-btn icon class="text-none" v-bind="props">
<v-badge content="6" color="primary" bordered>
<v-icon>mdi-bell-outline</v-icon>
</v-badge>
</v-btn>
</template>
<!-- dropdown card -->
<v-card>
<v-list three-line dense max-width="400">
<v-list-subheader class="pa-2 font-weight-bold">Notifications</v-list-subheader>
<div v-for="(item,index) in items" :key="index">
<v-divider v-if="index > 0 && index < items.length" inset></v-divider>
<v-list-item :title="item.title" :subtitle="item.subtitle">
<template v-slot:prepend>
<v-avatar :icon="item.icon" size="32" :color="item.color"></v-avatar>
</template>
<v-list-item-action class="align-self-center" v-text="item.time">
</v-list-item-action>
</v-list-item>
</div>
</v-list>
<div class="text-center py-2">
<v-btn small>See all</v-btn>
</div>
</v-card>
</v-menu>
</template>
<script lang="ts" setup>
const items = ref([
{
title: 'Brunch this weekend?',
color: 'primary',
icon: 'mdi-account-circle',
subtitle: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Sint, repudiandae?',
time: '3 min'
},
{
title: 'Summer BBQ',
color: 'success',
icon: 'mdi-email-outline',
subtitle: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Sint, repudiandae?',
time: '3 min'
},
{
title: 'Oui oui',
color: 'teal lighten-1',
icon: 'mdi-airplane-landing',
subtitle: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Sint, repudiandae?',
time: '4 min'
},
{
title: 'Disk capacity is at maximum',
color: 'teal accent-3',
icon: 'mdi-server',
subtitle: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Sint, repudiandae?',
time: '3 hr'
},
{
title: 'Recipe to try',
color: 'blue-grey lighten-2',
icon: 'mdi-noodles',
subtitle: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Sint, repudiandae?',
time: '8 hr'
}
])
</script>
================================================
FILE: src/components/toolbar/ToolbarUser.vue
================================================
<template>
<v-menu offset-y left transition="slide-y-transition">
<template v-slot:activator="{ props }">
<v-btn icon size="small" class="elevation-2" v-bind="props">
<v-badge
color="success"
dot
bordered
>
<v-avatar size="40" :class="{'bg-grey':!userInfo.userAvatar}">
<svg-icon v-if="!!userInfo.userAvatar" :name="userInfo.userAvatar"></svg-icon>
</v-avatar>
</v-badge>
</v-btn>
</template>
<!-- user menu list -->
<v-list dense nav>
<v-list-item v-if="!isLogin">
<v-list-item-title>{{ $t('usermenu.notSignin') }}</v-list-item-title>
</v-list-item>
<v-list-item
v-else
v-for="(item, index) in menu"
:key="index"
:to="item.link"
link
>
<template v-slot:prepend>
<v-icon size="small" :icon="item.icon"></v-icon>
</template>
<v-list-item-title>{{ $t(item.key) }}</v-list-item-title>
</v-list-item>
<v-divider class="my-1"></v-divider>
<v-list-item @click="logout" prepend-icon="mdi-logout-variant" :title="$t('menu.logout')">
</v-list-item>
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import {reactive} from "vue";
const menu = reactive(
[
{icon: 'mdi-account-box-outline', key: 'menu.profile', link: '/apps/manager-user/edit'},
{icon: 'mdi-format-list-checkbox', key: 'menu.todo', link: '/apps/todo'},
{icon: 'mdi-email-outline', key: 'menu.board', link: '/apps/board'},
{icon: 'mdi-forum-outline', key: 'menu.chat', link: '/apps/chat-channel/'}
]
)
const auth = useAuthStore();
const {isLogin, userInfo} = storeToRefs(auth)
const logout = () => {
window.$dialog?.show({
title: 'Logo out',
main: 'Are you sure logo out?',
confirm: () => {
window.$loadingOverly?.show()
setTimeout(() => {
auth.resetAuthStore()
}, 1000)
}
})
}
</script>
================================================
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<UserManagement.GenderKey, string> = {
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<UserManagement.UserStatusKey, string> = {
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<FormManagement.FormStatusKey, string> = {
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<AuthRoute.AllRouteKey, 'not-found'> = '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<T>(contextName = 'context') {
const injectKey: InjectionKey<T> = 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
================================================
<template>
<div class="d-flex text-center flex-column flex-md-row flex-grow-1">
<v-sheet class="layout-side mx-auto mx-md-1 d-none d-md-flex flex-md-column justify-space-between px-2">
<div class="mt-3 mt-md-10 pa-2">
<div class="text-h3 font-weight-bold text-primary">
{{ name }}
</div>
<div class="title my-2">Welcome! Let's build amazing things together.</div>
<v-btn to="/" class="my-4">Take me back</v-btn>
</div>
<img class="w-100" src="/images/illustrations/signin-illustration.svg"/>
</v-sheet>
<div class="pa-2 pa-md-4 flex-grow-1 align-center justify-center d-flex flex-column">
<div class="layout-content ma-auto w-100">
<router-view v-slot="{ Component }">
<v-fade-transition mode="out-in">
<component :is="Component"/>
</v-fade-transition>
</router-view>
</div>
<div class="overline mt-4">{{ title }} - v1.0.0</div>
</div>
</div>
</template>
<script setup lang="ts">
import {useAppInfo} from "@/composables/system";
const {title, name} = useAppInfo();
</script>
<style scoped>
.layout-side {
width: 420px;
}
.layout-content {
max-width: 480px;
}
</style>
================================================
FILE: src/layouts/BlankLayout/index.vue
================================================
<template>
<router-view v-slot="{ Component }">
<v-fade-transition mode="out-in">
<component :is="Component"/>
</v-fade-transition>
</router-view>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
================================================
FILE: src/layouts/DefaultLayout.vue
================================================
<template>
<!-- Navigation -->
<v-navigation-drawer
v-model="drawer"
floating
name="app-navigation"
:theme="theme.menuTheme"
class="elevation-1"
>
<!-- Navigation menu info -->
<div class="pa-2">
<div class="title font-weight-bold text-uppercase text-primary">{{ name }}</div>
<div class="overline text-grey">{{ version }}</div>
</div>
<!-- Navigation menu -->
<div class="py-1">
<main-menu :menu="menus"/>
</div>
<!-- Navigation menu footer -->
<template v-slot:append>
<!-- Footer navigation links -->
<div class="py-2 text-center">
<v-btn size="small"
:href="'https://next.vuetifyjs.com/en/'"
flat
>
{{ $t('menu.docs') }}
</v-btn>
</div>
</template>
</v-navigation-drawer>
<side-config-menu/>
<!-- Toolbar -->
<v-app-bar
class="overflow-visible px-2"
:theme="theme.toolbarTheme === 'global'? undefined :theme.toolbarTheme"
:flat="theme.isToolbarDetached"
:color="theme.isToolbarDetached?'background':undefined"
>
<v-card class="flex-grow-1 d-flex" :class="[theme.isToolbarDetached? 'pa-1 mt-3 mx-1' : 'pa-0 ma-0']"
:flat="!theme.isToolbarDetached"
>
<div class="d-flex flex-grow-1 align-center">
<v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
<v-spacer class="d-none d-lg-block"/>
<v-autocomplete
:placeholder="$t('menu.search')"
prepend-inner-icon="mdi-magnify"
hide-details
:items="routeStore.searchMenus"
item-title="meta.title"
item-value="path"
clearable
@update:modelValue="searchSelect"
variant="filled"
density="comfortable"
class="v-text-field-rounded"
single-line
>
</v-autocomplete>
<v-spacer class="d-none"/>
<toolbar-language/>
<div class="mr-1">
<toolbar-notifications/>
</div>
<toolbar-user/>
</div>
</v-card>
</v-app-bar>
<v-main>
<loading-progress-provider>
<v-container :fluid="!theme.isContentBoxed" class="h-100 position-relative">
<router-view v-slot="{ Component }">
<v-fade-transition mode="out-in">
<component :is="Component"/>
</v-fade-transition>
</router-view>
</v-container>
<v-footer app>
<v-spacer></v-spacer>
<div class="overline">
Built with
<v-icon small color="pink">mdi-github</v-icon>
<a class="text-decoration-none" href="https://github.com/sunhao1256/lulu-admin" target="_blank">@lulu</a>
</div>
</v-footer>
</loading-progress-provider>
</v-main>
</template>
<script setup lang="ts">
import LoadingProgressProvider from "@/components/provider/LoadingProgressLine";
import {computed} from 'vue'
import {useAppInfo, useRouterPush} from "@/composables";
const theme = useThemeStore()
const drawer = ref()
const routeStore = useRouteStore();
const menus = computed(() => routeStore.menus as App.GlobalMenuOption[]);
const {name, version} = useAppInfo();
const push = useRouterPush()
const searchSelect = (item: AuthRoute.Route) => {
if (item)
push.routerPush(item)
}
</script>
<style scoped>
.v-text-field-rounded :deep(.v-field__input) {
flex-direction: column;
justify-content: center;
}
</style>
================================================
FILE: src/layouts/ErrorLayout.vue
================================================
<template>
<div class="pa-2 pa-md-4 flex-grow-1 align-center justify-center d-flex flex-column">
<router-view/>
</div>
</template>
================================================
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<AuthRoute.AllRouteKey, 'not-found'>) => 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<boolean>('/getSmsCode', { phone });
}
export function fetchLogin(userName: string, password: string) {
return mockRequest.post<ApiAuth.Token>('/login', { userName, password });
}
export function fetchUserInfo() {
return mockRequest.get<ApiAuth.UserInfo>('/getUserInfo');
}
export function fetchUserRoutes(userId: string) {
return mockRequest.post<ApiRoute.Route>('/getUserRoutes', { userId });
}
export function fetchUpdateToken(refreshToken: string) {
return mockRequest.post<ApiAuth.Token>('/updateToken', { refreshToken });
}
================================================
FILE: src/service/api/chat.ts
================================================
import {mockRequest} from '../request';
export function fetchMessage() {
return mockRequest.post<ApiChatManagement.message>("/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<ApiUserManagement.User[]> | null): ApiCommon.PageResult<UserManagement.User[]> {
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<T, Y extends T>(transfer: (t: T) => Y) {
return (data: ApiCommon.PageResult<T[]> | null): ApiCommon.PageResult<Y[]> => {
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<ApiCommon.PageResult<ApiUserManagement.User[]> | null>('/getAllUserList');
return adapter(adapterOfFetchUserList, data);
};
export const fetchUser = async (id: string) => {
const data = await mockRequest.post<ApiUserManagement.User | null>(`/getUser/${id}`);
return adapter(adapterOfFetchUser, data);
};
export const fetchFormList = async () => {
const data = await mockRequest.post<ApiCommon.PageResult<ApiForm.Form[]> | null>(`/getAllFormList`);
return adapter(deriveFetchListAdapter<ApiForm.Form, FormManagement.Form>(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<T>(param: RequestParam): Promise<Service.RequestResult<T>> {
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<T>;
return res;
}
function get<T>(url: string, config?: AxiosRequestConfig) {
return asyncRequest<T>({url, method: 'get', axiosConfig: config});
}
function post<T>(url: string, data?: any, config?: AxiosRequestConfig) {
return asyncRequest<T>({url, method: 'post', data, axiosConfig: config});
}
function put<T>(url: string, data?: any, config?: AxiosRequestConfig) {
return asyncRequest<T>({url, method: 'put', data, axiosConfig: config});
}
function handleDelete<T>(url: string, config: AxiosRequestConfig) {
return asyncRequest<T>({url, method: 'delete', axiosConfig: config});
}
return {
get,
post,
put,
delete: handleDelete
};
}
interface RequestResultHook<T = any> {
data: Ref<T | null>;
error: Ref<Service.RequestError | null>;
loading: Ref<boolean>;
network: Ref<boolean>;
}
export function createHookRequest(axiosConfig: AxiosRequestConfig, backendConfig?: Service.BackendResultConfig) {
const customInstance = new CustomAxiosInstance(axiosConfig, backendConfig);
function useRequest<T>(param: RequestParam): RequestResultHook<T> {
const {loading, startLoading, endLoading} = useLoading();
const {bool: network, setBool: setNetwork} = useBoolean(window.navigator.onLine);
startLoading();
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref<Service.RequestError | null>(null);
function handleRequestResult(response: any) {
const res = response as Service.RequestResult<T>;
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<T>(url: string, config?: AxiosRequestConfig) {
return useRequest<T>({url, method: 'get', axiosConfig: config});
}
function post<T>(url: string, data?: any, config?: AxiosRequestConfig) {
return useRequest<T>({url, method: 'post', data, axiosConfig: config});
}
function put<T>(url: string, data?: any, config?: AxiosRequestConfig) {
return useRequest<T>({url, method: 'put', data, axiosConfig: config});
}
function handleDelete<T>(url: string, config: AxiosRequestConfig) {
return useRequest<T>({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<Auth.RoleType, { userName: string; password: string }> = {
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<K extends keyof ModelerStore>(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<T = any> {
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<User['gender']>;
/**
* 用户状态
* - 1: 启用
* - 2: 禁用
* - 3: 冻结
* - 4: 软删除
*/
type UserStatusKey = NonNullable<User['userStatus']>;
}
declare namespace FormManagement {
interface Form extends ApiForm.Form {
}
type FormStatusKey = NonNullable<Form['status']>;
}
================================================
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<Menu[]>
}
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<RouteKey,
| '403'
| '404'
| '500'
| 'login'
| 'not-found'
| 'dashboard_analytics'
| 'apps_manager-user_list'
| 'apps_manager-user_edit'
| 'apps_board'
| 'apps_todo_tasks'
| 'apps_todo_completed'
| 'apps_todo_label'
| 'apps_chat-channel'
| 'pages_error_notfound'
| 'pages_error_unexpected'
| 'other_menu-levels-2-1'
| 'other_menu-levels-3-1'
| 'other_menu-levels-3-2'
| 'flowable_design'
| 'form_list'
| 'form_design'
>;
}
================================================
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<K extends AllRouteKey = AllRouteKey> = AuthRouteUtils.GetRoutePath<K>;
type RouteComponentType = 'basic' | 'blank' | 'self' | 'auth' | 'error' | 'todo' | 'chat';
interface RouteMeta<K extends AuthRoute.RoutePath> {
title: string;
dynamicPath?: AuthRouteUtils.GetDynamicPath<K>;
singleLayout?: Extract<RouteComponentType, 'basic' | 'blank' | 'auth' | 'error' | 'todo' | 'chat'>;
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 = AllRouteKey> = K extends AllRouteKey
? {
name: K;
path: AuthRouteUtils.GetRoutePath<K>;
redirect?: AuthRouteUtils.GetRoutePath;
/**
* 路由组件
* - basic: 基础布局,具有公共部分的布局
* - blank: 空白布局
* - multi: 多级路由布局(三级路由或三级以上时,除第一级路由和最后一级路由,其余的采用该布局)
* - self: 作为子路由,使用自身的布局(作为最后一级路由,没有子路由)
*/
component?: RouteComponentType;
children?: Route[];
meta: RouteMeta<AuthRouteUtils.GetRoutePath<K>>;
} & Omit<import('vue-router').RouteRecordRaw, 'name' | 'path' | 'redirect' | 'component' | 'children' | 'meta'>
: never;
type RouteModule = Record<string, { default: Route }>;
}
declare namespace AuthRouteUtils {
type RouteKeySplitMark = '_';
type RoutePathSplitMark = '/';
type BlankString = '';
/** key转换成path */
type KeyToPath<K extends string> = 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 = AuthRoute.AllRouteKey> = K extends AuthRoute.AllRouteKey
? K extends AuthRoute.RootRouteKey
? AuthRoute.RootRoutePath
: K extends AuthRoute.NotFoundRouteKey
? AuthRoute.NotFoundRoutePath
: KeyToPath<K>
: never;
/** 获取一级路由(有子路由的一级路由和没有子路由的路由) */
type GetFirstDegreeRouteKey<K extends AuthRoute.RouteKey = AuthRoute.RouteKey> =
K extends `${infer _Left}${RouteKeySplitMark}${infer _Right}` ? never : K;
/** 获取有子路由的一级路由 */
type GetFirstDegreeRouteKeyWithChildren<K extends AuthRoute.RouteKey = AuthRoute.RouteKey> =
K extends `${infer Left}${RouteKeySplitMark}${infer _Right}` ? Left : never;
/** 单级路由的key (单级路由需要添加一个父级路由用于应用布局组件) */
type SingleRouteKey = Exclude<GetFirstDegreeRouteKey,
GetFirstDegreeRouteKeyWithChildren | AuthRoute.RootRouteKey | AuthRoute.NotFoundRouteKey>;
/** 单独路由父级路由key */
type SingleRouteParentKey = `${SingleRouteKey}-parent`;
/** 单独路由父级路由path */
type SingleRouteParentPath = KeyToPath<SingleRouteParentKey>;
/** 获取路由动态路径 */
type GetDynamicPath<P extends AuthRoute.RoutePath> =
| `${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<AuthRoute.RoutePath> {}
}
================================================
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<T = any> {
/** 请求错误 */
error: null;
/** 请求数据 */
data: T;
}
/** 自定义的请求失败结果 */
interface FailedResult {
/** 请求错误 */
error: RequestError;
/** 请求数据 */
data: null;
}
/** 自定义的请求结果 */
type RequestResult<T = any> = SuccessResult<T> | FailedResult;
/** 多个请求数据结果 */
type MultiRequestResult<T extends any[]> = T extends [infer First, ...infer Rest]
? [First] extends [any]
? Rest extends any[]
? [Service.RequestResult<First>, ...MultiRequestResult<Rest>]
: [Service.RequestResult<First>]
: Rest extends any[]
? MultiRequestResult<Rest>
: []
: [];
/** 请求结果的适配器函数 */
type ServiceAdapter<T = any, A extends any[] = any> = (...args: A) => T;
/** mock示例接口类型:后端接口返回的数据的类型 */
interface MockServiceResult<T = any> {
/** 状态码 */
code: string | number;
/** 接口数据 */
data: T;
/** 接口消息 */
message: string;
}
/** mock的响应option */
interface MockOption {
url: Record<string, any>;
body: Record<string, any>;
query: Record<string, any>;
headers: Record<string, any>;
}
}
/** 主题相关类型 */
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<import('vue-router').RouteLocationNormalizedLoaded, 'name' | 'fullPath' | 'meta'> {
/** 滚动的位置 */
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<T, K extends keyof T> = K extends keyof T ? true : false;
type GetFunArgs<F extends Noop> = F extends (...args: infer P) => any ? P : never;
type Writable<T> = { [K in keyof T]: T[K] };
type FirstOfArray<T extends any[]> = T extends [infer First, ...infer _Rest] ? First : never;
type LastOfArray<T extends any[]> = T extends [...infer _Rest, infer Last] ? Last : never;
// union to tuple
type Union2IntersectionFn<T> = (T extends unknown ? (k: () => T) => void : never) extends (k: infer R) => void
? R
: never;
type GetUnionLast<U> = Union2IntersectionFn<U> extends () => infer I ? I : never;
type UnionToTuple<T, R extends any[] = []> = [T] extends [never]
? R
: UnionToTuple<Exclude<T, GetUnionLast<T>>, [GetUnionLast<T>, ...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<T extends number>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.number;
}
export function isString<T extends string>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.string;
}
export function isBoolean<T extends boolean>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.boolean;
}
export function isNull<T extends null>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.null;
}
export function isUndefined<T extends undefined>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.undefined;
}
export function isObject<T extends Record<string, any>>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.object;
}
export function isArray<T extends any[]>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.array;
}
export function isFunction<T extends (...args: any[]) => any | void | never>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.function;
}
export function isDate<T extends Date>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.date;
}
export function isRegExp<T extends RegExp>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.regexp;
}
export function isPromise<T extends Promise<any>>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.promise;
}
export function isSet<T extends Set<any>>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.set;
}
export function isMap<T extends Map<any, any>>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.map;
}
export function isFile<T extends File>(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 Boolea
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
SYMBOL INDEX (390 symbols across 102 files)
FILE: .env-config.ts
type ServiceEnv (line 2) | type ServiceEnv = Record<ServiceEnvType, ServiceEnvConfig>;
function getServiceEnvConfig (line 30) | function getServiceEnvConfig(env: ImportMetaEnv) {
FILE: build/plugins/index.ts
function setupVitePlugins (line 7) | function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | Plugi...
FILE: build/plugins/unplugin.ts
function unplugin (line 12) | function unplugin(viteEnv: ImportMetaEnv) {
FILE: build/plugins/vuetify.ts
function vuetifyPlugin (line 3) | function vuetifyPlugin() {
FILE: build/utils/index.ts
function getRootPath (line 7) | function getRootPath() {
function getSrcPath (line 16) | function getSrcPath(srcName = 'src') {
FILE: mock/index.ts
function setupMockServer (line 4) | function setupMockServer() {
FILE: mock/model/auth.ts
type UserModel (line 1) | interface UserModel extends Auth.UserInfo {
FILE: src/components/provider/DialogProvider.tsx
type ContentType (line 7) | interface ContentType {
type DialogReactive (line 20) | interface DialogReactive {
type DialogApiInjection (line 31) | interface DialogApiInjection {
method setup (line 38) | setup() {
method render (line 94) | render() {
function useDialog (line 147) | function useDialog(): DialogApiInjection {
FILE: src/components/provider/LoadingOverlyProvider.tsx
type LoadingOverlyApiInjection (line 6) | interface LoadingOverlyApiInjection {
method setup (line 13) | setup() {
method render (line 29) | render() {
function useLoadingOverly (line 48) | function useLoadingOverly(): LoadingOverlyApiInjection {
FILE: src/components/provider/LoadingProgressLine.tsx
type LoadingProgressLineApiInjection (line 6) | interface LoadingProgressLineApiInjection {
method setup (line 13) | setup() {
method render (line 31) | render() {
function useLoadingProgressLine (line 48) | function useLoadingProgressLine(): LoadingProgressLineApiInjection {
FILE: src/components/provider/SnackbarProvider.tsx
type ContentType (line 7) | type ContentType = string | (() => VNodeChild)
type SnackBarReactive (line 11) | interface SnackBarReactive {
type SnackBarApiInjection (line 20) | interface SnackBarApiInjection {
method setup (line 29) | setup() {
method render (line 79) | render() {
function useSnackBar (line 120) | function useSnackBar(): SnackBarApiInjection {
FILE: src/composables/events.ts
function useGlobalEvents (line 5) | function useGlobalEvents() {
FILE: src/composables/router.ts
function useRouterPush (line 9) | function useRouterPush(inSetup = true) {
FILE: src/composables/system.ts
type AppInfo (line 6) | interface AppInfo {
type Package (line 13) | interface Package {
function useAppInfo (line 20) | function useAppInfo(): AppInfo {
function useDeviceInfo (line 32) | function useDeviceInfo() {
function usePermission (line 39) | function usePermission() {
FILE: src/configs/service.ts
constant REQUEST_TIMEOUT (line 1) | const REQUEST_TIMEOUT = 60 * 1000;
constant ERROR_MSG_DURATION (line 3) | const ERROR_MSG_DURATION = 3 * 1000;
constant DEFAULT_REQUEST_ERROR_CODE (line 5) | const DEFAULT_REQUEST_ERROR_CODE = 'DEFAULT';
constant DEFAULT_REQUEST_ERROR_MSG (line 7) | const DEFAULT_REQUEST_ERROR_MSG = '请求错误~';
constant REQUEST_TIMEOUT_CODE (line 9) | const REQUEST_TIMEOUT_CODE = 'ECONNABORTED';
constant REQUEST_TIMEOUT_MSG (line 11) | const REQUEST_TIMEOUT_MSG = '请求超时~';
constant NETWORK_ERROR_CODE (line 13) | const NETWORK_ERROR_CODE = 'NETWORK_ERROR';
constant NETWORK_ERROR_MSG (line 15) | const NETWORK_ERROR_MSG = '网络不可用~';
constant ERROR_STATUS (line 17) | const ERROR_STATUS = {
constant NO_ERROR_MSG_CODE (line 33) | const NO_ERROR_MSG_CODE: (string | number)[] = [];
constant REFRESH_TOKEN_CODE (line 35) | const REFRESH_TOKEN_CODE: (string | number)[] = [66666];
FILE: src/enum/business.ts
type EnumUserRole (line 1) | enum EnumUserRole {
type EnumLoginModule (line 6) | enum EnumLoginModule {
FILE: src/enum/common.ts
type EnumContentType (line 1) | enum EnumContentType {
type EnumDataType (line 7) | enum EnumDataType {
FILE: src/enum/system.ts
type EnumLayoutComponentName (line 1) | enum EnumLayoutComponentName {
type EnumThemeLayoutMode (line 10) | enum EnumThemeLayoutMode {
type EnumThemeTabMode (line 17) | enum EnumThemeTabMode {
type EnumThemeHorizontalMenuPosition (line 22) | enum EnumThemeHorizontalMenuPosition {
type EnumThemeAnimateMode (line 28) | enum EnumThemeAnimateMode {
FILE: src/filters/formatCurrency.ts
function formatCurrency (line 3) | function formatCurrency(value: number, currency?: CurrencyConfig.Currenc...
function formatPrice (line 12) | function formatPrice(price: number, currency: CurrencyConfig.Currency) {
function numberFormat (line 40) | function numberFormat(number: number, decimals: number, dec_point: strin...
FILE: src/filters/index.ts
function registerFilters (line 3) | function registerFilters(app: App) {
FILE: src/hooks/common/useBoolean.ts
function useBoolean (line 3) | function useBoolean(initValue = false) {
FILE: src/hooks/common/useBreadcrumb.ts
function useBreadcrumb (line 8) | function useBreadcrumb(rootPath: Exclude<AuthRoute.AllRouteKey, 'not-fou...
FILE: src/hooks/common/useContext.ts
function useContext (line 4) | function useContext<T>(contextName = 'context') {
FILE: src/hooks/common/useLoading.ts
function useLoading (line 3) | function useLoading(initValue = false) {
FILE: src/hooks/common/useLoadingEmpty.ts
function useLoadingEmpty (line 3) | function useLoadingEmpty(initLoading = false, initEmpty = false) {
FILE: src/hooks/common/useReload.ts
function useReload (line 4) | function useReload() {
FILE: src/main.ts
function setupApp (line 13) | async function setupApp() {
FILE: src/plugins/animate.ts
function animate (line 1) | function animate(node: HTMLElement, animationName: string, callBack?: ()...
FILE: src/plugins/clipboard.ts
function clipboard (line 1) | function clipboard(text: string, toastText = 'Copied to Clipboard') {
FILE: src/plugins/index.ts
function registerPlugins (line 14) | function registerPlugins(app: App) {
FILE: src/router/guard/dynamic.ts
function createDynamicRouteGuard (line 6) | async function createDynamicRouteGuard(
FILE: src/router/guard/index.ts
function createRouterGuard (line 6) | function createRouterGuard(router: Router) {
FILE: src/router/guard/permission.ts
function createPermissionGuard (line 7) | async function createPermissionGuard(
FILE: src/router/index.ts
function setupRouter (line 18) | async function setupRouter(app: App) {
FILE: src/router/routes/index.ts
constant ROOT_ROUTE (line 3) | const ROOT_ROUTE: AuthRoute.Route = {
FILE: src/service/api/auth.ts
function fetchSmsCode (line 3) | function fetchSmsCode(phone: string) {
function fetchLogin (line 7) | function fetchLogin(userName: string, password: string) {
function fetchUserInfo (line 11) | function fetchUserInfo() {
function fetchUserRoutes (line 15) | function fetchUserRoutes(userId: string) {
function fetchUpdateToken (line 19) | function fetchUpdateToken(refreshToken: string) {
FILE: src/service/api/chat.ts
function fetchMessage (line 3) | function fetchMessage() {
FILE: src/service/api/management.adapter.ts
function adapterOfFetchUserList (line 1) | function adapterOfFetchUserList(data: ApiCommon.PageResult<ApiUserManage...
function adapterOfFetchUser (line 23) | function adapterOfFetchUser(data: ApiUserManagement.User | null): UserMa...
function deriveFetchListAdapter (line 33) | function deriveFetchListAdapter<T, Y extends T>(transfer: (t: T) => Y) {
FILE: src/service/request/helpers.ts
function handleRefreshToken (line 10) | async function handleRefreshToken(axiosConfig: AxiosRequestConfig) {
FILE: src/service/request/instance.ts
class CustomAxiosInstance (line 14) | class CustomAxiosInstance {
method constructor (line 19) | constructor(
method setInterceptor (line 33) | setInterceptor() {
FILE: src/service/request/request.ts
type RequestMethod (line 7) | type RequestMethod = 'get' | 'post' | 'put' | 'delete';
type RequestParam (line 9) | interface RequestParam {
function createRequest (line 16) | function createRequest(axiosConfig: AxiosRequestConfig, backendConfig?: ...
type RequestResultHook (line 58) | interface RequestResultHook<T = any> {
function createHookRequest (line 65) | function createHookRequest(axiosConfig: AxiosRequestConfig, backendConfi...
function getRequestResponse (line 124) | async function getRequestResponse(params: {
FILE: src/store/auth/helpers.ts
function getToken (line 3) | function getToken() {
function getUserInfo (line 7) | function getUserInfo() {
function clearAuthStorage (line 18) | function clearAuthStorage() {
FILE: src/store/auth/index.ts
type AuthState (line 11) | interface AuthState {
method isLogin (line 24) | isLogin(state) {
method resetAuthStore (line 29) | resetAuthStore() {
method handleActionAfterLogin (line 47) | async handleActionAfterLogin(backendToken: ApiAuth.Token) {
method loginByToken (line 68) | async loginByToken(backendToken: ApiAuth.Token) {
method login (line 88) | async login(userName: string, password: string) {
method updateUserRole (line 97) | async updateUserRole(userRole: Auth.RoleType) {
FILE: src/store/flow/index.ts
type ModelerStore (line 6) | type ModelerStore = {
method setModeler (line 41) | setModeler(modeler: any) {
method setModules (line 44) | setModules<K extends keyof ModelerStore>(key: K, module: any) {
method setElement (line 47) | setElement(element: Base, id: string) {
FILE: src/store/index.ts
function setupStore (line 4) | function setupStore(app: App) {
FILE: src/store/route/index.ts
type RouteState (line 19) | interface RouteState {
method resetRouteStore (line 38) | resetRouteStore() {
method resetRoutes (line 42) | resetRoutes() {
method isConstantRoute (line 51) | isConstantRoute(name: AuthRoute.AllRouteKey) {
method isValidConstantRoute (line 55) | isValidConstantRoute(name: AuthRoute.AllRouteKey) {
method handleAuthRoute (line 60) | handleAuthRoute(routes: AuthRoute.Route[]) {
method handleUpdateRootRedirect (line 72) | handleUpdateRootRedirect(routeKey: AuthRoute.AllRouteKey) {
method initDynamicRoute (line 82) | async initDynamicRoute() {
method initStaticRoute (line 99) | async initStaticRoute() {
method initAuthRoute (line 107) | async initAuthRoute() {
FILE: src/store/subscribe/index.ts
function subscribeStore (line 3) | function subscribeStore() {
FILE: src/store/subscribe/theme.ts
function subscribeThemeStore (line 5) | function subscribeThemeStore() {
FILE: src/store/theme/helpers.ts
function initThemeSettings (line 4) | function initThemeSettings() {
FILE: src/store/theme/index.ts
method cacheThemeSettings (line 12) | cacheThemeSettings() {
FILE: src/typings/api.d.ts
type PageResult (line 2) | interface PageResult<T = any> {
type Token (line 11) | interface Token {
type UserInfo (line 16) | type UserInfo = Auth.UserInfo;
type Route (line 20) | interface Route {
type Form (line 27) | interface Form {
type Address (line 38) | interface Address {
type User (line 46) | interface User {
type message (line 63) | interface message {
type message (line 76) | interface message {
type message (line 89) | interface message {
FILE: src/typings/business.d.ts
type RoleType (line 2) | type RoleType = keyof typeof import('@/enum').EnumUserRole;
type UserInfo (line 4) | interface UserInfo {
type User (line 13) | interface User extends ApiUserManagement.User {
type GenderKey (line 22) | type GenderKey = NonNullable<User['gender']>;
type UserStatusKey (line 31) | type UserStatusKey = NonNullable<User['userStatus']>;
type Form (line 35) | interface Form extends ApiForm.Form {
type FormStatusKey (line 38) | type FormStatusKey = NonNullable<Form['status']>;
FILE: src/typings/config.d.ts
type Config (line 1) | interface Config {
type Currency (line 8) | interface Currency {
type Config (line 18) | interface Config {
type Config (line 25) | interface Config {
type Menu (line 60) | interface Menu {
type Config (line 70) | interface Config {
type Footer (line 75) | interface Footer {
FILE: src/typings/env.d.ts
type ServiceEnvType (line 1) | type ServiceEnvType = 'dev' | 'test' | 'prod';
type ServiceEnvConfig (line 3) | interface ServiceEnvConfig {
type ImportMetaEnv (line 10) | interface ImportMetaEnv {
type ImportMeta (line 29) | interface ImportMeta {
FILE: src/typings/filter.d.ts
type formatCurrency (line 2) | type formatCurrency = (value: number, currency?: CurrencyConfig.Currency...
type filtersConfig (line 5) | interface filtersConfig {
type ComponentCustomProperties (line 10) | interface ComponentCustomProperties {
FILE: src/typings/global.d.ts
type Window (line 1) | interface Window {
type StrategyAction (line 8) | type StrategyAction = [boolean, () => void];
FILE: src/typings/page-route.d.ts
type RootRouteKey (line 6) | type RootRouteKey = 'root';
type NotFoundRouteKey (line 12) | type NotFoundRouteKey = 'not-found';
type RouteKey (line 18) | type RouteKey =
type LastDegreeRouteKey (line 58) | type LastDegreeRouteKey = Extract<RouteKey,
FILE: src/typings/route.d.ts
type RootRoutePath (line 2) | type RootRoutePath = '/';
type NotFoundRoutePath (line 4) | type NotFoundRoutePath = '/:pathMatch(.*)*';
type RootRouteKey (line 6) | type RootRouteKey = PageRoute.RootRouteKey;
type NotFoundRouteKey (line 8) | type NotFoundRouteKey = PageRoute.NotFoundRouteKey;
type RouteKey (line 10) | type RouteKey = PageRoute.RouteKey;
type LastDegreeRouteKey (line 12) | type LastDegreeRouteKey = PageRoute.LastDegreeRouteKey;
type AllRouteKey (line 14) | type AllRouteKey = RouteKey | RootRouteKey | NotFoundRouteKey;
type RoutePath (line 16) | type RoutePath<K extends AllRouteKey = AllRouteKey> = AuthRouteUtils.Get...
type RouteComponentType (line 18) | type RouteComponentType = 'basic' | 'blank' | 'self' | 'auth' | 'error' ...
type RouteMeta (line 20) | interface RouteMeta<K extends AuthRoute.RoutePath> {
type Route (line 37) | type Route<K extends AllRouteKey = AllRouteKey> = K extends AllRouteKey
type RouteModule (line 55) | type RouteModule = Record<string, { default: Route }>;
type RouteKeySplitMark (line 59) | type RouteKeySplitMark = '_';
type RoutePathSplitMark (line 61) | type RoutePathSplitMark = '/';
type BlankString (line 63) | type BlankString = '';
type KeyToPath (line 66) | type KeyToPath<K extends string> = K extends `${infer _Left}${RouteKeySp...
type GetRoutePath (line 77) | type GetRoutePath<K extends AuthRoute.AllRouteKey = AuthRoute.AllRouteKe...
type GetFirstDegreeRouteKey (line 86) | type GetFirstDegreeRouteKey<K extends AuthRoute.RouteKey = AuthRoute.Rou...
type GetFirstDegreeRouteKeyWithChildren (line 90) | type GetFirstDegreeRouteKeyWithChildren<K extends AuthRoute.RouteKey = A...
type SingleRouteKey (line 94) | type SingleRouteKey = Exclude<GetFirstDegreeRouteKey,
type SingleRouteParentKey (line 98) | type SingleRouteParentKey = `${SingleRouteKey}-parent`;
type SingleRouteParentPath (line 101) | type SingleRouteParentPath = KeyToPath<SingleRouteParentKey>;
type GetDynamicPath (line 104) | type GetDynamicPath<P extends AuthRoute.RoutePath> =
FILE: src/typings/router.d.ts
type RouteMeta (line 4) | interface RouteMeta extends AuthRoute.RouteMeta<AuthRoute.RoutePath> {}
FILE: src/typings/storage.d.ts
type Session (line 3) | interface Session {
type Local (line 8) | interface Local {
FILE: src/typings/system.d.ts
type LayoutComponentName (line 4) | type LayoutComponentName = keyof typeof import('@/enum').EnumLayoutCompo...
type ThemeLayoutMode (line 7) | type ThemeLayoutMode = keyof typeof import('@/enum').EnumThemeLayoutMode;
type ThemeTabMode (line 10) | type ThemeTabMode = keyof typeof import('@/enum').EnumThemeTabMode;
type ThemeHorizontalMenuPosition (line 13) | type ThemeHorizontalMenuPosition = keyof typeof import('@/enum').EnumThe...
type ThemeAnimateMode (line 16) | type ThemeAnimateMode = keyof typeof import('@/enum').EnumThemeAnimateMode;
type LoginModuleKey (line 19) | type LoginModuleKey = keyof typeof import('@/enum').EnumLoginModule;
type RequestErrorType (line 30) | type RequestErrorType = 'axios' | 'http' | 'backend';
type RequestError (line 33) | interface RequestError {
type BackendResultConfig (line 43) | interface BackendResultConfig {
type SuccessResult (line 55) | interface SuccessResult<T = any> {
type FailedResult (line 63) | interface FailedResult {
type RequestResult (line 71) | type RequestResult<T = any> = SuccessResult<T> | FailedResult;
type MultiRequestResult (line 74) | type MultiRequestResult<T extends any[]> = T extends [infer First, ...in...
type ServiceAdapter (line 85) | type ServiceAdapter<T = any, A extends any[] = any> = (...args: A) => T;
type MockServiceResult (line 88) | interface MockServiceResult<T = any> {
type MockOption (line 98) | interface MockOption {
type Layout (line 110) | interface Layout {
type LayoutModeList (line 119) | interface LayoutModeList {
type OtherColor (line 125) | interface OtherColor {
type Header (line 137) | interface Header {
type Crumb (line 147) | interface Crumb {
type Tab (line 155) | interface Tab {
type ThemeTabModeList (line 169) | interface ThemeTabModeList {
type Sider (line 175) | interface Sider {
type Menu (line 191) | interface Menu {
type HorizontalMenuPositionList (line 199) | interface HorizontalMenuPositionList {
type Footer (line 205) | interface Footer {
type Page (line 215) | interface Page {
type AnimateModeList (line 225) | interface AnimateModeList {
type GlobalHeaderProps (line 233) | interface GlobalHeaderProps {
type GlobalMenuOption (line 242) | type GlobalMenuOption = {
type GlobalBreadcrumb (line 252) | type GlobalBreadcrumb = BreadcrumbItem
type GlobalTabRoute (line 255) | interface GlobalTabRoute
type Schema (line 266) | interface Schema {
FILE: src/typings/utils.d.ts
type Noop (line 2) | type Noop = (...args: any) => any;
type UnionInclude (line 4) | type UnionInclude<T, K extends keyof T> = K extends keyof T ? true : false;
type GetFunArgs (line 6) | type GetFunArgs<F extends Noop> = F extends (...args: infer P) => any ? ...
type Writable (line 8) | type Writable<T> = { [K in keyof T]: T[K] };
type FirstOfArray (line 10) | type FirstOfArray<T extends any[]> = T extends [infer First, ...infer _R...
type LastOfArray (line 12) | type LastOfArray<T extends any[]> = T extends [...infer _Rest, infer Las...
type Union2IntersectionFn (line 15) | type Union2IntersectionFn<T> = (T extends unknown ? (k: () => T) => void...
type GetUnionLast (line 18) | type GetUnionLast<U> = Union2IntersectionFn<U> extends () => infer I ? I...
type UnionToTuple (line 20) | type UnionToTuple<T, R extends any[] = []> = [T] extends [never]
FILE: src/typings/vuetify.d.ts
type BreadcrumbItem (line 3) | type BreadcrumbItem = import('vuetify/components').VBreadcrumbsItem
type DataTableHeader (line 4) | type DataTableHeader = import('vuetify/labs/VDataTable').VDataTable
type Snackbar (line 5) | type Snackbar = import('vuetify/components').VSnackbar
type Dialog (line 6) | type Dialog = import('vuetify/components').VDialog
type VCardTitle (line 7) | type VCardTitle = import('vuetify/components').VCardTitle
type VTextField (line 8) | type VTextField = import('vuetify/components').VTextField
type VSelect (line 9) | type VSelect = import('vuetify/components').VSelect
type VRadio (line 10) | type VRadio= import('vuetify/components').VRadio
type VCheckbox (line 11) | type VCheckbox= import('vuetify/components').VCheckbox
type VBtn (line 12) | type VBtn = import('vuetify/components').VBtn
type VCol (line 13) | type VCol= import('vuetify/components').VCol
FILE: src/utils/common/pattern.ts
function exeStrategyActions (line 1) | function exeStrategyActions(actions: Common.StrategyAction[]) {
FILE: src/utils/common/typeof.ts
function isNumber (line 3) | function isNumber<T extends number>(data: T | unknown): data is T {
function isString (line 7) | function isString<T extends string>(data: T | unknown): data is T {
function isBoolean (line 11) | function isBoolean<T extends boolean>(data: T | unknown): data is T {
function isNull (line 15) | function isNull<T extends null>(data: T | unknown): data is T {
function isUndefined (line 19) | function isUndefined<T extends undefined>(data: T | unknown): data is T {
function isObject (line 23) | function isObject<T extends Record<string, any>>(data: T | unknown): dat...
function isArray (line 27) | function isArray<T extends any[]>(data: T | unknown): data is T {
function isFunction (line 31) | function isFunction<T extends (...args: any[]) => any | void | never>(da...
function isDate (line 35) | function isDate<T extends Date>(data: T | unknown): data is T {
function isRegExp (line 39) | function isRegExp<T extends RegExp>(data: T | unknown): data is T {
function isPromise (line 43) | function isPromise<T extends Promise<any>>(data: T | unknown): data is T {
function isSet (line 47) | function isSet<T extends Set<any>>(data: T | unknown): data is T {
function isMap (line 51) | function isMap<T extends Map<any, any>>(data: T | unknown): data is T {
function isFile (line 55) | function isFile<T extends File>(data: T | unknown): data is T {
FILE: src/utils/crypto/index.ts
function encrypto (line 9) | function encrypto(data: any) {
function decrypto (line 18) | function decrypto(cipherText: string) {
FILE: src/utils/flow/EventEmitter.ts
class EventEmitter (line 6) | class EventEmitter {
method constructor (line 9) | constructor() {
method _addListener (line 12) | static _addListener(type: string, fn: any, context?: any, once?: any) {
method addListener (line 35) | static addListener(type: string, fn: any, context?: any) {
method on (line 39) | static on(type: string, fn: any, context?: any) {
method once (line 43) | static once(type: string, fn: any, context?: any) {
method emit (line 47) | static emit(type: any, ...rest: any[]) {
method removeListener (line 73) | static removeListener(type: any, fn: any) {
method removeAllListeners (line 109) | static removeAllListeners(type: any) {
method listeners (line 128) | static listeners(type: any) {
method listenerCount (line 140) | static listenerCount(type: any) {
method eventNames (line 148) | static eventNames() {
FILE: src/utils/flow/tools.ts
function noop (line 2) | function noop() {
function notEmpty (line 10) | function notEmpty(val: any) {
function notNull (line 23) | function notNull(val: any) {
function getRawType (line 32) | function getRawType(value: any) {
FILE: src/utils/router/auth.ts
function filterAuthRoutesByUserPermission (line 1) | function filterAuthRoutesByUserPermission(routes: AuthRoute.Route[], per...
function filterAuthRouteByUserPermission (line 5) | function filterAuthRouteByUserPermission(route: AuthRoute.Route, permiss...
FILE: src/utils/router/breadcrumb.ts
function getBreadcrumbByRouteKey (line 7) | function getBreadcrumbByRouteKey(activeKey: string, menus: App.GlobalMen...
function getBreadcrumbsByPredicate (line 13) | function getBreadcrumbsByPredicate(predicate: (menu: App.GlobalMenuOptio...
function getBreadcrumbMenu (line 24) | function getBreadcrumbMenu(predicate: (menu: App.GlobalMenuOption) => bo...
function getBreadcrumbMenuItem (line 41) | function getBreadcrumbMenuItem(predicate: (menu: App.GlobalMenuOption) =...
function transformBreadcrumbMenuToBreadcrumb (line 60) | function transformBreadcrumbMenuToBreadcrumb(menu: App.GlobalMenuOption,...
FILE: src/utils/router/cache.ts
function getCacheRoutes (line 7) | function getCacheRoutes(routes: RouteRecordRaw[]) {
function isKeepAlive (line 26) | function isKeepAlive(route: RouteRecordRaw) {
function hasChildren (line 33) | function hasChildren(route: RouteRecordRaw) {
FILE: src/utils/router/component.ts
type Lazy (line 6) | type Lazy<T> = () => Promise<T>;
type ModuleComponent (line 8) | interface ModuleComponent {
type LayoutComponent (line 12) | type LayoutComponent = Record<EnumType.LayoutComponentName, Lazy<ModuleC...
function getLayoutComponent (line 18) | function getLayoutComponent(layoutType: EnumType.LayoutComponentName) {
function getViewComponent (line 34) | function getViewComponent(routeKey: AuthRoute.LastDegreeRouteKey) {
function setViewComponentName (line 41) | function setViewComponentName(component: RouteComponent | Lazy<ModuleCom...
function isAsyncComponent (line 55) | function isAsyncComponent(component: RouteComponent | Lazy<ModuleCompone...
FILE: src/utils/router/helpers.ts
function getConstantRouteNames (line 5) | function getConstantRouteNames(routes: AuthRoute.Route[]) {
function getConstantRouteName (line 13) | function getConstantRouteName(route: AuthRoute.Route) {
FILE: src/utils/router/menu.ts
function transformAuthRouteToMenu (line 5) | function transformAuthRouteToMenu(routes: AuthRoute.Route[]): App.Global...
function getActiveKeyPathsOfMenus (line 40) | function getActiveKeyPathsOfMenus(activeKey: string, menus: App.GlobalMe...
function getActiveKeyPathsOfMenu (line 45) | function getActiveKeyPathsOfMenu(activeKey: string, menu: App.GlobalMenu...
function hideInMenu (line 57) | function hideInMenu(route: AuthRoute.Route) {
function addPartialProps (line 62) | function addPartialProps(config: {
FILE: src/utils/router/module.ts
function sortRoutes (line 5) | function sortRoutes(routes: AuthRoute.Route[]) {
function handleModuleRoutes (line 19) | function handleModuleRoutes(modules: AuthRoute.RouteModule) {
FILE: src/utils/router/regexp.ts
function getLoginModuleRegExp (line 2) | function getLoginModuleRegExp() {
FILE: src/utils/router/transform.ts
function transformAuthRouteToVueRoutes (line 5) | function transformAuthRouteToVueRoutes(routes: AuthRoute.Route[]) {
type ComponentAction (line 9) | type ComponentAction = Record<AuthRoute.RouteComponentType, () => void>;
function transformAuthRouteToVueRoute (line 11) | function transformAuthRouteToVueRoute(item: AuthRoute.Route) {
function transformAuthRouteToSearchMenus (line 129) | function transformAuthRouteToSearchMenus(routes: AuthRoute.Route[], tree...
function transformRouteNameToRoutePath (line 150) | function transformRouteNameToRoutePath(name: Exclude<AuthRoute.AllRouteK...
function transformRoutePathToRouteName (line 161) | function transformRoutePathToRouteName<K extends AuthRoute.RoutePath>(pa...
function hasHref (line 172) | function hasHref(item: AuthRoute.Route) {
function hasDynamicPath (line 176) | function hasDynamicPath(item: AuthRoute.Route) {
function hasComponent (line 180) | function hasComponent(item: AuthRoute.Route) {
function hasChildren (line 184) | function hasChildren(item: AuthRoute.Route) {
function isSingleRoute (line 188) | function isSingleRoute(item: AuthRoute.Route) {
FILE: src/utils/service/error.ts
type ErrorStatus (line 14) | type ErrorStatus = keyof typeof ERROR_STATUS;
function handleAxiosError (line 20) | function handleAxiosError(axiosError: AxiosError) {
function handleResponseError (line 64) | function handleResponseError(response: AxiosResponse) {
function handleBackendError (line 90) | function handleBackendError(backendResult: Record<string, any>, config: ...
FILE: src/utils/service/handler.ts
function handleServiceResult (line 2) | async function handleServiceResult<T = any>(error: Service.RequestError ...
function adapter (line 18) | function adapter<T extends Service.ServiceAdapter>(
FILE: src/utils/service/msg.ts
function addErrorMsg (line 6) | function addErrorMsg(error: Service.RequestError) {
function removeErrorMsg (line 9) | function removeErrorMsg(error: Service.RequestError) {
function hasErrorMsg (line 12) | function hasErrorMsg(error: Service.RequestError) {
function showErrorMsg (line 20) | function showErrorMsg(error: Service.RequestError) {
FILE: src/utils/service/transform.ts
function transformRequestData (line 11) | async function transformRequestData(requestData: any, contentType?: stri...
function handleFormData (line 26) | async function handleFormData(data: Record<string, any>) {
function transformFile (line 48) | async function transformFile(formData: FormData, key: string, file: File...
FILE: src/utils/storage/local.ts
type StorageData (line 2) | interface StorageData<T> {
function createLocalStorage (line 7) | function createLocalStorage<T extends StorageInterface.Local = StorageIn...
FILE: src/utils/storage/session.ts
function setSession (line 3) | function setSession(key: string, value: unknown) {
function getSession (line 8) | function getSession<T>(key: string) {
function removeSession (line 21) | function removeSession(key: string) {
function clearSession (line 25) | function clearSession() {
function createSessionStorage (line 29) | function createSessionStorage<T extends StorageInterface.Session = Stora...
FILE: src/views/board/types.ts
type state (line 1) | type state = 'TODO' | 'INPROGRESS' | 'TESTING' | 'DONE'
type card (line 2) | interface card {
FILE: src/views/flowable/bo-utils/conditionalUtil.ts
constant CONDITIONAL_SOURCES (line 6) | const CONDITIONAL_SOURCES = [
function isNotConditional (line 14) | function isNotConditional(element: any) {
function isConditionalSource (line 21) | function isConditionalSource(element: any) {
function getConditionalEventDefinition (line 25) | function getConditionalEventDefinition(element: any) {
function getEventDefinition (line 33) | function getEventDefinition(element: any, eventType: string) {
FILE: src/views/flowable/bo-utils/documentationUtil.ts
function getDocumentValue (line 5) | function getDocumentValue(element: Base): string {
function setDocumentValue (line 11) | function setDocumentValue(element: Base, value: string | undefined) {
constant DOCUMENTATION_TEXT_FORMAT (line 42) | const DOCUMENTATION_TEXT_FORMAT = 'text/plain'
function findDocumentation (line 44) | function findDocumentation(docs: any) {
FILE: src/views/flowable/bo-utils/idUtil.ts
function getIdValue (line 5) | function getIdValue(element: Base): string {
function setIdValue (line 9) | function setIdValue(element: Base, value: string) {
FILE: src/views/flowable/bo-utils/nameUtil.ts
function getNameValue (line 8) | function getNameValue(element: Base): string | undefined {
function setNameValue (line 24) | function setNameValue(element: Base, value: string): void {
function createCategoryValue (line 51) | function createCategoryValue(definitions: any, bpmnFactory: any): any {
function initializeCategory (line 63) | function initializeCategory(businessObject: any, rootElement: any, bpmnF...
FILE: src/views/flowable/bo-utils/processUtil.ts
function getProcessExecutable (line 6) | function getProcessExecutable(element: Base): boolean {
function setProcessExecutable (line 10) | function setProcessExecutable(element: Base, value: boolean) {
function getProcessVersionTag (line 19) | function getProcessVersionTag(element: Base): string | undefined {
function setProcessVersionTag (line 24) | function setProcessVersionTag(element: Base, value: string) {
FILE: src/views/flowable/bo-utils/userTaskUtil.ts
function isUserService (line 3) | function isUserService(element: any): boolean {
function isStartEvent (line 7) | function isStartEvent(element: any): boolean {
FILE: src/views/flowable/design/customTranslate.ts
function customTranslate (line 4) | function customTranslate(template: any, replacements: any) {
FILE: src/views/flowable/design/design.tsx
method setup (line 17) | setup(props, {emit, slots}) {
FILE: src/views/flowable/design/provider/parts/FormProps.js
function FormProps (line 5) | function FormProps(props) {
function Form (line 15) | function Form(props) {
FILE: src/views/flowable/design/provider/parts/SpellProps.js
function FormType (line 19) | function FormType(props) {
FILE: src/views/flowable/design/provider/selfProvider.js
constant LOW_PRIORITY (line 6) | const LOW_PRIORITY = 500;
function SelfProvider (line 9) | function SelfProvider(propertiesPanel, translate) {
function createMagicGroup (line 31) | function createMagicGroup(element, translate) {
function findGroup (line 43) | function findGroup(groups, id) {
function findEntry (line 47) | function findEntry(entries, id) {
function deleteById (line 51) | function deleteById(array, id) {
FILE: src/views/flowable/utils/BpmnValidator.ts
constant SPACE_REGEX (line 1) | const SPACE_REGEX = /\s/
constant QNAME_REGEX (line 5) | const QNAME_REGEX = /^([a-z][\w-.]*:)?[a-z_][\w-.]*$/i
constant ID_REGEX (line 8) | const ID_REGEX = /^[a-z_][\w-.]*$/i
function containsSpace (line 10) | function containsSpace(value: string) {
function isIdValid (line 23) | function isIdValid(element: any, idValue: any) {
function validateId (line 38) | function validateId(idValue: any) {
FILE: src/views/form/design/components/formitem.tsx
method setup (line 63) | setup(props, {emit}) {
FILE: src/views/form/design/components/formitemEdit.tsx
method setup (line 24) | setup(props, {emit}) {
FILE: src/views/form/design/form.d.ts
type formComponentGroup (line 1) | interface formComponentGroup {
type formComponent (line 6) | interface formComponent {
type formComponentConfig (line 16) | type formComponentConfig =
type formOption (line 33) | interface formOption {
type formComponentType (line 38) | type formComponentType = NonNullable<formComponent['type']>
type form (line 41) | interface form {
FILE: src/views/form/design/preview.tsx
method setup (line 26) | setup(props, {emit}) {
FILE: src/views/todo/store/index.ts
type TodoState (line 4) | interface TodoState {
method incompleteTasks (line 15) | incompleteTasks({taskList}) {
method completeTasks (line 18) | completeTasks({taskList}) {
method addTask (line 23) | addTask(task: Todo.Task) {
method updateTask (line 30) | updateTask(task: Todo.Task) {
method taskCompleted (line 35) | taskCompleted(task: Todo.Task) {
method taskIncomplete (line 39) | taskIncomplete(task: Todo.Task) {
method deleteTask (line 43) | deleteTask(task: Todo.Task) {
FILE: src/views/todo/typs/index.d.ts
type Task (line 2) | interface Task{
type Label (line 9) | interface Label{
Condensed preview — 238 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (389K chars).
[
{
"path": ".browserslistrc",
"chars": 40,
"preview": "> 1%\nlast 2 versions\nnot dead\nnot ie 11\n"
},
{
"path": ".editorconfig",
"chars": 121,
"preview": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true"
},
{
"path": ".env-config.ts",
"chars": 843,
"preview": "/** 请求服务的环境配置 */\ntype ServiceEnv = Record<ServiceEnvType, ServiceEnvConfig>;\n\n/** 不同请求服务的环境配置 */\nconst serviceEnv: Servi"
},
{
"path": ".gitignore",
"chars": 301,
"preview": ".DS_Store\nnode_modules\n/dist\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyar"
},
{
"path": "Makefile",
"chars": 378,
"preview": "ImageTag ?=v1.1.0\nImageName ?= sunhao1256/lulu-admin-frontend:$(ImageTag)\nPlatform ?=linux/amd64\n\nVERSION=$(shell git re"
},
{
"path": "README.md",
"chars": 1759,
"preview": "# Lulu Admin\n\nelegant admin template, based on Vue3 , TypeScript, Vuetify3, Axios\n\nform-generator , bpmn-js-camunda\n\n## "
},
{
"path": "build/index.ts",
"chars": 52,
"preview": "export * from './plugins';\nexport * from './utils';\n"
},
{
"path": "build/plugins/index.ts",
"chars": 335,
"preview": "import unplugin from \"./unplugin\";\nimport vuetify from \"./vuetify\";\nimport vue from '@vitejs/plugin-vue'\nimport mock fro"
},
{
"path": "build/plugins/mock.ts",
"chars": 402,
"preview": "import {viteMockServe} from 'vite-plugin-mock';\n\nexport default (env: ImportMetaEnv) => {\n const prodMock = true\n cons"
},
{
"path": "build/plugins/unplugin.ts",
"chars": 2291,
"preview": "import Components from 'unplugin-vue-components/vite';\nimport AutoImport from 'unplugin-auto-import/vite';\nimport Icons "
},
{
"path": "build/plugins/vuetify.ts",
"chars": 204,
"preview": "import vuetify from 'vite-plugin-vuetify'\n\nexport default function vuetifyPlugin() {\n return vuetify({\n autoImport: "
},
{
"path": "build/utils/index.ts",
"chars": 340,
"preview": "import path from 'path';\n\n/**\n * 获取项目根路径\n * @descrition 末尾不带斜杠\n */\nexport function getRootPath() {\n return path.resolve"
},
{
"path": "docker/.dockerignore",
"chars": 363,
"preview": "node_modules\n.DS_Store\ndist\n.npmrc\n.cache\n\ntests/server/static\ntests/server/static/upload\n\n.local\n# local env files\n.env"
},
{
"path": "docker/Dockerfile",
"chars": 186,
"preview": "FROM nginx:alpine as prod\n\nENV WORKDIR=/lulu-admin\n\nWORKDIR $WORKDIR\n\nARG version\nENV COMMITID=$version\n\n\nCOPY /dist /lu"
},
{
"path": "docker/nginx.conf",
"chars": 1056,
"preview": "user nginx;\nworker_processes 1;\nerror_log /var/log/nginx/error.log warn;\npid /var/run/nginx.pid;\n\nevents {\n w"
},
{
"path": "index.html",
"chars": 465,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" href=\"/favicon.ico\" />\n <m"
},
{
"path": "mock/api/auth.ts",
"chars": 2965,
"preview": "import type {MockMethod} from 'vite-plugin-mock';\nimport {userModel} from '../model';\nimport {mock} from \"mockjs\";\n\ncons"
},
{
"path": "mock/api/chat.ts",
"chars": 646,
"preview": "import {mock} from 'mockjs';\nimport type {MockMethod} from 'vite-plugin-mock';\n\nconst apis: MockMethod[] = [\n {\n url"
},
{
"path": "mock/api/index.ts",
"chars": 183,
"preview": "import auth from './auth';\nimport chat from './chat';\nimport route from './route';\nimport management from './management'"
},
{
"path": "mock/api/management.ts",
"chars": 2741,
"preview": "import {mock} from 'mockjs';\nimport type {MockMethod} from 'vite-plugin-mock';\n\nconst apis: MockMethod[] = [\n {\n url"
},
{
"path": "mock/api/route.ts",
"chars": 715,
"preview": "import type {MockMethod} from 'vite-plugin-mock';\nimport {routeModel, userModel} from '../model';\n\nconst apis: MockMetho"
},
{
"path": "mock/index.ts",
"chars": 174,
"preview": "import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';\nimport api from './api';\n\nexport functi"
},
{
"path": "mock/model/auth.ts",
"chars": 497,
"preview": "interface UserModel extends Auth.UserInfo {\n token: string;\n refreshToken: string;\n password: string;\n}\n\nexport const"
},
{
"path": "mock/model/index.ts",
"chars": 49,
"preview": "export * from './auth';\nexport * from './route';\n"
},
{
"path": "mock/model/route.ts",
"chars": 6765,
"preview": "export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {\n admin: [\n {\n name: 'dashboard',\n pa"
},
{
"path": "package.json",
"chars": 2050,
"preview": "{\n \"name\": \"lulu-admin\",\n \"version\": \"1.1.0\",\n \"homepage\": \"https://github.com/sunhao1256/lulu-admin\",\n \"repository\""
},
{
"path": "src/App.vue",
"chars": 280,
"preview": "<template>\n <v-app>\n <vuetify-provider>\n <router-view/>\n </vuetify-provider>\n </v-app>\n</template>\n\n<script"
},
{
"path": "src/assets/scss/settings.css",
"chars": 639,
"preview": "/* Error: @use rules must be written before any other rules.\n * ,\n * 2 | @use \"vuetify/settings\";\n * | ^^^^^^^^^^^^^"
},
{
"path": "src/assets/scss/settings.scss",
"chars": 1214,
"preview": "@use \"vuetify/variables\";\n@use \"vuetify/settings\" with (\n$spacer: variables.$spacer,\n$body-font-family: variables.$body-"
},
{
"path": "src/assets/scss/theme.css",
"chars": 2279,
"preview": "/**\n * Vuetify Styles Overrides\n */\n.v-application .d-flex {\n min-width: 0;\n}\n\n.v-application p {\n margin-bottom: 20px"
},
{
"path": "src/assets/scss/theme.scss",
"chars": 289,
"preview": "@use \"./vuetify/overrides\";\n\n@font-face {\n font-family: 'Quicksand';\n src: url(../fonts/Quicksand-VariableFont_wght.tt"
},
{
"path": "src/assets/scss/vuetify/overrides.scss",
"chars": 1525,
"preview": "@use \"./variables\" as *;\n@use \"vuetify/tools\" as *;\n\n.v-application {\n .title {\n font-size: map-deep-get($headings, "
},
{
"path": "src/assets/scss/vuetify/variables/_elevations.scss",
"chars": 4298,
"preview": "$shadow-key-umbra-opacity: rgba(85, 85, 85, 0.08) !default;\n$shadow-key-penumbra-opacity: rgba(85, 85, 85, 0.06) !defaul"
},
{
"path": "src/assets/scss/vuetify/variables/_font.scss",
"chars": 1220,
"preview": "$body-font-family: 'Quicksand', sans-serif;\n$font-size-root: 15px;\n$line-height-root: 1.5;\n\n$font-weights: (\n 'th"
},
{
"path": "src/assets/scss/vuetify/variables/_global.scss",
"chars": 2793,
"preview": "// Global border radius\n$border-radius-root: 6px;\n\n// Spacing\n$spacer: 8px;\n\n$grid-breakpoints: (\n 'xs': 0,\n "
},
{
"path": "src/assets/scss/vuetify/variables/_index.scss",
"chars": 68,
"preview": "@forward './_global';\n@forward './_font';\n@forward './_elevations';\n"
},
{
"path": "src/components/common/Breadcrumb.vue",
"chars": 588,
"preview": "<template>\n <v-breadcrumbs class=\"pa-0 py-2\" :items=\"breadcrumbs\" active-color=\"primary\">\n <template v-slot:title=\"{"
},
{
"path": "src/components/common/CopyLabel.vue",
"chars": 1052,
"preview": "<template>\n <div ref=\"copyLabel\" class=\"copylabel animate__faster\" @click.stop.prevent=\"copy\">{{ text }}\n <v-tooltip"
},
{
"path": "src/components/common/FlagIcon.vue",
"chars": 509,
"preview": "<template>\n <span class=\"fi\" :class=\"[`fi-${flag}`, { 'flag-round': round }]\"></span>\n</template>\n\n<script setup lang=\""
},
{
"path": "src/components/common/SideConfigMenu.vue",
"chars": 3778,
"preview": "<template>\n <div>\n <v-btn\n theme=\"dark\"\n ref=\"refButton\"\n class=\"drawer-button\"\n color=\"#ee44aa\""
},
{
"path": "src/components/common/SvgIcon.vue",
"chars": 596,
"preview": "<template>\n <svg aria-hidden=\"true\" v-bind=\"bindAttrs\" class=\"d-flex flex-column justify-end\" >\n <use :href=\"symbolI"
},
{
"path": "src/components/common/TrendPercent.vue",
"chars": 488,
"preview": "<template>\n <span>\n <span v-if=\"value === 0\">\n {{ value }}%\n </span>\n <span v-else-if=\"value > 0\" class=\""
},
{
"path": "src/components/dashboard/ActivityCard.vue",
"chars": 1924,
"preview": "<template>\n <v-card>\n <v-card-title>\n <div>{{ $t('dashboard.activity') }}</div>\n <v-spacer></v-spacer>\n "
},
{
"path": "src/components/dashboard/SalesCard.vue",
"chars": 3852,
"preview": "<template>\n <v-card class=\"d-flex flex-grow-1 bg-primary-darken-4 \" theme=\"dark\">\n\n <!-- loading spinner -->\n <di"
},
{
"path": "src/components/dashboard/SourcesCard.vue",
"chars": 2883,
"preview": "<template>\n <v-card class=\"d-flex flex-column flex-grow-1\">\n\n <!-- loading spinner -->\n <div v-if=\"loading\" class"
},
{
"path": "src/components/dashboard/TableCard.vue",
"chars": 3667,
"preview": "<template>\n <v-card>\n <v-card-title>{{ label }}</v-card-title>\n <v-data-table\n :headers=\"headers\"\n :ite"
},
{
"path": "src/components/dashboard/TodoCard.vue",
"chars": 272,
"preview": "<template>\n <tasks-page class=\"todo-card\"></tasks-page>\n</template>\n\n<script lang=\"ts\" setup>\nimport TasksPage from '@/"
},
{
"path": "src/components/dashboard/TrackCard.vue",
"chars": 2818,
"preview": "<template>\n <v-card class=\"d-flex flex-column flex-grow-1\">\n <div v-if=\"loading\" class=\"d-flex flex-grow-1 align-cen"
},
{
"path": "src/components/navigation/MainMenu.vue",
"chars": 414,
"preview": "<template>\n <v-list nav>\n <div v-for=\"(item,index) in menu\" :key=\"index\">\n <div v-if=\"item.label\" class=\"pa-1 m"
},
{
"path": "src/components/navigation/NavMenu.vue",
"chars": 905,
"preview": "<template>\n <div>\n <nav-menu-item v-for=\"(level1Item, level1Index) in menu\" :key=\"level1Index\" :menu-item=\"level1Ite"
},
{
"path": "src/components/navigation/NavMenuItem.vue",
"chars": 1897,
"preview": "<template>\n <div>\n <v-list-item\n v-if=\"!(menuItem.children && menuItem.children.length>0)\"\n :to=\"menuItem."
},
{
"path": "src/components/provider/DialogProvider.tsx",
"chars": 4269,
"preview": "import {defineComponent, provide, VNodeChild, ref, reactive, h} from 'vue'\nimport {createId} from 'seemly'\nimport {VCard"
},
{
"path": "src/components/provider/LoadingOverlyProvider.tsx",
"chars": 1206,
"preview": "import {defineComponent, ref} from 'vue'\nimport {VOverlay, VProgressCircular} from 'vuetify/components'\n\nexport const Lo"
},
{
"path": "src/components/provider/LoadingProgressLine.tsx",
"chars": 1340,
"preview": "import {defineComponent, ref} from 'vue'\nimport {VProgressLinear} from 'vuetify/components'\n\nexport const LoadingProgres"
},
{
"path": "src/components/provider/SnackbarProvider.tsx",
"chars": 3783,
"preview": "import {defineComponent, provide, VNodeChild, ref, reactive, h} from 'vue'\nimport {VSnackbar} from 'vuetify/components'\n"
},
{
"path": "src/components/provider/VuetifyProvider.vue",
"chars": 934,
"preview": "<template>\n <loading-overly-provider>\n <snackbar-provider>\n <dialog-provider>\n <slot/>\n <vuetify-"
},
{
"path": "src/components/provider/index.ts",
"chars": 146,
"preview": "export * from './SnackbarProvider'\nexport * from './LoadingOverlyProvider'\nexport * from './LoadingProgressLine'\nexport "
},
{
"path": "src/components/toolbar/ToolbarLanguage.vue",
"chars": 1556,
"preview": "<template>\n <v-menu\n >\n <template v-slot:activator=\"{ props }\">\n <v-btn text v-bind=\"props\">\n <flag-ico"
},
{
"path": "src/components/toolbar/ToolbarNotifications.vue",
"chars": 2233,
"preview": "<template>\n <v-menu offset-y left transition=\"slide-y-transition\">\n <template v-slot:activator=\"{ props }\">\n <v"
},
{
"path": "src/components/toolbar/ToolbarUser.vue",
"chars": 1977,
"preview": "<template>\n <v-menu offset-y left transition=\"slide-y-transition\">\n <template v-slot:activator=\"{ props }\">\n <v"
},
{
"path": "src/composables/events.ts",
"chars": 258,
"preview": "import { useEventListener } from '@vueuse/core';\nimport {useThemeStore} from \"@/store\";\n\n\nexport function useGlobalEvent"
},
{
"path": "src/composables/index.ts",
"chars": 52,
"preview": "export * from './router';\nexport * from './system';\n"
},
{
"path": "src/composables/router.ts",
"chars": 1786,
"preview": "import { useRouter } from 'vue-router';\nimport type { RouteLocationRaw } from 'vue-router';\nimport { router as globalRou"
},
{
"path": "src/composables/system.ts",
"chars": 1205,
"preview": "import UAParser from 'ua-parser-js';\nimport {useAuthStore} from '@/store';\nimport {isArray, isString} from '@/utils';\nim"
},
{
"path": "src/configs/currencies.ts",
"chars": 703,
"preview": "const config: CurrencyConfig.Config = {\n currency: {\n label: 'USD',\n decimalDigits: 2,\n decimalSeparator: '.',"
},
{
"path": "src/configs/index.ts",
"chars": 178,
"preview": "import locales from './locales'\nimport theme from './theme'\nimport currency from './currencies'\n\nconst config: Config = "
},
{
"path": "src/configs/locales.ts",
"chars": 507,
"preview": "import en from '../translations/en'\nimport zh from '../translations/zh'\n\nconst supported = ['en', 'zh']\nlet locale = 'en"
},
{
"path": "src/configs/service.ts",
"chars": 889,
"preview": "export const REQUEST_TIMEOUT = 60 * 1000;\n\nexport const ERROR_MSG_DURATION = 3 * 1000;\n\nexport const DEFAULT_REQUEST_ERR"
},
{
"path": "src/configs/theme.ts",
"chars": 1043,
"preview": "const themeConfig: ThemeConfig.Config = {\n\n primary: '#0096c7',\n\n followOs: true,\n\n globalTheme: 'light', // light | "
},
{
"path": "src/constants/business.ts",
"chars": 1039,
"preview": "/** 用户性别 */\nexport const genderLabels: Record<UserManagement.GenderKey, string> = {\n 0: 'female',\n 1: 'male',\n 2: 'un"
},
{
"path": "src/constants/index.ts",
"chars": 28,
"preview": "export * from './business';\n"
},
{
"path": "src/enum/business.ts",
"chars": 234,
"preview": "export enum EnumUserRole {\n admin = 'admin',\n user = 'commonUser'\n}\n\nexport enum EnumLoginModule {\n 'sign-in' = 'sigi"
},
{
"path": "src/enum/common.ts",
"chars": 589,
"preview": "export enum EnumContentType {\n json = 'application/json',\n formUrlencoded = 'application/x-www-form-urlencoded',\n for"
},
{
"path": "src/enum/index.ts",
"chars": 80,
"preview": "export * from './common';\nexport * from './system';\nexport * from './business';\n"
},
{
"path": "src/enum/system.ts",
"chars": 698,
"preview": "export enum EnumLayoutComponentName {\n basic = 'basic-layout',\n blank = 'blank-layout',\n auth = 'auth-layout',\n erro"
},
{
"path": "src/filters/formatCurrency.ts",
"chars": 1590,
"preview": "import configs from \"@/configs\";\n\nexport function formatCurrency(value: number, currency?: CurrencyConfig.Currency): num"
},
{
"path": "src/filters/index.ts",
"chars": 154,
"preview": "import {App} from \"vue\";\n\nexport function registerFilters(app: App) {\n app.config.globalProperties.$filters = {\n for"
},
{
"path": "src/hooks/common/index.ts",
"chars": 279,
"preview": "import useContext from './useContext';\nimport useBoolean from './useBoolean';\nimport useLoading from './useLoading';\nimp"
},
{
"path": "src/hooks/common/useBoolean.ts",
"chars": 407,
"preview": "import { ref } from 'vue';\n\nexport default function useBoolean(initValue = false) {\n const bool = ref(initValue);\n\n fu"
},
{
"path": "src/hooks/common/useBreadcrumb.ts",
"chars": 621,
"preview": "import {computed} from 'vue';\nimport {useRoute} from 'vue-router';\nimport {routePath} from '@/router';\nimport {useRouteS"
},
{
"path": "src/hooks/common/useContext.ts",
"chars": 412,
"preview": "import { inject, provide } from 'vue';\nimport type { InjectionKey } from 'vue';\n\nexport default function useContext<T>(c"
},
{
"path": "src/hooks/common/useLoading.ts",
"chars": 257,
"preview": "import useBoolean from './useBoolean';\n\nexport default function useLoading(initValue = false) {\n const { bool: loading,"
},
{
"path": "src/hooks/common/useLoadingEmpty.ts",
"chars": 378,
"preview": "import useBoolean from './useBoolean';\n\nexport default function useLoadingEmpty(initLoading = false, initEmpty = false) "
},
{
"path": "src/hooks/common/useReload.ts",
"chars": 425,
"preview": "import { nextTick } from 'vue';\nimport useBoolean from './useBoolean';\n\nexport default function useReload() {\n // 重载的标志"
},
{
"path": "src/hooks/index.ts",
"chars": 26,
"preview": "export * from './common';\n"
},
{
"path": "src/layouts/AuthLayout.vue",
"chars": 1224,
"preview": "<template>\n <div class=\"d-flex text-center flex-column flex-md-row flex-grow-1\">\n <v-sheet class=\"layout-side mx-aut"
},
{
"path": "src/layouts/BlankLayout/index.vue",
"chars": 237,
"preview": "<template>\n <router-view v-slot=\"{ Component }\">\n <v-fade-transition mode=\"out-in\">\n <component :is=\"Component\""
},
{
"path": "src/layouts/DefaultLayout.vue",
"chars": 3459,
"preview": "<template>\n <!-- Navigation -->\n <v-navigation-drawer\n v-model=\"drawer\"\n floating\n name=\"app-navigation\"\n "
},
{
"path": "src/layouts/ErrorLayout.vue",
"chars": 139,
"preview": "<template>\n <div class=\"pa-2 pa-md-4 flex-grow-1 align-center justify-center d-flex flex-column\">\n <router-view/>\n "
},
{
"path": "src/layouts/index.ts",
"chars": 439,
"preview": "const BasicLayout = () => import('./DefaultLayout.vue');\nconst BlankLayout = () => import('./BlankLayout/index.vue');\nco"
},
{
"path": "src/main.ts",
"chars": 394,
"preview": "/**\n * main.ts\n *\n * Bootstraps Vuetify and other plugins then mounts the App`\n */\n\n// Components\nimport App from './App"
},
{
"path": "src/plugins/animate.ts",
"chars": 449,
"preview": "export function animate(node: HTMLElement, animationName: string, callBack?: () => void) {\n\n node.classList.add('animat"
},
{
"path": "src/plugins/clipboard.ts",
"chars": 373,
"preview": "export function clipboard(text: string, toastText = 'Copied to Clipboard') {\n try {\n navigator.clipboard.writeText(t"
},
{
"path": "src/plugins/index.ts",
"chars": 340,
"preview": "/**\n * plugins/user.d.ts\n *\n * Automatically included in `./src/main.ts`\n */\n\n// Plugins\nimport VueApexCharts from 'vue3"
},
{
"path": "src/plugins/vue-i18n.ts",
"chars": 377,
"preview": "import {createI18n} from 'vue-i18n'\nimport config from '../configs'\n\nconst {locale, availableLocales, fallbackLocale} = "
},
{
"path": "src/plugins/vuetify.ts",
"chars": 853,
"preview": "/**\n * plugins/vuetify.ts\n *\n * Framework documentation: https://vuetifyjs.com`\n */\n\n// Styles\nimport '@mdi/font/css/mat"
},
{
"path": "src/router/guard/dynamic.ts",
"chars": 1348,
"preview": "import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';\nimport { routeName } from '@/router';\nim"
},
{
"path": "src/router/guard/index.ts",
"chars": 429,
"preview": "import type {Router} from 'vue-router';\nimport {useTitle} from '@vueuse/core';\nimport {createPermissionGuard} from './pe"
},
{
"path": "src/router/guard/permission.ts",
"chars": 1756,
"preview": "import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';\nimport { routeName } from '@/router';\nim"
},
{
"path": "src/router/helpers/index.ts",
"chars": 0,
"preview": ""
},
{
"path": "src/router/index.ts",
"chars": 960,
"preview": "import type {App} from \"vue\"\nimport { transformAuthRouteToVueRoutes } from '@/utils/router/transform';\nimport { transfor"
},
{
"path": "src/router/modules/dashboard.ts",
"chars": 505,
"preview": "const dashboard: AuthRoute.Route = {\n name: 'dashboard',\n path: '/dashboard',\n component: 'basic',\n children: [\n "
},
{
"path": "src/router/modules/index.ts",
"chars": 188,
"preview": "import { handleModuleRoutes } from '@/utils';\n\nconst modules = import.meta.glob('./**/*.ts', { eager: true }) as AuthRou"
},
{
"path": "src/router/modules/management.ts",
"chars": 2461,
"preview": "const management: AuthRoute.Route = {\n name: 'apps',\n path: '/apps',\n component: 'basic',\n children: [\n "
},
{
"path": "src/router/modules/pages.ts",
"chars": 912,
"preview": "const pages: AuthRoute.Route = {\n name: 'pages',\n path: '/pages',\n component: 'error',\n children: [\n {\n name"
},
{
"path": "src/router/routes/index.ts",
"chars": 1244,
"preview": "import {getLoginModuleRegExp} from '@/utils';\n\nexport const ROOT_ROUTE: AuthRoute.Route = {\n name: 'root',\n path: '/',"
},
{
"path": "src/service/api/auth.ts",
"chars": 656,
"preview": "import { mockRequest } from '../request';\n\nexport function fetchSmsCode(phone: string) {\n return mockRequest.post<boole"
},
{
"path": "src/service/api/chat.ts",
"chars": 145,
"preview": "import {mockRequest} from '../request';\n\nexport function fetchMessage() {\n return mockRequest.post<ApiChatManagement.me"
},
{
"path": "src/service/api/index.ts",
"chars": 78,
"preview": "export * from './auth';\nexport * from './chat';\nexport * from './management';\n"
},
{
"path": "src/service/api/management.adapter.ts",
"chars": 1192,
"preview": "export function adapterOfFetchUserList(data: ApiCommon.PageResult<ApiUserManagement.User[]> | null): ApiCommon.PageResul"
},
{
"path": "src/service/api/management.ts",
"chars": 901,
"preview": "import {adapter} from '@/utils';\nimport {mockRequest} from '../request';\nimport {adapterOfFetchUserList, adapterOfFetchU"
},
{
"path": "src/service/index.ts",
"chars": 23,
"preview": "export * from './api';\n"
},
{
"path": "src/service/request/helpers.ts",
"chars": 751,
"preview": "import type { AxiosRequestConfig } from 'axios';\nimport { useAuthStore } from '@/store';\nimport { localStg } from '@/uti"
},
{
"path": "src/service/request/index.ts",
"chars": 505,
"preview": "import { getServiceEnvConfig } from '~/.env-config';\nimport { createRequest } from './request';\n\nconst { url, urlPattern"
},
{
"path": "src/service/request/instance.ts",
"chars": 2481,
"preview": "import axios from 'axios';\nimport type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios';\nimport { REFRESH_"
},
{
"path": "src/service/request/request.ts",
"chars": 3985,
"preview": "import {ref} from 'vue';\nimport type {Ref} from 'vue';\nimport type {AxiosInstance, AxiosRequestConfig} from 'axios';\nimp"
},
{
"path": "src/store/auth/helpers.ts",
"chars": 465,
"preview": "import { localStg } from '@/utils';\n\nexport function getToken() {\n return localStg.get('token') || '';\n}\n\nexport functi"
},
{
"path": "src/store/auth/index.ts",
"chars": 3036,
"preview": "import {unref, nextTick} from 'vue';\nimport {defineStore} from 'pinia';\nimport {router} from '@/router';\nimport {fetchLo"
},
{
"path": "src/store/flow/index.ts",
"chars": 1377,
"preview": "import {defineStore} from 'pinia'\nimport {Base} from 'diagram-js/lib/model'\nimport Canvas from 'diagram-js/lib/core/Canv"
},
{
"path": "src/store/index.ts",
"chars": 276,
"preview": "import {createPinia} from 'pinia'\nimport type {App} from \"vue\";\n\nexport function setupStore(app: App) {\n const store = "
},
{
"path": "src/store/route/index.ts",
"chars": 3714,
"preview": "import {defineStore} from 'pinia';\nimport {ROOT_ROUTE, constantRoutes, router, routes as staticRoutes} from '@/router';\n"
},
{
"path": "src/store/subscribe/index.ts",
"chars": 106,
"preview": "import subscribeThemeStore from './theme';\n\nexport function subscribeStore() {\n subscribeThemeStore();\n}\n"
},
{
"path": "src/store/subscribe/theme.ts",
"chars": 933,
"preview": "import {effectScope, onScopeDispose, watch} from 'vue';\nimport {useOsTheme} from 'vooks';\nimport {useThemeStore} from '@"
},
{
"path": "src/store/theme/helpers.ts",
"chars": 247,
"preview": "import {localStg} from '@/utils';\nimport configs from \"@/configs\";\n\nexport function initThemeSettings() {\n const storag"
},
{
"path": "src/store/theme/index.ts",
"chars": 353,
"preview": "import {defineStore} from 'pinia'\nimport {localStg} from \"@/utils\";\nimport {initThemeSettings} from \"@/store/theme/helpe"
},
{
"path": "src/translations/en.ts",
"chars": 6982,
"preview": "export default {\n 'welcomeBack': 'welcome back',\n common: {\n add: 'Add',\n cancel: 'Cancel',\n confirm: 'confir"
},
{
"path": "src/translations/zh.ts",
"chars": 5799,
"preview": "export default {\n 'welcomeBack': '欢迎回来',\n 'common': {\n 'add': '加',\n 'confirm': '确认',\n 'cancel': '取消',\n 'de"
},
{
"path": "src/typings/api.d.ts",
"chars": 1613,
"preview": "declare namespace ApiCommon {\n interface PageResult<T = any> {\n pageNo: number,\n pageSize: number,\n list: T,\n "
},
{
"path": "src/typings/business.d.ts",
"chars": 686,
"preview": "declare namespace Auth {\n type RoleType = keyof typeof import('@/enum').EnumUserRole;\n\n interface UserInfo {\n userI"
},
{
"path": "src/typings/camunda.d.ts",
"chars": 286,
"preview": "declare module \"bpmn-js/*\"\ndeclare module \"diagram-js/*\"\ndeclare module \"@bpmn-io/*\"\ndeclare module \"bpmn-js-properties-"
},
{
"path": "src/typings/config.d.ts",
"chars": 1546,
"preview": "interface Config {\n theme: ThemeConfig.Config,\n locales: any\n currency: CurrencyConfig.Config,\n}\n\ndeclare namespace C"
},
{
"path": "src/typings/env.d.ts",
"chars": 961,
"preview": "type ServiceEnvType = 'dev' | 'test' | 'prod';\n\ninterface ServiceEnvConfig {\n url: string;\n urlPattern: '/url-pattern'"
},
{
"path": "src/typings/filter.d.ts",
"chars": 326,
"preview": "declare namespace filters {\n type formatCurrency = (value: number, currency?: CurrencyConfig.Currency) => number | stri"
},
{
"path": "src/typings/global.d.ts",
"chars": 350,
"preview": "interface Window {\n $snackBar?: import('@/components/provider').SnackBarApiInjection,\n $loadingOverly?: import('@/comp"
},
{
"path": "src/typings/page-route.d.ts",
"chars": 1722,
"preview": "declare namespace PageRoute {\n /**\n * the root route key\n * @translate 根路由\n */\n type RootRouteKey = 'root';\n\n /"
},
{
"path": "src/typings/route.d.ts",
"chars": 3518,
"preview": "declare namespace AuthRoute {\n type RootRoutePath = '/';\n\n type NotFoundRoutePath = '/:pathMatch(.*)*';\n\n type RootRo"
},
{
"path": "src/typings/router.d.ts",
"chars": 128,
"preview": "import 'vue-router';\n\ndeclare module 'vue-router' {\n interface RouteMeta extends AuthRoute.RouteMeta<AuthRoute.RoutePat"
},
{
"path": "src/typings/storage.d.ts",
"chars": 284,
"preview": "declare namespace StorageInterface {\n /** localStorage的存储数据的类型 */\n interface Session {\n demoKey: string;\n }\n\n /**"
},
{
"path": "src/typings/system.d.ts",
"chars": 5829,
"preview": "/** 枚举的key类型 */\ndeclare namespace EnumType {\n /** 布局组件名称 */\n type LayoutComponentName = keyof typeof import('@/enum')."
},
{
"path": "src/typings/utils.d.ts",
"chars": 861,
"preview": "declare namespace TypeUtil {\n type Noop = (...args: any) => any;\n\n type UnionInclude<T, K extends keyof T> = K extends"
},
{
"path": "src/typings/vuetify.d.ts",
"chars": 682,
"preview": "// export vuetify types.ts for ts\n\ntype BreadcrumbItem = import('vuetify/components').VBreadcrumbsItem['$props']\ntype Da"
},
{
"path": "src/utils/common/index.ts",
"chars": 53,
"preview": "export * from './typeof';\nexport * from './pattern';\n"
},
{
"path": "src/utils/common/pattern.ts",
"chars": 192,
"preview": "export function exeStrategyActions(actions: Common.StrategyAction[]) {\n actions.some(item => {\n const [flag, action]"
},
{
"path": "src/utils/common/typeof.ts",
"chars": 2170,
"preview": "import { EnumDataType } from '@/enum';\n\nexport function isNumber<T extends number>(data: T | unknown): data is T {\n ret"
},
{
"path": "src/utils/crypto/index.ts",
"chars": 553,
"preview": "import CryptoJS from 'crypto-js';\n\nconst CryptoSecret = '__CryptoJS_Secret__';\n\n/**\n * 加密数据\n * @param data - 数据\n */\nexpo"
},
{
"path": "src/utils/flow/EventEmitter.ts",
"chars": 4250,
"preview": "import {getRawType, notNull} from './tools'\n\nconst isArray = (obj: any) => getRawType(obj) === 'array'\nconst isNullOrUnd"
},
{
"path": "src/utils/flow/tools.ts",
"chars": 654,
"preview": "/* 空函数 */\nexport function noop() {\n}\n\n/**\n * 校验非空\n * @param {*} val\n * @return boolean\n */\nexport function notEmpty(val:"
},
{
"path": "src/utils/index.ts",
"chars": 129,
"preview": "export * from './service';\nexport * from './storage';\nexport * from './router';\nexport * from './common';\nexport * from "
},
{
"path": "src/utils/router/auth.ts",
"chars": 737,
"preview": "export function filterAuthRoutesByUserPermission(routes: AuthRoute.Route[], permission: Auth.RoleType) {\n return routes"
},
{
"path": "src/utils/router/breadcrumb.ts",
"chars": 2105,
"preview": "/**\n * 获取面包屑数据\n * @param activeKey - 当前页面路由的key\n * @param menus - 菜单数据\n * @param rootPath - 根路由路径\n */\nexport function ge"
},
{
"path": "src/utils/router/cache.ts",
"chars": 746,
"preview": "import type { RouteRecordRaw } from 'vue-router';\n\n/**\n * 获取缓存的路由对应组件的名称\n * @param routes - 转换后的vue路由\n */\nexport functio"
},
{
"path": "src/utils/router/component.ts",
"chars": 1541,
"preview": "import type {RouteComponent} from 'vue-router';\nimport {views} from '@/views';\nimport {isFunction} from '@/utils';\nimpor"
},
{
"path": "src/utils/router/helpers.ts",
"chars": 458,
"preview": "/**\n * 获取所有固定路由的名称集合\n * @param routes - 固定路由\n */\nexport function getConstantRouteNames(routes: AuthRoute.Route[]) {\n re"
},
{
"path": "src/utils/router/index.ts",
"chars": 240,
"preview": "export * from './transform';\nexport * from './breadcrumb';\nexport * from './component';\nexport * from './regexp';\nexport"
},
{
"path": "src/utils/router/menu.ts",
"chars": 1916,
"preview": "/**\n * 将权限路由转换成菜单\n * @param routes - 路由\n */\nexport function transformAuthRouteToMenu(routes: AuthRoute.Route[]): App.Glo"
},
{
"path": "src/utils/router/module.ts",
"chars": 673,
"preview": "/**\n * 权限路由排序\n * @param routes - 权限路由\n */\nexport function sortRoutes(routes: AuthRoute.Route[]) {\n return routes.sort(("
},
{
"path": "src/utils/router/regexp.ts",
"chars": 199,
"preview": "/** 获取登录页面模块的动态路由的正则 */\nexport function getLoginModuleRegExp() {\n const modules: EnumType.LoginModuleKey[] = ['forgot',"
},
{
"path": "src/utils/router/transform.ts",
"chars": 5303,
"preview": "import type {RouteRecordRaw} from 'vue-router';\nimport {getLayoutComponent, getViewComponent} from '@/utils';\nimport i18"
},
{
"path": "src/utils/service/error.ts",
"chars": 2533,
"preview": "import type { AxiosError, AxiosResponse } from 'axios';\nimport {\n DEFAULT_REQUEST_ERROR_CODE,\n DEFAULT_REQUEST_ERROR_M"
},
{
"path": "src/utils/service/handler.ts",
"chars": 976,
"preview": "/** 统一失败和成功的请求结果的数据类型 */\nexport async function handleServiceResult<T = any>(error: Service.RequestError | null, data: an"
},
{
"path": "src/utils/service/index.ts",
"chars": 81,
"preview": "export * from './transform';\nexport * from './error';\nexport * from './handler';\n"
},
{
"path": "src/utils/service/msg.ts",
"chars": 846,
"preview": "import { ERROR_MSG_DURATION, NO_ERROR_MSG_CODE } from '@/configs/service';\n\n/** 错误消息栈,防止同一错误同时出现 */\nconst errorMsgStack "
},
{
"path": "src/utils/service/transform.ts",
"chars": 1435,
"preview": "import qs from 'qs';\nimport FormData from 'form-data';\nimport { EnumContentType } from '@/enum';\nimport { isArray, isFil"
},
{
"path": "src/utils/storage/index.ts",
"chars": 52,
"preview": "export * from './local';\nexport * from './session';\n"
},
{
"path": "src/utils/storage/local.ts",
"chars": 1381,
"preview": "import { decrypto, encrypto } from '../crypto';\ninterface StorageData<T> {\n value: T;\n expire: number | null;\n}\n\nfunct"
},
{
"path": "src/utils/storage/session.ts",
"chars": 1330,
"preview": "import { decrypto, encrypto } from '../crypto';\n\nexport function setSession(key: string, value: unknown) {\n const json "
},
{
"path": "src/utils/vue/index.ts",
"chars": 474,
"preview": "import { createTextVNode, VNodeChild } from 'vue'\n\nexport const render = <T extends any[]>(\n r:\n | string\n | numb"
},
{
"path": "src/views/_builtin/auth/components/ForgotPage.vue",
"chars": 1899,
"preview": "<template>\n <v-card class=\"text-center pa-1\">\n <v-card-title class=\"justify-center text-h4 mb-2\">{{ $t('forgot.title"
},
{
"path": "src/views/_builtin/auth/components/ResetPage.vue",
"chars": 1642,
"preview": "<template>\n <v-card class=\"pa-2\">\n <v-card-title class=\"justify-center text-h4 mb-2\">Set new password</v-card-title>"
},
{
"path": "src/views/_builtin/auth/components/SigninPage.vue",
"chars": 3849,
"preview": "<template>\n <div>\n <v-card class=\"text-center pa-1\">\n <v-card-title class=\"justify-center text-h4 mb-2 font-we"
},
{
"path": "src/views/_builtin/auth/components/SignupPage.vue",
"chars": 4843,
"preview": "<template>\n <div>\n <v-card class=\"text-center pa-1\">\n <v-card-title class=\"justify-center text-h4 mb-2\">{{ $t('"
},
{
"path": "src/views/_builtin/auth/components/VerifyEmailPage.vue",
"chars": 1199,
"preview": "<template>\n <v-card class=\"pa-2\">\n <h1>Please verify the email</h1>\n <div class=\"mb-6 overline\">Please check your"
},
{
"path": "src/views/_builtin/auth/components/index.ts",
"chars": 273,
"preview": "import ForgotPg from './ForgotPage.vue';\nimport SigninPg from './SigninPage.vue';\nimport SignUpPg from './SignupPage.vue"
},
{
"path": "src/views/_builtin/auth/index.vue",
"chars": 977,
"preview": "<template>\n <transition name=\"fade-slide\" mode=\"out-in\" appear>\n <component :is=\"activeModule.component\"/>\n </trans"
},
{
"path": "src/views/_builtin/error/NotFoundPage.vue",
"chars": 540,
"preview": "<template>\n <v-card class=\"text-center w-full error-page pa-3\">\n <v-img src=\"/images/illustrations/404-illustration."
},
{
"path": "src/views/_builtin/error/UnexpectedPage.vue",
"chars": 527,
"preview": "<template>\n <v-card class=\"text-center w-full error-page pa-3\">\n <v-img src=\"/images/illustrations/500-illustration."
},
{
"path": "src/views/board/components/BoardCard.vue",
"chars": 1126,
"preview": "<template>\n <v-card @click=\"$emit('edit')\">\n <div class=\"font-weight-bold\">{{ card.title }}</div>\n <div>{{ card.d"
},
{
"path": "src/views/board/pages/BoardPage.vue",
"chars": 8346,
"preview": "<template>\n <div class=\"d-flex flex-grow-1 mt-2\">\n <div class=\"board d-flex flex-grow-1 flex-row\">\n\n <!-- board"
},
{
"path": "src/views/board/types.ts",
"chars": 187,
"preview": "export type state = 'TODO' | 'INPROGRESS' | 'TESTING' | 'DONE'\nexport interface card {\n id: string | number,\n title?: "
},
{
"path": "src/views/chart/ChannelMessage.vue",
"chars": 1201,
"preview": "<template>\n <div class=\"d-flex flex-grow-1\" :class=\"{ 'flex-row-reverse': isOwnMessage}\">\n <v-avatar size=\"40\" class"
},
{
"path": "src/views/chart/ChatChannel.vue",
"chars": 4205,
"preview": "<template>\n <div>\n <!-- channel toolbar -->\n <v-toolbar flat height=\"64\" color=\"surface\">\n <v-app-bar-nav-ic"
},
{
"path": "src/views/chart/ChatPage.vue",
"chars": 5357,
"preview": "<template>\n <div class=\"h-100\">\n <v-layout full-height :class=\"{'position-static':!lgAndUp}\">\n <div class=\"d-fl"
},
{
"path": "src/views/dashboard/index.vue",
"chars": 2996,
"preview": "<template>\n <div class=\"d-flex flex-grow-1 flex-column\">\n <v-row class=\"flex-grow-0 my-0\" dense>\n <v-col cols=\""
},
{
"path": "src/views/flowable/bo-utils/conditionalUtil.ts",
"chars": 1040,
"preview": "// helper ////////////////////\n\nimport {getBusinessObject, is, isAny} from \"bpmn-js/lib/util/ModelUtil\";\nimport {find} f"
},
{
"path": "src/views/flowable/bo-utils/documentationUtil.ts",
"chars": 1608,
"preview": "import {Base} from 'diagram-js/lib/model'\nimport {useModelStore} from '@/store'\nimport {without} from 'min-dash'\n\nexport"
},
{
"path": "src/views/flowable/bo-utils/idUtil.ts",
"chars": 574,
"preview": "import {Base} from 'diagram-js/lib/model'\nimport {useModelStore} from '@/store'\nimport {isIdValid} from \"@/views/flowabl"
},
{
"path": "src/views/flowable/bo-utils/nameUtil.ts",
"chars": 2336,
"preview": "import {useModelStore} from '@/store'\nimport {Base} from 'diagram-js/lib/model'\nimport {getBusinessObject, is} from 'bpm"
},
{
"path": "src/views/flowable/bo-utils/processUtil.ts",
"chars": 767,
"preview": "import {Base} from 'diagram-js/lib/model'\nimport {useModelStore} from '@/store'\n\nconst prefix = \"camunda\"\n\nexport functi"
},
{
"path": "src/views/flowable/bo-utils/userTaskUtil.ts",
"chars": 240,
"preview": "import {is} from \"bpmn-js/lib/util/ModelUtil\";\n\nexport function isUserService(element: any): boolean {\n return is(eleme"
},
{
"path": "src/views/flowable/design/customTranslate.ts",
"chars": 364,
"preview": "import translations from './translations';\n\n\nexport default function customTranslate(template: any, replacements: any) {"
},
{
"path": "src/views/flowable/design/demo3.bpmn",
"chars": 5737,
"preview": "<bpmn:definitions xmlns:bpmn=\"http://www.omg.org/spec/BPMN/20100524/MODEL\" xmlns:bpmndi=\"http://www.omg.org/spec/BPMN/20"
},
{
"path": "src/views/flowable/design/design.tsx",
"chars": 3015,
"preview": "import {defineComponent, ref, toRefs, nextTick} from 'vue'\nimport type {PropType} from 'vue'\nimport {useModelStore} from"
},
{
"path": "src/views/flowable/design/index.vue",
"chars": 6083,
"preview": "<template>\n <div class=\"flex-grow-1 h-100 d-flex flex-column\">\n <v-toolbar rounded flat elevation=\"1\" color=\"surface"
},
{
"path": "src/views/flowable/design/initModeler.ts",
"chars": 1097,
"preview": "import {markRaw} from 'vue'\nimport {useModelStore} from '@/store'\nimport BpmnModeler from \"bpmn-js/lib/Modeler\";\nimport "
},
{
"path": "src/views/flowable/design/propertiesPanel/components/actions.vue",
"chars": 1298,
"preview": "<template>\n <v-expansion-panel\n title=\"Actions\"\n >\n <v-expansion-panel-text>\n <v-select variant=\"outlined\" "
},
{
"path": "src/views/flowable/design/propertiesPanel/components/condition.vue",
"chars": 1280,
"preview": "<template>\n <v-expansion-panel\n title=\"Condition\"\n >\n <v-expansion-panel-text>\n <v-select variant=\"outlined"
},
{
"path": "src/views/flowable/design/propertiesPanel/components/documentation.vue",
"chars": 1004,
"preview": "<template>\n <v-expansion-panel\n title=\"Documentation\"\n >\n <v-expansion-panel-text>\n <v-textarea variant=\"ou"
},
{
"path": "src/views/flowable/design/propertiesPanel/components/form.vue",
"chars": 1164,
"preview": "<template>\n <v-expansion-panel\n title=\"Forms\"\n >\n <v-expansion-panel-text>\n <v-select variant=\"outlined\" la"
},
{
"path": "src/views/flowable/design/propertiesPanel/components/general.vue",
"chars": 2743,
"preview": "<template>\n\n <v-expansion-panel\n title=\"General\"\n >\n <v-expansion-panel-text>\n <v-text-field variant=\"outli"
},
{
"path": "src/views/flowable/design/propertiesPanel/components/userAssigne.vue",
"chars": 3068,
"preview": "<template>\n <v-expansion-panel\n title=\"User Assignment\"\n >\n <v-expansion-panel-text>\n <v-select label=\"Type"
}
]
// ... and 38 more files (download for full content)
About this extraction
This page contains the full source code of the sunhao1256/lulu-admin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 238 files (341.5 KB), approximately 99.8k tokens, and a symbol index with 390 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.