feat: 初始化项目结构并添加基础配置
添加前后端基础项目结构,包括.gitignore、package.json等配置文件 实现前端基础功能模块,包括路由、状态管理、API请求封装等 添加前端UI组件库和样式体系 配置开发环境Mock系统和构建工具链
34
front-end/src/App.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<vab-app />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DisableDevtool from 'disable-devtool'
|
||||
import { noDebugger } from '@/config/index'
|
||||
|
||||
defineOptions({
|
||||
name: 'App',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
onMounted(() => {
|
||||
// 是否允许生产环境进行代码调试,请前往config/cli.config.ts文件配置
|
||||
|
||||
setTimeout(() => {
|
||||
if (
|
||||
!location.hostname.includes('127') &&
|
||||
!location.hostname.includes('localhost') &&
|
||||
(location.hostname.includes('beautiful') ||
|
||||
location.hostname.includes('vuejs-core') ||
|
||||
noDebugger) &&
|
||||
route.query &&
|
||||
route.query.debugger !== 'auto'
|
||||
)
|
||||
DisableDevtool({
|
||||
url: 'https://vuejs-core.cn/debugger',
|
||||
timeOutUrl: 'https://vuejs-core.cn/debugger',
|
||||
})
|
||||
}, 500)
|
||||
})
|
||||
</script>
|
||||
25
front-end/src/api/area.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params?: any) {
|
||||
return request({
|
||||
url: '/area/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function doEdit(data: any) {
|
||||
return request({
|
||||
url: '/area/doEdit',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function doDelete(data: any) {
|
||||
return request({
|
||||
url: '/area/doDelete',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
9
front-end/src/api/defaultIcon.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getIconList(params?: any) {
|
||||
return request({
|
||||
url: '/defaultIcon/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
25
front-end/src/api/departmentManagement.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params?: any) {
|
||||
return request({
|
||||
url: '/departmentManagement/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function doEdit(data: any) {
|
||||
return request({
|
||||
url: '/departmentManagement/doEdit',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function doDelete(data: any) {
|
||||
return request({
|
||||
url: '/departmentManagement/doDelete',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
13
front-end/src/api/description.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList() {
|
||||
const params: any = {}
|
||||
if (process.env.NODE_ENV === 'production')
|
||||
params.u = btoa(process.env['VUE_A' + 'PP_GIT' + 'HUB_US' + 'ER_NAME'])
|
||||
|
||||
return request({
|
||||
url: 'https://api.vuejs-core.cn/getDescription',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
33
front-end/src/api/dictionaryManagement.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getTree(params?: any) {
|
||||
return request({
|
||||
url: '/dictionaryManagement/getTree',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function getList(params?: any) {
|
||||
return request({
|
||||
url: '/dictionaryManagement/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function doEdit(data: any) {
|
||||
return request({
|
||||
url: '/dictionaryManagement/doEdit',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function doDelete(data: any) {
|
||||
return request({
|
||||
url: '/dictionaryManagement/doDelete',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
9
front-end/src/api/goods.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params?: any) {
|
||||
return request({
|
||||
url: '/goods/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
25
front-end/src/api/menuManagement.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getTree(params?: any) {
|
||||
return request({
|
||||
url: '/menuManagement/getTree',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function doEdit(data: any) {
|
||||
return request({
|
||||
url: '/menuManagement/doEdit',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function doDelete(data: any) {
|
||||
return request({
|
||||
url: '/menuManagement/doDelete',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
8
front-end/src/api/notice.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList() {
|
||||
return request({
|
||||
url: '/notice/getList',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
8
front-end/src/api/publicKey.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getPublicKey() {
|
||||
return request({
|
||||
url: '/publicKey',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
15
front-end/src/api/refreshToken.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function expireToken() {
|
||||
return request({
|
||||
url: '/expireToken',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function refreshToken() {
|
||||
return request({
|
||||
url: '/refreshToken',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
25
front-end/src/api/roleManagement.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params?: any) {
|
||||
return request({
|
||||
url: '/roleManagement/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function doEdit(data: any) {
|
||||
return request({
|
||||
url: '/roleManagement/doEdit',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function doDelete(data: any) {
|
||||
return request({
|
||||
url: '/roleManagement/doDelete',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
8
front-end/src/api/router.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList() {
|
||||
return request({
|
||||
url: '/router/getList',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
8
front-end/src/api/search.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList() {
|
||||
return request({
|
||||
url: '/search/getList',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
9
front-end/src/api/systemLog.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params?: any) {
|
||||
return request({
|
||||
url: '/systemLog/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
25
front-end/src/api/table.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params?: any) {
|
||||
return request({
|
||||
url: '/table/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function doEdit(data: any) {
|
||||
return request({
|
||||
url: '/table/doEdit',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function doDelete(data: any) {
|
||||
return request({
|
||||
url: '/table/doDelete',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
9
front-end/src/api/taskManagement.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params?: any) {
|
||||
return request({
|
||||
url: '/taskManagement/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
47
front-end/src/api/user.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import request from '@/utils/request'
|
||||
import { encryptedData } from '@/utils/encrypt'
|
||||
import { loginRSA } from '@/config'
|
||||
|
||||
export async function login(data: any) {
|
||||
if (loginRSA) {
|
||||
data = await encryptedData(data)
|
||||
}
|
||||
return request({
|
||||
url: '/login',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export async function socialLogin(data: any) {
|
||||
if (loginRSA) {
|
||||
data = await encryptedData(data)
|
||||
}
|
||||
return request({
|
||||
url: '/socialLogin',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
return request({
|
||||
url: '/userInfo',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return request({
|
||||
url: '/logout',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function register(data: any) {
|
||||
return request({
|
||||
url: '/register',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
25
front-end/src/api/userManagement.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params?: any) {
|
||||
return request({
|
||||
url: '/userManagement/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function doEdit(data: any) {
|
||||
return request({
|
||||
url: '/userManagement/doEdit',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function doDelete(data: any) {
|
||||
return request({
|
||||
url: '/userManagement/doDelete',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
17
front-end/src/api/workflow.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getList(params?: any) {
|
||||
return request({
|
||||
url: '/workflow/getList',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function doEdit(data: any) {
|
||||
return request({
|
||||
url: '/workflow/doEdit',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
BIN
front-end/src/assets/cropper_images/user.gif
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
front-end/src/assets/empty_images/data_empty.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
front-end/src/assets/error_images/403.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
front-end/src/assets/error_images/404.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
front-end/src/assets/error_images/cloud.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
front-end/src/assets/index_images/image.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
front-end/src/assets/login_images/background.jpg
Normal file
|
After Width: | Height: | Size: 376 KiB |
BIN
front-end/src/assets/login_images/login_form.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
front-end/src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
front-end/src/assets/mobile_images/mobile.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
front-end/src/assets/rank_images/rank.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
front-end/src/assets/skm.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
front-end/src/assets/skm1.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
front-end/src/assets/skm2.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
front-end/src/assets/tabs_images/vab-tab.png
Normal file
|
After Width: | Height: | Size: 443 B |
BIN
front-end/src/assets/task_image/task.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
front-end/src/assets/theme_images/background-1.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
33
front-end/src/config/cli.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @description 导出vue/cli配置,以下所有配置修改需要重启项目
|
||||
*/
|
||||
module.exports = {
|
||||
// 开发以及部署时的URL
|
||||
// hash模式时在不确定二级目录名称的情况下建议使用""代表相对路径或者"/二级目录/"
|
||||
// history模式默认使用"/"或者"/二级目录/",记住只有hash时publicPath可以为空!!!
|
||||
publicPath: '',
|
||||
// 生产环境构建文件的目录名
|
||||
outputDir: 'dist',
|
||||
// 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录。
|
||||
assetsDir: 'static',
|
||||
// 开发环境每次保存时是否输出为eslint编译警告
|
||||
lintOnSave: true,
|
||||
// 进行编译的依赖
|
||||
transpileDependencies: [],
|
||||
// 开发环境端口号
|
||||
devPort: 15000,
|
||||
// 需要自动注入并加载的模块
|
||||
providePlugin: {},
|
||||
// npm run build时是否自动生成7z压缩包
|
||||
build7z: false,
|
||||
// npm run build时是否生成gzip
|
||||
buildGzip: false,
|
||||
// npm run build时是否开启图片压缩,由于国内网路原因image-webpack-loader必须使用cnpm安装,如无法使用cnpm,请配置false
|
||||
imageCompression: false,
|
||||
// pwa
|
||||
pwa: true,
|
||||
// 打包优化,如需实现服务器快速部署请配置false,如需提升网页加载速度请配置true
|
||||
buildOptimize: true,
|
||||
// 禁止在生产环境下使用调试
|
||||
noDebugger: true,
|
||||
}
|
||||
15
front-end/src/config/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @description 4个子配置,vue/cli配置|通用配置|主题配置|网络配置导出
|
||||
* config中的部分配置由vue.config.js读取,本质是node,故不可使用window等浏览器对象
|
||||
*/
|
||||
const cli = require('./cli.config')
|
||||
const setting = require('./setting.config')
|
||||
const theme = require('./theme.config')
|
||||
const network = require('./net.config')
|
||||
|
||||
module.exports = {
|
||||
...cli,
|
||||
...setting,
|
||||
...theme,
|
||||
...network,
|
||||
}
|
||||
27
front-end/src/config/net.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @description 导出网络配置
|
||||
**/
|
||||
module.exports = {
|
||||
// 默认的接口地址,开发环境和生产环境都会走/vab-mock-server
|
||||
// 正式项目可以选择自己配置成需要的接口地址,如"https://api.xxx.com"
|
||||
// 问号后边代表开发环境,冒号后边代表生产环境
|
||||
// 如果不需要测试环境解除以下注释即可
|
||||
// baseURL:
|
||||
// process.env.NODE_ENV === 'development'
|
||||
// ? '/vab-mock-server'
|
||||
// : '/vab-mock-server',
|
||||
|
||||
// 支持多环境接口地址配置的方法
|
||||
// 开发环境去.env.development改,生产环境去.env.production改,测试环境去.env.test改
|
||||
baseURL: `${process.env.VUE_APP_BASE_URL}`,
|
||||
// 配后端数据的接收方式application/json;charset=UTF-8 或 application/x-www-form-urlencoded;charset=UTF-8
|
||||
contentType: 'application/json;charset=UTF-8',
|
||||
// 最长请求时间
|
||||
requestTimeout: 10000,
|
||||
// 操作正常code,支持String、Array、int多种类型
|
||||
successCode: [200, 0, '200', '0'],
|
||||
// 数据状态的字段名称
|
||||
statusName: 'code',
|
||||
// 状态信息的字段名称
|
||||
messageName: 'msg',
|
||||
}
|
||||
67
front-end/src/config/setting.config.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @description 导出通用配置
|
||||
*/
|
||||
module.exports = {
|
||||
// 标题,此项修改后需要重启项目!!! (包括初次加载雪花屏的标题 页面的标题 浏览器的标题)
|
||||
title: 'Vue Admin Plus',
|
||||
// 标题分隔符
|
||||
titleSeparator: ' - ',
|
||||
// 标题是否反转
|
||||
// 如果为false: "page - title"
|
||||
// 如果为true : "title - page"
|
||||
titleReverse: false,
|
||||
// 简写
|
||||
abbreviation: 'vab-admin-plus',
|
||||
// pro版本copyright可随意修改
|
||||
copyright: 'zxwk1998',
|
||||
// 缓存路由的最大数量
|
||||
keepAliveMaxNum: 20,
|
||||
// 路由模式,是否为hash模式
|
||||
isHashRouterMode: true,
|
||||
// 不经过token校验的路由,白名单路由建议配置到与login页面同级,如果需要放行带传参的页面,请使用query传参,配置时只配置path即可
|
||||
routesWhiteList: ['/login', '/register', '/callback', '/404', '/403'],
|
||||
// 加载时显示文字
|
||||
loadingText: '正在加载中...',
|
||||
// token名称
|
||||
tokenName: 'token',
|
||||
// token在localStorage、sessionStorage、cookie存储的key的名称
|
||||
tokenTableName: 'admin-plus-token',
|
||||
// token存储位置localStorage sessionStorage cookie
|
||||
storage: 'localStorage',
|
||||
// token失效回退到登录页时是否记录本次的路由(是否记录当前tab页)
|
||||
recordRoute: true,
|
||||
// 是否开启logo,不显示时设置false,请填写src/icon路径下的图标名称
|
||||
// 如需使用内置RemixIcon图标,请自行去logo组件切换注释代码(内置svg雪碧图较大,对性能有一定影响)
|
||||
logo: 'vuejs-fill',
|
||||
// 语言类型zh、en
|
||||
i18n: 'zh',
|
||||
// 消息框消失时间
|
||||
messageDuration: 3000,
|
||||
// 在哪些环境下显示高亮错误 ['development', 'production']
|
||||
errorLog: 'development',
|
||||
// 是否开启登录拦截
|
||||
loginInterception: true,
|
||||
// 是否开启登录RSA加密
|
||||
loginRSA: false,
|
||||
// intelligence(前端导出路由)和 all(后端导出路由)两种方式
|
||||
authentication: 'intelligence',
|
||||
// 是否支持游客模式,支持情况下,访问白名单,可查看所有asyncRoutes
|
||||
supportVisit: false,
|
||||
// 是否开启roles字段进行角色权限控制(如果是all模式后端完全处理角色并进行json组装,可设置false不处理路由中的roles字段)
|
||||
rolesControl: true,
|
||||
// vertical column comprehensive common布局时是否只保持一个子菜单的展开
|
||||
uniqueOpened: false,
|
||||
// vertical column comprehensive common布局时默认展开的菜单path,使用逗号隔开建议只展开一个,true全部展开,false/[]不展开
|
||||
defaultOpeneds: [
|
||||
'/vab',
|
||||
'/vab/table',
|
||||
'/vab/icon',
|
||||
'/vab/form',
|
||||
'/vab/editor',
|
||||
'/other/drag',
|
||||
],
|
||||
// 需要加loading层的请求,防止重复提交
|
||||
debounce: ['doEdit'],
|
||||
// 分栏布局和综合布局时,是否点击一级菜单默认开启二级菜单(默认第一个,可通过redirect自定义)
|
||||
openFirstMenu: true,
|
||||
}
|
||||
45
front-end/src/config/theme.config.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @description 导出主题配置,注意事项:此配置下的项修改后需清理浏览器缓存!!!
|
||||
*/
|
||||
module.exports = {
|
||||
// 布局种类:横向布局horizontal、纵向布局vertical、分栏布局column、综合布局comprehensive、常规布局common、浮动布局float
|
||||
layout: 'column',
|
||||
// 主题名称:默认blue-black、blue-white、green-black、green-white、渐变ocean、red-white、red-black
|
||||
themeName: 'blue-black',
|
||||
// 菜单背景 none、vab-background
|
||||
background: 'none',
|
||||
// 菜单宽度,仅支持px,建议大小:266px、277px、288px,其余尺寸会影响美观
|
||||
menuWidth: '266px',
|
||||
// 分栏风格(仅针对分栏布局column时生效):横向风格horizontal、纵向风格vertical、卡片风格card、箭头风格arrow
|
||||
columnStyle: 'card',
|
||||
// 是否固定头部固定
|
||||
fixedHeader: true,
|
||||
// 是否开启顶部进度条
|
||||
showProgressBar: true,
|
||||
// 是否开启标签页
|
||||
showTabs: true,
|
||||
// 显示标签页时标签页样式:卡片风格card、灵动风格smart、圆滑风格smooth
|
||||
tabsBarStyle: 'smooth',
|
||||
// 是否标签页图标
|
||||
showTabsIcon: true,
|
||||
// 是否开启语言选择组件
|
||||
showLanguage: true,
|
||||
// 是否开启刷新组件
|
||||
showRefresh: true,
|
||||
// 是否开启搜索组件
|
||||
showSearch: true,
|
||||
// 是否开启主题组件
|
||||
showTheme: true,
|
||||
// 是否开启通知组件
|
||||
showNotice: true,
|
||||
// 是否开启全屏组件
|
||||
showFullScreen: true,
|
||||
// 是否开启右侧悬浮窗
|
||||
showThemeSetting: true,
|
||||
//纵向布局、常规布局、综合布局时是否默认收起左侧菜单(不支持分栏布局、横向布局)
|
||||
foldSidebar: false,
|
||||
// 是否开启页面动画
|
||||
showPageTransition: true,
|
||||
// 是否开启锁屏
|
||||
showLock: true,
|
||||
}
|
||||
44
front-end/src/i18n/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './locales/en.json'
|
||||
import pinia from '@/store'
|
||||
import { useSettingsStore } from '@/store/modules/settings'
|
||||
import type { LanguageType } from '/#/store'
|
||||
|
||||
const messages: Record<LanguageType, any> = {
|
||||
en: {
|
||||
...en,
|
||||
},
|
||||
zh: {},
|
||||
}
|
||||
|
||||
function getLanguage() {
|
||||
const { getLanguage } = useSettingsStore(pinia)
|
||||
return getLanguage
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: getLanguage(),
|
||||
fallbackLocale: 'zh',
|
||||
messages,
|
||||
})
|
||||
|
||||
export function setupI18n(app: any) {
|
||||
app.use(i18n)
|
||||
return i18n
|
||||
}
|
||||
|
||||
export function translate(message: string | undefined) {
|
||||
if (!message) {
|
||||
return ''
|
||||
}
|
||||
return (
|
||||
[getLanguage(), 'vabI18n', message].reduce(
|
||||
(o, k) => (o || {})[k],
|
||||
messages as any
|
||||
) || message
|
||||
)
|
||||
}
|
||||
|
||||
export { default as enLocale } from 'element-plus/dist/locale/en'
|
||||
export { default as zhLocale } from 'element-plus/dist/locale/zh-cn'
|
||||
172
front-end/src/i18n/locales/en.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"vabI18n": {
|
||||
"403": "403",
|
||||
"404": "404",
|
||||
"Css动画": "Cssfx",
|
||||
"Excel": "Excel",
|
||||
"按钮": "Button",
|
||||
"保存": "Save",
|
||||
"编辑器": "Editor",
|
||||
"标签": "Tabs",
|
||||
"标签风格": "Tabs style",
|
||||
"标签开启时生效": "Effective when the label is opened",
|
||||
"标签图标": "Tabs icon",
|
||||
"表单": "Form",
|
||||
"表格": "Table",
|
||||
"不固定": "No fixed",
|
||||
"布局": "Layouts",
|
||||
"布局配置仅在电脑视窗下生效,手机视窗时将默认锁定为纵向布局": "The layout configuration only takes effect in the computer window,the vertical layout will be locked in the mobile window by default",
|
||||
"部门管理": "Department management",
|
||||
"菜单背景": "Background",
|
||||
"菜单管理": "Menu management",
|
||||
"菜单宽度": "Menu width",
|
||||
"仓库": "Store",
|
||||
"常规": "Common",
|
||||
"常规图标": "Awesome icon",
|
||||
"常用设置": "Common settings",
|
||||
"错误日志模拟": "Log",
|
||||
"错误页": "Error",
|
||||
"打印": "Print",
|
||||
"单选框": "Radip",
|
||||
"导出Excel": "Export excel",
|
||||
"导出合并Excel": "Export merge header excel",
|
||||
"导出选中行Excel": "Export selected excel",
|
||||
"登录": "Login",
|
||||
"第三方登录": "Social login",
|
||||
"动态Meta": "Dynamic meta",
|
||||
"动态表格": "Dynamic table",
|
||||
"动态路径参数": "Dynamic segment",
|
||||
"动态锚点": "Dynamic anchor",
|
||||
"多标签": "Tabs",
|
||||
"'多级路由1-1'": "Menu1-1",
|
||||
"'多级路由1-1-1'": "Menu1-1-1",
|
||||
"多级路由缓存": "Menu1",
|
||||
"多选框": "Checkbox",
|
||||
"分步表单": "Step form",
|
||||
"分栏": "Column",
|
||||
"分栏布局时生效": "Column layout takes effect",
|
||||
"分栏风格": "Column style",
|
||||
"分享": "Share",
|
||||
"浮动": "Float",
|
||||
"富文本编辑器": "Rich text editor",
|
||||
"个人中心": "User center",
|
||||
"更多": "More",
|
||||
"更新日志": "Change log",
|
||||
"工具": "Tools",
|
||||
"工作流": "Workflow",
|
||||
"工作台": "Workbench",
|
||||
"购买源码": "Buy",
|
||||
"固定": "Fixed",
|
||||
"关闭": "Close",
|
||||
"关闭其他": "Close other",
|
||||
"关闭全部": "Close all",
|
||||
"关闭右侧": "Close right",
|
||||
"关闭左侧": "Close left",
|
||||
"国际化": "Language",
|
||||
"海洋之心": "Ocean",
|
||||
"横向": "Horizontal",
|
||||
"红白": "Red white",
|
||||
"红黑": "Red black",
|
||||
"滑块": "Slider",
|
||||
"欢迎来到": "Welcome to",
|
||||
"恢复默认": "Defalut",
|
||||
"获取验证码": "Get captcha",
|
||||
"计数器": "Input number",
|
||||
"加载": "Loading",
|
||||
"渐变": "Ocean",
|
||||
"箭头": "Arrow",
|
||||
"角色管理": "Role management",
|
||||
"角色权限": "Roles",
|
||||
"解锁": "Unlock",
|
||||
"进度条": "Progress",
|
||||
"卡片": "Card",
|
||||
"卡片拖拽": "Card drag",
|
||||
"开关": "Switch",
|
||||
"开启": "Open",
|
||||
"看板": "Dashboard",
|
||||
"拷贝源码": "Code",
|
||||
"蓝白": "Blue white",
|
||||
"蓝黑": "Blue black",
|
||||
"列表": "List",
|
||||
"灵动": "Smart",
|
||||
"绿白": "Green white",
|
||||
"绿黑": "Green black",
|
||||
"绿荫草场": "Green",
|
||||
"密码不能少于6位": "The password cannot be less than 6 digits",
|
||||
"描述": "Description",
|
||||
"默认": "Default",
|
||||
"配置": "Settings",
|
||||
"碰触纯白": "White",
|
||||
"评分": "rate",
|
||||
"屏幕已锁定": "Screen already locked",
|
||||
"其他": "Other",
|
||||
"其它设置": "Other settings",
|
||||
"切换壁纸": "Switch wallpaper",
|
||||
"清空消息": "Clear message",
|
||||
"清理缓存": "Claer",
|
||||
"请输入密码": "Please input a password",
|
||||
"请输入手机号": "Please enter your mobile phone number",
|
||||
"请输入手机验证码": "Please input the mobile phone verification code",
|
||||
"请输入用户名": "Please enter one user name",
|
||||
"请输入正确的手机号": "Please enter the correct mobile phone number",
|
||||
"全屏": "Full screen",
|
||||
"取色器": "Color picker",
|
||||
"任务管理": "Task management",
|
||||
"日历": "Calendar",
|
||||
"日期时间选择器": "Date time picker",
|
||||
"日期选择器": "Date picker",
|
||||
"上传": "Upload",
|
||||
"时间线": "Timeline",
|
||||
"时间选择器": "Time picker",
|
||||
"视频播放器": "Player",
|
||||
"手机预览": "Mobile preview",
|
||||
"首页": "Home",
|
||||
"输入框": "Input",
|
||||
"数字自增长": "Count",
|
||||
"刷新": "Refresh",
|
||||
"搜索": "Search",
|
||||
"随机换肤": "Random",
|
||||
"锁屏": "Lock screen",
|
||||
"水印": "Watermark",
|
||||
"弹窗拖拽": "Diaglog Drag",
|
||||
"腾讯文档": "Wang editor",
|
||||
"通知": "Notice",
|
||||
"头部固定": "Header",
|
||||
"头像裁剪": "Head cropper",
|
||||
"图标": "Icon",
|
||||
"图标选择器": "Icon selector",
|
||||
"图表": "Echarts",
|
||||
"退出登录": "Logout",
|
||||
"拖拽": "Drag",
|
||||
"外链": "External links",
|
||||
"文字链接": "Link",
|
||||
"无分栏": "No column",
|
||||
"无框": "No layout",
|
||||
"物料市场": "Material market",
|
||||
"物料源": "Material",
|
||||
"系统日志": "System log",
|
||||
"默认图标": "Default icon",
|
||||
"行内编辑表格": "Inline edit table",
|
||||
"选择器": "Select",
|
||||
"验证码": "Verification code",
|
||||
"页面动画": "Page transition",
|
||||
"用户管理": "User management",
|
||||
"用户名不能为空": "The user name cannot be empty",
|
||||
"邮件": "Email",
|
||||
"语音合成": "Speech synthesis",
|
||||
"圆滑": "Smooth",
|
||||
"月上重火": "Red",
|
||||
"支持纵向布局、分栏布局、综合布局、常规布局,不支持横向布局、浮动布局": "Vertical layout, column layout, comprehensive layout and general layout are supported, while horizontal layout and floating layout are not supported",
|
||||
"主题": "Theme",
|
||||
"主题配置": "Theme",
|
||||
"注册": "Register",
|
||||
"字典管理": "Dictionary management",
|
||||
"自定义表格": "Custom table",
|
||||
"自定义图标": "Custom svg",
|
||||
"综合": "Comprehensive",
|
||||
"综合表单": "Comprehensive form",
|
||||
"综合表格": "Comprehensive table",
|
||||
"纵向": "Vertical",
|
||||
"组件": "Part"
|
||||
}
|
||||
}
|
||||
2
front-end/src/icon/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
const icons = require.context('.', true, /\.svg$/)
|
||||
icons.keys().map(icons)
|
||||
24
front-end/src/icon/vab.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="550px" height="400px"
|
||||
xml:space="preserve">
|
||||
<g id="PathID_1" transform="matrix(10.7099, 0, 0, 10.7099, 76.4, 396.15)" opacity="1">
|
||||
<path style="fill: #41b882; fill-opacity: 1;"
|
||||
d="M3.75 -36.65L18.4 -36.65Q22.75 -36.65 24.85 -36.25Q27 -35.9 28.7 -34.75Q30.4 -33.6 31.5 -31.7Q32.65 -29.8 32.65 -27.4Q32.65 -24.85 31.25 -22.7Q29.85 -20.55 27.5 -19.5Q30.85 -18.5 32.65 -16.15Q34.45 -13.8 34.45 -10.6Q34.45 -8.1 33.25 -5.75Q32.1 -3.4 30.1 -1.95Q28.1 -0.55 25.15 -0.25Q23.3 -0.05 16.2 0L3.75 0L3.75 -36.65M11.15 -30.55L11.15 -22.1L16 -22.1Q20.3 -22.1 21.35 -22.2Q23.25 -22.4 24.35 -23.5Q25.45 -24.6 25.45 -26.35Q25.45 -28.05 24.5 -29.1Q23.55 -30.2 21.7 -30.4Q20.6 -30.55 15.4 -30.55L11.15 -30.55M11.15 -16L11.15 -6.2L18 -6.2Q22 -6.2 23.05 -6.4Q24.7 -6.7 25.75 -7.85Q26.8 -9.05 26.8 -11Q26.8 -12.65 26 -13.8Q25.2 -14.95 23.65 -15.45Q22.15 -16 17.1 -16L11.15 -16"/>
|
||||
</g>
|
||||
<g id="PathID_2" transform="matrix(10.7099, 0, 0, 10.7099, 76.4, 396.15)" opacity="1">
|
||||
</g>
|
||||
<g id="PathID_3" transform="matrix(5.31826, 0, 0, 2.59618, 172.9, 161.55)" opacity="1">
|
||||
<path style="fill: #35495e; fill-opacity: 1;"
|
||||
d="M3.75 -36.65L17.25 -36.65Q21.8 -36.65 24.2 -35.95Q27.45 -35 29.75 -32.55Q32.05 -30.15 33.25 -26.6Q34.45 -23.1 34.45 -17.95Q34.45 -13.45 33.3 -10.15Q31.95 -6.15 29.4 -3.7Q27.45 -1.8 24.2 -0.75Q21.75 0 17.65 0L3.75 0L3.75 -36.65M11.15 -30.45L11.15 -6.2L16.65 -6.2Q19.75 -6.2 21.1 -6.55Q22.9 -6.95 24.1 -8Q25.3 -9.1 26.05 -11.55Q26.8 -14.05 26.8 -18.3Q26.8 -22.55 26.05 -24.8Q25.3 -27.1 23.95 -28.35Q22.6 -29.65 20.5 -30.1Q18.95 -30.45 14.45 -30.45L11.15 -30.45"/>
|
||||
</g>
|
||||
<g id="PathID_4" transform="matrix(5.31826, 0, 0, 2.59618, 172.9, 161.55)" opacity="1">
|
||||
</g>
|
||||
<g id="PathID_5" transform="matrix(5.78477, 0, 0, 3.1825, 171.7, 333.8)" opacity="1">
|
||||
<path style="fill: #35495e; fill-opacity: 1;"
|
||||
d="M3.75 -36.65L17.25 -36.65Q21.8 -36.65 24.2 -35.95Q27.45 -35 29.75 -32.55Q32.05 -30.15 33.25 -26.6Q34.45 -23.1 34.45 -17.95Q34.45 -13.45 33.3 -10.15Q31.95 -6.15 29.4 -3.7Q27.45 -1.8 24.2 -0.75Q21.75 0 17.65 0L3.75 0L3.75 -36.65M11.15 -30.45L11.15 -6.2L16.65 -6.2Q19.75 -6.2 21.1 -6.55Q22.9 -6.95 24.1 -8Q25.3 -9.1 26.05 -11.55Q26.8 -14.05 26.8 -18.3Q26.8 -22.55 26.05 -24.8Q25.3 -27.1 23.95 -28.35Q22.6 -29.65 20.5 -30.1Q18.95 -30.45 14.45 -30.45L11.15 -30.45"/>
|
||||
</g>
|
||||
<g id="PathID_6" transform="matrix(5.78477, 0, 0, 3.1825, 171.7, 333.8)" opacity="1">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
4
front-end/src/icon/vuejs-fill.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M1 3h4l7 12 7-12h4L12 22 1 3zm8.667 0L12 7l2.333-4h4.035L12 14 5.632 3h4.035z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 200 B |
36
front-end/src/main.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { baseURL, pwa } from './config'
|
||||
import { setupVab } from '~/library'
|
||||
import { setupI18n } from '@/i18n'
|
||||
import { setupStore } from '@/store'
|
||||
import { setupRouter } from '@/router'
|
||||
import { validateSecretKey } from '@/utils'
|
||||
|
||||
/**
|
||||
* @description 正式环境默认使用mock,正式项目记得注释后再打包
|
||||
*/
|
||||
import { isExternal } from '@/utils/validate'
|
||||
|
||||
validateSecretKey()
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && !isExternal(baseURL)) {
|
||||
const { mockXHR } = require('@/utils/static')
|
||||
mockXHR()
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 生产环境启用组件初始化,编译,渲染和补丁性能跟踪。仅在开发模式和支持 Performance.mark API的浏览器中工作。
|
||||
*/
|
||||
//if (process.env.NODE_ENV === 'development') app.config.performance = true
|
||||
|
||||
if (pwa) require('./registerServiceWorker')
|
||||
|
||||
setupVab(app)
|
||||
setupI18n(app)
|
||||
setupStore(app)
|
||||
setupRouter(app)
|
||||
.isReady()
|
||||
.then(() => app.mount('#app'))
|
||||
135
front-end/src/plugins/VabAnchor/index.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="vab-anchor">
|
||||
<div v-for="(item, key) in floorList" :key="key" :class="'floor' + key">
|
||||
<slot v-if="key === key" :name="'floor' + key" />
|
||||
</div>
|
||||
<vab-card
|
||||
:body-style="{
|
||||
padding: '20px 10px 20px 10px',
|
||||
}"
|
||||
shadow="never"
|
||||
style="position: fixed; top: 170px; right: 68px"
|
||||
>
|
||||
<el-tabs
|
||||
v-model="step"
|
||||
tab-position="right"
|
||||
@tab-click="handleClick"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="(item, key) in floorList"
|
||||
:key="key"
|
||||
:label="item.title"
|
||||
/>
|
||||
</el-tabs>
|
||||
</vab-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VabAnchor',
|
||||
props: {
|
||||
floorList: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [
|
||||
{ title: '锚点1' },
|
||||
{ title: '锚点2' },
|
||||
{ title: '锚点3' },
|
||||
{ title: '锚点4' },
|
||||
{ title: '锚点5' },
|
||||
]
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
step: '0',
|
||||
scrolltop: 0,
|
||||
floorObject: {},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
scrolltop(val) {
|
||||
val += 200
|
||||
const floorObject = this.floorObject
|
||||
for (let i = 0; i <= this.floorList.length + 1; i++) {
|
||||
if (
|
||||
val > floorObject[`floor${i}`] &&
|
||||
(val <= floorObject[`floor${Number.parseInt(i + 1)}`] ||
|
||||
val <= Number.POSITIVE_INFINITY)
|
||||
) {
|
||||
this.step = `${i}`
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getFloorDistance()
|
||||
document.querySelector('#app').addEventListener('scroll', () => {
|
||||
this.scrolltop = document.querySelector('#app').scrollTop
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
handleClick({ index }) {
|
||||
this.anchors(index)
|
||||
},
|
||||
anchors(item) {
|
||||
this.pulleyRoll(
|
||||
this.floorObject[`floor${item}`],
|
||||
this.scrolltop
|
||||
)
|
||||
},
|
||||
pulleyRoll(top, distance) {
|
||||
if (distance < top) {
|
||||
const smallInterval = (top - distance) / 50
|
||||
let i = 0
|
||||
const timer = setInterval(() => {
|
||||
i++
|
||||
distance += smallInterval
|
||||
document.querySelector('#app').scrollTop = distance
|
||||
if (i == 50) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 10)
|
||||
} else if (distance > top) {
|
||||
const smallInterval = (distance - top) / 50
|
||||
let i = 0
|
||||
const timer = setInterval(() => {
|
||||
i++
|
||||
distance -= smallInterval
|
||||
document.querySelector('#app').scrollTop = distance
|
||||
if (i == 50) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
},
|
||||
getFloorDistance() {
|
||||
for (let i = 0; i < this.floorList.length; i++) {
|
||||
this.floorObject[`floor${i}`] =
|
||||
document.getElementsByClassName(
|
||||
`floor${i}`
|
||||
)[0].offsetTop
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-anchor {
|
||||
[class*='floor'] {
|
||||
height: 780px;
|
||||
padding: $base-padding;
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: #b5ff8a;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background: #6db9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
33
front-end/src/plugins/VabAvatarList/index.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
const props: any = defineProps({
|
||||
avatarList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vab-avatar-list">
|
||||
<el-tooltip
|
||||
v-for="(item, index) in props.avatarList"
|
||||
:key="index"
|
||||
:content="item.username"
|
||||
effect="dark"
|
||||
placement="top-start"
|
||||
>
|
||||
<el-avatar :size="40" :src="item.avatar" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-avatar-list {
|
||||
:deep(.el-avatar) {
|
||||
display: inline-block;
|
||||
margin-left: -15px;
|
||||
cursor: pointer;
|
||||
border: 3px solid var(--el-color-white);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1122
front-end/src/plugins/VabCalendar.ts
Normal file
287
front-end/src/plugins/VabChart/index.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div class="echarts" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import debounce from 'lodash/debounce'
|
||||
import theme from './theme/vab-echarts-theme.json'
|
||||
import { addListener, removeListener } from 'resize-detector'
|
||||
|
||||
const INIT_TRIGGERS = ['theme', 'initOptions', 'autoResize']
|
||||
const REWATCH_TRIGGERS = ['manualUpdate', 'watchShallow']
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
theme: {
|
||||
type: [String, Object],
|
||||
default: () => {},
|
||||
},
|
||||
initOptions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
autoResize: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
watchShallow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
manualUpdate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastArea: 0,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
group(group) {
|
||||
this.chart.group = group
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initOptionsWatcher()
|
||||
INIT_TRIGGERS.forEach((prop) => {
|
||||
this.$watch(
|
||||
prop,
|
||||
() => {
|
||||
this.refresh()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
})
|
||||
REWATCH_TRIGGERS.forEach((prop) => {
|
||||
this.$watch(prop, () => {
|
||||
this.initOptionsWatcher()
|
||||
this.refresh()
|
||||
})
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
if (this.option) {
|
||||
echarts.registerTheme('vab-echarts-theme', theme)
|
||||
this.init()
|
||||
}
|
||||
},
|
||||
activated() {
|
||||
if (this.autoResize) {
|
||||
this.chart && this.chart.resize()
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
if (this.chart) {
|
||||
this.destroy()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mergeOptions(option, notMerge, lazyUpdate) {
|
||||
if (this.manualUpdate) {
|
||||
this.manualOptions = option
|
||||
}
|
||||
if (!this.chart) {
|
||||
this.init(option)
|
||||
} else {
|
||||
this.delegateMethod(
|
||||
'setOption',
|
||||
option,
|
||||
notMerge,
|
||||
lazyUpdate
|
||||
)
|
||||
}
|
||||
},
|
||||
appendData(params) {
|
||||
this.delegateMethod('appendData', params)
|
||||
},
|
||||
resize(option) {
|
||||
this.delegateMethod('resize', option)
|
||||
},
|
||||
dispatchAction(payload) {
|
||||
this.delegateMethod('dispatchAction', payload)
|
||||
},
|
||||
convertToPixel(finder, value) {
|
||||
return this.delegateMethod('convertToPixel', finder, value)
|
||||
},
|
||||
convertFromPixel(finder, value) {
|
||||
return this.delegateMethod('convertFromPixel', finder, value)
|
||||
},
|
||||
containPixel(finder, value) {
|
||||
return this.delegateMethod('containPixel', finder, value)
|
||||
},
|
||||
showLoading(type, option) {
|
||||
this.delegateMethod('showLoading', type, option)
|
||||
},
|
||||
hideLoading() {
|
||||
this.delegateMethod('hideLoading')
|
||||
},
|
||||
getDataURL(option) {
|
||||
return this.delegateMethod('getDataURL', option)
|
||||
},
|
||||
getConnectedDataURL(option) {
|
||||
return this.delegateMethod('getConnectedDataURL', option)
|
||||
},
|
||||
clear() {
|
||||
this.delegateMethod('clear')
|
||||
},
|
||||
dispose() {
|
||||
this.delegateMethod('dispose')
|
||||
},
|
||||
delegateMethod(name, ...args) {
|
||||
if (!this.chart) {
|
||||
this.init()
|
||||
}
|
||||
return this.chart[name](...args)
|
||||
},
|
||||
delegateGet(methodName) {
|
||||
if (!this.chart) {
|
||||
this.init()
|
||||
}
|
||||
return this.chart[methodName]()
|
||||
},
|
||||
getArea() {
|
||||
return this.$el.offsetWidth * this.$el.offsetHeight
|
||||
},
|
||||
init(option) {
|
||||
if (this.chart) {
|
||||
return
|
||||
}
|
||||
const chart = echarts.init(
|
||||
this.$el,
|
||||
this.theme,
|
||||
this.initOptions
|
||||
)
|
||||
if (this.group) {
|
||||
chart.group = this.group
|
||||
}
|
||||
chart.clear()
|
||||
chart.setOption(
|
||||
option || this.manualOptions || this.option || {},
|
||||
true
|
||||
)
|
||||
Object.keys(this.$attrs).forEach((event) => {
|
||||
const handler = this.$attrs[event]
|
||||
if (event.indexOf('zr:') === 0) {
|
||||
chart.getZr().on(event.slice(3), handler)
|
||||
} else {
|
||||
chart.on(event, handler)
|
||||
}
|
||||
})
|
||||
if (this.autoResize) {
|
||||
this.lastArea = this.getArea()
|
||||
this.__resizeHandler = debounce(
|
||||
() => {
|
||||
if (this.lastArea === 0) {
|
||||
this.mergeOptions({}, true)
|
||||
this.resize()
|
||||
this.mergeOptions(
|
||||
this.option || this.manualOptions || {},
|
||||
true
|
||||
)
|
||||
} else {
|
||||
this.resize()
|
||||
}
|
||||
this.lastArea = this.getArea()
|
||||
},
|
||||
100,
|
||||
{ leading: true }
|
||||
)
|
||||
addListener(this.$el, this.__resizeHandler)
|
||||
}
|
||||
this.chart = chart
|
||||
Object.defineProperties(this, {
|
||||
width: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return this.delegateGet('getWidth')
|
||||
},
|
||||
},
|
||||
height: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return this.delegateGet('getHeight')
|
||||
},
|
||||
},
|
||||
isDisposed: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return !!this.delegateGet('isDisposed')
|
||||
},
|
||||
},
|
||||
computedOptions: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return this.delegateGet('getOption')
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
initOptionsWatcher() {
|
||||
if (this.__unwatchOptions) {
|
||||
this.__unwatchOptions()
|
||||
this.__unwatchOptions = null
|
||||
}
|
||||
if (!this.manualUpdate) {
|
||||
this.__unwatchOptions = this.$watch(
|
||||
'option',
|
||||
(val, oldVal) => {
|
||||
if (!this.chart && val) {
|
||||
this.init()
|
||||
} else {
|
||||
this.chart.setOption(val, val !== oldVal)
|
||||
}
|
||||
},
|
||||
{ deep: !this.watchShallow }
|
||||
)
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
if (this.autoResize) {
|
||||
removeListener(this.$el, this.__resizeHandler)
|
||||
}
|
||||
this.dispose()
|
||||
this.chart = null
|
||||
},
|
||||
refresh() {
|
||||
if (this.chart) {
|
||||
this.destroy()
|
||||
this.init()
|
||||
}
|
||||
},
|
||||
},
|
||||
connect(group) {
|
||||
if (typeof group !== 'string') {
|
||||
group = group.map((chart) => chart.chart)
|
||||
}
|
||||
echarts.connect(group)
|
||||
},
|
||||
disconnect(group) {
|
||||
echarts.disConnect(group)
|
||||
},
|
||||
getMap(mapName) {
|
||||
return echarts.getMap(mapName)
|
||||
},
|
||||
registerMap(mapName, geoJSON, specialAreas) {
|
||||
echarts.registerMap(mapName, geoJSON, specialAreas)
|
||||
},
|
||||
graphic: echarts.graphic,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.echarts {
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
317
front-end/src/plugins/VabChart/theme/vab-echarts-theme.json
Normal file
@@ -0,0 +1,317 @@
|
||||
{
|
||||
"color": ["#1890FF", "#36CBCB", "#4ECB73", "#FBD437", "#F2637B", "#975FE5"],
|
||||
"backgroundColor": "rgba(252,252,252,0)",
|
||||
"textStyle": {},
|
||||
"title": {
|
||||
"textStyle": {
|
||||
"color": "#666666"
|
||||
},
|
||||
"subtextStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"line": {
|
||||
"itemStyle": {
|
||||
"borderWidth": "2"
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": "3"
|
||||
}
|
||||
},
|
||||
"symbolSize": "8",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": false
|
||||
},
|
||||
"radar": {
|
||||
"itemStyle": {
|
||||
"borderWidth": "2"
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": "3"
|
||||
}
|
||||
},
|
||||
"symbolSize": "8",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": false
|
||||
},
|
||||
"bar": {
|
||||
"itemStyle": {
|
||||
"barBorderWidth": 0,
|
||||
"barBorderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"pie": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"scatter": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"boxplot": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"parallel": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"sankey": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"funnel": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"candlestick": {
|
||||
"itemStyle": {
|
||||
"color": "#e6a0d2",
|
||||
"color0": "transparent",
|
||||
"borderColor": "#e6a0d2",
|
||||
"borderColor0": "#1890FF",
|
||||
"borderWidth": "2"
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": "1",
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"symbolSize": "8",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": false,
|
||||
"color": ["#1890FF", "#36CBCB", "#4ECB73", "#FBD437", "#F2637B", "#975FE5"],
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"itemStyle": {
|
||||
"areaColor": "#eeeeee",
|
||||
"borderColor": "#aaaaaa",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
}
|
||||
},
|
||||
"geo": {
|
||||
"itemStyle": {
|
||||
"areaColor": "#eeeeee",
|
||||
"borderColor": "#aaaaaa",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
}
|
||||
},
|
||||
"categoryAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#999999"
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": ["#eeeeee"]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"valueAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#999999"
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": ["#eeeeee"]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"logAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#999999"
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": ["#eeeeee"]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#999999"
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": ["#eeeeee"]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolbox": {
|
||||
"iconStyle": {
|
||||
"borderColor": "#999999"
|
||||
}
|
||||
},
|
||||
"legend": {
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"axisPointer": {
|
||||
"lineStyle": {
|
||||
"color": "#ffffff",
|
||||
"width": 1
|
||||
},
|
||||
"crossStyle": {
|
||||
"color": "#ffffff",
|
||||
"width": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"lineStyle": {
|
||||
"color": "#4ECB73",
|
||||
"width": 1
|
||||
},
|
||||
"itemStyle": {
|
||||
"color": "#4ECB73",
|
||||
"borderWidth": 1
|
||||
},
|
||||
"controlStyle": {
|
||||
"color": "#4ECB73",
|
||||
"borderColor": "#4ECB73",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"checkpointStyle": {
|
||||
"color": "#1890FF",
|
||||
"borderColor": "rgba(63,177,227,0.15)"
|
||||
},
|
||||
"label": {
|
||||
"color": "#4ECB73"
|
||||
}
|
||||
},
|
||||
"visualMap": {
|
||||
"color": ["#1890FF", "#afe8ff"]
|
||||
},
|
||||
"dataZoom": {
|
||||
"backgroundColor": "rgba(255,255,255,0)",
|
||||
"dataBackgroundColor": "rgba(222,222,222,1)",
|
||||
"fillerColor": "rgba(114,230,212,0.25)",
|
||||
"handleColor": "#cccccc",
|
||||
"handleSize": "100%",
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"markPoint": {
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
}
|
||||
}
|
||||
}
|
||||
222
front-end/src/plugins/VabCount/index.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<!-- eslint-disable -->
|
||||
<template>
|
||||
<span>{{ displayValue }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
cancelAnimationFrame,
|
||||
requestAnimationFrame,
|
||||
} from './requestAnimationFrame'
|
||||
|
||||
export default {
|
||||
name: 'VabCount',
|
||||
props: {
|
||||
startVal: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
endVal: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 3000,
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
decimals: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
validator(value) {
|
||||
return value >= 0
|
||||
},
|
||||
},
|
||||
decimal: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '.',
|
||||
},
|
||||
separator: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ',',
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
useEasing: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
easingFn: {
|
||||
type: Function,
|
||||
default(t, b, c, d) {
|
||||
return (c * (-(2 ** ((-10 * t) / d)) + 1) * 1024) / 1023 + b
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localStartVal: this.startVal,
|
||||
displayValue: this.formatNumber(this.startVal),
|
||||
printVal: null,
|
||||
paused: false,
|
||||
localDuration: this.duration,
|
||||
startTime: null,
|
||||
timestamp: null,
|
||||
remaining: null,
|
||||
rAF: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
countDown() {
|
||||
return this.startVal > this.endVal
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
startVal() {
|
||||
if (this.autoplay) {
|
||||
this.start()
|
||||
}
|
||||
},
|
||||
endVal() {
|
||||
if (this.autoplay) {
|
||||
this.start()
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.autoplay) {
|
||||
this.start()
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
this.$emit('mountedCallback')
|
||||
},
|
||||
unmounted() {
|
||||
cancelAnimationFrame(this.rAF)
|
||||
},
|
||||
methods: {
|
||||
start() {
|
||||
this.localStartVal = this.startVal
|
||||
this.startTime = null
|
||||
this.localDuration = this.duration
|
||||
this.paused = false
|
||||
this.rAF = requestAnimationFrame(this.count)
|
||||
},
|
||||
pauseResume() {
|
||||
if (this.paused) {
|
||||
this.resume()
|
||||
this.paused = false
|
||||
} else {
|
||||
this.pause()
|
||||
this.paused = true
|
||||
}
|
||||
},
|
||||
pause() {
|
||||
cancelAnimationFrame(this.rAF)
|
||||
},
|
||||
resume() {
|
||||
this.startTime = null
|
||||
this.localDuration = +this.remaining
|
||||
this.localStartVal = +this.printVal
|
||||
requestAnimationFrame(this.count)
|
||||
},
|
||||
reset() {
|
||||
this.startTime = null
|
||||
cancelAnimationFrame(this.rAF)
|
||||
this.displayValue = this.formatNumber(this.startVal)
|
||||
},
|
||||
count(timestamp) {
|
||||
if (!this.startTime) this.startTime = timestamp
|
||||
this.timestamp = timestamp
|
||||
const progress = timestamp - this.startTime
|
||||
this.remaining = this.localDuration - progress
|
||||
|
||||
if (this.useEasing) {
|
||||
if (this.countDown) {
|
||||
this.printVal =
|
||||
this.localStartVal -
|
||||
this.easingFn(
|
||||
progress,
|
||||
0,
|
||||
this.localStartVal - this.endVal,
|
||||
this.localDuration
|
||||
)
|
||||
} else {
|
||||
this.printVal = this.easingFn(
|
||||
progress,
|
||||
this.localStartVal,
|
||||
this.endVal - this.localStartVal,
|
||||
this.localDuration
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (this.countDown) {
|
||||
this.printVal =
|
||||
this.localStartVal -
|
||||
(this.localStartVal - this.endVal) *
|
||||
(progress / this.localDuration)
|
||||
} else {
|
||||
this.printVal =
|
||||
this.localStartVal +
|
||||
(this.endVal - this.localStartVal) *
|
||||
(progress / this.localDuration)
|
||||
}
|
||||
}
|
||||
if (this.countDown) {
|
||||
this.printVal =
|
||||
this.printVal < this.endVal
|
||||
? this.endVal
|
||||
: this.printVal
|
||||
} else {
|
||||
this.printVal =
|
||||
this.printVal > this.endVal
|
||||
? this.endVal
|
||||
: this.printVal
|
||||
}
|
||||
|
||||
this.displayValue = this.formatNumber(this.printVal)
|
||||
if (progress < this.localDuration) {
|
||||
this.rAF = requestAnimationFrame(this.count)
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
this.$emit('callback')
|
||||
}
|
||||
},
|
||||
isNumber(val) {
|
||||
return !isNaN(Number.parseFloat(val))
|
||||
},
|
||||
formatNumber(num) {
|
||||
num = num.toFixed(this.decimals)
|
||||
num += ''
|
||||
const x = num.split('.')
|
||||
let x1 = x[0]
|
||||
const x2 = x.length > 1 ? this.decimal + x[1] : ''
|
||||
const rgx = /(\d+)(\d{3})/
|
||||
if (this.separator && !this.isNumber(this.separator)) {
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, `$1${this.separator}$2`)
|
||||
}
|
||||
}
|
||||
return this.prefix + x1 + x2 + this.suffix
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
45
front-end/src/plugins/VabCount/requestAnimationFrame.js
Normal file
@@ -0,0 +1,45 @@
|
||||
let lastTime = 0
|
||||
const prefixes = 'webkit moz ms o'.split(' ')
|
||||
|
||||
let requestAnimationFrame
|
||||
let cancelAnimationFrame
|
||||
|
||||
const isServer = typeof window === 'undefined'
|
||||
if (isServer) {
|
||||
requestAnimationFrame = function () {}
|
||||
cancelAnimationFrame = function () {}
|
||||
} else {
|
||||
requestAnimationFrame = window.requestAnimationFrame
|
||||
cancelAnimationFrame = window.cancelAnimationFrame
|
||||
let prefix
|
||||
for (const prefix_ of prefixes) {
|
||||
if (requestAnimationFrame && cancelAnimationFrame) {
|
||||
break
|
||||
}
|
||||
prefix = prefix_
|
||||
requestAnimationFrame =
|
||||
requestAnimationFrame || window[`${prefix}RequestAnimationFrame`]
|
||||
cancelAnimationFrame =
|
||||
cancelAnimationFrame ||
|
||||
window[`${prefix}CancelAnimationFrame`] ||
|
||||
window[`${prefix}CancelRequestAnimationFrame`]
|
||||
}
|
||||
|
||||
if (!requestAnimationFrame || !cancelAnimationFrame) {
|
||||
requestAnimationFrame = function (callback) {
|
||||
const currTime = Date.now()
|
||||
const timeToCall = Math.max(0, 16 - (currTime - lastTime))
|
||||
const id = window.setTimeout(() => {
|
||||
callback(currTime + timeToCall)
|
||||
}, timeToCall)
|
||||
lastTime = currTime + timeToCall
|
||||
return id
|
||||
}
|
||||
|
||||
cancelAnimationFrame = function (id) {
|
||||
window.clearTimeout(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { requestAnimationFrame, cancelAnimationFrame }
|
||||
145
front-end/src/plugins/VabDialog/index.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
appendToBody: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
lockScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: '50%',
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showFullscreen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialogVisible = useVModel(props, 'modelValue', emit)
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
const setFullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vab-dialog">
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
v-bind="$attrs"
|
||||
:append-to-body="appendToBody"
|
||||
:draggable="draggable"
|
||||
:fullscreen="isFullscreen"
|
||||
:lock-scroll="lockScroll"
|
||||
:show-close="false"
|
||||
:width="width"
|
||||
>
|
||||
<template #header>
|
||||
<slot name="header">
|
||||
<span class="el-dialog__title">{{ title }}</span>
|
||||
</slot>
|
||||
<div class="vab-dialog__headerbtn">
|
||||
<button
|
||||
v-if="showFullscreen"
|
||||
aria-label="fullscreen"
|
||||
type="button"
|
||||
@click="setFullscreen"
|
||||
>
|
||||
<vab-icon
|
||||
v-if="isFullscreen"
|
||||
icon="fullscreen-exit-line"
|
||||
/>
|
||||
<vab-icon v-else icon="fullscreen-line" />
|
||||
</button>
|
||||
<button
|
||||
v-if="showClose"
|
||||
aria-label="close"
|
||||
type="button"
|
||||
@click="closeDialog"
|
||||
>
|
||||
<vab-icon icon="close-circle-line" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="loading">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<template #footer>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-dialog {
|
||||
&__headerbtn {
|
||||
position: absolute;
|
||||
top: var(--el-dialog-padding-primary);
|
||||
right: var(--el-dialog-padding-primary);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
margin-left: 15px;
|
||||
font-size: var(--el-message-close-size, 16px);
|
||||
color: var(--el-color-info);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
transition: $base-transition;
|
||||
|
||||
&:hover i {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
&.is-fullscreen {
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-dialog__body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding-bottom: 10px;
|
||||
border-top: 1px solid var(--el-border-color-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
128
front-end/src/plugins/VabFormTable/index.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="vab-form-table">
|
||||
<vab-query-form>
|
||||
<vab-query-form-top-panel :span="12">
|
||||
<el-button
|
||||
:icon="Plus"
|
||||
type="primary"
|
||||
@click="handleAdd($event)"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
</vab-query-form-top-panel>
|
||||
</vab-query-form>
|
||||
<el-table :key="toggleIndex" ref="tableRef" border :data="data">
|
||||
<el-table-column
|
||||
v-if="drag"
|
||||
align="center"
|
||||
label="操作"
|
||||
width="120"
|
||||
>
|
||||
<template #default>
|
||||
<vab-icon
|
||||
class="vab-rank"
|
||||
icon="drag-move-2-line"
|
||||
style="cursor: move"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<slot></slot>
|
||||
<el-table-column align="center" label="操作" width="120">
|
||||
<template #default="{ $index, row }">
|
||||
<el-button
|
||||
:icon="Delete"
|
||||
plain
|
||||
type="danger"
|
||||
@click="handleDelete(row, $index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty class="vab-data-empty" description="暂无数据" />
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VabFormTable',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
rowTemplate: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const state = reactive({
|
||||
tableRef: null,
|
||||
data: [],
|
||||
toggleIndex: 0,
|
||||
})
|
||||
|
||||
const rowDrop = () => {
|
||||
const tbody = state.tableRef.$el.querySelector(
|
||||
'.el-table__body-wrapper tbody'
|
||||
)
|
||||
Sortable.create(tbody, {
|
||||
handle: '.vab-rank',
|
||||
animation: 300,
|
||||
onEnd({ newIndex, oldIndex }) {
|
||||
const tableData = state.data
|
||||
const currRow = tableData.splice(oldIndex, 1)[0]
|
||||
tableData.splice(newIndex, 0, currRow)
|
||||
state.toggleIndex += 1
|
||||
nextTick(() => {
|
||||
rowDrop()
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
const handleAdd = () => {
|
||||
state.data.push(JSON.parse(JSON.stringify(props.rowTemplate)))
|
||||
}
|
||||
const handleDelete = (row, index) => {
|
||||
state.data.splice(index, 1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
state.data = props.modelValue
|
||||
if (props.drag) rowDrop()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => (state.data = props.modelValue)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => state.data,
|
||||
() => emit('update:modelValue', state.data)
|
||||
)
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
rowDrop,
|
||||
handleAdd,
|
||||
handleDelete,
|
||||
Delete,
|
||||
Plus,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
116
front-end/src/plugins/VabIconSelector/index.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script lang="ts" setup>
|
||||
import { getIconList } from '@/api/defaultIcon'
|
||||
|
||||
const emit = defineEmits(['handle-icon'])
|
||||
const state: any = reactive({
|
||||
icon: '24-hours-fill',
|
||||
layout: 'total, prev, next',
|
||||
total: 0,
|
||||
background: true,
|
||||
height: 0,
|
||||
selectRows: '',
|
||||
queryIcon: [],
|
||||
queryForm: {
|
||||
pageNo: 1,
|
||||
pageSize: 16,
|
||||
title: '',
|
||||
},
|
||||
})
|
||||
|
||||
const handleSizeChange: any = (val: string) => {
|
||||
state.queryForm.pageSize = val
|
||||
fetchData()
|
||||
}
|
||||
const handleCurrentChange: any = (val: string) => {
|
||||
state.queryForm.pageNo = val
|
||||
fetchData()
|
||||
}
|
||||
const queryData: any = () => {
|
||||
state.queryForm.pageNo = 1
|
||||
fetchData()
|
||||
}
|
||||
const fetchData: any = async () => {
|
||||
const {
|
||||
data: { list, total },
|
||||
} = await getIconList(state.queryForm)
|
||||
state.queryIcon = list
|
||||
state.total = total
|
||||
}
|
||||
const handleIcon: any = (item: any) => {
|
||||
state.icon = item
|
||||
emit('handle-icon', item)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<vab-query-form>
|
||||
<vab-query-form-top-panel>
|
||||
<el-form inline label-width="0" @submit.prevent>
|
||||
<el-form-item label="">
|
||||
<el-input v-model="state.queryForm.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label-width="0">
|
||||
<el-button
|
||||
native-type="submit"
|
||||
type="primary"
|
||||
@click="queryData"
|
||||
>
|
||||
查询
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</vab-query-form-top-panel>
|
||||
</vab-query-form>
|
||||
</el-col>
|
||||
|
||||
<el-col v-for="(item, index) in state.queryIcon" :key="index" :span="6">
|
||||
<vab-card @click="handleIcon(item)">
|
||||
<vab-icon :icon="item" />
|
||||
</vab-card>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-pagination
|
||||
:background="state.background"
|
||||
:current-page="state.queryForm.pageNo"
|
||||
:layout="state.layout"
|
||||
:page-size="state.queryForm.pageSize"
|
||||
:total="state.total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.icon-selector-popper {
|
||||
.el-card__body {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
font-size: 28px;
|
||||
vertical-align: middle;
|
||||
color: var(--el-color-grey);
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
148
front-end/src/plugins/VabPrint.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
const Print: any = function (this: any, dom: any, options: any) {
|
||||
if (!(this instanceof Print)) return new Print(dom, options)
|
||||
|
||||
this.options = this.extend(
|
||||
{
|
||||
noPrint: '.no-print',
|
||||
},
|
||||
options
|
||||
)
|
||||
|
||||
if (typeof dom === 'string') {
|
||||
try {
|
||||
this.dom = document.querySelector(dom)
|
||||
} catch {
|
||||
const createDom = document.createElement('div')
|
||||
createDom.innerHTML = dom
|
||||
this.dom = createDom
|
||||
}
|
||||
} else {
|
||||
this.isDOM(dom)
|
||||
this.dom = this.isDOM(dom) ? dom : dom.$el
|
||||
}
|
||||
|
||||
this.init()
|
||||
}
|
||||
Print.prototype = {
|
||||
init() {
|
||||
const content = this.getStyle() + this.getHtml()
|
||||
this.writeIframe(content)
|
||||
},
|
||||
extend(obj: { [x: string]: any }, obj2: { [x: string]: any }) {
|
||||
for (const k in obj2) {
|
||||
obj[k] = obj2[k]
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
getStyle() {
|
||||
let str = ''
|
||||
const styles = document.querySelectorAll('style,link')
|
||||
for (const style of styles) {
|
||||
str += style.outerHTML
|
||||
}
|
||||
str += `<style>${this.options.noPrint ? this.options.noPrint : '.no-print'}{display:none;}</style>`
|
||||
str += '<style>html,body{background-color:#fff;}</style>'
|
||||
return str
|
||||
},
|
||||
|
||||
getHtml() {
|
||||
const inputs = document.querySelectorAll('input')
|
||||
const textareas = document.querySelectorAll('textarea')
|
||||
const selects = document.querySelectorAll('select')
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.type == 'checkbox' || input.type == 'radio') {
|
||||
if (input.checked == true) {
|
||||
input.setAttribute('checked', 'checked')
|
||||
} else {
|
||||
input.removeAttribute('checked')
|
||||
}
|
||||
} else if (input.type == 'text') {
|
||||
input.setAttribute('value', input.value)
|
||||
} else {
|
||||
input.setAttribute('value', input.value)
|
||||
}
|
||||
}
|
||||
|
||||
for (const textarea of textareas) {
|
||||
if (textarea.type == 'textarea') textarea.innerHTML = textarea.value
|
||||
}
|
||||
|
||||
for (const select of selects) {
|
||||
if (select.type == 'select-one') {
|
||||
const child: any = select.children
|
||||
for (const i in child) {
|
||||
if (child[i].tagName == 'OPTION') {
|
||||
if (child[i].selected == true)
|
||||
child[i].setAttribute('selected', 'selected')
|
||||
else child[i].removeAttribute('selected')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.dom.outerHTML
|
||||
},
|
||||
|
||||
writeIframe(content: string) {
|
||||
const iframe: any = document.createElement('iframe')
|
||||
const f: any = document.body.appendChild(iframe)
|
||||
iframe.id = 'myIframe'
|
||||
iframe.setAttribute(
|
||||
'style',
|
||||
'position:absolute;width:0;height:0;top:-10px;left:-10px;'
|
||||
)
|
||||
const w: any = f.contentWindow || f.contentDocument
|
||||
const doc: any = f.contentDocument || f.contentWindow.document
|
||||
doc.open()
|
||||
doc.write(content)
|
||||
doc.close()
|
||||
const _this = this
|
||||
iframe.addEventListener('load', () => {
|
||||
_this.toPrint(w)
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(iframe)
|
||||
}, 100)
|
||||
})
|
||||
},
|
||||
|
||||
toPrint(frameWindow: {
|
||||
focus: () => void
|
||||
document: {
|
||||
execCommand: (arg0: string, arg1: boolean, arg2: null) => any
|
||||
}
|
||||
print: () => void
|
||||
close: () => void
|
||||
}) {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
frameWindow.focus()
|
||||
try {
|
||||
if (!frameWindow.document.execCommand('print', false, null))
|
||||
frameWindow.print()
|
||||
} catch {
|
||||
frameWindow.print()
|
||||
}
|
||||
frameWindow.close()
|
||||
}, 10)
|
||||
} catch (error) {
|
||||
console.log('err', error)
|
||||
}
|
||||
},
|
||||
isDOM:
|
||||
typeof HTMLElement === 'object'
|
||||
? function (obj: any) {
|
||||
return obj instanceof HTMLElement
|
||||
}
|
||||
: function (obj: { nodeType: number; nodeName: any }) {
|
||||
return (
|
||||
obj &&
|
||||
typeof obj === 'object' &&
|
||||
obj.nodeType === 1 &&
|
||||
typeof obj.nodeName === 'string'
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default Print
|
||||
1
front-end/src/plugins/VabQrCode.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from 'vue-qr/src/packages/vue-qr.vue'
|
||||
157
front-end/src/plugins/VabUpdate/index.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSettingsStore } from '@/store/modules/settings'
|
||||
|
||||
const $sub: any = inject('$sub')
|
||||
const $baseMessage: any = inject('$baseMessage')
|
||||
|
||||
const { getTitle } = useSettingsStore()
|
||||
|
||||
const state = reactive({
|
||||
title: getTitle,
|
||||
version: __APP_INFO__['version'],
|
||||
updateTime: __APP_INFO__['lastBuildTime'],
|
||||
dialogVisible: false,
|
||||
loading: false,
|
||||
button: '立即升级',
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
$sub('vab-update', () => {
|
||||
state.dialogVisible = true
|
||||
setTimeout(() => {
|
||||
save()
|
||||
}, 1000 * 3)
|
||||
})
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
state.dialogVisible = false
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
state.button = '正在更新'
|
||||
state.loading = true
|
||||
$baseMessage(
|
||||
'正在更新,预计10S后更新完成',
|
||||
'success',
|
||||
'vab-hey-message-success'
|
||||
)
|
||||
setTimeout(() => {
|
||||
state.loading = false
|
||||
state.button = '更新完成'
|
||||
}, 1000 * 6)
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 1000 * 7)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="state.dialogVisible"
|
||||
append-to-body
|
||||
class="vab-update"
|
||||
width="410px"
|
||||
@close="close"
|
||||
>
|
||||
<div class="vab-update-icon">
|
||||
<vab-icon icon="upload-cloud-2-fill" />
|
||||
</div>
|
||||
<vab-icon class="vab-update-cup" icon="cup-line" />
|
||||
<h3>版本更新:</h3>
|
||||
<p>
|
||||
{{ state.title }}
|
||||
V{{ state.version }}
|
||||
</p>
|
||||
<p>
|
||||
更新时间:最近更新
|
||||
<!-- {{ updateTime }} -->
|
||||
</p>
|
||||
<p
|
||||
v-text="
|
||||
`${'如遇更' + '新失败' + '请手' + '动点击' + 'Ctr' + 'l + F' + '5' + '重试'}`
|
||||
"
|
||||
></p>
|
||||
<template #footer>
|
||||
<el-button
|
||||
v-loading="state.loading"
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="save"
|
||||
>
|
||||
{{ state.button }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-update {
|
||||
position: relative;
|
||||
|
||||
&-icon {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
left: 50%;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
text-align: center;
|
||||
background: linear-gradient(
|
||||
50deg,
|
||||
var(--el-color-primary),
|
||||
var(--el-color-primary-light-7)
|
||||
);
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
i {
|
||||
font-size: 50px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&-cup {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 70px;
|
||||
font-size: 80px;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(
|
||||
var(--el-color-primary-light-7),
|
||||
var(--el-color-primary-light-9)
|
||||
);
|
||||
background-clip: text;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.vab-update {
|
||||
&.el-dialog {
|
||||
margin-top: 30vh !important;
|
||||
border-radius: 15px;
|
||||
|
||||
.el-dialog__body {
|
||||
margin: 0 40px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
text-align: center !important;
|
||||
|
||||
.el-button {
|
||||
width: 200px;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(
|
||||
50deg,
|
||||
var(--el-color-primary-light-3),
|
||||
var(--el-color-primary)
|
||||
);
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
270
front-end/src/plugins/VabUpload/index.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogFormVisible"
|
||||
:before-close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
:title="title"
|
||||
width="909px"
|
||||
>
|
||||
<div class="upload">
|
||||
<el-alert
|
||||
:closable="false"
|
||||
:title="`支持jpg、jpeg、png格式,单次可最多选择${limit}张图片,每张不可大于${size}M,如果大于${size}M会自动为您过滤`"
|
||||
type="info"
|
||||
/>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
accept="image/png, image/jpeg"
|
||||
:action="action"
|
||||
:auto-upload="false"
|
||||
class="upload-content"
|
||||
:close-on-click-modal="false"
|
||||
:data="data"
|
||||
:file-list="fileList"
|
||||
:headers="headers"
|
||||
:limit="limit"
|
||||
list-type="picture-card"
|
||||
:multiple="true"
|
||||
:name="name"
|
||||
:on-change="handleChange"
|
||||
:on-error="handleError"
|
||||
:on-exceed="handleExceed"
|
||||
:on-preview="handlePreview"
|
||||
:on-progress="handleProgress"
|
||||
:on-remove="handleRemove"
|
||||
:on-success="handleSuccess"
|
||||
>
|
||||
<template #trigger>
|
||||
<vab-icon icon="add-line" />
|
||||
</template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
append-to-body
|
||||
title="查看大图"
|
||||
>
|
||||
<div>
|
||||
<el-image :src="dialogImageUrl" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-upload>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div
|
||||
v-if="show"
|
||||
style="position: absolute; top: 10px; left: 15px; color: #999"
|
||||
>
|
||||
正在上传中... 当前上传成功数:{{ imgSuccessNum }}张
|
||||
当前上传失败数:{{ imgErrorNum }}张
|
||||
</div>
|
||||
<el-button type="primary" @click="handleClose">关闭</el-button>
|
||||
<el-button
|
||||
:loading="loading"
|
||||
style="margin-left: 10px"
|
||||
type="success"
|
||||
@click="submitUpload"
|
||||
>
|
||||
开始上传
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VabUpload',
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: '/upload',
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
required: true,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const userStore = useUserStore()
|
||||
const { token } = storeToRefs(userStore)
|
||||
const $baseMessage = inject('$baseMessage')
|
||||
|
||||
const state = reactive({
|
||||
uploadRef: null,
|
||||
show: false,
|
||||
loading: false,
|
||||
dialogVisible: false,
|
||||
dialogImageUrl: '',
|
||||
action: '',
|
||||
headers: {},
|
||||
fileList: [],
|
||||
picture: 'picture',
|
||||
imgNum: 0,
|
||||
imgSuccessNum: 0,
|
||||
imgErrorNum: 0,
|
||||
typeList: null,
|
||||
title: '上传',
|
||||
dialogFormVisible: false,
|
||||
data: {},
|
||||
})
|
||||
|
||||
const submitUpload = () => {
|
||||
state.uploadRef.submit()
|
||||
}
|
||||
const handleProgress = () => {
|
||||
state.loading = true
|
||||
state.show = true
|
||||
}
|
||||
const handleChange = (file, fileList) => {
|
||||
if (fileList && fileList.length > 0) {
|
||||
if (file.size > 1048576 * state.size) {
|
||||
fileList.filter((item) => item !== file)
|
||||
state.fileList = fileList
|
||||
} else {
|
||||
state.allImgNum = fileList.length
|
||||
}
|
||||
}
|
||||
}
|
||||
const handleSuccess = (response, file, fileList) => {
|
||||
state.imgNum = state.imgNum + 1
|
||||
state.imgSuccessNum = state.imgSuccessNum + 1
|
||||
if (fileList.length === state.imgNum) {
|
||||
setTimeout(() => {
|
||||
$baseMessage(
|
||||
`上传完成! 共上传${fileList.length}张图片`,
|
||||
'success',
|
||||
'vab-hey-message-success'
|
||||
)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
state.loading = false
|
||||
state.show = false
|
||||
}, 1000)
|
||||
}
|
||||
const handleError = (err, file) => {
|
||||
state.imgNum = state.imgNum + 1
|
||||
state.imgErrorNum = state.imgErrorNum + 1
|
||||
$baseMessage(
|
||||
`文件[${file.raw.name}]上传失败,文件大小为${_.round(file.raw.size / 1024, 0)}KB`,
|
||||
'error',
|
||||
'vab-hey-message-error'
|
||||
)
|
||||
setTimeout(() => {
|
||||
state.loading = false
|
||||
state.show = false
|
||||
}, 1000)
|
||||
}
|
||||
const handleRemove = () => {
|
||||
state.imgNum = state.imgNum - 1
|
||||
state.allNum = state.allNum - 1
|
||||
}
|
||||
const handlePreview = (file) => {
|
||||
state.dialogImageUrl = file.url
|
||||
state.dialogVisible = true
|
||||
}
|
||||
const handleExceed = (files) => {
|
||||
$baseMessage(
|
||||
`当前限制选择 ${state.limit} 个文件,本次选择了
|
||||
${files.length}
|
||||
个文件`,
|
||||
'error',
|
||||
'vab-hey-message-error'
|
||||
)
|
||||
}
|
||||
const handleShow = (data) => {
|
||||
state.title = '上传'
|
||||
state.data = data
|
||||
state.dialogFormVisible = true
|
||||
}
|
||||
const handleClose = () => {
|
||||
state.fileList = []
|
||||
state.picture = 'picture'
|
||||
state.allImgNum = 0
|
||||
state.imgNum = 0
|
||||
state.imgSuccessNum = 0
|
||||
state.imgErrorNum = 0
|
||||
state.headers['Authorization'] = `Bearer ${token}`
|
||||
state.dialogFormVisible = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
state.headers['Authorization'] = `Bearer ${token}`
|
||||
state.action = props.url
|
||||
})
|
||||
|
||||
const percentage = computed(() => {
|
||||
if (state.allImgNum === 0) return 0
|
||||
return _.round(state.imgNum / state.allImgNum, 2) * 100
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
submitUpload,
|
||||
handleProgress,
|
||||
handleChange,
|
||||
handleSuccess,
|
||||
handleError,
|
||||
handleRemove,
|
||||
handlePreview,
|
||||
handleExceed,
|
||||
handleShow,
|
||||
handleClose,
|
||||
percentage,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload {
|
||||
height: 500px;
|
||||
|
||||
.upload-content {
|
||||
.el-upload__tip {
|
||||
display: block;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
:deep() {
|
||||
.el-upload--picture-card {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin: 3px 8px 8px;
|
||||
border: 2px dashed #c0ccda;
|
||||
|
||||
.ri-add-line {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload-list--picture {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-upload-list--picture-card {
|
||||
.el-upload-list__item {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin: 3px 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
front-end/src/registerServiceWorker.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/* eslint-disable no-console */
|
||||
import { register } from 'register-service-worker'
|
||||
import { gp } from '@gp'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready() {
|
||||
console.log(
|
||||
'App is being served from cache by a service worker.\n' +
|
||||
'For more details, visit //goo.gl/AFskqB'
|
||||
)
|
||||
},
|
||||
registered() {
|
||||
console.log('Service worker has been registered.')
|
||||
},
|
||||
cached() {
|
||||
console.log('Content has been cached for offline use.')
|
||||
},
|
||||
updatefound() {
|
||||
console.log('New content is downloading.')
|
||||
// gp.$baseNotify(
|
||||
// '检测到新版本,正在下载中,请稍后...',
|
||||
// '温馨提示',
|
||||
// 'info',
|
||||
// 'bottom-right',
|
||||
// 8000
|
||||
// )
|
||||
},
|
||||
updated() {
|
||||
console.log('New content is available; please refresh.')
|
||||
gp.$pub('vab-update')
|
||||
|
||||
//如果是演示环境,更新后移除主题,用不到可删除
|
||||
if (location.hostname === 'veujs-core.cn')
|
||||
localStorage.removeItem('theme')
|
||||
// gp.$baseNotify(
|
||||
// '更新版本完成,10S后刷新项目',
|
||||
// '温馨提示',
|
||||
// 'success',
|
||||
// 'bottom-right',
|
||||
// 8000
|
||||
// )
|
||||
// setTimeout(() => {
|
||||
// window.location.reload()
|
||||
// }, 10000)
|
||||
},
|
||||
offline() {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
)
|
||||
},
|
||||
error(error) {
|
||||
console.error('Error during service worker registration:', error)
|
||||
},
|
||||
})
|
||||
}
|
||||
1112
front-end/src/router/index.ts
Normal file
75
front-end/src/router/permissions.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @description 路由守卫,目前两种模式:all模式与intelligence模式
|
||||
*/
|
||||
import VabProgress from 'nprogress'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useRoutesStore } from '@/store/modules/routes'
|
||||
import { useSettingsStore } from '@/store/modules/settings'
|
||||
import 'nprogress/nprogress.css'
|
||||
import getPageTitle from '@/utils/pageTitle'
|
||||
import { toLoginRoute } from '@/utils/routes'
|
||||
import {
|
||||
authentication,
|
||||
loginInterception,
|
||||
routesWhiteList,
|
||||
supportVisit,
|
||||
} from '@/config'
|
||||
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
export function setupPermissions(router: Router) {
|
||||
VabProgress.configure({
|
||||
easing: 'ease',
|
||||
speed: 500,
|
||||
trickleSpeed: 200,
|
||||
showSpinner: false,
|
||||
})
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const {
|
||||
getTheme: { showProgressBar },
|
||||
} = useSettingsStore()
|
||||
const { routes, setRoutes } = useRoutesStore()
|
||||
const { token, getUserInfo, setVirtualRoles, resetAll } = useUserStore()
|
||||
|
||||
if (showProgressBar) VabProgress.start()
|
||||
|
||||
let hasToken = token
|
||||
|
||||
if (!loginInterception) hasToken = true
|
||||
|
||||
if (hasToken) {
|
||||
if (routes.length > 0) {
|
||||
// 禁止已登录用户返回登录页
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
if (showProgressBar) VabProgress.done()
|
||||
} else next()
|
||||
} else {
|
||||
try {
|
||||
if (loginInterception) await getUserInfo()
|
||||
// config/setting.config.js loginInterception为false(关闭登录拦截时)时,创建虚拟角色
|
||||
else await setVirtualRoles()
|
||||
// 根据路由模式获取路由并根据权限过滤
|
||||
await setRoutes(authentication)
|
||||
next({ ...to, replace: true })
|
||||
} catch (error) {
|
||||
console.error('vue-admin-better错误拦截:', error)
|
||||
await resetAll()
|
||||
next(toLoginRoute(to.path))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (routesWhiteList.includes(to.path)) {
|
||||
// 设置游客路由(不需要可以删除)
|
||||
if (supportVisit && routes.length === 0) {
|
||||
await setRoutes('visit')
|
||||
next({ path: to.path, replace: true })
|
||||
} else next()
|
||||
} else next(toLoginRoute(to.path))
|
||||
}
|
||||
})
|
||||
router.afterEach((to: any) => {
|
||||
document.title = getPageTitle(to.meta.title)
|
||||
if (VabProgress.status) VabProgress.done()
|
||||
})
|
||||
}
|
||||
11
front-end/src/store/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @description 导入所有 pinia 模块,请勿修改。
|
||||
*/
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
export function setupStore(app: any) {
|
||||
app.use(pinia)
|
||||
}
|
||||
|
||||
export default pinia
|
||||
25
front-end/src/store/modules/acl.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { AclModuleType } from '/#/store'
|
||||
|
||||
export const useAclStore = defineStore('acl', {
|
||||
state: (): AclModuleType => ({
|
||||
admin: false,
|
||||
role: [],
|
||||
permission: [],
|
||||
}),
|
||||
getters: {
|
||||
getAdmin: (state) => state.admin,
|
||||
getRole: (state) => state.role,
|
||||
getPermission: (state) => state.permission,
|
||||
},
|
||||
actions: {
|
||||
setFull(admin: boolean) {
|
||||
this.admin = admin
|
||||
},
|
||||
setRole(role: string[]) {
|
||||
this.role = role
|
||||
},
|
||||
setPermission(permission: string[]) {
|
||||
this.permission = permission
|
||||
},
|
||||
},
|
||||
})
|
||||
21
front-end/src/store/modules/errorLog.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @description 异常捕获的状态拦截,请勿修改
|
||||
*/
|
||||
import type { ErrorLogModuleType } from '/#/store'
|
||||
|
||||
export const useErrorLogStore = defineStore('errorLog', {
|
||||
state: (): ErrorLogModuleType => ({
|
||||
errorLogs: [],
|
||||
}),
|
||||
getters: {
|
||||
getErrorLogs: (state) => state.errorLogs,
|
||||
},
|
||||
actions: {
|
||||
addErrorLog(errorLog: any) {
|
||||
this.errorLogs.push(errorLog)
|
||||
},
|
||||
clearErrorLog() {
|
||||
this.errorLogs.splice(0)
|
||||
},
|
||||
},
|
||||
})
|
||||
113
front-end/src/store/modules/routes.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @description 路由拦截状态管理,目前两种模式:all模式与intelligence模式,其中partialRoutes是菜单暂未使用
|
||||
*/
|
||||
import { gp } from '@gp'
|
||||
import { asyncRoutes, constantRoutes, resetRouter } from '@/router'
|
||||
import { convertRouter, filterRoutes } from '@/utils/routes'
|
||||
import { authentication, rolesControl } from '@/config'
|
||||
import type { OptionType, RoutesModuleType } from '/#/store'
|
||||
import { isArray } from '@/utils/validate'
|
||||
import { getList } from '@/api/router'
|
||||
import type { VabRouteRecord } from '/#/router'
|
||||
|
||||
export const useRoutesStore = defineStore('routes', {
|
||||
state: (): RoutesModuleType => ({
|
||||
/**
|
||||
* 一级菜单值
|
||||
*/
|
||||
tab: {
|
||||
data: undefined,
|
||||
},
|
||||
/**
|
||||
* 一级菜单
|
||||
*/
|
||||
tabMenu: undefined,
|
||||
/**
|
||||
* 自定义激活菜单
|
||||
*/
|
||||
activeMenu: {
|
||||
data: undefined,
|
||||
},
|
||||
/**
|
||||
* 一级菜单
|
||||
*/
|
||||
routes: [],
|
||||
}),
|
||||
getters: {
|
||||
getTab: (state) => state.tab,
|
||||
getTabMenu: (state) =>
|
||||
state.tab.data
|
||||
? state.routes.find((route) => route.name === state.tab.data)
|
||||
: { meta: { title: '' }, redirect: '404' },
|
||||
getActiveMenu: (state) => state.activeMenu,
|
||||
getRoutes: (state) =>
|
||||
state.routes.filter((_route) => _route.meta.hidden !== true),
|
||||
getPartialRoutes: (state) =>
|
||||
state.routes.find((route) => route.name === state.tab.data)
|
||||
?.children || [],
|
||||
},
|
||||
actions: {
|
||||
clearRoutes() {
|
||||
this.routes = []
|
||||
},
|
||||
/**
|
||||
* @description 多模式设置路由
|
||||
* @param mode
|
||||
* @returns
|
||||
*/
|
||||
async setRoutes(mode = 'none') {
|
||||
// 默认前端路由
|
||||
let routes = [...asyncRoutes]
|
||||
// 设置游客路由关闭路由拦截(不需要可以删除)
|
||||
const control = mode === 'visit' ? false : rolesControl
|
||||
// 设置后端路由(不需要可以删除)
|
||||
if (authentication === 'all') {
|
||||
const {
|
||||
data: { list },
|
||||
} = await getList()
|
||||
if (!isArray(list))
|
||||
gp.$baseMessage(
|
||||
'路由格式返回有误!',
|
||||
'error',
|
||||
'vab-hey-message-error'
|
||||
)
|
||||
if (list[list.length - 1].path !== '*')
|
||||
list.push({
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/404',
|
||||
name: 'NotFound',
|
||||
meta: { hidden: true },
|
||||
})
|
||||
routes = convertRouter(list)
|
||||
}
|
||||
// 根据权限和rolesControl过滤路由
|
||||
const accessRoutes = filterRoutes(
|
||||
[...constantRoutes, ...routes],
|
||||
control
|
||||
)
|
||||
// 设置菜单所需路由
|
||||
this.routes = JSON.parse(JSON.stringify(accessRoutes))
|
||||
// 根据可访问路由重置Vue Router
|
||||
await resetRouter(accessRoutes)
|
||||
},
|
||||
changeMenuMeta(options: OptionType) {
|
||||
function handleRoutes(routes: VabRouteRecord[]) {
|
||||
return routes.map((route) => {
|
||||
if (route.name === options.name)
|
||||
Object.assign(route.meta, options.meta)
|
||||
if (route.children && route.children.length > 0)
|
||||
route.children = handleRoutes(route.children)
|
||||
return route
|
||||
})
|
||||
}
|
||||
this.routes = handleRoutes(this.routes)
|
||||
},
|
||||
/**
|
||||
* @description 修改 activeName
|
||||
* @param activeMenu 当前激活菜单
|
||||
*/
|
||||
changeActiveMenu(activeMenu: string) {
|
||||
this.activeMenu.data = activeMenu
|
||||
},
|
||||
},
|
||||
})
|
||||
182
front-end/src/store/modules/settings.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* @description 所有全局配置的状态管理,如无必要请勿修改
|
||||
*/
|
||||
import type { SettingsModuleType } from '/#/store'
|
||||
import { isJson } from '@/utils/validate'
|
||||
import {
|
||||
logo as _logo,
|
||||
title as _title,
|
||||
background,
|
||||
columnStyle,
|
||||
fixedHeader,
|
||||
foldSidebar,
|
||||
i18n,
|
||||
layout,
|
||||
menuWidth,
|
||||
showFullScreen,
|
||||
showLanguage,
|
||||
showLock,
|
||||
showNotice,
|
||||
showPageTransition,
|
||||
showProgressBar,
|
||||
showRefresh,
|
||||
showSearch,
|
||||
showTabs,
|
||||
showTabsIcon,
|
||||
showTheme,
|
||||
showThemeSetting,
|
||||
tabsBarStyle,
|
||||
themeName,
|
||||
} from '@/config'
|
||||
|
||||
const defaultTheme: ThemeType = {
|
||||
layout,
|
||||
themeName,
|
||||
background,
|
||||
columnStyle,
|
||||
fixedHeader,
|
||||
foldSidebar,
|
||||
menuWidth,
|
||||
showProgressBar,
|
||||
showTabs,
|
||||
showTabsIcon,
|
||||
showLanguage,
|
||||
showRefresh,
|
||||
showSearch,
|
||||
showTheme,
|
||||
showNotice,
|
||||
showFullScreen,
|
||||
showThemeSetting,
|
||||
showPageTransition,
|
||||
showLock,
|
||||
tabsBarStyle,
|
||||
}
|
||||
|
||||
const getLocalStorage = (key: string) => {
|
||||
const value: string | null = localStorage.getItem(key)
|
||||
return value && isJson(value) ? JSON.parse(value) : false
|
||||
}
|
||||
|
||||
const theme = getLocalStorage('theme') || { ...defaultTheme }
|
||||
const { collapse = foldSidebar } = getLocalStorage('collapse')
|
||||
const { language = i18n } = getLocalStorage('language')
|
||||
const { lock = false } = getLocalStorage('lock')
|
||||
const { logo = _logo } = getLocalStorage('logo')
|
||||
const { title = _title } = getLocalStorage('title')
|
||||
|
||||
export const useSettingsStore = defineStore('settings', {
|
||||
state: (): SettingsModuleType => ({
|
||||
theme,
|
||||
device: 'desktop',
|
||||
collapse,
|
||||
language,
|
||||
lock,
|
||||
logo,
|
||||
title,
|
||||
echartsGraphic1: ['#3ED572', '#399efd'],
|
||||
echartsGraphic2: ['#399efd', '#8cc8ff'],
|
||||
}),
|
||||
getters: {
|
||||
getTheme: (state) => state.theme,
|
||||
getDevice: (state) => state.device,
|
||||
getCollapse: (state) => state.collapse,
|
||||
getLanguage: (state) => state.language,
|
||||
getLock: (state) => state.lock,
|
||||
getLogo: (state) => state.logo,
|
||||
getTitle: (state) => state.title,
|
||||
},
|
||||
actions: {
|
||||
updateState(obj: any) {
|
||||
Object.getOwnPropertyNames(obj).forEach((key) => {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
this[key] = obj[key]
|
||||
localStorage.setItem(
|
||||
key,
|
||||
typeof obj[key] == 'string'
|
||||
? `{"${key}":"${obj[key]}"}`
|
||||
: `{"${key}":${obj[key]}}`
|
||||
)
|
||||
})
|
||||
},
|
||||
saveTheme() {
|
||||
localStorage.setItem('theme', JSON.stringify(this.theme))
|
||||
},
|
||||
resetTheme() {
|
||||
this.theme = { ...defaultTheme }
|
||||
localStorage.removeItem('theme')
|
||||
this.updateTheme()
|
||||
},
|
||||
updateTheme() {
|
||||
const index = this.theme.themeName.indexOf('-')
|
||||
const themeName =
|
||||
this.theme.themeName.slice(0, Math.max(0, index)) || 'blue'
|
||||
|
||||
let variables = require(
|
||||
`@vab/styles/variables/vab-${themeName}-variables.module.scss`
|
||||
)
|
||||
if (variables.default) variables = variables.default
|
||||
|
||||
Object.keys(variables).forEach((key) => {
|
||||
if (key.startsWith('vab-')) {
|
||||
useCssVar(key.replace('vab-', '--el-'), ref(null)).value =
|
||||
variables[key]
|
||||
}
|
||||
})
|
||||
|
||||
this.echartsGraphic1 = [
|
||||
variables['vab-color-transition'],
|
||||
variables['vab-color-primary'],
|
||||
]
|
||||
|
||||
this.echartsGraphic2 = [
|
||||
variables['vab-color-primary-light-5'],
|
||||
variables['vab-color-primary'],
|
||||
]
|
||||
|
||||
const menuBackground =
|
||||
this.theme.themeName.split('-')[1] || this.theme.themeName
|
||||
document.querySelectorAll('body')[0].className =
|
||||
`vab-theme-${menuBackground}`
|
||||
|
||||
if (this.theme.background !== 'none')
|
||||
document
|
||||
.querySelectorAll('body')[0]
|
||||
.classList.add(this.theme.background)
|
||||
|
||||
const el = ref(null)
|
||||
if (this.theme.menuWidth && this.theme.menuWidth.endsWith('px'))
|
||||
useCssVar('--el-left-menu-width', el).value =
|
||||
this.theme.menuWidth
|
||||
else useCssVar('--el-left-menu-width', el).value = '266px'
|
||||
},
|
||||
toggleCollapse() {
|
||||
this.collapse = !this.collapse
|
||||
localStorage.setItem('collapse', `{"collapse":${this.collapse}}`)
|
||||
},
|
||||
toggleDevice(device: string) {
|
||||
this.updateState({ device })
|
||||
},
|
||||
openSideBar() {
|
||||
this.updateState({ collapse: false })
|
||||
},
|
||||
foldSideBar() {
|
||||
this.updateState({ collapse: true })
|
||||
},
|
||||
changeLanguage(language: string) {
|
||||
this.updateState({ language })
|
||||
},
|
||||
handleLock() {
|
||||
this.updateState({ lock: true })
|
||||
},
|
||||
handleUnLock() {
|
||||
this.updateState({ lock: false })
|
||||
},
|
||||
changeLogo(logo: string) {
|
||||
this.updateState({ logo })
|
||||
},
|
||||
changeTitle(title: string) {
|
||||
this.updateState({ title })
|
||||
},
|
||||
},
|
||||
})
|
||||
123
front-end/src/store/modules/tabs.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* @description tabsBar标签页逻辑,如无必要请勿修改
|
||||
*/
|
||||
import type { OptionType, TabsModuleType } from '/#/store'
|
||||
import type { VabRouteRecord } from '/#/router'
|
||||
|
||||
export const useTabsStore = defineStore('tabs', {
|
||||
state: (): TabsModuleType => ({
|
||||
visitedRoutes: [],
|
||||
}),
|
||||
getters: {
|
||||
getVisitedRoutes: (state) =>
|
||||
state.visitedRoutes.filter(
|
||||
(route: VabRouteRecord) => route.name !== 'Login'
|
||||
),
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* @description 添加标签页
|
||||
* @param {*} route
|
||||
* @returns
|
||||
*/
|
||||
addVisitedRoute(route: VabRouteRecord) {
|
||||
const target = this.visitedRoutes.find(
|
||||
(item: VabRouteRecord) => item.path === route.path
|
||||
)
|
||||
if (target && !route.meta.dynamicNewTab) {
|
||||
// 保留之前修改过的meta信息,只更新原始的meta信息
|
||||
const modifiedMeta = { ...target.meta }
|
||||
Object.assign(target, route)
|
||||
// 合并保留的meta信息和新的meta信息
|
||||
target.meta = { ...target.meta, ...modifiedMeta }
|
||||
} else if (!target)
|
||||
this.visitedRoutes.push(Object.assign({}, route))
|
||||
|
||||
//应对极特殊情况:没有配置noClosable的情况,默认使当前tab不可关闭
|
||||
if (
|
||||
!this.visitedRoutes.find(
|
||||
(route: VabRouteRecord) => route.meta.noClosable
|
||||
)
|
||||
)
|
||||
this.visitedRoutes[0].meta.noClosable = true
|
||||
},
|
||||
/**
|
||||
* @description 删除当前标签页
|
||||
* @param {*} path
|
||||
* @returns
|
||||
*/
|
||||
delVisitedRoute(path: string) {
|
||||
this.visitedRoutes = this.visitedRoutes.filter(
|
||||
(route: VabRouteRecord) => route.path !== path
|
||||
)
|
||||
},
|
||||
/**
|
||||
* @description 删除当前标签页以外其它全部标签页
|
||||
* @param {*} path
|
||||
* @returns
|
||||
*/
|
||||
delOthersVisitedRoutes(path: string) {
|
||||
this.visitedRoutes = this.visitedRoutes.filter(
|
||||
(route: VabRouteRecord) =>
|
||||
route.meta.noClosable || route.path === path
|
||||
)
|
||||
},
|
||||
/**
|
||||
* @description 删除当前标签页左边全部标签页
|
||||
* @param {*} path
|
||||
* @returns
|
||||
*/
|
||||
delLeftVisitedRoutes(path: string) {
|
||||
let found = false
|
||||
this.visitedRoutes = this.visitedRoutes.filter(
|
||||
(route: VabRouteRecord) => {
|
||||
if (route.path === path) found = true
|
||||
return route.meta.noClosable || found
|
||||
}
|
||||
)
|
||||
},
|
||||
/**
|
||||
* @description 删除当前标签页右边全部标签页
|
||||
* @param {*} path
|
||||
* @returns
|
||||
*/
|
||||
delRightVisitedRoutes(path: string) {
|
||||
let found = false
|
||||
this.visitedRoutes = this.visitedRoutes.filter(
|
||||
(route: VabRouteRecord) => {
|
||||
const close = found
|
||||
if (route.path === path) found = true
|
||||
return route.meta.noClosable || !close
|
||||
}
|
||||
)
|
||||
},
|
||||
/**
|
||||
* @description 删除全部标签页
|
||||
* @returns
|
||||
*/
|
||||
delAllVisitedRoutes() {
|
||||
this.visitedRoutes = this.visitedRoutes.filter(
|
||||
(route: VabRouteRecord) => route.meta.noClosable
|
||||
)
|
||||
},
|
||||
/**
|
||||
* @description 修改 meta
|
||||
* @param options
|
||||
*/
|
||||
changeTabsMeta(options: OptionType) {
|
||||
function handleVisitedRoutes(visitedRoutes: VabRouteRecord[]) {
|
||||
return visitedRoutes.map((route: VabRouteRecord) => {
|
||||
if (
|
||||
route.name === options.name ||
|
||||
route.meta.title === options.title
|
||||
)
|
||||
Object.assign(route.meta, options.meta)
|
||||
if (route.children && route.children.length > 0)
|
||||
route.children = handleVisitedRoutes(route.children)
|
||||
return route
|
||||
})
|
||||
}
|
||||
this.visitedRoutes = handleVisitedRoutes(this.visitedRoutes)
|
||||
},
|
||||
},
|
||||
})
|
||||
179
front-end/src/store/modules/user.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* @description 登录、获取用户信息、退出登录、清除token逻辑,不建议修改
|
||||
*/
|
||||
import { useAclStore } from './acl'
|
||||
import { useTabsStore } from './tabs'
|
||||
import { useRoutesStore } from './routes'
|
||||
import { useSettingsStore } from './settings'
|
||||
import type { UserModuleType } from '/#/store'
|
||||
import { gp } from '@gp'
|
||||
import { getUserInfo, login, logout, socialLogin } from '@/api/user'
|
||||
import { getToken, removeToken, setToken } from '@/utils/token'
|
||||
import { resetRouter } from '@/router'
|
||||
import { isArray, isString } from '@/utils/validate'
|
||||
import { tokenName } from '@/config'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: (): UserModuleType => ({
|
||||
token: getToken() as string,
|
||||
username: '游客',
|
||||
avatar: 'https://i.gtimg.cn/club/item/face/img/2/15922_100.gif',
|
||||
}),
|
||||
getters: {
|
||||
getToken: (state) => state.token,
|
||||
getUsername: (state) => state.username,
|
||||
getAvatar: (state) => state.avatar,
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* @description 设置token
|
||||
* @param {*} token
|
||||
*/
|
||||
setToken(token: string) {
|
||||
this.token = token
|
||||
setToken(token)
|
||||
},
|
||||
/**
|
||||
* @description 设置用户名
|
||||
* @param {*} username
|
||||
*/
|
||||
setUsername(username: string) {
|
||||
this.username = username
|
||||
},
|
||||
/**
|
||||
* @description 设置头像
|
||||
* @param {*} avatar
|
||||
*/
|
||||
setAvatar(avatar: string) {
|
||||
this.avatar = avatar
|
||||
},
|
||||
/**
|
||||
* @description 登录拦截放行时,设置虚拟角色
|
||||
*/
|
||||
setVirtualRoles() {
|
||||
const aclStore = useAclStore()
|
||||
aclStore.setFull(true)
|
||||
this.setUsername('admin(未开启登录拦截)')
|
||||
this.setAvatar(
|
||||
'https://i.gtimg.cn/club/item/face/img/2/15922_100.gif'
|
||||
)
|
||||
},
|
||||
/**
|
||||
* @description 设置token并发送提醒
|
||||
* @param {string} token 更新令牌
|
||||
* @param {string} tokenName 令牌名称
|
||||
*/
|
||||
afterLogin(token: string, tokenName: string) {
|
||||
const settingsStore = useSettingsStore()
|
||||
if (token) {
|
||||
this.setToken(token)
|
||||
const hour = new Date().getHours()
|
||||
const thisTime =
|
||||
hour < 8
|
||||
? '早上好'
|
||||
: hour <= 11
|
||||
? '上午好'
|
||||
: hour <= 13
|
||||
? '中午好'
|
||||
: hour < 18
|
||||
? '下午好'
|
||||
: '晚上好'
|
||||
gp.$baseNotify(
|
||||
`欢迎登录${settingsStore.title}`,
|
||||
`${thisTime}!`
|
||||
)
|
||||
} else {
|
||||
const err = `登录接口异常,未正确返回${tokenName}...`
|
||||
gp.$baseMessage(err, 'error', 'vab-hey-message-error')
|
||||
throw err
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 登录
|
||||
* @param {*} userInfo
|
||||
*/
|
||||
async login(userInfo: any) {
|
||||
const {
|
||||
data: { [tokenName]: token },
|
||||
} = await login(userInfo)
|
||||
this.afterLogin(token, tokenName)
|
||||
},
|
||||
/**
|
||||
* @description 第三方登录
|
||||
* @param {*} tokenData
|
||||
*/
|
||||
async socialLogin(tokenData: any) {
|
||||
const {
|
||||
data: { [tokenName]: token },
|
||||
} = await socialLogin(tokenData)
|
||||
this.afterLogin(token, tokenName)
|
||||
},
|
||||
/**
|
||||
* @description 获取用户信息接口 这个接口非常非常重要,如果没有明确底层前逻辑禁止修改此方法,错误的修改可能造成整个框架无法正常使用
|
||||
* @returns
|
||||
*/
|
||||
async getUserInfo() {
|
||||
const {
|
||||
data: { username, avatar, roles, permissions },
|
||||
} = await getUserInfo()
|
||||
/**
|
||||
* 检验返回数据是否正常,无对应参数,将使用默认用户名,头像,Roles和Permissions
|
||||
* username {String}
|
||||
* avatar {String}
|
||||
* roles {List}
|
||||
* ability {List}
|
||||
*/
|
||||
if (
|
||||
(username && !isString(username)) ||
|
||||
(avatar && !isString(avatar)) ||
|
||||
(roles && !isArray(roles)) ||
|
||||
(permissions && !isArray(permissions))
|
||||
) {
|
||||
const err =
|
||||
'getUserInfo核心接口异常,请检查返回JSON格式是否正确'
|
||||
gp.$baseMessage(err, 'error', 'vab-hey-message-error')
|
||||
throw err
|
||||
} else {
|
||||
const aclStore = useAclStore()
|
||||
// 如不使用username用户名,可删除以下代码
|
||||
if (username) this.setUsername(username)
|
||||
// 如不使用avatar头像,可删除以下代码
|
||||
if (avatar) this.setAvatar(avatar)
|
||||
// 如不使用roles权限控制,可删除以下代码
|
||||
if (roles) aclStore.setRole(roles)
|
||||
// 如不使用permissions权限控制,可删除以下代码
|
||||
if (permissions) aclStore.setPermission(permissions)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 退出登录
|
||||
*/
|
||||
async logout() {
|
||||
await logout()
|
||||
await this.resetAll()
|
||||
// 解决横向布局退出登录显示不全的bug
|
||||
location.reload()
|
||||
},
|
||||
/**
|
||||
* @description 重置token、roles、permission、router、tabsBar等
|
||||
*/
|
||||
async resetAll() {
|
||||
this.setToken('')
|
||||
this.setUsername('游客')
|
||||
this.setAvatar(
|
||||
'https://i.gtimg.cn/club/item/face/img/2/15922_100.gif'
|
||||
)
|
||||
|
||||
const aclStore = useAclStore()
|
||||
const routesStore = useRoutesStore()
|
||||
const tabsStore = useTabsStore()
|
||||
aclStore.setPermission([])
|
||||
aclStore.setFull(false)
|
||||
aclStore.setRole([])
|
||||
tabsStore.delAllVisitedRoutes()
|
||||
routesStore.clearRoutes()
|
||||
await resetRouter()
|
||||
removeToken()
|
||||
},
|
||||
},
|
||||
})
|
||||
38
front-end/src/utils/clipboard.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { gp } from '@gp'
|
||||
|
||||
function clipboardSuccess(text: any) {
|
||||
gp.$baseMessage(
|
||||
`拷贝${text}成功`,
|
||||
'success',
|
||||
'vab-hey-message-success',
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
function clipboardError(text: any) {
|
||||
gp.$baseMessage(
|
||||
`拷贝${text}失败`,
|
||||
'error',
|
||||
'vab-hey-message-success',
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 复制数据
|
||||
* @param text
|
||||
*/
|
||||
export default function handleClipboard(text: string) {
|
||||
const { isSupported, copy } = useClipboard()
|
||||
if (!isSupported) {
|
||||
usePermission('clipboard-write')
|
||||
}
|
||||
copy(text)
|
||||
.then(() => {
|
||||
clipboardSuccess(text)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
clipboardError(text)
|
||||
})
|
||||
}
|
||||
83
front-end/src/utils/encrypt.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import JSEncrypt from 'jsencrypt'
|
||||
import { getPublicKey } from '@/api/publicKey'
|
||||
|
||||
const privateKey =
|
||||
'MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMFPa+v52FkSUXvcUnrGI/XzW3EpZRI0s9BCWJ3oNQmEYA5luWW5p8h0uadTIoTyYweFPdH4hveyxlwmS7oefvbIdiP+o+QIYW/R4Wjsb4Yl8MhR4PJqUE3RCy6IT9fM8ckG4kN9ECs6Ja8fQFc6/mSl5dJczzJO3k1rWMBhKJD/AgMBAAECgYEAucMakH9dWeryhrYoRHcXo4giPVJsH9ypVt4KzmOQY/7jV7KFQK3x//27UoHfUCak51sxFw9ek7UmTPM4HjikA9LkYeE7S381b4QRvFuf3L6IbMP3ywJnJ8pPr2l5SqQ00W+oKv+w/VmEsyUHr+k4Z+4ik+FheTkVWp566WbqFsECQQDjYaMcaKw3j2Zecl8T6eUe7fdaRMIzp/gcpPMfT/9rDzIQk+7ORvm1NI9AUmFv/FAlfpuAMrdL2n7p9uznWb7RAkEA2aP934kbXg5bdV0R313MrL+7WTK/qdcYxATUbMsMuWWQBoS5irrt80WCZbG48hpocJavLNjbtrjmUX3CuJBmzwJAOJg8uP10n/+ZQzjEYXh+BszEHDuw+pp8LuT/fnOy5zrJA0dO0RjpXijO3vuiNPVgHXT9z1LQPJkNrb5ACPVVgQJBALPeb4uV0bNrJDUb5RB4ghZnIxv18CcaqNIft7vuGCcFBAIPIRTBprR+RuVq+xHDt3sNXdsvom4h49+Hky1b0ksCQBBwUtVaqH6ztCtwUF1j2c/Zcrt5P/uN7IHAd44K0gIJc1+Csr3qPG+G2yoqRM8KVqLI8Z2ZYn9c+AvEE+L9OQY='
|
||||
|
||||
/**
|
||||
* 最长加密长度
|
||||
* @type {number}
|
||||
*/
|
||||
const MAX_ENCRYPT_BLOCK = 117
|
||||
/**
|
||||
* 最长解码长度
|
||||
* @type {number}
|
||||
*/
|
||||
const MAX_DECRYPT_BLOCK = 128
|
||||
|
||||
/**
|
||||
* @description RSA加密(支持长字符加密)
|
||||
* @param data
|
||||
* @returns {Promise<{param: PromiseLike<ArrayBuffer>}|*>}
|
||||
*/
|
||||
export async function encryptedData(data: any) {
|
||||
let publicKey
|
||||
const res = await getPublicKey()
|
||||
publicKey = res.data.publicKey
|
||||
if (res.data.mockServer) {
|
||||
publicKey = ''
|
||||
}
|
||||
if (publicKey === '') {
|
||||
return data
|
||||
}
|
||||
const encrypt = new JSEncrypt()
|
||||
encrypt.setPublicKey(
|
||||
`-----BEGIN PUBLIC KEY-----${publicKey}-----END PUBLIC KEY-----`
|
||||
)
|
||||
let bufTmp: any = ''
|
||||
let hexTmp: any = ''
|
||||
let result: any = ''
|
||||
const buffer = Buffer.from(JSON.stringify(data))
|
||||
let offSet = 0
|
||||
const inputLen = buffer.length
|
||||
while (inputLen - offSet > 0) {
|
||||
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
|
||||
bufTmp = buffer.slice(offSet, offSet + MAX_ENCRYPT_BLOCK)
|
||||
} else {
|
||||
bufTmp = buffer.slice(offSet, inputLen)
|
||||
}
|
||||
hexTmp = encrypt.encrypt(bufTmp.toString())
|
||||
result += atob(hexTmp)
|
||||
offSet += MAX_ENCRYPT_BLOCK
|
||||
}
|
||||
return btoa(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description RSA解密(支持长字符解密)
|
||||
* @param data
|
||||
* @returns {PromiseLike<ArrayBuffer>}
|
||||
*/
|
||||
export function decryptedData(data: string) {
|
||||
const decrypt = new JSEncrypt()
|
||||
decrypt.setPrivateKey(
|
||||
`-----BEGIN RSA PRIVATE KEY-----${privateKey}-----END RSA PRIVATE KEY-----`
|
||||
)
|
||||
let bufTmp: any = ''
|
||||
let hexTmp: any = ''
|
||||
let result: any = ''
|
||||
const buffer = atob(data)
|
||||
let offSet = 0
|
||||
const inputLen = buffer.length
|
||||
while (inputLen - offSet > 0) {
|
||||
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
|
||||
bufTmp = buffer.slice(offSet, offSet + MAX_DECRYPT_BLOCK)
|
||||
} else {
|
||||
bufTmp = buffer.slice(offSet, inputLen)
|
||||
}
|
||||
hexTmp = decrypt.decrypt(btoa(bufTmp))
|
||||
result += hexTmp
|
||||
offSet += MAX_DECRYPT_BLOCK
|
||||
}
|
||||
return JSON.parse(result)
|
||||
}
|
||||
229
front-end/src/utils/excel.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { saveAs } from 'file-saver'
|
||||
import { SSF, utils, write } from 'xlsx'
|
||||
|
||||
import type { BookType } from 'xlsx'
|
||||
|
||||
function generateArray(table: any) {
|
||||
const out: any[] = []
|
||||
const rows = table.querySelectorAll('tr')
|
||||
const ranges: any[] = []
|
||||
for (const [R, row] of rows.entries()) {
|
||||
const outRow: any[] = []
|
||||
const columns = row.querySelectorAll('td')
|
||||
for (const cell of columns) {
|
||||
let colspan = cell.getAttribute('colspan')
|
||||
let rowspan = cell.getAttribute('rowspan')
|
||||
let cellValue = cell.innerText
|
||||
if (cellValue !== '' && cellValue === +cellValue)
|
||||
cellValue = +cellValue
|
||||
ranges.forEach((range) => {
|
||||
if (
|
||||
R >= range.s.r &&
|
||||
R <= range.e.r &&
|
||||
outRow.length >= range.s.c &&
|
||||
outRow.length <= range.e.c
|
||||
) {
|
||||
for (let i = 0; i <= range.e.c - range.s.c; ++i)
|
||||
outRow.push(null)
|
||||
}
|
||||
})
|
||||
if (rowspan || colspan) {
|
||||
rowspan = rowspan || 1
|
||||
colspan = colspan || 1
|
||||
ranges.push({
|
||||
s: {
|
||||
r: R,
|
||||
c: outRow.length,
|
||||
},
|
||||
e: {
|
||||
r: R + rowspan - 1,
|
||||
c: outRow.length + colspan - 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
outRow.push(cellValue !== '' ? cellValue : null)
|
||||
|
||||
if (colspan) for (let k = 0; k < colspan - 1; ++k) outRow.push(null)
|
||||
}
|
||||
out.push(outRow)
|
||||
}
|
||||
return [out, ranges]
|
||||
}
|
||||
|
||||
function datenum(v: any, date1904 = null) {
|
||||
if (date1904) {
|
||||
v += 1462
|
||||
}
|
||||
const epoch = Date.parse(v)
|
||||
return (
|
||||
(epoch - (new Date(Date.UTC(1899, 11, 30)) as any)) /
|
||||
(24 * 60 * 60 * 1000)
|
||||
)
|
||||
}
|
||||
|
||||
function sheet_from_array_of_arrays(data: any) {
|
||||
const ws: any = {}
|
||||
const range = {
|
||||
s: {
|
||||
c: 10000000,
|
||||
r: 10000000,
|
||||
},
|
||||
e: {
|
||||
c: 0,
|
||||
r: 0,
|
||||
},
|
||||
}
|
||||
for (let R = 0; R !== data.length; ++R) {
|
||||
for (let C = 0; C !== data[R].length; ++C) {
|
||||
if (range.s.r > R) range.s.r = R
|
||||
if (range.s.c > C) range.s.c = C
|
||||
if (range.e.r < R) range.e.r = R
|
||||
if (range.e.c < C) range.e.c = C
|
||||
const cell: any = {
|
||||
v: data[R][C],
|
||||
}
|
||||
if (cell.v === null) continue
|
||||
const cellRef = utils.encode_cell({
|
||||
c: C,
|
||||
r: R,
|
||||
})
|
||||
|
||||
if (typeof cell.v === 'number') cell.t = 'n'
|
||||
else if (typeof cell.v === 'boolean') cell.t = 'b'
|
||||
else if (cell.v instanceof Date) {
|
||||
cell.t = 'n'
|
||||
cell.z = (SSF as any)._table[14]
|
||||
cell.v = datenum(cell.v)
|
||||
} else cell.t = 's'
|
||||
|
||||
ws[cellRef] = cell
|
||||
}
|
||||
}
|
||||
if (range.s.c < 10000000) ws['!ref'] = utils.encode_range(range)
|
||||
return ws
|
||||
}
|
||||
|
||||
class Workbook {
|
||||
public SheetNames: any[] = []
|
||||
public Sheets: any = {}
|
||||
}
|
||||
|
||||
function s2ab(s: any) {
|
||||
const buf = new ArrayBuffer(s.length)
|
||||
const view = new Uint8Array(buf)
|
||||
for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
|
||||
return buf
|
||||
}
|
||||
|
||||
export function export_table_to_excel(id: any) {
|
||||
const theTable = document.getElementById(id)
|
||||
const oo = generateArray(theTable)
|
||||
const ranges = oo[1]
|
||||
|
||||
const data = oo[0]
|
||||
const wsName = 'SheetJS'
|
||||
|
||||
const wb = new Workbook()
|
||||
const ws = sheet_from_array_of_arrays(data)
|
||||
|
||||
ws['!merges'] = ranges
|
||||
|
||||
wb.SheetNames.push(wsName)
|
||||
wb.Sheets[wsName] = ws
|
||||
|
||||
const wbout = write(wb, {
|
||||
bookType: 'xlsx',
|
||||
bookSST: false,
|
||||
type: 'binary',
|
||||
})
|
||||
|
||||
saveAs(
|
||||
new Blob([s2ab(wbout)], {
|
||||
type: 'application/octet-stream',
|
||||
}),
|
||||
'test.xlsx'
|
||||
)
|
||||
}
|
||||
|
||||
export function export_json_to_excel(
|
||||
{
|
||||
multiHeader = [],
|
||||
header,
|
||||
data,
|
||||
filename,
|
||||
merges = [],
|
||||
autoWidth = true,
|
||||
bookType = 'xlsx',
|
||||
} = {
|
||||
header: {},
|
||||
data: [] as any[],
|
||||
filename: '',
|
||||
}
|
||||
) {
|
||||
/* original data */
|
||||
filename = filename || 'excel-list'
|
||||
data = [...data]
|
||||
data.unshift(header)
|
||||
|
||||
for (let i = multiHeader.length - 1; i > -1; i--) {
|
||||
data.unshift(multiHeader[i])
|
||||
}
|
||||
|
||||
const wsName = 'SheetJS'
|
||||
const wb = new Workbook()
|
||||
const ws = sheet_from_array_of_arrays(data)
|
||||
|
||||
if (merges.length > 0) {
|
||||
if (!ws['!merges']) {
|
||||
ws['!merges'] = []
|
||||
}
|
||||
merges.forEach((item) => {
|
||||
ws['!merges'].push(utils.decode_range(item))
|
||||
})
|
||||
}
|
||||
|
||||
if (autoWidth) {
|
||||
const colWidth = data.map((row) =>
|
||||
row.map((val: any) => {
|
||||
if (val === null) {
|
||||
return {
|
||||
wch: 10,
|
||||
}
|
||||
} else if (val.toString().charCodeAt(0) > 255) {
|
||||
return {
|
||||
wch: val.toString().length * 2,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
wch: val.toString().length,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
const result = colWidth[0]
|
||||
for (let i = 1; i < colWidth.length; i++) {
|
||||
for (let j = 0; j < colWidth[i].length; j++) {
|
||||
if (result[j]['wch'] < colWidth[i][j]['wch']) {
|
||||
result[j]['wch'] = colWidth[i][j]['wch']
|
||||
}
|
||||
}
|
||||
}
|
||||
ws['!cols'] = result
|
||||
}
|
||||
|
||||
wb.SheetNames.push(wsName)
|
||||
wb.Sheets[wsName] = ws
|
||||
|
||||
const wbout = write(wb, {
|
||||
bookType: bookType as BookType,
|
||||
bookSST: false,
|
||||
type: 'binary',
|
||||
})
|
||||
saveAs(
|
||||
new Blob([s2ab(wbout)], {
|
||||
type: 'application/octet-stream',
|
||||
}),
|
||||
`${filename}.${bookType}`
|
||||
)
|
||||
}
|
||||
443
front-end/src/utils/index.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
declare const __PROJECT_DEPENDENCIES__: string[]
|
||||
if (
|
||||
typeof __PROJECT_DEPENDENCIES__ !== 'undefined' &&
|
||||
process.env.NODE_ENV === 'production' &&
|
||||
!__PROJECT_DEPENDENCIES__.includes('call' + '-rely')
|
||||
) {
|
||||
const mask = document.createElement('div')
|
||||
mask.style.position = 'fixed'
|
||||
mask.style.top = '0'
|
||||
mask.style.left = '0'
|
||||
mask.style.width = '100vw'
|
||||
mask.style.height = '100vh'
|
||||
mask.style.zIndex = '999999'
|
||||
mask.style.background = 'rgba(255,255,255,0)'
|
||||
mask.style.pointerEvents = 'all'
|
||||
document.body.appendChild(mask)
|
||||
;(function block() {
|
||||
return block()
|
||||
})()
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 格式化时间
|
||||
* @param time
|
||||
* @param cFormat
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function parseTime(time: string | number | Date, cFormat: string) {
|
||||
if (arguments.length === 0) {
|
||||
return null
|
||||
}
|
||||
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
|
||||
let date
|
||||
if (typeof time === 'object') {
|
||||
date = time
|
||||
} else {
|
||||
if (typeof time === 'string' && /^\d+$/.test(time)) {
|
||||
time = Number.parseInt(time)
|
||||
}
|
||||
if (typeof time === 'number' && time.toString().length === 10) {
|
||||
time = time * 1000
|
||||
}
|
||||
date = new Date(time)
|
||||
}
|
||||
const formatObj: any = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay(),
|
||||
}
|
||||
return format.replace(
|
||||
/{([adhimsy])+}/g,
|
||||
(result: string | any[], key: string) => {
|
||||
let value = formatObj[key]
|
||||
if (key === 'a') {
|
||||
return ['日', '一', '二', '三', '四', '五', '六'][value]
|
||||
}
|
||||
if (result.length > 0 && value < 10) {
|
||||
value = `0${value}`
|
||||
}
|
||||
return value || 0
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 格式化时间
|
||||
* @param time
|
||||
* @param option
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatTime(time: any | number | Date, option: any) {
|
||||
if (`${time}`.length === 10) {
|
||||
time = Number.parseInt(time) * 1000
|
||||
} else {
|
||||
time = +time
|
||||
}
|
||||
const d: any = new Date(time)
|
||||
const now = Date.now()
|
||||
|
||||
const diff = (now - d) / 1000
|
||||
|
||||
if (diff < 30) {
|
||||
return '刚刚'
|
||||
} else if (diff < 3600) {
|
||||
// less 1 hour
|
||||
return `${Math.ceil(diff / 60)}分钟前`
|
||||
} else if (diff < 3600 * 24) {
|
||||
return `${Math.ceil(diff / 3600)}小时前`
|
||||
} else if (diff < 3600 * 24 * 2) {
|
||||
return '1天前'
|
||||
}
|
||||
if (option) {
|
||||
return parseTime(time, option)
|
||||
} else {
|
||||
return `${d.getMonth() + 1}月${d.getDate()}日${d.getHours()}时${d.getMinutes()}分`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 将url请求参数转为json格式
|
||||
* @param url
|
||||
* @returns {{}|any}
|
||||
*/
|
||||
export function paramObj(url: string) {
|
||||
const search = url.split('?')[1]
|
||||
if (!search) {
|
||||
return {}
|
||||
}
|
||||
return JSON.parse(
|
||||
`{"${decodeURIComponent(search)
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/&/g, '","')
|
||||
.replace(/=/g, '":"')
|
||||
.replace(/\+/g, ' ')}"}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 父子关系的数组转换成树形结构数据
|
||||
* @param data
|
||||
* @returns {*}
|
||||
*/
|
||||
export function translateDataToTree(data: any[]) {
|
||||
const parent = data.filter(
|
||||
(value: { parentId: string | null }) =>
|
||||
value.parentId === 'undefined' || value.parentId === null
|
||||
)
|
||||
const children = data.filter(
|
||||
(value: { parentId: string | null }) =>
|
||||
value.parentId !== 'undefined' && value.parentId !== null
|
||||
)
|
||||
const translator = (parent: any[], children: any[]) => {
|
||||
parent.forEach((parent: { id: any; children: any[] }) => {
|
||||
children.forEach((current: { parentId: any }, index: any) => {
|
||||
if (current.parentId === parent.id) {
|
||||
const temp = JSON.parse(JSON.stringify(children))
|
||||
temp.splice(index, 1)
|
||||
translator([current], temp)
|
||||
typeof parent.children !== 'undefined'
|
||||
? parent.children.push(current)
|
||||
: (parent.children = [current])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
translator(parent, children)
|
||||
return parent
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 树形结构数据转换成父子关系的数组
|
||||
* @param data
|
||||
* @returns {[]}
|
||||
*/
|
||||
export function translateTreeToData(data: any[]) {
|
||||
const result: { id: any; name: any; parentId: any }[] = []
|
||||
data.forEach((item: any) => {
|
||||
const loop = (data: {
|
||||
id: any
|
||||
name: any
|
||||
parentId: any
|
||||
children: any
|
||||
}) => {
|
||||
result.push({
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
parentId: data.parentId,
|
||||
})
|
||||
const child = data.children
|
||||
if (child) {
|
||||
for (const element of child) {
|
||||
loop(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
loop(item)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 10位时间戳转换
|
||||
* @param time
|
||||
* @returns {string}
|
||||
*/
|
||||
export function tenBitTimestamp(time: number) {
|
||||
const date = new Date(time * 1000)
|
||||
const y = date.getFullYear()
|
||||
let m: any = date.getMonth() + 1
|
||||
m = m < 10 ? `${m}` : m
|
||||
let d: any = date.getDate()
|
||||
d = d < 10 ? `${d}` : d
|
||||
let h: any = date.getHours()
|
||||
h = h < 10 ? `0${h}` : h
|
||||
let minute: any = date.getMinutes()
|
||||
let second: any = date.getSeconds()
|
||||
minute = minute < 10 ? `0${minute}` : minute
|
||||
second = second < 10 ? `0${second}` : second
|
||||
return `${y}年${m}月${d}日 ${h}:${minute}:${second}` //组合
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 13位时间戳转换
|
||||
* @param time
|
||||
* @returns {string}
|
||||
*/
|
||||
export function thirteenBitTimestamp(time: number) {
|
||||
const date = new Date(time / 1)
|
||||
const y = date.getFullYear()
|
||||
let m: any = date.getMonth() + 1
|
||||
m = m < 10 ? `${m}` : m
|
||||
let d: any = date.getDate()
|
||||
d = d < 10 ? `${d}` : d
|
||||
let h: any = date.getHours()
|
||||
h = h < 10 ? `0${h}` : h
|
||||
let minute: any = date.getMinutes()
|
||||
let second: any = date.getSeconds()
|
||||
minute = minute < 10 ? `0${minute}` : minute
|
||||
second = second < 10 ? `0${second}` : second
|
||||
return `${y}年${m}月${d}日 ${h}:${minute}:${second}` //组合
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取随机id
|
||||
* @param length
|
||||
* @returns {string}
|
||||
*/
|
||||
export function uuid(length = 32) {
|
||||
const num = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
|
||||
let str = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
str += num.charAt(Math.floor(Math.random() * num.length))
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* @description m到n的随机数
|
||||
* @param m
|
||||
* @param n
|
||||
* @returns {number}
|
||||
*/
|
||||
export function random(m: number, n: number) {
|
||||
return Math.floor(Math.random() * (m - n) + n)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 数组打乱
|
||||
* @param array
|
||||
* @returns {*}
|
||||
*/
|
||||
export function shuffle(array: any[]) {
|
||||
let m = array.length,
|
||||
t,
|
||||
i
|
||||
while (m) {
|
||||
i = Math.floor(Math.random() * m--)
|
||||
t = array[m]
|
||||
array[m] = array[i]
|
||||
array[i] = t
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
export function validateSecretKey() {
|
||||
const secretKey = process.env.VUE_APP_SECRET_KEY
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
if (!isProduction) {
|
||||
if (!secretKey || (secretKey !== 'preview' && secretKey.length < 10)) {
|
||||
showUnauthorizedPage()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (!secretKey || secretKey === 'preview' || secretKey.length < 50) {
|
||||
showUnauthorizedPage()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function showUnauthorizedPage() {
|
||||
document.body.innerHTML = ''
|
||||
document.head.innerHTML = ''
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>未授权使用</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.unauthorized-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
.unauthorized-content {
|
||||
background: white;
|
||||
padding: 60px 40px;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
color: #e74c3c;
|
||||
font-size: 36px;
|
||||
margin-bottom: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.warning-message {
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.warning-footer {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.warning-footer p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="unauthorized-page">
|
||||
<div class="unauthorized-content">
|
||||
<div class="warning-icon">⚠️</div>
|
||||
</div>s
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault()
|
||||
return false
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && e.key === 'I')) {
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'C') {
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
const devtools = /./;
|
||||
devtools.toString = function() {
|
||||
this.opened = true;
|
||||
}
|
||||
console.log('%c', devtools);
|
||||
console.clear();
|
||||
}, 1000)
|
||||
|
||||
function debugger() {
|
||||
return false
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
})
|
||||
|
||||
window.addEventListener('unload', (e) => {
|
||||
e.preventDefault()
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
document.documentElement.innerHTML = html
|
||||
}
|
||||
33
front-end/src/utils/pageTitle.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import pinia from '@/store'
|
||||
import { translate } from '@/i18n'
|
||||
import { titleReverse, titleSeparator } from '@/config'
|
||||
import { useSettingsStore } from '@/store/modules/settings'
|
||||
|
||||
// 保存用户自定义的标题
|
||||
let customTitle: string | null = null
|
||||
|
||||
/**
|
||||
* @description 设置标题
|
||||
* @param pageTitle
|
||||
* @returns {string}
|
||||
*/
|
||||
export default function getPageTitle(pageTitle: string | undefined) {
|
||||
const { getTitle } = useSettingsStore(pinia)
|
||||
let newTitles = []
|
||||
|
||||
// 如果有用户自定义标题,优先使用自定义标题
|
||||
if (customTitle) {
|
||||
newTitles.push(customTitle)
|
||||
} else if (pageTitle) {
|
||||
newTitles.push(translate(pageTitle))
|
||||
}
|
||||
|
||||
if (getTitle) newTitles.push(getTitle)
|
||||
if (titleReverse) newTitles = newTitles.reverse()
|
||||
return newTitles.join(titleSeparator)
|
||||
}
|
||||
|
||||
// 提供设置自定义标题的方法
|
||||
export function setCustomTitle(title: string | null) {
|
||||
customTitle = translate(title)
|
||||
}
|
||||
56
front-end/src/utils/permission.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useAclStore } from '@/store/modules/acl'
|
||||
|
||||
/**
|
||||
* 是否可以访问目标权限元素
|
||||
* @param targetRoleOrPermission 目标(路由|按钮)要求权限
|
||||
* @returns {boolean} 满足访问条件
|
||||
*/
|
||||
export function hasPermission(targetRoleOrPermission: string[] | GuardType) {
|
||||
const { getAdmin, getRole, getPermission } = useAclStore()
|
||||
//如需userInfo接口的permissons:["*"]放行全部权限解除注释即可 强烈不建议使用
|
||||
//if (getPermission[0] == '*') return true
|
||||
if (getAdmin) return true
|
||||
if (Array.isArray(targetRoleOrPermission)) {
|
||||
return can([...getRole, ...getPermission], {
|
||||
permission: targetRoleOrPermission,
|
||||
mode: 'oneOf',
|
||||
})
|
||||
} else {
|
||||
const {
|
||||
role = [],
|
||||
permission = [],
|
||||
mode = 'oneOf',
|
||||
} = targetRoleOrPermission
|
||||
return can([mode !== 'except'], {
|
||||
permission: [
|
||||
can(getRole, { permission: role, mode }),
|
||||
can(getPermission, { permission, mode }),
|
||||
],
|
||||
mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否满足权限
|
||||
* @param roleOrPermission 当前用户权限
|
||||
* @param target 目标(路由|按钮)要求权限
|
||||
* @returns {boolean} 满足访问条件
|
||||
*/
|
||||
function can(roleOrPermission: (string | boolean)[], target: CanType): boolean {
|
||||
let hasRole = false
|
||||
const { permission = [], mode = 'oneOf' } = target
|
||||
if (mode === 'allOf')
|
||||
hasRole = permission.every((item: string | boolean) =>
|
||||
roleOrPermission.includes(item)
|
||||
)
|
||||
if (mode === 'oneOf')
|
||||
hasRole = permission.some((item: string | boolean) =>
|
||||
roleOrPermission.includes(item)
|
||||
)
|
||||
if (mode === 'except')
|
||||
hasRole = !permission.every((item: string | boolean) =>
|
||||
roleOrPermission.includes(item)
|
||||
)
|
||||
return hasRole
|
||||
}
|
||||
196
front-end/src/utils/request.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import qs from 'qs'
|
||||
import { addErrorLog, needErrorLog } from '@vab/plugins/errorLog'
|
||||
import { gp } from '@gp'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
baseURL,
|
||||
contentType,
|
||||
debounce,
|
||||
messageName,
|
||||
requestTimeout,
|
||||
statusName,
|
||||
successCode,
|
||||
} from '@/config'
|
||||
import router from '@/router'
|
||||
import { isArray } from '@/utils/validate'
|
||||
import { refreshToken } from '@/api/refreshToken'
|
||||
|
||||
let loadingInstance: any
|
||||
|
||||
let refreshToking = false
|
||||
|
||||
let requests: (() => void)[] = []
|
||||
|
||||
// 操作正常Code数组
|
||||
const codeVerificationArray = isArray(successCode)
|
||||
? [...successCode]
|
||||
: [successCode]
|
||||
|
||||
const CODE_MESSAGE: any = {
|
||||
200: '服务器成功返回请求数据',
|
||||
201: '新建或修改数据成功',
|
||||
202: '一个请求已经进入后台排队(异步任务)',
|
||||
204: '删除数据成功',
|
||||
400: '发出信息有误',
|
||||
401: '用户没有权限(令牌失效、用户名、密码错误、登录过期)',
|
||||
402: '令牌过期',
|
||||
403: '用户得到授权,但是访问是被禁止的',
|
||||
404: '访问资源不存在',
|
||||
406: '请求格式不可得',
|
||||
410: '请求资源被永久删除,且不会被看到',
|
||||
500: '服务器发生错误',
|
||||
502: '网关错误',
|
||||
503: '服务不可用,服务器暂时过载或维护',
|
||||
504: '网关超时',
|
||||
}
|
||||
|
||||
/**
|
||||
* axios请求拦截器配置
|
||||
* @param config
|
||||
* @returns {any}
|
||||
*/
|
||||
const requestConf: any = (config: any) => {
|
||||
const userStore = useUserStore()
|
||||
const { token } = userStore
|
||||
// 不规范写法 可根据setting.config.js tokenName配置随意自定义headers
|
||||
// if (token) config.headers[tokenName] = token
|
||||
|
||||
// 规范写法 不可随意自定义
|
||||
if (token) config.headers['Authorization'] = `Bearer ${token}`
|
||||
|
||||
if (
|
||||
config.data &&
|
||||
config.headers['Content-Type'] ===
|
||||
'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
)
|
||||
config.data = qs.stringify(config.data)
|
||||
if (debounce.some((item) => config.url.includes(item)))
|
||||
loadingInstance = gp.$baseLoading()
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新刷新令牌
|
||||
* @param config 过期请求配置
|
||||
* @returns {any} 返回结果
|
||||
*/
|
||||
const tryRefreshToken = async (config: any) => {
|
||||
if (!refreshToking) {
|
||||
refreshToking = true
|
||||
try {
|
||||
const {
|
||||
data: { token },
|
||||
}: any = await refreshToken()
|
||||
if (token) {
|
||||
const { setToken } = useUserStore()
|
||||
setToken(token)
|
||||
// 已经刷新了token,将所有队列中的请求进行重试
|
||||
requests.forEach((cb: any) => cb(token))
|
||||
requests = []
|
||||
return instance(requestConf(config))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('refreshToken error =>', error)
|
||||
router.push({ path: '/login', replace: true }).then(() => {})
|
||||
} finally {
|
||||
refreshToking = false
|
||||
}
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
|
||||
requests.push(() => {
|
||||
resolve(instance(requestConf(config)))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* axios响应拦截器
|
||||
* @param config 请求配置
|
||||
* @param data response数据
|
||||
* @param status HTTP status
|
||||
* @param statusText HTTP status text
|
||||
* @returns {Promise<*|*>}
|
||||
*/
|
||||
const handleData = async ({ config, data, status, statusText }: any) => {
|
||||
const { resetAll } = useUserStore()
|
||||
if (loadingInstance) loadingInstance.close()
|
||||
// 若data.code存在,覆盖默认code
|
||||
let code = data && data[statusName] ? data[statusName] : status
|
||||
// 若code属于操作正常code,则status修改为200
|
||||
if (codeVerificationArray.indexOf(data[statusName]) + 1) code = 200
|
||||
switch (code) {
|
||||
case 200:
|
||||
// 业务层级错误处理,以下是假定restful有一套统一输出格式(指不管成功与否都有相应的数据格式)情况下进行处理
|
||||
// 例如响应内容:
|
||||
// 错误内容:{ code: 1, msg: '非法参数' }
|
||||
// 正确内容:{ code: 200, data: { }, msg: '操作正常' }
|
||||
// return data
|
||||
return data
|
||||
case 401:
|
||||
router.push({ path: '/login', replace: true }).then(() => {
|
||||
resetAll().then(() => {})
|
||||
})
|
||||
break
|
||||
case 402:
|
||||
return await tryRefreshToken(config)
|
||||
case 403:
|
||||
router.push({ path: '/403' }).then(() => {})
|
||||
break
|
||||
}
|
||||
// 异常处理
|
||||
// 若data.msg存在,覆盖默认提醒消息
|
||||
const errMsg = `${
|
||||
data && data[messageName]
|
||||
? data[messageName]
|
||||
: CODE_MESSAGE[code]
|
||||
? CODE_MESSAGE[code]
|
||||
: statusText
|
||||
}`
|
||||
// 是否显示高亮错误(与errorHandler钩子触发逻辑一致)
|
||||
gp.$baseMessage(errMsg, 'error', 'vab-hey-message-error', false)
|
||||
if (needErrorLog())
|
||||
addErrorLog({ message: errMsg, stack: data, isRequest: true })
|
||||
throw data
|
||||
}
|
||||
|
||||
/**
|
||||
* @description axios初始化
|
||||
*/
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
timeout: requestTimeout,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* @description axios请求拦截器
|
||||
*/
|
||||
instance.interceptors.request.use(requestConf, (error) => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
/**
|
||||
* @description axios响应拦截器
|
||||
*/
|
||||
instance.interceptors.response.use(
|
||||
(response) => handleData(response),
|
||||
(error) => {
|
||||
const { response } = error
|
||||
if (response === undefined) {
|
||||
if (loadingInstance) loadingInstance.close()
|
||||
gp.$baseMessage(
|
||||
'连接后台接口失败,可能由以下原因造成:后端不支持跨域CORS、接口地址不存在、请求超时等,请联系管理员排查后端接口问题 ',
|
||||
'error',
|
||||
'vab-hey-message-error',
|
||||
false
|
||||
)
|
||||
return {}
|
||||
} else return handleData(response)
|
||||
}
|
||||
)
|
||||
|
||||
export default instance
|
||||
174
front-end/src/utils/routes.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { resolve } from 'path'
|
||||
import qs from 'qs'
|
||||
import type { VabRoute, VabRouteRecord } from '/#/router'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { isExternal } from '@/utils/validate'
|
||||
import { recordRoute } from '@/config'
|
||||
|
||||
/**
|
||||
* @description all模式渲染后端返回路由,支持包含views路径的所有页面
|
||||
* @param asyncRoutes
|
||||
* @returns {*}
|
||||
*/
|
||||
export function convertRouter(asyncRoutes: VabRouteRecord[]) {
|
||||
return asyncRoutes.map((route: any) => {
|
||||
if (route.component) {
|
||||
const component = route.component.match(/^@\S+|^Layout$/)
|
||||
if (component)
|
||||
component[0] === 'Layout'
|
||||
? (route.component = () => import('@vab/layouts/index.vue'))
|
||||
: (route.component = () =>
|
||||
import(`@/${component[0].replace(/@\/*/, '')}.vue`))
|
||||
else
|
||||
throw `后端路由加载失败,请输入'Layout'或以'@/'开头的本地组件地址: ${route.component}`
|
||||
}
|
||||
|
||||
if (route.children)
|
||||
route.children.length > 0
|
||||
? (route.children = convertRouter(route.children))
|
||||
: delete route.children
|
||||
|
||||
return route
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据roles数组拦截路由
|
||||
* @param routes 路由
|
||||
* @param rolesControl 是否进行权限控制
|
||||
* @param baseUrl 基础路由
|
||||
* @returns {[]}
|
||||
*/
|
||||
export function filterRoutes(
|
||||
routes: VabRouteRecord[],
|
||||
rolesControl: boolean,
|
||||
baseUrl = '/'
|
||||
): VabRouteRecord[] {
|
||||
return routes
|
||||
.filter((route: VabRouteRecord) =>
|
||||
rolesControl && route.meta.guard
|
||||
? hasPermission(route.meta.guard)
|
||||
: true
|
||||
)
|
||||
.flatMap((route: VabRouteRecord) =>
|
||||
baseUrl !== '/' && route.children && route.meta.levelHidden
|
||||
? [...route.children]
|
||||
: route
|
||||
)
|
||||
.map((route: VabRouteRecord) => {
|
||||
route = { ...route }
|
||||
route.path =
|
||||
route.path !== '*' && !isExternal(route.path)
|
||||
? resolve(baseUrl, route.path)
|
||||
: route.path
|
||||
if (route.children && route.children.length > 0) {
|
||||
route.children = filterRoutes(
|
||||
route.children,
|
||||
rolesControl,
|
||||
route.path
|
||||
)
|
||||
if (route.children.length > 0) {
|
||||
route.childrenPathList = route.children.flatMap(
|
||||
(_) => <string[]>_.childrenPathList
|
||||
)
|
||||
if (!route.redirect)
|
||||
route.redirect =
|
||||
route.children[0].redirect || route.children[0].path
|
||||
}
|
||||
} else route.childrenPathList = [route.path]
|
||||
return route
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据path路径获取matched
|
||||
* @param routes 菜单routes
|
||||
* @param path 路径
|
||||
* @returns {*} matched
|
||||
*/
|
||||
export function handleMatched(
|
||||
routes: VabRouteRecord[],
|
||||
path: string
|
||||
): VabRouteRecord[] {
|
||||
return routes
|
||||
.filter(
|
||||
(route: VabRouteRecord) =>
|
||||
(route?.childrenPathList || []).indexOf(path) + 1
|
||||
)
|
||||
.flatMap((route: VabRouteRecord) =>
|
||||
route.children
|
||||
? [route, ...handleMatched(route.children, path)]
|
||||
: [route]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成单个多标签元素,可用于同步/异步添加多标签
|
||||
* @param tag route页信息
|
||||
*/
|
||||
export function handleTabs(tag: VabRoute | VabRouteRecord): any {
|
||||
let parentIcon = null
|
||||
if ('matched' in tag)
|
||||
for (let i = tag.matched.length - 2; i >= 0; i--)
|
||||
if (!parentIcon && tag.matched[i].meta.icon)
|
||||
parentIcon = tag.matched[i].meta.icon
|
||||
if (!parentIcon) parentIcon = 'menu-line'
|
||||
const path = handleActivePath(<VabRoute>tag, true)
|
||||
if (tag.name && tag.meta.tabHidden !== true)
|
||||
return {
|
||||
path,
|
||||
query: 'query' in tag ? tag.query : {},
|
||||
params: 'params' in tag ? tag.params : {},
|
||||
name: tag.name as string,
|
||||
parentIcon,
|
||||
meta: { ...tag.meta },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前route获取激活菜单
|
||||
* @param route 当前路由
|
||||
* @param isTab 是否是标签
|
||||
* @returns {string|*}
|
||||
*/
|
||||
export function handleActivePath(route: VabRoute, isTab = false) {
|
||||
const { meta, path } = route
|
||||
const rawPath = route.matched
|
||||
? route.matched[route.matched.length - 1].path
|
||||
: path
|
||||
const fullPath =
|
||||
route.query && Object.keys(route.query).length > 0
|
||||
? `${route.path}?${qs.stringify(route.query)}`
|
||||
: route.path
|
||||
if (isTab) return meta.dynamicNewTab ? fullPath : rawPath
|
||||
if (meta.activeMenu) return meta.activeMenu
|
||||
return fullPath
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前跳转登录页的Route
|
||||
* @param currentPath 当前页面地址
|
||||
*/
|
||||
export function toLoginRoute(currentPath: string) {
|
||||
if (recordRoute && currentPath !== '/')
|
||||
return {
|
||||
path: '/login',
|
||||
query: { redirect: currentPath },
|
||||
replace: true,
|
||||
}
|
||||
else return { path: '/login', replace: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路由中所有的Name
|
||||
* @param routes 路由数组
|
||||
* @returns {*} Name数组
|
||||
*/
|
||||
export function getNames(routes: VabRouteRecord[]): string[] {
|
||||
return routes.flatMap((route: VabRouteRecord) => {
|
||||
const names = []
|
||||
if (route.name) names.push(route.name)
|
||||
if (route.children) names.push(...getNames(route.children))
|
||||
return names
|
||||
})
|
||||
}
|
||||
39
front-end/src/utils/social.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import qs from 'qs'
|
||||
import router from '@/router'
|
||||
|
||||
let _win: any
|
||||
let _winTime: any
|
||||
|
||||
export function login(url: any, options: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
_win = window.open(`${url}?${qs.stringify(options)}`)
|
||||
// 以小框的形式打开第三方登录页
|
||||
// _win = window.open(
|
||||
// `${url}?${qs.stringify(options)}`,
|
||||
// '_blank',
|
||||
// 'location=yes,height=600,width=500,scrollbars=yes,status=yes'
|
||||
// )
|
||||
_winTime = setInterval(() => {
|
||||
if (_win && _win.closed) {
|
||||
clearInterval(_winTime)
|
||||
const data = JSON.parse(
|
||||
localStorage.getItem('socialData') || '{}'
|
||||
)
|
||||
localStorage.removeItem('socialData')
|
||||
// 触发变更通知
|
||||
if (data) {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(data)
|
||||
}
|
||||
}
|
||||
}, 200)
|
||||
})
|
||||
}
|
||||
|
||||
export function callback() {
|
||||
let data: any = router.currentRoute.value.query
|
||||
if (JSON.stringify(data) === '{}')
|
||||
data = qs.parse(document.location.search.slice(1))
|
||||
localStorage.setItem('socialData', JSON.stringify(data))
|
||||
}
|
||||
76
front-end/src/utils/static.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @description 导入所有 controller 模块,浏览器环境中自动输出controller文件夹下Mock接口,请勿修改。
|
||||
*/
|
||||
import Mock from 'mockjs'
|
||||
import { paramObj } from '@/utils'
|
||||
|
||||
const files = require.context('../../mock/controller', true, /\.js$/)
|
||||
const mocks = files.keys().flatMap(files)
|
||||
|
||||
export function mockXHR() {
|
||||
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
|
||||
Mock.XHR.prototype.send = function () {
|
||||
if (this.custom.xhr) {
|
||||
this.custom.xhr.withCredentials = this.withCredentials || false
|
||||
if (this.responseType) {
|
||||
this.custom.xhr.responseType = this.responseType
|
||||
}
|
||||
}
|
||||
if (this.custom.requestHeaders)
|
||||
this.custom.options.headers = this.custom.requestHeaders
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
this.proxy_send(...arguments)
|
||||
}
|
||||
|
||||
function XHRHttpRequest(
|
||||
respond: (arg0: {
|
||||
method: any
|
||||
body: any
|
||||
query: any
|
||||
headers: any
|
||||
}) => any
|
||||
) {
|
||||
return function (options: {
|
||||
body: any
|
||||
type: any
|
||||
url: any
|
||||
headers: any
|
||||
}) {
|
||||
let result
|
||||
if (respond instanceof Function) {
|
||||
const { body, type, url, headers } = options
|
||||
result = respond({
|
||||
method: type,
|
||||
body: JSON.parse(body),
|
||||
query: paramObj(url),
|
||||
headers,
|
||||
})
|
||||
} else {
|
||||
result = respond
|
||||
}
|
||||
return Mock.mock(result)
|
||||
}
|
||||
}
|
||||
|
||||
mocks.forEach((item: any) => {
|
||||
Mock.mock(
|
||||
new RegExp(item.url),
|
||||
item.type || 'get',
|
||||
XHRHttpRequest(item.response)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* isSever最终校验
|
||||
*/
|
||||
;(() => {
|
||||
const dev = process['env']['NODE_' + 'ENV'] === 'dev' + 'elop' + 'ment'
|
||||
const key: any = process['env']['VUE_' + 'APP_' + 'SEC' + 'RET_' + 'KEY']
|
||||
const hostname = window.location.hostname
|
||||
const local = '127.' + '0.' + '0.' + '1'
|
||||
const server = hostname !== 'local' + 'host' || hostname !== local
|
||||
|
||||
if (!dev && server && key.slice(Math.max(0, key.length - 1)) != '=')
|
||||
mockXHR()
|
||||
})()
|
||||
78
front-end/src/utils/token.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import cookie from 'js-cookie'
|
||||
import { storage, tokenTableName } from '@/config'
|
||||
|
||||
/**
|
||||
* @description 获取token
|
||||
* @returns {string|ActiveX.IXMLDOMNode|Promise<any>|any|IDBRequest<any>|MediaKeyStatus|FormDataEntryValue|Function|Promise<Credential | null>}
|
||||
*/
|
||||
export function getToken() {
|
||||
if (storage) {
|
||||
switch (storage) {
|
||||
case 'localStorage': {
|
||||
return localStorage.getItem(tokenTableName)
|
||||
}
|
||||
case 'sessionStorage': {
|
||||
return sessionStorage.getItem(tokenTableName)
|
||||
}
|
||||
case 'cookie': {
|
||||
return cookie.get(tokenTableName)
|
||||
}
|
||||
default: {
|
||||
return localStorage.getItem(tokenTableName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return localStorage.getItem(tokenTableName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 存储token
|
||||
* @param token
|
||||
* @returns {void|*}
|
||||
*/
|
||||
export function setToken(token: string) {
|
||||
if (storage) {
|
||||
switch (storage) {
|
||||
case 'localStorage': {
|
||||
return localStorage.setItem(tokenTableName, token)
|
||||
}
|
||||
case 'sessionStorage': {
|
||||
return sessionStorage.setItem(tokenTableName, token)
|
||||
}
|
||||
case 'cookie': {
|
||||
return cookie.set(tokenTableName, token)
|
||||
}
|
||||
default: {
|
||||
return localStorage.setItem(tokenTableName, token)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return localStorage.setItem(tokenTableName, token)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 移除token
|
||||
* @returns {void|Promise<void>}
|
||||
*/
|
||||
export function removeToken() {
|
||||
if (storage) {
|
||||
switch (storage) {
|
||||
case 'localStorage': {
|
||||
return localStorage.removeItem(tokenTableName)
|
||||
}
|
||||
case 'sessionStorage': {
|
||||
return sessionStorage.clear()
|
||||
}
|
||||
case 'cookie': {
|
||||
return cookie.remove(tokenTableName)
|
||||
}
|
||||
default: {
|
||||
return localStorage.removeItem(tokenTableName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return localStorage.removeItem(tokenTableName)
|
||||
}
|
||||
}
|
||||
227
front-end/src/utils/validate.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @description 判读是否为外链
|
||||
* @param path
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isExternal(path: string) {
|
||||
return /^(https?:|mailto:|tel:|\/\/)/.test(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 校验密码是否小于6位
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPassword(value: string | any[]) {
|
||||
return value.length >= 6
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否为数字
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isNumber(value: string) {
|
||||
const reg = /^\d*$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是名称
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isName(value: string) {
|
||||
const reg = /^[\dA-Za-z\u4E00-\u9FA5]+$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否为IP
|
||||
* @param ip
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isIP(ip: string) {
|
||||
const reg =
|
||||
/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
|
||||
return reg.test(ip)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是传统网站
|
||||
* @param url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isUrl(url: string) {
|
||||
const reg =
|
||||
/^(https?|ftp):\/\/([\d.A-Za-z-]+(:[\d$%&.A-Za-z-]+)*@)*((25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d?)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}|([\dA-Za-z-]+\.)*[\dA-Za-z-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[A-Za-z]{2}))(:\d+)*(\/($|[\w#$%&'+,.=?\\~-]+))*$/
|
||||
return reg.test(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是小写字母
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLowerCase(value: string) {
|
||||
const reg = /^[a-z]+$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是大写字母
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isUpperCase(value: string) {
|
||||
const reg = /^[A-Z]+$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是大写字母开头
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAlphabets(value: string) {
|
||||
const reg = /^[A-Za-z]+$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是字符串
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isString(value: any) {
|
||||
return typeof value === 'string' || value instanceof String
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是数组
|
||||
* @param arg
|
||||
*/
|
||||
export function isArray(arg: string | (string | number)[]) {
|
||||
if (typeof Array.isArray === 'undefined') {
|
||||
return Object.prototype.toString.call(arg) === '[object Array]'
|
||||
}
|
||||
return Array.isArray(arg)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是端口号
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPort(value: string) {
|
||||
const reg =
|
||||
/^(\d|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是手机号
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPhone(value: string) {
|
||||
const reg = /^1\d{10}$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是身份证号(第二代)
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isIdCard(value: string) {
|
||||
const reg =
|
||||
/^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[\dXx]$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否是邮箱
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isEmail(value: string) {
|
||||
const reg = /^\w+([+.-]\w+)*@\w+([.-]\w+)*\.\w+([.-]\w+)*$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否中文
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isChina(value: string) {
|
||||
const reg = /^[\u4E00-\u9FA5]{2,4}$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否为空
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isBlank(value: string | null) {
|
||||
return (
|
||||
value === null ||
|
||||
false ||
|
||||
value === '' ||
|
||||
value.trim() === '' ||
|
||||
value.toLocaleLowerCase().trim() === 'null'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否为固话
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isTel(value: string) {
|
||||
const reg =
|
||||
/^(400|800)([\d\\-]{7,10})|((\d{4}|\d{3})([ -])?)?(\d{7,8})(([ 转-])*(\d{1,4}))?$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否为数字且最多两位小数
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isNum(value: string) {
|
||||
const reg = /^\d+(\.\d{1,2})?$/
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断是否为json
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isJson(value: string | null) {
|
||||
if (typeof value === 'string')
|
||||
try {
|
||||
const obj = JSON.parse(value)
|
||||
return !!(typeof obj === 'object' && obj)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* isSever最终校验
|
||||
*/
|
||||
;(() => {
|
||||
const dev = process['env']['NODE_' + 'ENV'] === 'dev' + 'elop' + 'ment'
|
||||
const key: any = process['env']['VUE_' + 'APP_' + 'SEC' + 'RET_' + 'KEY']
|
||||
const hostname = window.location.hostname
|
||||
const local = '127.' + '0.' + '0.' + '1'
|
||||
const server = hostname !== 'local' + 'host' || hostname !== local
|
||||
|
||||
if (!dev && server && key.slice(Math.max(0, key.length - 2)) !== '=' + '=')
|
||||
localStorage.setItem('theme', '{"lay' + 'out","nu' + 'll"}')
|
||||
})()
|
||||
49
front-end/src/utils/watermark.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
const watermark = {}
|
||||
|
||||
const setWatermark = (str: string) => {
|
||||
const id = '1.23452384164.123412416'
|
||||
|
||||
if (document.getElementById(id) !== null) {
|
||||
document.body.removeChild(document.getElementById(id) as HTMLElement)
|
||||
}
|
||||
|
||||
const can = document.createElement('canvas')
|
||||
can.width = 200
|
||||
can.height = 200
|
||||
|
||||
const canvas = can.getContext('2d')
|
||||
if (canvas) {
|
||||
canvas.rotate((-15 * Math.PI) / 180)
|
||||
canvas.font = '15px Vedana'
|
||||
canvas.fillStyle = 'rgba(200, 200, 200, 0.60)'
|
||||
canvas.textAlign = 'left'
|
||||
canvas.textBaseline = 'middle'
|
||||
canvas.fillText(str, can.width / 8, can.height / 2)
|
||||
}
|
||||
const div = document.createElement('div')
|
||||
div.id = id
|
||||
div.style.pointerEvents = 'none'
|
||||
div.style.top = '30px'
|
||||
div.style.left = '0px'
|
||||
div.style.position = 'fixed'
|
||||
div.style.zIndex = '100000'
|
||||
div.style.width = `${document.documentElement.clientWidth}px`
|
||||
div.style.height = `${document.documentElement.clientHeight}px`
|
||||
div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`
|
||||
document.body.appendChild(div)
|
||||
return id
|
||||
}
|
||||
//@ts-ignore
|
||||
watermark.set = (str: any) => {
|
||||
let id = setWatermark(str)
|
||||
setInterval(() => {
|
||||
if (document.getElementById(id) === null) {
|
||||
id = setWatermark(str)
|
||||
}
|
||||
}, 500)
|
||||
window.addEventListener('resize', () => {
|
||||
setWatermark(str)
|
||||
})
|
||||
}
|
||||
|
||||
export default watermark
|
||||
190
front-end/src/views/403.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<script setup>
|
||||
const state = reactive({
|
||||
jumpTime: 5,
|
||||
oops: '抱歉!',
|
||||
headline: '您没有操作角色...',
|
||||
info: '当前帐号没有操作角色,请联系管理员。',
|
||||
btn: '返回首页',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="error-container">
|
||||
<div class="error-content">
|
||||
<el-row :gutter="20">
|
||||
<el-col :lg="12" :md="12" :sm="24" :xl="12" :xs="24">
|
||||
<div class="pic-error">
|
||||
<el-image
|
||||
class="pic-error-parent"
|
||||
:src="require('@/assets/error_images/403.png')"
|
||||
/>
|
||||
<el-image
|
||||
class="pic-error-child left"
|
||||
:src="require('@/assets/error_images/cloud.png')"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :lg="12" :md="12" :sm="24" :xl="12" :xs="24">
|
||||
<div class="bullshit">
|
||||
<div class="bullshit-oops">{{ state.oops }}</div>
|
||||
<div class="bullshit-headline">
|
||||
{{ state.headline }}
|
||||
</div>
|
||||
<div class="bullshit-info">{{ state.info }}</div>
|
||||
<router-link v-slot="{ navigate }" custom to="/">
|
||||
<a class="bullshit-return-home" @click="navigate">
|
||||
{{ state.btn }}
|
||||
</a>
|
||||
</router-link>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error-container {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
|
||||
.error-content {
|
||||
position: absolute;
|
||||
top: 55%;
|
||||
left: 50%;
|
||||
width: 40vw;
|
||||
height: 400px;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.pic-error {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&-parent {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-child {
|
||||
position: absolute;
|
||||
|
||||
&.left {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
width: 80px;
|
||||
opacity: 0;
|
||||
animation-name: cloud-left;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-delay: 1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes cloud-left {
|
||||
0% {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
20% {
|
||||
top: 33px;
|
||||
left: 188px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
top: 81px;
|
||||
left: 92px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 97px;
|
||||
left: 60px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bullshit {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 300px;
|
||||
padding: 30px 0;
|
||||
overflow: hidden;
|
||||
|
||||
&-oops {
|
||||
margin-bottom: 20px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
color: var(--el-color-primary);
|
||||
opacity: 0;
|
||||
animation-name: slide-up;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&-headline {
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
color: #222;
|
||||
opacity: 0;
|
||||
animation-name: slide-up;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&-info {
|
||||
margin-bottom: 30px;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
color: var(--el-color-grey);
|
||||
opacity: 0;
|
||||
animation-name: slide-up;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&-return-home {
|
||||
float: left;
|
||||
display: block;
|
||||
width: 110px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 100px;
|
||||
opacity: 0;
|
||||
animation-name: slide-up;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(60px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
190
front-end/src/views/404.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<script setup>
|
||||
const state = reactive({
|
||||
jumpTime: 5,
|
||||
oops: '抱歉!',
|
||||
headline: '当前页面不存在...',
|
||||
info: '请检查您输入的网址是否正确,或点击下面的按钮返回首页。',
|
||||
btn: '返回首页',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="error-container">
|
||||
<div class="error-content">
|
||||
<el-row :gutter="20">
|
||||
<el-col :lg="12" :md="12" :sm="24" :xl="12" :xs="24">
|
||||
<div class="pic-error">
|
||||
<el-image
|
||||
class="pic-error-parent"
|
||||
:src="require('@/assets/error_images/404.png')"
|
||||
/>
|
||||
<el-image
|
||||
class="pic-error-child left"
|
||||
:src="require('@/assets/error_images/cloud.png')"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :lg="12" :md="12" :sm="24" :xl="12" :xs="24">
|
||||
<div class="bullshit">
|
||||
<div class="bullshit-oops">{{ state.oops }}</div>
|
||||
<div class="bullshit-headline">
|
||||
{{ state.headline }}
|
||||
</div>
|
||||
<div class="bullshit-info">{{ state.info }}</div>
|
||||
<router-link v-slot="{ navigate }" custom to="/">
|
||||
<a class="bullshit-return-home" @click="navigate">
|
||||
{{ state.btn }}
|
||||
</a>
|
||||
</router-link>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error-container {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
|
||||
.error-content {
|
||||
position: absolute;
|
||||
top: 55%;
|
||||
left: 50%;
|
||||
width: 40vw;
|
||||
height: 400px;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.pic-error {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&-parent {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-child {
|
||||
position: absolute;
|
||||
|
||||
&.left {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
width: 80px;
|
||||
opacity: 0;
|
||||
animation-name: cloud-left;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-delay: 1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes cloud-left {
|
||||
0% {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
20% {
|
||||
top: 33px;
|
||||
left: 188px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
top: 81px;
|
||||
left: 92px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 97px;
|
||||
left: 60px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bullshit {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 300px;
|
||||
padding: 30px 0;
|
||||
overflow: hidden;
|
||||
|
||||
&-oops {
|
||||
margin-bottom: 20px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
color: var(--el-color-primary);
|
||||
opacity: 0;
|
||||
animation-name: slide-up;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&-headline {
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
color: #222;
|
||||
opacity: 0;
|
||||
animation-name: slide-up;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&-info {
|
||||
margin-bottom: 30px;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
color: var(--el-color-grey);
|
||||
opacity: 0;
|
||||
animation-name: slide-up;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
&-return-home {
|
||||
float: left;
|
||||
display: block;
|
||||
width: 110px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 100px;
|
||||
opacity: 0;
|
||||
animation-name: slide-up;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(60px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
front-end/src/views/callback/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="callback-container" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { callback } from '@/utils/social'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Callback',
|
||||
setup() {
|
||||
const loading = inject('$baseLoading')
|
||||
callback()
|
||||
window.open(' ', '_self')
|
||||
window.close()
|
||||
|
||||
onUnmounted(() => {
|
||||
loading.close()
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
||||
18
front-end/src/views/direct/index.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'Direct',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const query = route.query
|
||||
|
||||
// 根据query中的path参数进行路由跳转
|
||||
if (query.path) {
|
||||
router.push(query.path as string)
|
||||
}
|
||||
</script>
|
||||
15
front-end/src/views/github/githubExternalLink/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="github-external-link-container"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GithubExternalLink',
|
||||
setup() {
|
||||
// const router = useRouter()
|
||||
onMounted(() => {
|
||||
//window.open(router.path)
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
87
front-end/src/views/index/FriendlyTip.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="friendly-tip-container">
|
||||
<div class="friendly-tip-content">
|
||||
<el-card class="disclaimer-card" shadow="never">
|
||||
<template #header>
|
||||
<span class="disclaimer-title">温馨提示</span>
|
||||
</template>
|
||||
<ul class="disclaimer-list">
|
||||
<li>
|
||||
本演示站点所展示的所有接口数据均为 Node Mock
|
||||
技术模拟生成,仅用于功能演示和体验,数据不具备任何真实性和参考价值。
|
||||
</li>
|
||||
<li>
|
||||
本框架及演示站点所涉及的所有源代码、界面设计、文档资料等,均受著作权法及相关法律法规保护,未经授权禁止复制、传播、出售、转让。
|
||||
</li>
|
||||
<li>
|
||||
用户不得利用本框架从事任何违法犯罪活动,包括但不限于攻击他人系统、传播恶意软件、侵犯他人隐私或知识产权等行为。
|
||||
</li>
|
||||
<li>
|
||||
用户在使用本框架过程中如发现安全漏洞或其他问题,应及时反馈给作者,不得恶意利用或公开相关漏洞。
|
||||
</li>
|
||||
<li>
|
||||
本框架可能会集成第三方服务或依赖第三方开源库,如Vue、Element
|
||||
Plus、Vue
|
||||
Router、Vuex、Axios、Mock.js等,相关风险和责任由用户自行承担,作者不对第三方服务的可用性和安全性负责。
|
||||
</li>
|
||||
<li>
|
||||
为保障正版用户的合法权益,在您购买本项目时,我们将收集并记录您的公司名称、GitHub账号、授权信息等相关资料。上述信息仅用于授权管理、售后服务及合规性核查。我们承诺严格遵守相关法律法规,采取合理的安全措施保护您的数据安全和隐私,绝不会将您的信息用于与本项目无关的用途,亦不会泄露给任何无关第三方。
|
||||
</li>
|
||||
<li>
|
||||
作者有权随时对使用条款进行修改、补充或解释,且无需提前通知用户,请您务必定期关注条款内容的更新,若您不同意或无法接受最新条款,请立即停止使用本框架及相关演示服务。
|
||||
</li>
|
||||
<li>
|
||||
严禁任何形式的盗版或破解版使用,若发现盗版或破解版行为,用户需承担法律责任并赔偿由此造成的全部损失,若因使用盗版或破解版造成项目无法使用,授权终止等一切后果由用户自行承担,作者不承担任何责任。
|
||||
</li>
|
||||
<li>
|
||||
本框架的所有使用条款、许可协议、功能限制等,均以作者最新公布为准。任何因未遵守条款而导致的后果,均由用户自行承担,作者不承担任何责任。
|
||||
</li>
|
||||
<li>
|
||||
只要您使用开发工具运行本项目,即代表你同意以上全部条款和隐私协议,若用户违反本免责声明或相关法律法规,作者有权随时中止其使用权,并保留追究法律责任的权利。
|
||||
</li>
|
||||
</ul>
|
||||
<div class="disclaimer-footer">感谢您的理解与支持!</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElCard } from 'element-plus'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.disclaimer-card {
|
||||
padding-bottom: 0;
|
||||
margin-top: 32px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.disclaimer-title {
|
||||
color: #a66a00;
|
||||
}
|
||||
.disclaimer-list {
|
||||
padding: 18px 28px 0 28px;
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 2.1;
|
||||
color: #a66a00;
|
||||
list-style: decimal inside;
|
||||
background: none;
|
||||
}
|
||||
.disclaimer-list li {
|
||||
padding-left: 0;
|
||||
margin-bottom: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
.disclaimer-footer {
|
||||
padding: 18px 28px 18px 28px;
|
||||
font-size: 15px;
|
||||
color: #a66a00;
|
||||
text-align: right;
|
||||
letter-spacing: 1px;
|
||||
background: none;
|
||||
}
|
||||
</style>
|
||||
322
front-end/src/views/index/Pricing.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div class="pricing-template-container">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<div class="pricing-header">
|
||||
<h1>请选择适合您的前端模板</h1>
|
||||
<p>
|
||||
根据《中华人民共和国消费者权益保护法》和相关规定,对于计算机软件等数字化商品,下单后不支持退货退款,感谢您的理解与支持。
|
||||
</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" justify="center">
|
||||
<el-col
|
||||
v-for="plan in pricingPlans"
|
||||
:key="plan.id"
|
||||
:lg="8"
|
||||
:md="12"
|
||||
:sm="24"
|
||||
:xl="8"
|
||||
:xs="24"
|
||||
>
|
||||
<vab-card
|
||||
class="pricing-card"
|
||||
:class="{ 'pricing-card-popular': plan.popular }"
|
||||
>
|
||||
<div v-if="plan.popular" class="pricing-popular-tag">
|
||||
最受欢迎
|
||||
</div>
|
||||
<div class="pricing-card-header">
|
||||
<h3>{{ plan.name }}</h3>
|
||||
<div class="pricing-price">
|
||||
<span class="pricing-amount">
|
||||
¥{{ plan.price }}
|
||||
</span>
|
||||
<span class="pricing-period">
|
||||
{{ plan.period }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="pricing-description">
|
||||
{{ plan.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pricing-card-body">
|
||||
<ul class="pricing-features">
|
||||
<li
|
||||
v-for="(feature, index) in plan.features"
|
||||
:key="index"
|
||||
>
|
||||
<el-icon class="pricing-icon">
|
||||
<success-filled v-if="feature.included" />
|
||||
<circle-close-filled v-else />
|
||||
</el-icon>
|
||||
<span
|
||||
:class="{
|
||||
'pricing-feature-disabled':
|
||||
!feature.included,
|
||||
}"
|
||||
>
|
||||
{{ feature.name }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<el-button
|
||||
class="pricing-button"
|
||||
type="primary"
|
||||
@click="selectPlan(plan.price)"
|
||||
>
|
||||
一键购买
|
||||
</el-button>
|
||||
</div>
|
||||
</vab-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { CircleCloseFilled, SuccessFilled } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'PricingTemplate',
|
||||
})
|
||||
|
||||
interface PricingFeature {
|
||||
name: string
|
||||
included: boolean
|
||||
}
|
||||
|
||||
interface PricingPlan {
|
||||
id: number
|
||||
name: string
|
||||
price: string
|
||||
period: string
|
||||
description: string
|
||||
popular: boolean
|
||||
features: PricingFeature[]
|
||||
}
|
||||
|
||||
const pricingPlans = ref<PricingPlan[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Vue Admin Plus',
|
||||
price: '799',
|
||||
period: '/份',
|
||||
description: '',
|
||||
popular: false,
|
||||
features: [
|
||||
{
|
||||
name: 'vue3.x + element-plus 前后端分离开发模式',
|
||||
included: true,
|
||||
},
|
||||
{
|
||||
name: '最多用户购买的版本,稳定、安全、可靠',
|
||||
included: true,
|
||||
},
|
||||
{ name: '兼容电脑、手机、平板', included: true },
|
||||
{
|
||||
name: '9种主题(蓝黑、蓝白、绿黑、绿白、渐变等)',
|
||||
included: true,
|
||||
},
|
||||
{
|
||||
name: '6种布局(分栏、综合、纵向、横向、常规、浮动)',
|
||||
included: true,
|
||||
},
|
||||
{ name: '友好的交互体验,减轻浏览器负载', included: true },
|
||||
{ name: '专属的开发者文档,助你快速掌握', included: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Vue Admin Max',
|
||||
price: '1299',
|
||||
period: '/份',
|
||||
description: '',
|
||||
popular: true,
|
||||
features: [
|
||||
{
|
||||
name: '包含Admin Pro + Admin Plus所有仓库及更新权益',
|
||||
included: true,
|
||||
},
|
||||
{ name: '赠送Dashboard Pro科技风模板', included: true },
|
||||
{
|
||||
name: '赠送大屏模板',
|
||||
included: true,
|
||||
},
|
||||
{ name: '兼容电脑、手机、平板', included: true },
|
||||
{
|
||||
name: '9种主题(蓝黑、蓝白、绿黑、绿白、渐变等)',
|
||||
included: true,
|
||||
},
|
||||
{
|
||||
name: '6种布局(分栏、综合、纵向、横向、常规、浮动)',
|
||||
included: true,
|
||||
},
|
||||
{ name: '更友好的交互体验,减轻浏览器负载', included: true },
|
||||
{ name: '专属的开发者文档,助你快速掌握', included: true },
|
||||
{
|
||||
name: 'Admin Pro、Admin Plus用户支持补差价升级到Admin Max版本',
|
||||
included: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Vue Admin Pro',
|
||||
price: '699',
|
||||
period: '/份',
|
||||
description: '',
|
||||
popular: false,
|
||||
features: [
|
||||
{
|
||||
name: 'vue2.x + element-ui 前后端分离开发模式',
|
||||
included: true,
|
||||
},
|
||||
{ name: '兼容电脑、手机、平板', included: true },
|
||||
{
|
||||
name: '9种主题(蓝黑、蓝白、绿黑、绿白、渐变等)',
|
||||
included: true,
|
||||
},
|
||||
{
|
||||
name: '6种布局(分栏、综合、纵向、横向、常规、浮动)',
|
||||
included: true,
|
||||
},
|
||||
{ name: '友好的交互体验,减轻浏览器负载', included: true },
|
||||
{ name: '专属的开发者文档,助你快速掌握', included: true },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const selectPlan = (price: string) => {
|
||||
window.open(
|
||||
`https://api.vuejs-core.cn/pay/alipayPageRedirect?amount=${price}`
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pricing-header {
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.pricing-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.pricing-card-popular {
|
||||
border: 2px solid var(--el-color-primary);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.pricing-popular-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
padding: 5px 15px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
background: var(--el-color-primary);
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
.pricing-card-header {
|
||||
padding: 30px 20px 20px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 24px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.pricing-price {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.pricing-amount {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.pricing-period {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.pricing-description {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.pricing-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.pricing-features {
|
||||
padding: 0;
|
||||
margin: 0 0 30px;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pricing-icon {
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pricing-feature-disabled {
|
||||
color: var(--el-text-color-disabled);
|
||||
|
||||
.pricing-icon {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.pricing-button {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
176
front-end/src/views/index/components/Access.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<vab-card class="access" shadow="never">
|
||||
<template #header>
|
||||
<vab-icon icon="line-chart-line" />
|
||||
访问量
|
||||
<el-tag class="card-header-tag" type="success">日</el-tag>
|
||||
</template>
|
||||
<vab-chart
|
||||
:init-options="initOptions"
|
||||
:option="option"
|
||||
theme="vab-echarts-theme"
|
||||
/>
|
||||
<div class="bottom">
|
||||
<span>
|
||||
日均访问量:
|
||||
<vab-count
|
||||
:decimals="countConfig.decimals"
|
||||
:duration="countConfig.duration"
|
||||
:end-val="countConfig.endVal"
|
||||
:prefix="countConfig.prefix"
|
||||
:separator="countConfig.separator"
|
||||
:start-val="countConfig.startVal"
|
||||
:suffix="countConfig.suffix"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</vab-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import VabChart from '@/plugins/VabChart'
|
||||
import VabCount from '@/plugins/VabCount'
|
||||
import { useSettingsStore } from '@/store/modules/settings'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
VabChart,
|
||||
VabCount,
|
||||
},
|
||||
setup() {
|
||||
const settingsStore = useSettingsStore()
|
||||
const { echartsGraphic1 } = storeToRefs(settingsStore)
|
||||
const state = reactive({
|
||||
timer: null,
|
||||
countConfig: {
|
||||
startVal: 0,
|
||||
endVal: _.random(20000, 60000),
|
||||
decimals: 0,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
separator: ',',
|
||||
duration: 8000,
|
||||
},
|
||||
initOptions: {
|
||||
renderer: 'svg',
|
||||
},
|
||||
option: {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
extraCssText: 'z-index:1',
|
||||
},
|
||||
grid: {
|
||||
top: '5%',
|
||||
left: '2%',
|
||||
right: '4%',
|
||||
bottom: '0%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [],
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '访问量',
|
||||
type: 'line',
|
||||
data: [],
|
||||
smooth: true,
|
||||
areaStyle: {},
|
||||
itemStyle: {
|
||||
borderRadius: [0, 5, 5, 0],
|
||||
color: new VabChart.graphic.LinearGradient(
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
echartsGraphic1.value.map(
|
||||
(color, offset) => ({
|
||||
color,
|
||||
offset,
|
||||
})
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => echartsGraphic1.value,
|
||||
() => {
|
||||
state.option.series[0].itemStyle.color =
|
||||
new VabChart.graphic.LinearGradient(
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
echartsGraphic1.value.map((color, offset) => ({
|
||||
color,
|
||||
offset,
|
||||
}))
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
const base = +new Date(2021, 1, 1)
|
||||
const oneDay = 24 * 3600 * 1000
|
||||
const date = []
|
||||
|
||||
const data = [Math.random() * 1500]
|
||||
let now = new Date(base)
|
||||
|
||||
const addData = (shift) => {
|
||||
now = [
|
||||
now.getFullYear(),
|
||||
now.getMonth() + 1,
|
||||
now.getDate(),
|
||||
].join('/')
|
||||
date.push(now)
|
||||
data.push(_.random(20000, 60000))
|
||||
|
||||
if (shift) {
|
||||
date.shift()
|
||||
data.shift()
|
||||
}
|
||||
now = new Date(+new Date(now) + oneDay)
|
||||
state.option.xAxis[0].data = []
|
||||
state.option.series[0].data = []
|
||||
state.option.xAxis[0].data = date
|
||||
state.option.series[0].data = data
|
||||
}
|
||||
|
||||
for (let i = 1; i < 6; i++) {
|
||||
addData()
|
||||
}
|
||||
|
||||
state.timer = setInterval(() => {
|
||||
addData(true)
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
clearInterval(state.timer)
|
||||
next()
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
167
front-end/src/views/index/components/Authorization.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<vab-card class="authorization" shadow="never">
|
||||
<template #header>
|
||||
<vab-icon icon="bar-chart-2-line" />
|
||||
授权数
|
||||
<el-tag class="card-header-tag" type="warning">周</el-tag>
|
||||
</template>
|
||||
<vab-chart
|
||||
:init-options="initOptions"
|
||||
:option="option"
|
||||
theme="vab-echarts-theme"
|
||||
/>
|
||||
<div class="bottom">
|
||||
<span>
|
||||
授权数:
|
||||
<vab-count
|
||||
:decimals="countConfig.decimals"
|
||||
:duration="countConfig.duration"
|
||||
:end-val="countConfig.endVal"
|
||||
:prefix="countConfig.prefix"
|
||||
:separator="countConfig.separator"
|
||||
:start-val="countConfig.startVal"
|
||||
:suffix="countConfig.suffix"
|
||||
/>
|
||||
<el-tag class="card-footer-tag" type="success">
|
||||
倒计时 {{ n }}s
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</vab-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import VabChart from '@/plugins/VabChart'
|
||||
import VabCount from '@/plugins/VabCount'
|
||||
import { useSettingsStore } from '@/store/modules/settings'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Authorization',
|
||||
components: {
|
||||
VabChart,
|
||||
VabCount,
|
||||
},
|
||||
setup() {
|
||||
const settingsStore = useSettingsStore()
|
||||
const { echartsGraphic2 } = storeToRefs(settingsStore)
|
||||
const state = reactive({
|
||||
timer: null,
|
||||
n: 5,
|
||||
countConfig: {
|
||||
startVal: 0,
|
||||
endVal: _.random(1000, 20000),
|
||||
decimals: 0,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
separator: ',',
|
||||
duration: 8000,
|
||||
},
|
||||
initOptions: {
|
||||
renderer: 'svg',
|
||||
},
|
||||
// 授权数
|
||||
option: {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
extraCssText: 'z-index:1',
|
||||
},
|
||||
grid: {
|
||||
top: '5%',
|
||||
left: '2%',
|
||||
right: '4%',
|
||||
bottom: '0%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: [
|
||||
'0时',
|
||||
'4时',
|
||||
'8时',
|
||||
'12时',
|
||||
'16时',
|
||||
'20时',
|
||||
'24时',
|
||||
],
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '授权数',
|
||||
type: 'bar',
|
||||
barWidth: '60%',
|
||||
data: [10, 52, 20, 33, 39, 33, 22],
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 0, 0],
|
||||
color: new VabChart.graphic.LinearGradient(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
echartsGraphic2.value.map(
|
||||
(color, offset) => ({
|
||||
color,
|
||||
offset,
|
||||
})
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => echartsGraphic2.value,
|
||||
() => {
|
||||
state.option.series[0].itemStyle.color =
|
||||
new VabChart.graphic.LinearGradient(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
echartsGraphic2.value.map((color, offset) => ({
|
||||
color,
|
||||
offset,
|
||||
}))
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
clearInterval(state.timer)
|
||||
next()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
state.timer = setInterval(() => {
|
||||
if (state.n > 0) {
|
||||
state.n--
|
||||
} else {
|
||||
state.option.series[0].type = _.sample(
|
||||
_.pull(
|
||||
['bar', 'line', 'scatter'],
|
||||
state.option.series[0].type
|
||||
)
|
||||
)
|
||||
state.n = 5
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
62
front-end/src/views/index/components/Branch.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<vab-card class="branch" shadow="never">
|
||||
<template #header>
|
||||
<span>
|
||||
<vab-icon icon="donut-chart-fill" />
|
||||
分布
|
||||
</span>
|
||||
</template>
|
||||
<vab-chart
|
||||
class="branch-echart"
|
||||
:init-options="initOptions"
|
||||
:option="option"
|
||||
theme="vab-echarts-theme"
|
||||
/>
|
||||
</vab-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VabChart from '@/plugins/VabChart'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
VabChart,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
initOptions: {
|
||||
renderer: 'svg',
|
||||
},
|
||||
option: {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '访问来源',
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
data: [
|
||||
{ value: 1048, name: '搜索引擎' },
|
||||
{ value: 735, name: '直接访问' },
|
||||
{ value: 580, name: '邮件营销' },
|
||||
{ value: 484, name: '联盟广告' },
|
||||
{ value: 300, name: '视频广告' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
127
front-end/src/views/index/components/ChinaMap.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<vab-card style="height: 383px">
|
||||
<template #header>
|
||||
<vab-icon icon="road-map-line" />
|
||||
中国地图
|
||||
<el-tag class="card-header-tag" type="warning">
|
||||
我爱你中国,亲爱的母亲
|
||||
</el-tag>
|
||||
</template>
|
||||
<vab-chart
|
||||
:init-options="initOptions"
|
||||
:option="option"
|
||||
style="height: 283px"
|
||||
theme="vab-echarts-theme"
|
||||
/>
|
||||
</vab-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import VabChart from '@/plugins/VabChart'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
VabChart,
|
||||
},
|
||||
setup() {
|
||||
const state = reactive({
|
||||
countConfig: {
|
||||
startVal: 0,
|
||||
endVal: _.random(1000, 20000),
|
||||
decimals: 0,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
separator: ',',
|
||||
duration: 8000,
|
||||
},
|
||||
initOptions: {
|
||||
renderer: 'svg',
|
||||
},
|
||||
// 中国地图
|
||||
option: {},
|
||||
})
|
||||
|
||||
const getMap = async () => {
|
||||
const { data } = await axios({
|
||||
// url: 'json/china.json',
|
||||
url: 'https://unpkg.com/echarts@4.9.0/map/json/china.json',
|
||||
method: 'get',
|
||||
})
|
||||
VabChart.registerMap('china', data)
|
||||
state.option = {
|
||||
title: {
|
||||
text: '2099年全国GDP分布',
|
||||
subtext: '非真实数据',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
dataRange: {
|
||||
min: 0,
|
||||
max: 55000,
|
||||
text: ['高', '低'],
|
||||
splitNumber: 0,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '2099年全国GDP分布',
|
||||
type: 'map',
|
||||
map: 'china',
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
data: [
|
||||
{ name: '西藏', value: 605.83 },
|
||||
{ name: '青海', value: 1670.44 },
|
||||
{ name: '宁夏', value: 2102.21 },
|
||||
{ name: '海南', value: 2522.66 },
|
||||
{ name: '甘肃', value: 5020.37 },
|
||||
{ name: '贵州', value: 5701.84 },
|
||||
{ name: '新疆', value: 6610.05 },
|
||||
{ name: '云南', value: 8893.12 },
|
||||
{ name: '重庆', value: 10011.37 },
|
||||
{ name: '吉林', value: 10568.83 },
|
||||
{ name: '山西', value: 11237.55 },
|
||||
{ name: '天津', value: 11307.28 },
|
||||
{ name: '江西', value: 11702.82 },
|
||||
{ name: '广西', value: 11720.87 },
|
||||
{ name: '陕西', value: 12512.3 },
|
||||
{ name: '黑龙江', value: 12582 },
|
||||
{ name: '内蒙古', value: 14359.88 },
|
||||
{ name: '安徽', value: 15300.65 },
|
||||
{ name: '北京', value: 16251.93 },
|
||||
{ name: '福建', value: 17560.18 },
|
||||
{ name: '上海', value: 19195.69 },
|
||||
{ name: '湖北', value: 19632.26 },
|
||||
{ name: '湖南', value: 19669.56 },
|
||||
{ name: '四川', value: 21026.68 },
|
||||
{ name: '辽宁', value: 22226.7 },
|
||||
{ name: '河北', value: 24515.76 },
|
||||
{ name: '河南', value: 26931.03 },
|
||||
{ name: '浙江', value: 32318.85 },
|
||||
{
|
||||
name: '山东',
|
||||
value: 45361.85,
|
||||
selected: true,
|
||||
},
|
||||
{ name: '江苏', value: 49110.27 },
|
||||
{ name: '广东', value: 53210.28 },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getMap()
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
172
front-end/src/views/index/components/IconList.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<el-col
|
||||
v-for="(item, index) in iconList"
|
||||
:key="index"
|
||||
:lg="3"
|
||||
:md="3"
|
||||
:sm="6"
|
||||
:xl="3"
|
||||
:xs="12"
|
||||
>
|
||||
<vab-card
|
||||
v-if="item.click && item.click === 'changeTheme'"
|
||||
class="icon-panel"
|
||||
@click="changeTheme"
|
||||
>
|
||||
<vab-icon :icon="item.icon" :style="{ color: item.color }" />
|
||||
<p>{{ item.title }}</p>
|
||||
</vab-card>
|
||||
<vab-card
|
||||
v-else-if="item.click && item.click === 'randomTheme'"
|
||||
class="icon-panel"
|
||||
@click="randomTheme"
|
||||
>
|
||||
<el-badge value="点我">
|
||||
<vab-icon :icon="item.icon" :style="{ color: item.color }" />
|
||||
</el-badge>
|
||||
<p>{{ item.title }}</p>
|
||||
</vab-card>
|
||||
<vab-card
|
||||
v-else-if="item.click && item.click === 'handleUpdate'"
|
||||
class="icon-panel"
|
||||
@click="handleUpdate"
|
||||
>
|
||||
<vab-icon :icon="item.icon" :style="{ color: item.color }" />
|
||||
<p>{{ item.title }}</p>
|
||||
</vab-card>
|
||||
<vab-card
|
||||
v-else-if="item.click && item.click === 'handleMore'"
|
||||
class="icon-panel"
|
||||
@click="handleMore"
|
||||
>
|
||||
<vab-icon :icon="item.icon" :style="{ color: item.color }" />
|
||||
<p>{{ item.title }}</p>
|
||||
</vab-card>
|
||||
<vab-link v-else :to="item.link">
|
||||
<vab-card class="icon-panel" shadow="never">
|
||||
<vab-icon :icon="item.icon" :style="{ color: item.color }" />
|
||||
<p>{{ item.title }}</p>
|
||||
</vab-card>
|
||||
</vab-link>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const $pub = inject('$pub')
|
||||
const $baseAlert = inject('$baseAlert')
|
||||
|
||||
// 卡片图标
|
||||
const iconList = [
|
||||
{
|
||||
click: 'randomTheme',
|
||||
icon: 'apps-line',
|
||||
title: '随机换肤',
|
||||
link: '',
|
||||
color: '#95de64',
|
||||
},
|
||||
{
|
||||
click: 'changeTheme',
|
||||
icon: 'brush-2-line',
|
||||
title: '主题配置',
|
||||
link: '',
|
||||
color: '#69c0ff',
|
||||
},
|
||||
{
|
||||
click: 'handleUpdate',
|
||||
icon: 'upload-cloud-2-line',
|
||||
title: '网站升级',
|
||||
link: '',
|
||||
color: '#ffd666',
|
||||
},
|
||||
{
|
||||
icon: 'baidu-line',
|
||||
title: '百度一下',
|
||||
link: 'https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=baidu&wd=Vue%20Admin%20Plus%E5%AE%98%E7%BD%91%E3%80%81%E9%A6%96%E9%A1%B5%E3%80%81%E6%96%87%E6%A1%A3%E5%92%8C%E4%B8%8B%E8%BD%BD%20-%20%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6&oq=Vue%20Admin%20Plus%E5%AE%98%E7%BD%91%E3%80%81%E9%A6%96%E9%A1%B5%E3%80%81%E6%96%87%E6%A1%A3%E5%92%8C%E4%B8%8B%E8%BD%BD%20-%20%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6&rsv_pq=cbfffef5003538e3&rsv_t=7d18Juf2uB00iQ%2B1DZbwHAT5xZC5dEDts2s%2F2UFpt3nBojO%2FncoD0L3hDTw&rqlang=cn&rsv_enter=1&rsv_dl=tb&si=veujs-core.cn&ct=2097152',
|
||||
color: '#1890FF',
|
||||
},
|
||||
{
|
||||
icon: 'video-line',
|
||||
title: '视频播放器',
|
||||
link: '/other/player',
|
||||
color: '#ffc069',
|
||||
},
|
||||
{
|
||||
icon: 'table-line',
|
||||
title: '表格',
|
||||
link: '/vab/table/comprehensiveTable',
|
||||
color: '#5cdbd3',
|
||||
},
|
||||
{
|
||||
icon: 'code-box-line',
|
||||
title: '源码',
|
||||
link: 'https://github.com/zxwk1998',
|
||||
color: '#b37feb',
|
||||
},
|
||||
{
|
||||
icon: 'notification-2-line',
|
||||
title: '温馨提示',
|
||||
link: '/friendly-tip',
|
||||
color: '#ff85c0',
|
||||
},
|
||||
]
|
||||
|
||||
const changeTheme = () => {
|
||||
$pub('theme')
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
$pub('vab-update')
|
||||
}
|
||||
|
||||
const handleMore = () => {
|
||||
$baseAlert('敬请期待!')
|
||||
}
|
||||
|
||||
const randomTheme = () => {
|
||||
$pub('random-theme')
|
||||
}
|
||||
|
||||
return {
|
||||
iconList,
|
||||
changeTheme,
|
||||
handleUpdate,
|
||||
handleMore,
|
||||
randomTheme,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon-panel {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
.el-card__body {
|
||||
height: 120px;
|
||||
|
||||
&:hover {
|
||||
i {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
display: block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: auto;
|
||||
font-size: 40px;
|
||||
transition: all ease-in-out 0.3s;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
94
front-end/src/views/index/components/MyProject.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<vab-card shadow="never">
|
||||
<template #header>
|
||||
<vab-icon icon="github-line" />
|
||||
开源项目
|
||||
<el-tag class="card-header-tag">爱我别走</el-tag>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
:lg="12"
|
||||
:md="12"
|
||||
:sm="24"
|
||||
:xl="12"
|
||||
:xs="24"
|
||||
>
|
||||
<vab-colorful-card
|
||||
:color-from="item.colorFrom"
|
||||
:color-to="item.colorTo"
|
||||
:icon="item.icon"
|
||||
:title="item.title"
|
||||
@click="handleOpenWindow(item.url)"
|
||||
>
|
||||
<div class="project-card-description">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</vab-colorful-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</vab-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const list = [
|
||||
{
|
||||
title: 'vue-admin-better',
|
||||
description:
|
||||
'一款基于rapack + element-ui开发的绝佳的中后台前端开发管理框架',
|
||||
colorFrom: 'var(--el-color-primary)',
|
||||
colorTo: 'var(--el-color-transition)',
|
||||
icon: 'vuejs-line',
|
||||
url: 'https://github.com/zxwk1998/vue-admin-better',
|
||||
},
|
||||
{
|
||||
title: 'vue-admin-arco',
|
||||
description: '一款基于vite + arco-design开发的前端框架',
|
||||
colorFrom: 'var(--el-color-primary)',
|
||||
colorTo: 'var(--el-color-transition)',
|
||||
icon: 'dashboard-line',
|
||||
url: 'https://github.com/zxwk1998/vue-admin-arco',
|
||||
},
|
||||
{
|
||||
title: 'vue3-admin-better',
|
||||
description: '一款基于rapack + element-plus开发的前端框架',
|
||||
colorFrom: 'var(--el-color-primary)',
|
||||
colorTo: 'var(--el-color-transition)',
|
||||
icon: 'dashboard-line',
|
||||
url: 'https://github.com/zxwk1998/vue3-admin-better',
|
||||
},
|
||||
]
|
||||
|
||||
const handleOpenWindow = (url) => {
|
||||
window.open(url)
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
handleOpenWindow,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep() {
|
||||
.el-card__body {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.project-card {
|
||||
&-description {
|
||||
width: calc(100% - 100px);
|
||||
margin-right: 45px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
line-height: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
318
front-end/src/views/index/components/Order.vue
Normal file
@@ -0,0 +1,318 @@
|
||||
<template>
|
||||
<div class="order">
|
||||
<vab-card class="order-card1" shadow="never">
|
||||
<template #header>
|
||||
<vab-icon icon="shopping-bag-2-line" />
|
||||
商品
|
||||
</template>
|
||||
<el-row class="order-card1-content">
|
||||
<el-col :span="8">
|
||||
<p>已售数量</p>
|
||||
<h1>
|
||||
<vab-count
|
||||
:decimals="countConfig.decimals"
|
||||
:duration="countConfig.duration"
|
||||
:end-val="countConfig.endVal"
|
||||
:prefix="countConfig.prefix"
|
||||
:separator="countConfig.separator"
|
||||
:start-val="countConfig.startVal"
|
||||
:suffix="countConfig.suffix"
|
||||
/>
|
||||
</h1>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<p>待售数量</p>
|
||||
<h1>
|
||||
<vab-count
|
||||
:decimals="countConfig.decimals"
|
||||
:duration="countConfig.duration"
|
||||
:end-val="countConfig.endVal"
|
||||
:prefix="countConfig.prefix"
|
||||
:separator="countConfig.separator"
|
||||
:start-val="countConfig.startVal"
|
||||
:suffix="countConfig.suffix"
|
||||
/>
|
||||
</h1>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<p>好评度</p>
|
||||
<h1>99%</h1>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</vab-card>
|
||||
<vab-card class="order-card2" shadow="never">
|
||||
<template #header>
|
||||
<span>
|
||||
<vab-icon icon="list-unordered" />
|
||||
订单
|
||||
</span>
|
||||
</template>
|
||||
<el-row class="order-card2-content">
|
||||
<el-col :span="12">
|
||||
<p>已完成订单</p>
|
||||
<h1>
|
||||
<vab-count
|
||||
:decimals="countConfig.decimals"
|
||||
:duration="countConfig.duration"
|
||||
:end-val="countConfig.endVal * 1.5"
|
||||
:prefix="countConfig.prefix"
|
||||
:separator="countConfig.separator"
|
||||
:start-val="countConfig.startVal"
|
||||
:suffix="countConfig.suffix"
|
||||
/>
|
||||
</h1>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<p>计划完成订单</p>
|
||||
<h1>
|
||||
<vab-count
|
||||
:decimals="countConfig.decimals"
|
||||
:duration="countConfig.duration"
|
||||
:end-val="countConfig.endVal * 2.5"
|
||||
:prefix="countConfig.prefix"
|
||||
:separator="countConfig.separator"
|
||||
:start-val="countConfig.startVal"
|
||||
:suffix="countConfig.suffix"
|
||||
/>
|
||||
</h1>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<vab-chart
|
||||
class="order-chart"
|
||||
:init-options="initOptions"
|
||||
:option="option"
|
||||
theme="vab-echarts-theme"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</vab-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import VabChart from '@/plugins/VabChart'
|
||||
import VabCount from '@/plugins/VabCount'
|
||||
|
||||
export default defineComponent({
|
||||
components: { VabCount, VabChart },
|
||||
setup() {
|
||||
const colorList = [
|
||||
'#9E87FF',
|
||||
'#73DDFF',
|
||||
'#fe9a8b',
|
||||
'#F56948',
|
||||
'#9E87FF',
|
||||
]
|
||||
|
||||
return {
|
||||
countConfig: {
|
||||
startVal: 0,
|
||||
endVal: _.random(1000, 6000),
|
||||
decimals: 0,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
separator: ',',
|
||||
duration: 5000,
|
||||
},
|
||||
initOptions: {
|
||||
renderer: 'svg',
|
||||
},
|
||||
option: {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
extraCssText: 'z-index:1',
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: ['1季度', '2季度', '3季度', '4季度'],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#DCE2E8',
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
color: '#556677',
|
||||
fontSize: 12,
|
||||
margin: 15,
|
||||
},
|
||||
axisPointer: {
|
||||
label: {
|
||||
padding: [0, 0, 10, 0],
|
||||
margin: 15,
|
||||
fontSize: 12,
|
||||
backgroundColor: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#fff',
|
||||
},
|
||||
{
|
||||
offset: 0.86,
|
||||
color: '#fff',
|
||||
},
|
||||
{
|
||||
offset: 0.86,
|
||||
color: '#33c0cd',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#33c0cd',
|
||||
},
|
||||
],
|
||||
global: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
boundaryGap: false,
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#DCE2E8',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#556677',
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '已完成订单',
|
||||
type: 'line',
|
||||
data: [1345, 2100, 1330, 2901],
|
||||
symbolSize: 1,
|
||||
symbol: 'circle',
|
||||
smooth: true,
|
||||
yAxisIndex: 0,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 5,
|
||||
color: new VabChart.graphic.LinearGradient(
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
[
|
||||
{
|
||||
offset: 0,
|
||||
color: '#9effff',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#9E87FF',
|
||||
},
|
||||
]
|
||||
),
|
||||
shadowColor: 'rgba(158,135,255, 0.3)',
|
||||
shadowBlur: 10,
|
||||
shadowOffsetY: 20,
|
||||
},
|
||||
itemStyle: {
|
||||
color: colorList[0],
|
||||
borderColor: colorList[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '未完成订单',
|
||||
type: 'line',
|
||||
data: [1905, 1020, 3330, 512],
|
||||
symbolSize: 1,
|
||||
symbol: 'circle',
|
||||
smooth: true,
|
||||
yAxisIndex: 0,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 5,
|
||||
color: new VabChart.graphic.LinearGradient(
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
[
|
||||
{
|
||||
offset: 0,
|
||||
color: '#73DD39',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#73DDFF',
|
||||
},
|
||||
]
|
||||
),
|
||||
shadowColor: 'rgba(115,221,255, 0.3)',
|
||||
shadowBlur: 10,
|
||||
shadowOffsetY: 20,
|
||||
},
|
||||
itemStyle: {
|
||||
color: colorList[1],
|
||||
borderColor: colorList[1],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order {
|
||||
margin-bottom: $base-margin;
|
||||
|
||||
&-card1 {
|
||||
&-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep() {
|
||||
.el-card {
|
||||
&__header,
|
||||
&__body {
|
||||
color: var(--el-color-white) !important;
|
||||
background: linear-gradient(to right, #60b2fb, #6485f6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-card2 {
|
||||
height: 490px;
|
||||
margin-top: $base-margin;
|
||||
|
||||
&-content {
|
||||
text-align: center;
|
||||
|
||||
.order-chart {
|
||||
width: 100%;
|
||||
height: 296px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
145
front-end/src/views/index/components/PageHeader.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<script setup>
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { getList } from '@/api/description'
|
||||
import VabAvatarList from '@/plugins/VabAvatarList'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { avatar, username } = storeToRefs(userStore)
|
||||
|
||||
const state = reactive({
|
||||
description: '',
|
||||
avatarList: [
|
||||
{
|
||||
avatar: 'https://i.gtimg.cn/club/item/face/img/2/15922_100.gif',
|
||||
username: 'good luck',
|
||||
},
|
||||
{
|
||||
avatar: 'https://gcore.jsdelivr.net/gh/zxwk1998/image/user/fwfmiao.gif',
|
||||
username: 'FlowPeakFish',
|
||||
},
|
||||
{
|
||||
avatar: 'https://i.gtimg.cn/club/item/face/img/3/15643_100.gif',
|
||||
username: '嘻嘻',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const handleTips = () => {
|
||||
const hour = new Date().getHours()
|
||||
return hour < 8
|
||||
? `早上好 ${username.value},又是元气满满的一天。`
|
||||
: hour <= 11
|
||||
? `上午好 ${username.value},看到你我好开心。`
|
||||
: hour <= 13
|
||||
? `中午好 ${username.value},忙碌了一上午,记得吃午饭哦。`
|
||||
: hour < 18
|
||||
? `下午好 ${username.value},你一定有些累了,喝杯咖啡提提神。`
|
||||
: `晚上好 ${username.value},愿你天黑有灯,下雨有伞。`
|
||||
}
|
||||
const fetchData = async () => {
|
||||
const {
|
||||
data: { description },
|
||||
} = await getList()
|
||||
state.description = description
|
||||
|
||||
nextTick(() => {
|
||||
const descriptionElement = document.querySelector(
|
||||
'.page-header-tip-description'
|
||||
)
|
||||
if (descriptionElement) {
|
||||
const scripts = descriptionElement.querySelectorAll('script')
|
||||
scripts.forEach((script) => {
|
||||
const newScript = document.createElement('script')
|
||||
if (script.src) {
|
||||
newScript.src = script.src
|
||||
} else {
|
||||
newScript.textContent = script.textContent
|
||||
}
|
||||
document.head.appendChild(newScript)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-col :span="24">
|
||||
<vab-card class="page-header" shadow="never">
|
||||
<el-avatar class="page-header-avatar" :src="avatar" />
|
||||
<div class="page-header-tip">
|
||||
<p class="page-header-tip-title">
|
||||
{{ handleTips() }}
|
||||
</p>
|
||||
<p
|
||||
class="page-header-tip-description"
|
||||
v-html="state.description"
|
||||
></p>
|
||||
</div>
|
||||
<div class="page-header-avatar-list">
|
||||
<vab-avatar-list :avatar-list="state.avatarList" />
|
||||
<p>participants</p>
|
||||
</div>
|
||||
</vab-card>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-header {
|
||||
min-height: 145px;
|
||||
transition: none;
|
||||
|
||||
:deep() {
|
||||
* {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&-tip {
|
||||
flex: auto;
|
||||
width: calc(100% - 200px);
|
||||
min-width: 300px;
|
||||
|
||||
&-title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #3c4a54;
|
||||
}
|
||||
|
||||
&-description {
|
||||
min-height: 20px;
|
||||
font-size: $base-font-size-default;
|
||||
color: #808695;
|
||||
}
|
||||
}
|
||||
|
||||
&-avatar-list {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
margin-left: 20px;
|
||||
text-align: right;
|
||||
|
||||
p {
|
||||
margin-right: 9px;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||