feat: 初始化项目结构并添加基础配置

添加前后端基础项目结构,包括.gitignore、package.json等配置文件
实现前端基础功能模块,包括路由、状态管理、API请求封装等
添加前端UI组件库和样式体系
配置开发环境Mock系统和构建工具链
This commit is contained in:
2026-03-18 14:03:35 +08:00
parent fc53f5620e
commit 9a387f3eec
504 changed files with 80629 additions and 0 deletions

34
front-end/src/App.vue Normal file
View 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
View 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,
})
}

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getIconList(params?: any) {
return request({
url: '/defaultIcon/getList',
method: 'get',
params,
})
}

View 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,
})
}

View 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,
})
}

View 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,
})
}

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getList(params?: any) {
return request({
url: '/goods/getList',
method: 'get',
params,
})
}

View 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,
})
}

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function getList() {
return request({
url: '/notice/getList',
method: 'get',
})
}

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function getPublicKey() {
return request({
url: '/publicKey',
method: 'get',
})
}

View 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',
})
}

View 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,
})
}

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function getList() {
return request({
url: '/router/getList',
method: 'get',
})
}

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function getList() {
return request({
url: '/search/getList',
method: 'get',
})
}

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getList(params?: any) {
return request({
url: '/systemLog/getList',
method: 'get',
params,
})
}

View 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,
})
}

View 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
View 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,
})
}

View 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,
})
}

View 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,
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

View 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,
}

View 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,
}

View 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',
}

View 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,
}

View 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,
}

View 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'

View 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"
}
}

View File

@@ -0,0 +1,2 @@
const icons = require.context('.', true, /\.svg$/)
icons.keys().map(icons)

View 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

View 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
View 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'))

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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"
}
}
}

View 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>

View 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 }

View 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>

View 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>

View 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>

View 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

View File

@@ -0,0 +1 @@
export { default } from 'vue-qr/src/packages/vue-qr.vue'

View 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>

View 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>

View 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)
},
})
}

File diff suppressed because it is too large Load Diff

View 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()
})
}

View File

@@ -0,0 +1,11 @@
/**
* @description 导入所有 pinia 模块,请勿修改。
*/
const pinia = createPinia()
export function setupStore(app: any) {
app.use(pinia)
}
export default pinia

View 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
},
},
})

View 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)
},
},
})

View 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
},
},
})

View 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 })
},
},
})

View 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)
},
},
})

View 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()
},
},
})

View 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)
})
}

View 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)
}

View 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}`
)
}

View 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
}

View 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)
}

View 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
}

View 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

View 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
})
}

View 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))
}

View 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()
})()

View 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)
}
}

View 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"}')
})()

View 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
View 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
View 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>

View 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>

View 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>

View 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>

View 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>
本框架可能会集成第三方服务或依赖第三方开源库如VueElement
PlusVue
RouterVuexAxiosMock.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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

Some files were not shown because too many files have changed in this diff Show More