1
Some checks failed
Deploy to GitHub Pages / Deploy to GitHub Pages (push) Has been cancelled

This commit is contained in:
2026-03-10 10:13:01 +08:00
parent 810cbb38c2
commit 9a5a22c11a
213 changed files with 33677 additions and 0 deletions

12
src/App.vue Normal file
View File

@ -0,0 +1,12 @@
<template>
<div id="vue-admin-better">
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
mounted() {},
}
</script>

8
src/api/ad.js Normal file
View File

@ -0,0 +1,8 @@
import request from '@/utils/request'
export function getList() {
return request({
url: 'https://api.vuejs-core.cn/getAd',
method: 'get',
})
}

9
src/api/colorfulIcon.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getIconList(data) {
return request({
url: '/colorfulIcon/getList',
method: 'post',
data,
})
}

19
src/api/github.js Normal file
View File

@ -0,0 +1,19 @@
import request from 'axios'
export function getRepos(params) {
return request({
url: 'https://api.github.com/repos/zxwk1998/vue-admin-better',
method: 'get',
params,
timeout: 10000,
})
}
export function getStargazers(params) {
return request({
url: 'https://api.github.com/repos/zxwk1998/vue-admin-better/stargazers',
method: 'get',
params,
timeout: 10000,
})
}

9
src/api/goodsList.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getList(data) {
return request({
url: '/goodsList/getList',
method: 'post',
data,
})
}

9
src/api/icon.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getIconList(data) {
return request({
url: '/icon/getList',
method: 'post',
data,
})
}

8
src/api/markdown.js Normal file
View File

@ -0,0 +1,8 @@
import request from 'axios'
export function getList() {
return request({
url: 'https://gcore.jsdelivr.net/gh/prettier/prettier@master/docs/options.md',
method: 'get',
})
}

25
src/api/menuManagement.js Normal file
View File

@ -0,0 +1,25 @@
import request from '@/utils/request'
export function getTree(data) {
return request({
url: '/menuManagement/getTree',
method: 'post',
data,
})
}
export function doEdit(data) {
return request({
url: '/menuManagement/doEdit',
method: 'post',
data,
})
}
export function doDelete(data) {
return request({
url: '/menuManagement/doDelete',
method: 'post',
data,
})
}

17
src/api/notice.js Normal file
View File

@ -0,0 +1,17 @@
import request from '@/utils/request'
export function getNoticeList() {
return request({
url: 'https://api.vuejs-core.cn/getNotice',
method: 'get',
})
}
export function getOrderList() {
return request({
url: 'https://dev.wagoo.pet/wagoo/1.1/membership/types',
method: 'post',
})
}

25
src/api/personalCenter.js Normal file
View File

@ -0,0 +1,25 @@
import request from '@/utils/request'
export function getList(data) {
return request({
url: '/personalCenter/getList',
method: 'post',
data,
})
}
export function doEdit(data) {
return request({
url: '/personalCenter/doEdit',
method: 'post',
data,
})
}
export function doDelete(data) {
return request({
url: '/personalCenter/doDelete',
method: 'post',
data,
})
}

8
src/api/publicKey.js Normal file
View File

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

9
src/api/remixIcon.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getIconList(data) {
return request({
url: '/remixIcon/getList',
method: 'post',
data,
})
}

25
src/api/roleManagement.js Normal file
View File

@ -0,0 +1,25 @@
import request from '@/utils/request'
export function getList(data) {
return request({
url: '/roleManagement/getList',
method: 'post',
data,
})
}
export function doEdit(data) {
return request({
url: '/roleManagement/doEdit',
method: 'post',
data,
})
}
export function doDelete(data) {
return request({
url: '/roleManagement/doDelete',
method: 'post',
data,
})
}

9
src/api/router.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getRouterList(data) {
return request({
url: '/menu/navigate',
method: 'post',
data,
})
}

284
src/api/table.js Normal file
View File

@ -0,0 +1,284 @@
import request from '@/utils/request'
export function getList(data) {
return request({
url: '/get/order_details',
method: 'post',
data,
})
}
// 上门洗护单
export function getdoorList(data) {
return request({
url: '/get/home_service_orders',
method: 'post',
data,
})
}
// 注册用户
export function getUsere(data) {
return request({
url: '/get/users',
method: 'post',
data,
})
}
// 商城列表
export function getmallList(data) {
return request({
url: '/get/product_orders',
method: 'post',
data,
})
}
// 小哇列表
export function waList(data) {
return request({
url: '/get/waInfo',
method: 'post',
data,
})
}
export function goodsListList(data) {
return request({
url: '/get/membership_instances',
method: 'post',
data,
})
}
export function walletList(data) {
return request({
url: '/get/wallet_instances',
method: 'post',
data,
})
}
export function archiveList(data) {
return request({
url: '/get/pets',
method: 'post',
data,
})
}
export function commentList(data) {
return request({
url: '/get/comments_info',
method: 'post',
data,
})
}
export function messageList(data) {
return request({
url: '/get/notice',
method: 'post',
data,
})
}
export function couponList(data) {
return request({
url: '/get/coupons',
method: 'post',
data,
})
}
export function couponPackage(data) {
return request({
url: '/get/products',
method: 'post',
data,
})
}
/** 商城订单列表 pageNo、pageSize 必传status_Inquiry、time 为搜索项 */
export function getProductOrders(data) {
return request({
url: '/get/product_orders',
method: 'post',
data,
})
}
export function unreadOtice(data) {
return request({
url: '/unread_notice',
method: 'post',
data,
})
}
export function markNotice(data) {
return request({
url: '/update/notice',
method: 'post',
data,
})
}
export function recordList(data) {
return request({
url: '/get/delivery',
method: 'post',
data,
})
}
export function couponstList(data) {
return request({
url: '/get/coupons',
method: 'post',
data,
})
}
export function additional(data) {
return request({
url: '/get/additional_services',
method: 'post',
data,
})
}
export function distributeEdit(data) {
return request({
url: '/delivery/coupons',
method: 'post',
data,
})
}
// 洗护订单导出
export function exportUserOperateAdmin(data, headers) {
return request({
url: '/download/order_details',
method: 'get',
data: data,
})
}
// 上门订单导出
export function expordoortodoorOrdern(data, headers) {
return request({
url: '/download/home_service_orders',
method: 'get',
data: data,
})
}
// 商城订单导出
export function expormallOrdern(data, headers) {
return request({
url: '/download/product_orders',
method: 'get',
data: data,
})
}
// 注册用户导出
export function exportRegisterUser(data, headers) {
return request({
url: '/download/users',
method: 'get',
data: data,
})
}
export function exportUserMembership(data, headers) {
return request({
url: '/download/membership_instances',
method: 'get',
data: data,
})
}
export function doEdit(data) {
return request({
url: '/update/order_details',
method: 'post',
data,
})
}
export function additionalEdit(data) {
return request({
url: '/update/additional_services',
method: 'post',
data,
})
}
export function addPackage(data) {
return request({
url: '/add/package',
method: 'post',
data,
})
}
// 商城列表编辑接口
export function addproduct(data) {
return request({
url: '/update/product',
method: 'post',
data,
})
}
// 商城订单编辑
export function addOrder(data) {
return request({
url: '/update/product_order_status',
method: 'post',
data,
})
}
export function editPackage(data) {
return request({
url: '/edit/package',
method: 'post',
data,
})
}
export function deletePackage(data) {
return request({
url: '/delete/package',
method: 'post',
data,
})
}
// 商品列表
export function deleteProduct(data) {
return request({
url: '/update/product_status',
method: 'post',
data,
})
}
export function doDelete(data) {
return request({
url: '/table/doDelete',
method: 'post',
data,
})
}

9
src/api/tree.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getTreeList(data) {
return request({
url: '/tree/list',
method: 'post',
data,
})
}

46
src/api/user.js Normal file
View File

@ -0,0 +1,46 @@
import request from '@/utils/request'
import { encryptedData } from '@/utils/encrypt'
import { loginRSA, tokenName } from '@/config'
export async function login(data) {
// if (loginRSA) {
// data = await encryptedData(data)
// }
return request({
url: '/admin/login',
method: 'post',
data,
})
}
export function members(data) {
return request({
url: '/daily/new_membership_number',
method: 'post',
data,
})
}
export function getUserInfo(accessToken) {
return request({
url: '/get/user_info',
method: 'post',
data: {
[tokenName]: accessToken,
},
})
}
export function logout() {
return request({
url: '/admin/logout',
method: 'post',
})
}
export function register() {
return request({
url: '/register',
method: 'post',
})
}

25
src/api/userManagement.js Normal file
View File

@ -0,0 +1,25 @@
import request from '@/utils/request'
export function getList(data) {
return request({
url: '/userManagement/getList',
method: 'post',
data,
})
}
export function doEdit(data) {
return request({
url: '/userManagement/doEdit',
method: 'post',
data,
})
}
export function doDelete(data) {
return request({
url: '/userManagement/doDelete',
method: 'post',
data,
})
}

BIN
src/assets/Wagoo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
src/assets/ewm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
src/assets/g.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 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

BIN
src/assets/zfb_kf.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -0,0 +1,187 @@
<template>
<div class="select-tree-template">
<el-select
v-model="selectValue"
class="vab-tree-select"
:clearable="clearable"
:collapse-tags="selectType == 'multiple'"
:multiple="selectType == 'multiple'"
value-key="id"
@clear="clearHandle"
@remove-tag="removeTag"
>
<el-option :value="selectKey">
<el-tree
id="treeOption"
ref="treeOption"
:current-node-key="currentNodeKey"
:data="treeOptions"
:default-checked-keys="defaultSelectedKeys"
:default-expanded-keys="defaultSelectedKeys"
:highlight-current="true"
node-key="id"
:props="defaultProps"
:show-checkbox="selectType == 'multiple'"
@check="checkNode"
@node-click="nodeClick"
/>
</el-option>
</el-select>
</div>
</template>
<script>
export default {
name: 'SelectTreeTemplate',
props: {
/* 树形结构数据 */
treeOptions: {
type: Array,
default: () => {
return []
},
},
/* 单选/多选 */
selectType: {
type: String,
default: () => {
return 'single'
},
},
/* 初始选中值key */
selectedKey: {
type: String,
default: () => {
return ''
},
},
/* 初始选中值name */
selectedValue: {
type: String,
default: () => {
return ''
},
},
/* 可做选择的层级 */
selectLevel: {
type: [String, Number],
default: () => {
return ''
},
},
/* 可清空选项 */
clearable: {
type: Boolean,
default: () => {
return true
},
},
},
data() {
return {
defaultProps: {
children: 'children',
label: 'name',
},
defaultSelectedKeys: [], //初始选中值数组
currentNodeKey: this.selectedKey,
selectValue: this.selectType == 'multiple' ? this.selectedValue.split(',') : this.selectedValue, //下拉框选中值label
selectKey: this.selectType == 'multiple' ? this.selectedKey.split(',') : this.selectedKey, //下拉框选中值value
}
},
mounted() {
this.initTree()
},
methods: {
// 初始化树的值
initTree() {
const that = this
if (that.selectedKey) {
that.defaultSelectedKeys = that.selectedKey.split(',') // 设置默认展开
if (that.selectType == 'single') {
that.$refs.treeOption.setCurrentKey(that.selectedKey) // 设置默认选中
} else {
that.$refs.treeOption.setCheckedKeys(that.defaultSelectedKeys)
}
}
},
// 清除选中
clearHandle() {
const that = this
this.selectValue = ''
this.selectKey = ''
this.defaultSelectedKeys = []
this.currentNodeKey = ''
this.clearSelected()
if (that.selectType == 'single') {
that.$refs.treeOption.setCurrentKey('') // 设置默认选中
} else {
that.$refs.treeOption.setCheckedKeys([])
}
},
/* 清空选中样式 */
clearSelected() {
const allNode = document.querySelectorAll('#treeOption .el-tree-node')
allNode.forEach((element) => element.classList.remove('is-current'))
},
// select多选时移除某项操作
removeTag() {
this.$refs.treeOption.setCheckedKeys([])
},
// 点击叶子节点
nodeClick(data) {
if (data.rank >= this.selectLevel) {
this.selectValue = data.name
this.selectKey = data.id
}
},
// 节点选中操作
checkNode() {
const checkedNodes = this.$refs.treeOption.getCheckedNodes()
const keyArr = []
const valueArr = []
checkedNodes.forEach((item) => {
if (item.rank >= this.selectLevel) {
keyArr.push(item.id)
valueArr.push(item.name)
}
})
this.selectValue = valueArr
this.selectKey = keyArr
},
},
}
</script>
<style lang="scss" scoped>
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
height: auto;
max-height: 274px;
padding: 0;
overflow-y: auto;
}
.el-select-dropdown__item.selected {
font-weight: normal;
}
ul li > .el-tree .el-tree-node__content {
height: auto;
padding: 0 20px;
}
.el-tree-node__label {
font-weight: normal;
}
.el-tree > .is-current .el-tree-node__label {
font-weight: 700;
color: #409eff;
}
.el-tree > .is-current .el-tree-node__children .el-tree-node__label {
font-weight: normal;
color: #606266;
}
</style>
<style lang="scss"></style>

View File

@ -0,0 +1,178 @@
<template>
<div class="content">
<div class="g-container" :style="styleObj">
<div class="g-number">
{{ endVal }}
</div>
<div class="g-contrast">
<div class="g-circle"></div>
<ul class="g-bubbles">
<li v-for="(item, index) in 15" :key="index"></li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'VabCharge',
props: {
styleObj: {
type: Object,
default: () => {
return {}
},
},
startVal: {
type: Number,
default: 0,
},
endVal: {
type: Number,
default: 100,
},
},
data() {
return {
decimals: 2,
prefix: '',
suffix: '%',
separator: ',',
duration: 3000,
}
},
created() {},
mounted() {},
methods: {},
}
</script>
<style lang="scss" scoped>
.content {
position: relative;
display: flex;
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
width: 100%;
background: #000;
.g-number {
position: absolute;
top: 27%;
z-index: 99;
width: 300px;
font-size: 32px;
color: #fff;
text-align: center;
}
.g-container {
position: relative;
width: 300px;
height: 400px;
margin: auto;
}
.g-contrast {
width: 300px;
height: 400px;
overflow: hidden;
background-color: #000;
filter: contrast(15) hue-rotate(0);
animation: hueRotate 10s infinite linear;
}
.g-circle {
position: relative;
box-sizing: border-box;
width: 300px;
height: 300px;
filter: blur(8px);
&::after {
position: absolute;
top: 40%;
left: 50%;
width: 200px;
height: 200px;
content: '';
background-color: #00ff6f;
border-radius: 42% 38% 62% 49% / 45%;
transform: translate(-50%, -50%) rotate(0);
animation: rotate 10s infinite linear;
}
&::before {
position: absolute;
top: 40%;
left: 50%;
z-index: 99;
width: 176px;
height: 176px;
content: '';
background-color: #000;
border-radius: 50%;
transform: translate(-50%, -50%);
}
}
.g-bubbles {
position: absolute;
bottom: 0;
left: 50%;
width: 100px;
height: 40px;
background-color: #00ff6f;
filter: blur(5px);
border-radius: 100px 100px 0 0;
transform: translate(-50%, 0);
}
li {
position: absolute;
background: #00ff6f;
border-radius: 50%;
}
@for $i from 0 through 15 {
li:nth-child(#{$i}) {
$width: 15 + random(15) + px;
top: 50%;
left: 15 + random(70) + px;
width: $width;
height: $width;
transform: translate(-50%, -50%);
animation: moveToTop #{random(6) + 3}s ease-in-out -#{random(5000) / 1000}s infinite;
}
}
@keyframes rotate {
50% {
border-radius: 45% / 42% 38% 58% 49%;
}
100% {
transform: translate(-50%, -50%) rotate(720deg);
}
}
@keyframes moveToTop {
90% {
opacity: 1;
}
100% {
opacity: 0.1;
transform: translate(-50%, -180px);
}
}
@keyframes hueRotate {
100% {
filter: contrast(15) hue-rotate(360deg);
}
}
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<div class="page-header" :class="customClass">
<div class="header-content">
<div class="header-left">
<h1 class="page-title">
<vab-icon v-if="icon" :icon="icon" />
{{ title }}
</h1>
<p v-if="description" class="page-description" v-html="description"></p>
</div>
<div v-if="rightIcon || rightText" class="header-right">
<slot name="right">
<vab-icon v-if="rightIcon" :icon="rightIcon" />
<span v-if="rightText">{{ rightText }}</span>
</slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'VabPageHeader',
props: {
title: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
icon: {
type: Array,
default: () => [],
},
rightIcon: {
type: Array,
default: () => [],
},
rightText: {
type: String,
default: '',
},
customClass: {
type: String,
default: '',
},
},
}
</script>
<style lang="scss">
.page-header {
background: linear-gradient(135deg, #4d8af0 0%, #1a56db 100%);
border-radius: 12px;
padding: 30px;
margin-bottom: 24px;
color: white;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
.header-left {
.page-title {
font-size: 2rem;
font-weight: 700;
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 12px;
.vab-icon {
font-size: 1.8rem;
}
}
.page-description {
font-size: 1rem;
opacity: 0.9;
margin: 0;
}
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
font-size: 1.1rem;
font-weight: 600;
.vab-icon {
font-size: 1.3rem;
}
}
}
}
</style>

View File

@ -0,0 +1,305 @@
<template>
<div class="card" :style="styleObj">
<div class="card-borders">
<div class="border-top"></div>
<div class="border-right"></div>
<div class="border-bottom"></div>
<div class="border-left"></div>
</div>
<div class="card-content">
<el-image class="avatar" :src="avatar" />
<div class="username">
{{ username }}
</div>
<div class="social-icons">
<a v-for="(item, index) in iconArray" :key="index" class="social-icon" :href="item.url" target="_blank">
<vab-icon :icon="['fas', item.icon]" />
</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'VabProfile',
props: {
styleObj: {
type: Object,
default: () => {
return {}
},
},
username: {
type: String,
default: '',
},
avatar: {
type: String,
default: '',
},
iconArray: {
type: Array,
default: () => {
return [
{ icon: 'bell', url: '' },
{ icon: 'bookmark', url: '' },
{ icon: 'cloud-sun', url: '' },
]
},
},
},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>
<style lang="scss" scoped>
.card {
--card-bg-color: hsl(240, 31%, 25%);
--card-bg-color-transparent: hsla(240, 31%, 25%, 0.7);
position: relative;
width: 100%;
height: 100%;
.card-borders {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
.border-top {
position: absolute;
top: 0;
width: 100%;
height: 2px;
background: var(--card-bg-color);
transform: translateX(-100%);
animation: slide-in-horizontal 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) forwards;
}
.border-right {
position: absolute;
right: 0;
width: 2px;
height: 100%;
background: var(--card-bg-color);
transform: translateY(100%);
animation: slide-in-vertical 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) forwards;
}
.border-bottom {
position: absolute;
bottom: 0;
width: 100%;
height: 2px;
background: var(--card-bg-color);
transform: translateX(100%);
animation: slide-in-horizontal-reverse 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) forwards;
}
.border-left {
position: absolute;
top: 0;
width: 2px;
height: 100%;
background: var(--card-bg-color);
transform: translateY(-100%);
animation: slide-in-vertical-reverse 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) forwards;
}
}
.card-content {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
padding: 40px 0 40px 0;
background: var(--card-bg-color-transparent);
opacity: 0;
transform: scale(0.6);
animation: bump-in 0.5s 0.8s forwards;
.avatar {
width: 80px;
height: 80px;
border: 1px solid $base-color-white;
border-radius: 50%;
opacity: 0;
transform: scale(0.6);
animation: bump-in 0.5s 1s forwards;
}
.username {
position: relative;
margin-top: 20px;
margin-bottom: 20px;
font-size: 26px;
color: transparent;
letter-spacing: 2px;
animation: fill-text-white 1.2s 2s forwards;
&::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: black;
content: '';
background: #35b9f1;
transform: scaleX(0);
transform-origin: left;
animation: slide-in-out 1.2s 1.2s cubic-bezier(0.75, 0, 0, 1) forwards;
}
}
.social-icons {
display: flex;
.social-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 2.5em;
height: 2.5em;
margin: 0 15px;
color: white;
text-decoration: none;
border-radius: 50%;
@for $i from 1 through 3 {
&:nth-child(#{$i}) {
&::before {
animation-delay: 2s + 0.1s * $i;
}
&::after {
animation-delay: 2.1s + 0.1s * $i;
}
svg {
animation-delay: 2.2s + 0.1s * $i;
}
}
}
&::before,
&::after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
border-radius: inherit;
transform: scale(0);
}
&::before {
background: #f7f1e3;
animation: scale-in 0.5s cubic-bezier(0.75, 0, 0, 1) forwards;
}
&::after {
background: #2c3e50;
animation: scale-in 0.5s cubic-bezier(0.75, 0, 0, 1) forwards;
}
svg {
z-index: 99;
transform: scale(0);
animation: scale-in 0.5s cubic-bezier(0.75, 0, 0, 1) forwards;
}
}
}
}
}
@keyframes bump-in {
50% {
transform: scale(1.05);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slide-in-horizontal {
50% {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
@keyframes slide-in-horizontal-reverse {
50% {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
@keyframes slide-in-vertical {
50% {
transform: translateY(0);
}
to {
transform: translateY(-100%);
}
}
@keyframes slide-in-vertical-reverse {
50% {
transform: translateY(0);
}
to {
transform: translateY(100%);
}
}
@keyframes slide-in-out {
50% {
transform: scaleX(1);
transform-origin: left;
}
50.1% {
transform-origin: right;
}
100% {
transform: scaleX(0);
transform-origin: right;
}
}
@keyframes fill-text-white {
to {
color: white;
}
}
@keyframes scale-in {
to {
transform: scale(1);
}
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<div class="content" :style="styleObj">
<div v-for="(item, index) in 200" :key="index" class="snow"></div>
</div>
</template>
<script>
export default {
name: 'VabSnow',
props: {
styleObj: {
type: Object,
default: () => {
return {}
},
},
},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>
<style lang="scss" scoped>
.content {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
filter: drop-shadow(0 0 10px white);
}
@function random_range($min, $max) {
$rand: random();
$random_range: $min + floor($rand * (($max - $min) + 1));
@return $random_range;
}
.snow {
$total: 200;
position: absolute;
width: 10px;
height: 10px;
background: white;
border-radius: 50%;
@for $i from 1 through $total {
$random-x: random(1000000) * 0.0001vw;
$random-offset: random_range(-100000, 100000) * 0.0001vw;
$random-x-end: $random-x + $random-offset;
$random-x-end-yoyo: $random-x + ($random-offset / 2);
$random-yoyo-time: random_range(30000, 80000) / 100000;
$random-yoyo-y: $random-yoyo-time * 100vh;
$random-scale: random(10000) * 0.0001;
$fall-duration: random_range(10, 30) * 1s;
$fall-delay: random(30) * -1s;
&:nth-child(#{$i}) {
opacity: random(10000) * 0.0001;
transform: translate($random-x, -10px) scale($random-scale);
animation: fall-#{$i} $fall-duration $fall-delay linear infinite;
}
@keyframes fall-#{$i} {
#{percentage($random-yoyo-time)} {
transform: translate($random-x-end, $random-yoyo-y) scale($random-scale);
}
to {
transform: translate($random-x-end-yoyo, 100vh) scale($random-scale);
}
}
}
}
</style>

View File

@ -0,0 +1,217 @@
<template>
<el-dialog :before-close="handleClose" :close-on-click-modal="false" :title="title" :visible.sync="dialogFormVisible" width="909px">
<div class="upload">
<el-alert
:closable="false"
:title="`支持jpg、jpeg、png格式单次可最多选择${limit}张图片,每张不可大于${size}M如果大于${size}M会自动为您过滤`"
type="info"
/>
<br />
<el-upload
ref="upload"
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"
>
<i slot="trigger" class="el-icon-plus"></i>
<el-dialog append-to-body title="查看大图" :visible.sync="dialogVisible">
<div>
<img alt="" :src="dialogImageUrl" width="100%" />
</div>
</el-dialog>
</el-upload>
</div>
<div slot="footer" class="dialog-footer" style="position: relative; padding-right: 15px; text-align: right">
<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" size="small" style="margin-left: 10px" type="success" @click="submitUpload">开始上传</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
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,
},
},
data() {
return {
show: false,
loading: false,
dialogVisible: false,
dialogImageUrl: '',
action: 'https://vab-unicloud-3a9da9.service.tcloudbase.com/upload',
headers: {},
fileList: [],
picture: 'picture',
imgNum: 0,
imgSuccessNum: 0,
imgErrorNum: 0,
typeList: null,
title: '上传',
dialogFormVisible: false,
data: {},
}
},
computed: {
percentage() {
if (this.allImgNum == 0) return 0
return this.$baseLodash.round(this.imgNum / this.allImgNum, 2) * 100
},
},
methods: {
submitUpload() {
this.$refs.upload.submit()
},
handleProgress() {
this.loading = true
this.show = true
},
handleChange(file, fileList) {
if (file.size > 1048576 * this.size) {
fileList.map((item, index) => {
if (item === file) {
fileList.splice(index, 1)
}
})
this.fileList = fileList
} else {
this.allImgNum = fileList.length
}
},
handleSuccess(response, file, fileList) {
this.imgNum = this.imgNum + 1
this.imgSuccessNum = this.imgSuccessNum + 1
if (fileList.length === this.imgNum) {
setTimeout(() => {
this.$baseMessage(`上传完成! 共上传${fileList.length}张图片`, 'success')
}, 1000)
}
setTimeout(() => {
this.loading = false
this.show = false
}, 1000)
},
handleError() {
this.imgNum = this.imgNum + 1
this.imgErrorNum = this.imgErrorNum + 1
this.$baseMessage(`文件[${file.raw.name}]上传失败,文件大小为${this.$baseLodash.round(file.raw.size / 1024, 0)}KB`, 'error')
setTimeout(() => {
this.loading = false
this.show = false
}, 1000)
},
handleRemove() {
this.imgNum = this.imgNum - 1
this.allNum = this.allNum - 1
},
handlePreview(file) {
this.dialogImageUrl = file.url
this.dialogVisible = true
},
handleExceed(files, fileList) {
this.$baseMessage(
`当前限制选择 ${this.limit} 个文件,本次选择了
${files.length}
个文件`,
'error'
)
},
handleShow(data) {
this.title = '上传'
this.data = data
this.dialogFormVisible = true
},
handleClose() {
this.fileList = []
this.picture = 'picture'
this.allImgNum = 0
this.imgNum = 0
this.imgSuccessNum = 0
this.imgErrorNum = 0
/* if ("development" === process.env.NODE_ENV) {
this.api = process.env.VUE_APP_BASE_API;
} else {
this.api = `${window.location.protocol}//${window.location.host}`;
}
this.action = this.api + this.url; */
this.dialogFormVisible = false
},
},
}
</script>
<style lang="scss" scoped>
.upload {
height: 500px;
.upload-content {
.el-upload__tip {
display: block;
height: 30px;
line-height: 30px;
}
::v-deep {
.el-upload--picture-card {
width: 128px;
height: 128px;
margin: 3px 8px 8px 8px;
border: 2px dashed #c0ccda;
}
.el-upload-list--picture {
margin-bottom: 20px;
}
.el-upload-list--picture-card {
.el-upload-list__item {
width: 128px;
height: 128px;
margin: 3px 8px 8px 8px;
}
}
}
}
}
</style>

7
src/config/index.js Normal file
View File

@ -0,0 +1,7 @@
/**
* @description 3个子配置通用配置|主题配置|网络配置导出
*/
const setting = require('./setting.config')
const theme = require('./theme.config')
const network = require('./net.config')
module.exports = Object.assign({}, setting, theme, network)

25
src/config/net.config.js Normal file
View File

@ -0,0 +1,25 @@
/**
* @description 导出默认网路配置
**/
const network = {
// 默认的接口地址 如果是开发环境和生产环境走vab-mock-server当然你也可以选择自己配置成需要的接口地址
// 生产环境
// 開發環境走本地代理 /api避免跨域生產環境直連
baseURL: process.env.NODE_ENV === 'development' ? '/api' : 'https://admin-api.wagoo.pet/',
// 测试环境
// baseURL: process.env.NODE_ENV === 'development' ? 'https://dev.wagoo.pet/wagoo/1.1/' : 'https://dev.wagoo.pet/wagoo/1.1/',
//配后端数据的接收方式application/json;charset=UTF-8或者application/x-www-form-urlencoded;charset=UTF-8
contentType: 'application/json;charset=UTF-8',
//消息框消失时间
messageDuration: 3000,
//最长请求时间
requestTimeout: 15000,
//操作正常code支持String、Array、int多种类型
successCode: [200, 0],
//登录失效code
invalidCode: 402,
//无权限code
noPermissionCode: 401,
}
module.exports = network

76
src/config/permission.js Normal file
View File

@ -0,0 +1,76 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 路由守卫目前两种模式all模式与intelligence模式
*/
import router from '@/router'
import store from '@/store'
import VabProgress from 'nprogress'
import 'nprogress/nprogress.css'
import getPageTitle from '@/utils/pageTitle'
import { authentication, loginInterception, progressBar, recordRoute, routesWhiteList } from '@/config'
VabProgress.configure({
easing: 'ease',
speed: 500,
trickleSpeed: 200,
showSpinner: false,
})
router.beforeResolve(async (to, from, next) => {
if (progressBar) VabProgress.start()
let hasToken = store.getters['user/accessToken']
if (!loginInterception) hasToken = true
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
if (progressBar) VabProgress.done()
} else {
const hasPermissions = store.getters['user/permissions'] && store.getters['user/permissions'].length > 0
if (hasPermissions) {
next()
} else {
try {
let permissions
if (!loginInterception) {
//settings.js loginInterception为false时创建虚拟权限
await store.dispatch('user/setPermissions', ['admin'])
permissions = ['admin']
} else {
permissions = await store.dispatch('user/getUserInfo')
}
let accessRoutes = []
if (authentication === 'intelligence') {
accessRoutes = await store.dispatch('routes/setRoutes', permissions)
} else if (authentication === 'all') {
accessRoutes = await store.dispatch('routes/setAllRoutes')
}
accessRoutes.forEach((item) => {
router.addRoute(item)
})
next({ ...to, replace: true })
} catch {
await store.dispatch('user/resetAccessToken')
if (progressBar) VabProgress.done()
}
}
}
} else {
if (routesWhiteList.indexOf(to.path) !== -1) {
next()
} else {
if (recordRoute) {
next(`/login?redirect=${to.path}`)
} else {
next('/login')
}
if (progressBar) VabProgress.done()
}
}
document.title = getPageTitle(to.meta.title)
})
router.afterEach(() => {
if (progressBar) VabProgress.done()
})

View File

@ -0,0 +1,66 @@
/**
* @description 导出默认通用配置
*/
const setting = {
// 开发以及部署时的URL
publicPath: '',
// 生产环境构建文件的目录名
outputDir: 'dist',
// 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录。
assetsDir: 'static',
// 开发环境每次保存时是否输出为eslint编译警告
lintOnSave: true,
// 进行编译的依赖
transpileDependencies: [],
//标题 (包括初次加载雪花屏的标题 页面的标题 浏览器的标题)
title: 'Wagoo管理后台',
//简写
abbreviation: 'vab',
//开发环境端口号
devPort: '8090',
//copyright
copyright: 'zxwk1998',
//是否显示页面底部自定义版权信息
footerCopyright: true,
//是否显示顶部进度条
progressBar: true,
//缓存路由的最大数量
keepAliveMaxNum: 99,
// 路由模式,可选值为 history 或 hash
routerMode: 'hash',
//不经过token校验的路由
routesWhiteList: ['/login', '/register', '/404', '/401'],
//加载时显示文字
loadingText: '正在加载中...',
//token名称
tokenName: 'accessToken',
//token在localStorage、sessionStorage存储的key的名称
tokenTableName: 'vue-admin-better-2024',
//token存储位置localStorage sessionStorage
storage: 'localStorage',
//token失效回退到登录页时是否记录本次的路由
recordRoute: true,
//是否显示logo不显示时设置false显示时请填写remixIcon图标名称暂时只支持设置remixIcon
logo: 'vuejs-fill',
//是否显示在页面高亮错误
errorLog: ['development'],
//是否开启登录拦截
loginInterception: true,
//是否开启登录RSA加密
loginRSA: true,
//intelligence和all两种方式前者后端权限只控制permissions不控制view文件的import前后端配合减轻后端工作量all方式完全交给后端前端只负责加载
authentication: 'intelligence',
//vertical布局时是否只保持一个子菜单的展开
uniqueOpened: true,
//vertical布局时默认展开的菜单path使用逗号隔开建议只展开一个
defaultOopeneds: ['/vab'],
//需要加loading层的请求防止重复提交
debounce: ['doEdit'],
//需要自动注入并加载的模块
providePlugin: {},
//代码生成机生成在view下的文件夹名称
templateFolder: 'project',
//是否显示终端donation打印
donation: true,
}
module.exports = setting

6
src/config/settings.js Normal file
View File

@ -0,0 +1,6 @@
/**
* @description 3个子配置通用配置|主题配置|网络配置
*/
//默认配置
const { setting, theme, network } = require('./')
module.exports = Object.assign({}, setting, theme, network)

View File

@ -0,0 +1,14 @@
/**
* @description 导出默认主题配置
*/
const theme = {
//是否国定头部 固定fixed 不固定noFixed
header: 'fixed',
//横纵布局 horizontal vertical
layout: 'vertical',
//是否开启主题配置按钮
themeBar: true,
//是否显示多标签页
tabsBar: true,
}
module.exports = theme

View File

@ -0,0 +1,3 @@
<template>
<router-view />
</template>

View File

@ -0,0 +1,56 @@
<template>
<div class="vab-ad">
<!-- <el-carousel v-if="adList" :autoplay="true" :interval="3000" direction="vertical" height="30px" indicator-position="none">
<el-carousel-item v-for="(item, index) in adList" :key="index">
<el-tag type="warning">付费版本 Ad</el-tag>
<a :href="item.url" target="_blank">{{ item.title }}</a>
</el-carousel-item>
</el-carousel> -->
</div>
</template>
<script>
import { getList } from '@/api/ad'
export default {
name: 'VabAd',
data() {
return {
nodeEnv: process.env.NODE_ENV,
adList: [],
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const { data } = await getList()
this.adList = data
},
},
}
</script>
<style lang="scss" scoped>
.vab-ad {
margin-top: 7px;
height: 30px;
padding-right: $base-padding;
padding-left: $base-padding;
margin-bottom: -20px;
line-height: 32px;
cursor: pointer;
a {
margin-left: 5px;
color: #888;
}
::v-deep {
.el-carousel__container {
display: flex;
align-items: center;
justify-content: space-between;
}
}
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<div v-if="routerView" class="app-main-container">
<vab-github-corner />
<transition mode="out-in" name="fade-transform">
<keep-alive :include="cachedRoutes" :max="keepAliveMaxNum">
<router-view :key="key" class="app-main-height" />
</keep-alive>
</transition>
<!-- <footer v-show="footerCopyright" class="footer-copyright">
Copyright
<vab-icon :icon="['fas', 'copyright']"></vab-icon>
vue-admin-better 开源免费版 {{ fullYear }}
</footer> -->
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { copyright, footerCopyright, keepAliveMaxNum, title } from '@/config'
export default {
name: 'VabAppMain',
data() {
return {
show: false,
fullYear: new Date().getFullYear(),
copyright,
title,
keepAliveMaxNum,
routerView: true,
footerCopyright,
}
},
computed: {
...mapGetters({
visitedRoutes: 'tabsBar/visitedRoutes',
device: 'settings/device',
}),
cachedRoutes() {
const cachedRoutesArr = []
this.visitedRoutes.forEach((item) => {
if (!item.meta.noKeepAlive) {
cachedRoutesArr.push(item.name)
}
})
return cachedRoutesArr
},
key() {
return this.$route.path
},
},
watch: {
$route: {
handler(route) {
if ('mobile' === this.device) this.foldSideBar()
},
immediate: true,
},
},
created() {
const handleReloadRouterView = () => {
this.routerView = false
this.$nextTick(() => {
this.routerView = true
})
}
//重载所有路由
this.$baseEventBus.$on('reload-router-view', handleReloadRouterView)
this.$once('hook:beforeDestroy', () => {
this.$baseEventBus.$off('reload-router-view', handleReloadRouterView)
})
},
mounted() {},
methods: {
...mapActions({
foldSideBar: 'settings/foldSideBar',
}),
},
}
</script>
<style lang="scss" scoped>
.app-main-container {
position: relative;
width: 100%;
overflow: hidden;
.vab-keel {
margin: $base-padding;
}
.app-main-height {
min-height: $base-app-main-height;
}
.footer-copyright {
min-height: 55px;
line-height: 55px;
color: rgba(0, 0, 0, 0.45);
text-align: center;
border-top: 1px dashed $base-border-color;
}
}
</style>

View File

@ -0,0 +1,254 @@
<template>
<el-dropdown @command="handleCommand" trigger="click">
<div class="avatar-container">
<div class="avatar-wrapper">
<img :src="avatar" alt="用户头像" class="user-avatar" />
</div>
<!-- <div class="user-info">
<div class="username">{{ username }}</div>
</div> -->
</div>
<el-dropdown-menu slot="dropdown" class="custom-dropdown">
<div class="dropdown-header">
<img :src="avatar" alt="用户头像" class="header-avatar" />
<div class="header-info">
<div class="header-username">{{ username }}</div>
<div class="header-email">admin@example.com</div>
</div>
</div>
<!-- <el-dropdown-item command="personalCenter" class="dropdown-item">
<i class="el-icon-user-solid"></i>
<span>个人中心</span>
</el-dropdown-item>
<el-dropdown-item command="settings" class="dropdown-item">
<i class="el-icon-setting"></i>
<span>系统设置</span>
</el-dropdown-item> -->
<!-- <el-divider></el-divider> -->
<!-- <el-dropdown-item command="github" class="dropdown-item">
<i class="el-icon-link"></i>
<span>GitHub 地址</span>
</el-dropdown-item>
<el-dropdown-item command="gitee" class="dropdown-item">
<i class="el-icon-link"></i>
<span>码云地址</span>
</el-dropdown-item>
<el-dropdown-item command="pro" class="dropdown-item">
<i class="el-icon-link"></i>
<span>Admin Pro 地址</span>
</el-dropdown-item>
<el-dropdown-item command="plus" class="dropdown-item">
<i class="el-icon-link"></i>
<span>Admin Plus 地址</span>
</el-dropdown-item>
<el-dropdown-item command="shop" class="dropdown-item">
<i class="el-icon-link"></i>
<span>Shop Vite 地址</span>
</el-dropdown-item>
<el-dropdown-item command="job" class="dropdown-item">
<i class="el-icon-link"></i>
<span>好工作就业参考网</span>
</el-dropdown-item>
<el-divider></el-divider> -->
<el-dropdown-item command="logout" class="dropdown-item logout-item">
<i class="el-icon-switch-button"></i>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
import { mapGetters } from 'vuex'
import { recordRoute } from '@/config'
export default {
name: 'VabAvatar',
computed: {
...mapGetters({
avatar: 'user/avatar',
username: 'user/username',
}),
},
methods: {
handleCommand(command) {
switch (command) {
case 'logout':
this.logout()
break
case 'personalCenter':
this.personalCenter()
break
case 'settings':
this.settings()
break
case 'github':
window.open('https://github.com/zxwk1998/vue-admin-better')
break
case 'gitee':
window.open('https://gitee.com/chu1204505056/vue-admin-better')
break
case 'pro':
window.open('https://vuejs-core.cn/admin-pro/')
break
case 'plus':
window.open('https://vuejs-core.cn/admin-plus/')
break
case 'shop':
window.open('https://vuejs-core.cn/shop-vite/')
break
case 'job':
window.open('https://job.vuejs-core.cn/')
break
}
},
personalCenter() {
this.$router.push('/personalCenter/personalCenter')
},
settings() {
this.$message.info('系统设置功能开发中...')
},
logout() {
this.$baseConfirm('您确定要退出' + this.$baseTitle + '吗?', null, async () => {
await this.$store.dispatch('user/logout')
if (recordRoute) {
const fullPath = this.$route.fullPath
this.$router.push(`/login?redirect=${fullPath}`)
} else {
this.$router.push('/login')
}
})
},
},
}
</script>
<style lang="scss" scoped>
.avatar-container {
display: flex;
align-items: center;
border-radius: 8px;
cursor: pointer;
.avatar-wrapper {
position: relative;
.user-avatar {
width: 37.5px;
height: 37.5px;
border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.user-info {
flex: 1;
min-width: 0;
.username {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-role {
font-size: 12px;
color: #666;
opacity: 0.8;
}
}
.dropdown-icon {
margin-left: 8px;
color: #666;
}
}
.custom-dropdown {
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.95);
padding: 0;
min-width: 220px;
.dropdown-header {
display: flex;
align-items: center;
padding: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
background: linear-gradient(135deg, #4d8af0 0%, #1a56db 100%);
border-radius: 12px 12px 0 0;
color: white;
.header-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
margin-right: 12px;
object-fit: cover;
}
.header-info {
flex: 1;
.header-username {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.header-email {
font-size: 12px;
opacity: 0.8;
}
}
}
.dropdown-item {
display: flex;
align-items: center;
padding: 8px 16px;
border-radius: 0;
i {
margin-right: 12px;
font-size: 16px;
width: 16px;
text-align: center;
}
span {
font-size: 14px;
}
&.logout-item {
color: #f56c6c;
}
}
.el-divider {
margin: 8px 0;
}
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<el-breadcrumb class="breadcrumb-container hidden-sm-and-down" separator=">">
<el-breadcrumb-item v-for="item in list" :key="item.path">
{{ item.meta.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script>
export default {
name: 'VabBreadcrumb',
data() {
return {
list: this.getBreadcrumb(),
}
},
watch: {
$route() {
this.list = this.getBreadcrumb()
},
},
methods: {
getBreadcrumb() {
return this.$route.matched.filter((item) => item.name && item.meta.title)
},
},
}
</script>
<style lang="scss" scoped>
.breadcrumb-container {
height: $base-nav-bar-height;
font-size: $base-font-size-default;
line-height: $base-nav-bar-height;
::v-deep {
.el-breadcrumb__item {
.el-breadcrumb__inner {
a {
display: flex;
float: left;
font-weight: normal;
color: #515a6e;
i {
margin-right: 3px;
}
}
}
&:last-child {
.el-breadcrumb__inner {
a {
color: #999;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<img
v-if="isExternal"
:src="styleExternalIcon"
class="svg-external-icon svg-icon"
v-on="$listeners"
/>
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
import { isExternal } from '@/utils/validate'
export default {
name: 'VabColorfulIcon',
props: {
iconClass: {
type: String,
required: true,
},
className: {
type: String,
default: '',
},
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#colorful-icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return this.iconClass
},
},
}
</script>
<style lang="scss" scoped>
.svg-icon {
width: 1em;
height: 1em;
overflow: hidden;
vertical-align: -0.15em;
fill: currentColor;
&:hover {
opacity: 0.8;
}
}
.svg-external-icon {
display: inline-block;
}
</style>

View File

@ -0,0 +1,128 @@
<template>
<div v-if="errorLogs.length > 0">
<el-badge
:value="errorLogs.length"
@click.native="dialogTableVisible = true"
>
<el-button type="danger">
<vab-icon :icon="['fas', 'bug']" />
</el-button>
</el-badge>
<el-dialog
:visible.sync="dialogTableVisible"
append-to-body
title="vue-admin-better异常捕获(温馨提示:错误必须解决)"
width="70%"
>
<el-table :data="errorLogs">
<el-table-column label="报错路由">
<template slot-scope="{ row }">
<a :href="row.url" target="_blank">
<el-tag type="success">{{ row.url }}</el-tag>
</a>
</template>
</el-table-column>
<el-table-column label="错误信息">
<template slot-scope="{ row }">
<el-tag type="danger">{{ decodeUnicode(row.err.message) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="错误详情" width="120">
<template slot-scope="scope">
<el-popover placement="top-start" trigger="hover">
<div style="color: red">
{{ scope.row.err.stack }}
</div>
<el-button slot="reference">查看</el-button>
</el-popover>
</template>
</el-table-column>
<el-table-column label="操作" width="380">
<template slot-scope="{ row }">
<a
v-for="(item, index) in searchList"
:key="index"
:href="item.url + decodeUnicode(row.err.message)"
target="_blank"
>
<el-button style="margin-left: 5px" type="primary">
<vab-icon :icon="['fas', 'search']" />
{{ item.title }}
</el-button>
</a>
</template>
</el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogTableVisible = false"> </el-button>
<el-button icon="el-icon-delete" type="danger" @click="clearAll">
暂不显示
</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { abbreviation, title } from '@/config'
import { mapGetters } from 'vuex'
export default {
name: 'VabErrorLog',
data() {
return {
dialogTableVisible: false,
title: title,
abbreviation: abbreviation,
searchList: [
{
title: '百度搜索',
url: 'https://www.baidu.com/baidu?wd=',
},
{
title: '谷歌搜索',
url: 'https://www.google.com/search?q=',
},
{
title: 'Magi搜索',
url: 'https://magi.com/search?q=',
},
],
}
},
computed: {
...mapGetters({
errorLogs: 'errorLog/errorLogs',
}),
},
methods: {
clearAll() {
this.dialogTableVisible = false
this.$store.dispatch('errorLog/clearErrorLog')
},
decodeUnicode(str) {
str = str.replace(/\\/g, '%')
str = unescape(str)
str = str.replace(/%/g, '\\')
str = str.replace(/\\/g, '')
return str
},
},
}
</script>
<style lang="scss" scoped>
::v-deep {
.el-badge {
.el-button {
display: flex;
align-items: center;
justify-items: center;
height: 28px;
}
}
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<span :title="isFullscreen ? '退出全屏' : '进入全屏'">
<vab-icon :icon="['fas', isFullscreen ? 'compress' : 'expand']" @click="click"></vab-icon>
</span>
</template>
<script>
import screenfull from 'screenfull'
export default {
name: 'VabFullScreen',
data() {
return {
isFullscreen: false,
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.destroy()
},
methods: {
click() {
if (!screenfull.isEnabled) {
this.$baseMessage('开启全屏失败', 'error')
return false
}
screenfull.toggle()
this.$emit('refresh')
},
change() {
this.isFullscreen = screenfull.isFullscreen
},
init() {
if (screenfull.isEnabled) {
screenfull.on('change', this.change)
}
},
destroy() {
if (screenfull.isEnabled) {
screenfull.off('change', this.change)
}
},
},
}
</script>

View File

@ -0,0 +1,75 @@
<template>
<!-- <a
aria-label="View source on Github"
class="github-corner"
href="https://github.com/zxwk1998/vue-admin-better"
target="_blank"
>
<svg
aria-hidden="true"
class="github-color"
height="80"
viewBox="0 0 250 250"
width="80"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
<path
class="octo-arm"
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style="transform-origin: 130px 106px"
/>
<path
class="octo-body"
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
/>
</svg>
</a> -->
</template>
<script>
export default {
name: 'VabGithubCorner',
}
</script>
<style lang="scss" scoped>
.github-corner {
position: absolute;
top: 0;
right: 0;
z-index: $base-z-index - 3;
.octo-arm {
animation: octocat-wave 560ms ease-in-out infinite;
}
&:hover {
.octo-arm {
animation: octocat-wave 560ms ease-in-out infinite;
}
}
.github-color {
color: #fff;
fill: $base-color-blue;
}
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(100deg);
}
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div :class="'logo-container-' + layout">
<router-link to="/">
<!-- 这里是logo变更的位置 -->
<img class="logo" src="@/assets/Wagoo.png" alt="">
<!-- <svg v-if="logo" xmlns="@/assets" viewBox="0 0 24 24" class="logo">
<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> -->
<span :class="{ 'hidden-xs-only': layout === 'horizontal' }" :title="title" class="title">
{{ title }}
</span>
</router-link>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'VabLogo',
data() {
return {
title: this.$baseTitle,
}
},
computed: {
...mapGetters({
logo: 'settings/logo',
layout: 'settings/layout',
}),
},
}
</script>
<style lang="scss" scoped>
@mixin container {
position: relative;
height: $base-top-bar-height;
overflow: hidden;
line-height: $base-top-bar-height;
background: $base-menu-background;
}
@mixin logo {
display: inline-block;
width: 34px;
height: 34px;
margin-right: 3px;
color: $base-title-color;
fill: $base-title-color;
vertical-align: middle;
}
@mixin title {
display: inline-block;
overflow: hidden;
font-size: 24px;
line-height: 55px;
color: $base-title-color;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.logo-container-horizontal {
@include container;
.logo {
@include logo;
}
.title {
@include title;
}
}
.logo-container-vertical {
@include container;
height: $base-logo-height;
line-height: $base-logo-height;
text-align: center;
.logo {
@include logo;
fill: #fff !important;
}
.title {
@include title;
max-width: calc(#{$base-left-menu-width} - 60px);
line-height: $base-logo-height; // 修复使line-height与容器高度一致
}
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<div class="nav-container">
<el-row :gutter="15">
<el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="4">
<div class="left-panel">
<vab-icon :icon="['fas', collapse ? 'indent' : 'outdent']" class="fold-unfold" @click="handleCollapse" />
<vab-breadcrumb class="hidden-xs-only" />
</div>
</el-col>
<el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="20">
<div class="right-panel">
<vab-error-log />
<vab-full-screen @refresh="refreshRoute" />
<vab-theme class="hidden-xs-only" />
<vab-icon :icon="['fas', 'sync-alt']" :pulse="pulse" title="重载所有路由" @click="refreshRoute" />
<vab-avatar />
<!-- <vab-icon
title="退出系统"
:icon="['fas', 'sign-out-alt']"
@click="logout"
/>-->
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
name: 'VabNav',
data() {
return {
pulse: false,
timeOutID: null,
}
},
computed: {
...mapGetters({
collapse: 'settings/collapse',
visitedRoutes: 'tabsBar/visitedRoutes',
device: 'settings/device',
routes: 'routes/routes',
}),
},
methods: {
...mapActions({
changeCollapse: 'settings/changeCollapse',
}),
handleCollapse() {
this.changeCollapse()
},
async refreshRoute() {
this.$baseEventBus.$emit('reload-router-view')
this.pulse = true
this.timeOutID = setTimeout(() => {
this.pulse = false
}, 1000)
},
},
beforeDestroy() {
clearTimeout(this.timeOutID)
},
}
</script>
<style lang="scss" scoped>
.nav-container {
position: relative;
height: $base-nav-bar-height;
padding-right: $base-padding;
padding-left: $base-padding;
overflow: hidden;
user-select: none;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.06), 0 4px 12px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.6);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.3) 100%);
pointer-events: none;
}
.left-panel {
position: relative;
display: flex;
align-items: center;
justify-items: center;
height: $base-nav-bar-height;
}
::v-deep {
.fold-unfold {
margin-right: 12px;
}
svg {
width: 1em;
height: 1em;
padding: 9px;
color: rgba(0, 0, 0, 0.7);
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 50%;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
&:hover {
color: rgba(0, 0, 0, 0.9);
background: rgba(255, 255, 255, 0.9);
border-color: rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.08);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05);
}
}
button {
svg {
margin-right: 0;
color: rgba(255, 255, 255, 0.95);
background: linear-gradient(135deg, rgba(77, 138, 240, 0.9) 0%, rgba(52, 120, 246, 0.95) 100%);
border-color: rgba(77, 138, 240, 0.8);
cursor: pointer;
fill: rgba(255, 255, 255, 0.95);
&:hover {
background: linear-gradient(135deg, rgba(77, 138, 240, 1) 0%, rgba(52, 120, 246, 1) 100%);
border-color: rgba(77, 138, 240, 1);
box-shadow: 0 4px 12px rgba(77, 138, 240, 0.3), 0 2px 4px rgba(77, 138, 240, 0.2);
}
}
}
.el-badge {
margin-right: 0;
.el-button {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 12px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.08);
}
}
}
.user-name {
color: rgba(0, 0, 0, 0.8);
font-weight: 500;
}
.user-avatar {
border: 2px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
}
}
.right-panel {
position: relative;
display: flex;
align-content: center;
align-items: center;
justify-content: flex-end;
height: $base-nav-bar-height;
gap: 12px;
}
}
</style>

View File

@ -0,0 +1,20 @@
<template>
<el-col :span="24">
<div class="bottom-panel">
<slot></slot>
</div>
</el-col>
</template>
<script>
export default {
name: 'VabQueryFormBottomPanel',
props: {},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>

View File

@ -0,0 +1,25 @@
<template>
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
<div class="left-panel">
<slot></slot>
</div>
</el-col>
</template>
<script>
export default {
name: 'VabQueryFormLeftPanel',
props: {
span: {
type: Number,
default: 14,
},
},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>

View File

@ -0,0 +1,25 @@
<template>
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
<div class="right-panel">
<slot></slot>
</div>
</el-col>
</template>
<script>
export default {
name: 'VabQueryFormRightPanel',
props: {
span: {
type: Number,
default: 10,
},
},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>

View File

@ -0,0 +1,20 @@
<template>
<el-col :span="24">
<div class="top-panel">
<slot></slot>
</div>
</el-col>
</template>
<script>
export default {
name: 'VabQueryFormTopPanel',
props: {},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>

View File

@ -0,0 +1,63 @@
<template>
<el-row :gutter="0" class="vab-query-form">
<slot></slot>
</el-row>
</template>
<script>
export default {
name: 'VabQueryForm',
props: {},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>
<style lang="scss" scoped>
@mixin panel {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
}
.vab-query-form {
margin-bottom: 10px;
::v-deep {
.top-panel {
@include panel;
}
.bottom-panel {
@include panel;
padding-top: 14px;
border-top: 1px solid #dcdfe6;
}
.left-panel {
@include panel;
> .el-button,
.el-form-item {
margin: 5px;
}
}
.right-panel {
@include panel;
justify-content: flex-end;
.el-form-item {
margin: 5px;
}
}
}
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<el-menu-item :index="handlePath(routeChildren.path)" @click="handleLink">
<vab-icon
v-if="routeChildren.meta.icon"
:icon="['fas', routeChildren.meta.icon]"
class="vab-fas-icon"
/>
<span>{{ routeChildren.meta.title }}</span>
<el-tag
v-if="routeChildren.meta && routeChildren.meta.badge"
effect="dark"
type="danger"
>
{{ routeChildren.meta.badge }}
</el-tag>
</el-menu-item>
</template>
<script>
import { isExternal } from '@/utils/validate'
import path from 'path'
export default {
name: 'VabMenuItem',
props: {
routeChildren: {
type: Object,
default() {
return null
},
},
item: {
type: Object,
default() {
return null
},
},
fullPath: {
type: String,
default: '',
},
},
methods: {
handlePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.fullPath)) {
return this.fullPath
}
return path.resolve(this.fullPath, routePath)
},
handleLink() {
const routePath = this.routeChildren.path
const target = this.routeChildren.meta.target
if (target === '_blank') {
if (isExternal(routePath)) {
window.open(routePath)
} else if (isExternal(this.fullPath)) {
window.open(this.fullPath)
} else if (
this.$route.path !== path.resolve(this.fullPath, routePath)
) {
let routeData = this.$router.resolve(
path.resolve(this.fullPath, routePath)
)
window.open(routeData.href)
}
} else {
if (isExternal(routePath)) {
window.location.href = routePath
} else if (isExternal(this.fullPath)) {
window.location.href = this.fullPath
} else if (
this.$route.path !== path.resolve(this.fullPath, routePath)
) {
this.$router.push(path.resolve(this.fullPath, routePath))
}
}
},
},
}
</script>

View File

@ -0,0 +1,96 @@
<template>
<component :is="menuComponent" v-if="!item.hidden" :full-path="fullPath" :item="item" :route-children="routeChildren">
<template v-if="item.children && item.children.length">
<vab-side-item v-for="route in item.children" :key="route.path" :full-path="handlePath(route.path)" :item="route" />
</template>
</component>
</template>
<script>
import { isExternal } from '@/utils/validate'
import path from 'path'
export default {
name: 'VabSideItem',
props: {
item: {
type: Object,
required: true,
},
fullPath: {
type: String,
default: '',
},
},
data() {
this.onlyOneChild = null
return {}
},
computed: {
menuComponent() {
if (
this.handleChildren(this.item.children, this.item) &&
(!this.routeChildren.children || this.routeChildren.notShowChildren) &&
!this.item.alwaysShow
) {
return 'VabMenuItem'
} else {
return 'VabSubmenu'
}
},
},
methods: {
handleChildren(children = [], parent) {
if (children === null) children = []
const showChildren = children.filter((item) => {
if (item.hidden) {
return false
} else {
this.routeChildren = item
return true
}
})
if (showChildren.length === 1) {
return true
}
if (showChildren.length === 0) {
this.routeChildren = {
...parent,
path: '',
notShowChildren: true,
}
return true
}
return false
},
handlePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.fullPath)) {
return this.fullPath
}
return path.resolve(this.fullPath, routePath)
},
},
}
</script>
<style lang="scss" scoped>
.vab-nav-icon {
margin-right: 4px;
}
::v-deep {
.el-tag {
float: right;
height: 16px;
padding-right: 4px;
padding-left: 4px;
margin-top: calc((#{$base-menu-item-height} - 16px) / 2);
line-height: 16px;
border: 0;
}
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<el-submenu ref="subMenu" :index="handlePath(item.path)" :popper-append-to-body="false">
<template slot="title">
<vab-icon v-if="item.meta && item.meta.icon" :icon="['fas', item.meta.icon]" class="vab-fas-icon" />
<vab-remix-icon v-if="item.meta && item.meta.remixIcon" :icon-class="item.meta.remixIcon" class="vab-remix-icon" />
<span>{{ item.meta.title }}</span>
</template>
<slot />
</el-submenu>
</template>
<script>
import { isExternal } from '@/utils/validate'
import path from 'path'
export default {
name: 'VabSubmenu',
props: {
routeChildren: {
type: Object,
default() {
return null
},
},
item: {
type: Object,
default() {
return null
},
},
fullPath: {
type: String,
default: '',
},
},
methods: {
handlePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.fullPath)) {
return this.fullPath
}
return path.resolve(this.fullPath, routePath)
},
},
}
</script>

View File

@ -0,0 +1,141 @@
<template>
<el-scrollbar :class="{ 'is-collapse': collapse }" class="side-container">
<vab-logo />
<el-menu
active-text-color=" hsla(0, 0%, 100%, 0.95)"
background-color="#191a23"
:collapse="collapse"
:collapse-transition="false"
:default-active="activeMenu"
:default-openeds="defaultOpens"
text-color=" hsla(0, 0%, 100%, 0.95)"
:unique-opened="uniqueOpened"
mode="vertical"
>
<template v-for="route in routes">
<vab-side-item :full-path="route.path" :item="route" />
</template>
</el-menu>
</el-scrollbar>
</template>
<script>
import variables from '@/styles/variables.scss'
import { mapGetters } from 'vuex'
import { defaultOopeneds, uniqueOpened } from '@/config'
export default {
name: 'VabSide',
data() {
return {
uniqueOpened,
}
},
computed: {
...mapGetters({
collapse: 'settings/collapse',
routes: 'routes/routes',
}),
defaultOpens() {
if (this.collapse) {
}
return defaultOopeneds
},
activeMenu() {
const route = this.$route
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
variables() {
return variables
},
},
}
</script>
<style lang="scss" scoped>
@mixin active {
&:hover {
color: $base-color-white;
background-color: $base-menu-background-active !important;
}
&.is-active {
color: $base-color-white;
background-color: $base-menu-background-active !important;
}
}
.side-container {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: $base-z-index;
width: $base-left-menu-width;
height: 100vh;
overflow: hidden;
background: $base-menu-background;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
transition: width $base-transition-time;
&.is-collapse {
width: $base-left-menu-width-min;
border-right: 0;
::v-deep {
.el-menu {
transition: width $base-transition-time;
}
.el-menu--collapse {
border-right: 0;
.el-submenu__icon-arrow {
right: 10px;
margin-top: -3px;
}
.el-menu-item,
.el-submenu {
text-align: center;
}
}
}
}
::v-deep {
.el-scrollbar__wrap {
overflow-x: hidden;
}
.el-menu {
border: 0;
.vab-fas-icon {
padding-right: 3px;
font-size: $base-font-size-default;
display: inline-block;
width: 14px;
}
.vab-remix-icon {
padding-right: 3px;
font-size: $base-font-size-default + 2;
}
}
.el-menu-item,
.el-submenu__title {
height: $base-menu-item-height;
line-height: $base-menu-item-height;
vertical-align: middle;
}
.el-menu-item {
@include active;
}
}
}
</style>

View File

@ -0,0 +1,761 @@
<template>
<div id="tabs-container" class="tabs-container">
<el-tabs v-model="tabActive" class="tabs-content" type="card" @tab-click="handleTabClick" @tab-remove="handleTabRemove">
<el-tab-pane v-for="item in visitedRoutes" :key="item.path" :closable="!isAffix(item)" :name="item.path">
<template slot="label">
<vab-icon v-if="getTabIcon(item)" :icon="['fas', getTabIcon(item)]" class="tab-icon" />
<span>{{ item.meta.title }}</span>
</template>
</el-tab-pane>
</el-tabs>
<el-dropdown v-if="false">
<div @mouseenter="handleMouseEnter">
<i class="el-icon-message-solid el-icon--right"></i>
<span style="cursor: pointer;" >
消息
</span>
<span style="color:red;">({{messageList.length + total }})</span>
</div>
<el-dropdown-menu slot="dropdown" class="tabs-more">
<div style="display: flex;justify-content: center;margin-top:20px;">
<h3 class="message">
消息通知
</h3>
<span style="color:red;">({{ messageList.length + total }})</span>
</div>
<div class="allmark" @click="allRead" v-if="messageList.length + total != 0 ">
<p class="y">全部已读</p>
<!-- <img class="gg" alt="" src="@/assets/g.png" /> -->
</div>
<div class="order" v-for="(item, index) in list"
:key="item.id">
<div :class="[ markIndex2 == index? ' order-item' : 'order-item2' ]" @mouseenter="marklist2(item,index)" @mouseleave="handleMouseLeave">
<div class="tick">
<div class="time">
{{item.created_at}}
</div>
<div class="mark" v-if="markIndex2 == index" @click="markRead(item)">
<p class="y">标记已读</p>
<img class="gg" alt="" src="@/assets/g.png" />
</div>
</div>
<div class="od-item2">
{{item.content}}
</div>
</div>
</div>
<div class="order" v-for="(item, index) in messageList"
:key="index">
<div :class="[ markIndex == index? ' order-item' : 'order-item2' ]" @mouseenter="marklist(item,index)" @mouseleave="handleMouseLeave2">
<div class="tick">
<div class="time">
{{item.date}}
</div>
<div class="mark" v-if="markIndex == index" @click="markRead2(item)">
<p class="y">标记已读</p>
<img class="gg" alt="" src="@/assets/g.png" />
</div>
</div>
<div class="od-item2">
{{item.content}}
</div>
</div>
</div>
<!-- <el-dropdown-item command="closeOtherstabs">
<vab-icon :icon="['fas', 'times-circle']" />
关闭其他
</el-dropdown-item>
<el-dropdown-item command="closeLefttabs">
<vab-icon :icon="['fas', 'arrow-alt-circle-left']"></vab-icon>
关闭左侧
</el-dropdown-item>
<el-dropdown-item command="closeRighttabs">
<vab-icon :icon="['fas', 'arrow-alt-circle-right']"></vab-icon>
关闭右侧
</el-dropdown-item>
<el-dropdown-item command="closeAlltabs">
<vab-icon :icon="['fas', 'ban']"></vab-icon>
关闭全部
</el-dropdown-item> -->
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
import path from 'path'
import { mapGetters } from 'vuex'
import { unreadOtice,markNotice } from '@/api/table'
import { recordRoute } from '@/config'
export default {
name: 'VabTabs',
data() {
return {
list:[],
total:'',
affixtabs: [],
tabActive: '',
markIndex:-1,
markIndex2:-1,
mark:false,
messages: [],
ws: null,
messageList:[]
}
},
computed: {
...mapGetters({
visitedRoutes: 'tabsBar/visitedRoutes',
routes: 'routes/routes',
}),
},
watch: {
$route: {
handler(route) {
this.inittabs()
this.addtabs()
let tabActive = ''
this.visitedRoutes.forEach((item, index) => {
if (item.path === this.$route.path) {
tabActive = item.path
}
})
this.tabActive = tabActive
},
immediate: true,
},
},
created(){
// this.connect();
// this.fetchData()
},
mounted() {
// console.log(111)
setTimeout(() => {
this.$store.dispatch('user/logout')
const fullPath = this.$route.fullPath
this.$router.push(`/login?redirect=${fullPath}`)
},
86400000);
},
methods: {
async fetchData() {
this.listLoading = true
const { data, total } = await unreadOtice(this.queryForm)
this.list = data
// console.log( this.list,'--')
this.total = total
this.timeOutID = setTimeout(() => {
this.listLoading = false
}, 500)
},
connect() {
this.ws = new WebSocket('wss://dev.wagoo.pet/wagoo/1.1/ws/admin'); // 确保使用wss加密的WebSocket或ws非加密的WebSocket
this.ws.onopen = this.onOpen;
this.ws.onmessage = this.onMessage;
this.ws.onerror = this.onError;
this.ws.onclose = this.onClose;
},
onOpen() {
console.log('已经链接到服务');
setTimeout(() => {
this.ws.send('Hello Server!');
}, 500)
},
onMessage(event) {
// console.log(event,'--')
this.messageList.push(JSON.parse(event.data))
this.messageList.reverse()
// console.log( this.messageList,'什么东东')
// console.log( this.messageList,'???')
// const message = JSON.parse(event.data);
// this.messages.push(message); // 更新消息列表
},
onError(event) {
console.error('WebSocket连接出错:', event);
},
onClose(event) {
console.log('WebSocket连接已关闭', event);
this.connect()
// 可以在这里重新连接例如使用setTimeout或setInterval
},
disconnect() {
if (this.ws) {
this.ws.close();
}
},
beforeDestroy() {
this.disconnect(); // 组件销毁时关闭WebSocket连接
},
async markRead(it){
const queryForm = {
id:it.id
}
const res = await markNotice(queryForm)
if(res.code === 200){
this.fetchData()
}
// console.log(it,'??')
},
async allRead(){
this.messageList = []
const queryForm = {
id:'0'
}
const res = await markNotice(queryForm)
if(res.code === 200){
this.fetchData()
}
},
async markRead2(it){
console.log(it,'?')
const queryForm = {
id:it.id
}
const res = await markNotice(queryForm)
if(res.code === 200){
this.fetchData()
}
// console.log(it,'??')
},
marklist(item,index){
this.markIndex = index
// console.log( item);
},
marklist2(item,index){
this.markIndex2 = index
// console.log( item);
},
handleMouseLeave2(){
this.markIndex = -1
},
handleMouseLeave(){
// console.log(111)
this.markIndex2 = -1
},
handleMouseEnter(event) {
// this.fetchData()
// console.log('鼠标移入事件被触发', event);
// 在这里编写你的处理代码
},
async handleTabRemove(tabActive) {
let view
this.visitedRoutes.forEach((item, index) => {
if (tabActive == item.path) {
view = item
}
})
const { visitedRoutes } = await this.$store.dispatch('tabsBar/delRoute', view)
if (this.isActive(view)) {
this.toLastTag(visitedRoutes, view)
}
},
handleTabClick(tab) {
const route = this.visitedRoutes.filter((item, index) => {
if (tab.index == index) return item
})[0]
if (this.$route.path !== route.path) {
this.$router.push({
path: route.path,
query: route.query,
fullPath: route.fullPath,
})
} else {
return false
}
},
isActive(route) {
return route.path === this.$route.path
},
isAffix(tag) {
return tag.meta && tag.meta.affix
},
filterAffixtabs(routes, basePath = '/') {
let tabs = []
routes.forEach((route) => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tabs.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta },
})
}
if (route.children) {
const temptabs = this.filterAffixtabs(route.children, route.path)
if (temptabs.length >= 1) {
tabs = [...tabs, ...temptabs]
}
}
})
return tabs
},
inittabs() {
const affixtabs = (this.affixtabs = this.filterAffixtabs(this.routes))
for (const tag of affixtabs) {
if (tag.name) {
this.$store.dispatch('tabsBar/addVisitedRoute', tag)
}
}
},
addtabs() {
const { name } = this.$route
if (name) {
this.$store.dispatch('tabsBar/addVisitedRoute', this.$route)
}
return false
},
handleCommand(command) {
switch (command) {
case 'refreshRoute':
this.refreshRoute()
break
case 'closeOtherstabs':
this.closeOtherstabs()
break
case 'closeLefttabs':
this.closeLefttabs()
break
case 'closeRighttabs':
this.closeRighttabs()
break
case 'closeAlltabs':
this.closeAlltabs()
break
}
},
async refreshRoute() {
this.$baseEventBus.$emit('reloadrouter-view')
},
async closeSelectedTag(view) {
const { visitedRoutes } = await this.$store.dispatch('tabsBar/delRoute', view)
if (this.isActive(view)) {
this.toLastTag(visitedRoutes, view)
}
},
async closeOtherstabs() {
const view = await this.toThisTag()
await this.$store.dispatch('tabsBar/delOthersRoutes', view)
},
async closeLefttabs() {
const view = await this.toThisTag()
await this.$store.dispatch('tabsBar/delLeftRoutes', view)
},
async closeRighttabs() {
const view = await this.toThisTag()
await this.$store.dispatch('tabsBar/delRightRoutes', view)
},
async closeAlltabs() {
const view = await this.toThisTag()
const { visitedRoutes } = await this.$store.dispatch('tabsBar/delAllRoutes')
if (this.affixtabs.some((tag) => tag.path === view.path)) {
return
}
this.toLastTag(visitedRoutes, view)
},
toLastTag(visitedRoutes, view) {
const latestView = visitedRoutes.slice(-1)[0]
if (latestView) {
this.$router.push(latestView)
} else {
this.$router.push('/')
}
},
async toThisTag() {
const view = this.visitedRoutes.filter((item, index) => {
if (item.path === this.$route.fullPath) {
return item
}
})[0]
if (this.$route.path !== view.path) this.$router.push(view)
return view
},
getTabIcon(item) {
// 如果当前路由有图标,直接返回
if (item.meta && item.meta.icon) {
return item.meta.icon
}
// 查找父级路由的图标
const parentIcon = this.findParentIcon(item.path)
if (parentIcon) {
return parentIcon
}
return null
},
findParentIcon(path) {
// 从路径中提取父级路径
const pathParts = path.split('/').filter((part) => part)
if (pathParts.length <= 1) {
return null
}
// 查找父级路由
const findRouteByPath = (routes, targetPath) => {
for (const route of routes) {
if (route.path === targetPath) {
return route
}
if (route.children) {
const found = findRouteByPath(route.children, targetPath)
if (found) return found
}
}
return null
}
// 尝试查找父级路径
for (let i = pathParts.length - 1; i > 0; i--) {
const parentPath = '/' + pathParts.slice(0, i).join('/')
const parentRoute = findRouteByPath(this.routes, parentPath)
if (parentRoute && parentRoute.meta && parentRoute.meta.icon) {
return parentRoute.meta.icon
}
}
return null
},
},
}
</script>
<style lang="scss" scoped>
.tabs-container {
position: relative;
box-sizing: border-box;
display: flex;
align-content: center;
align-items: center;
justify-content: space-between;
height: 54px;
padding-right: $base-padding;
padding-left: $base-padding;
user-select: none;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.6);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0.2) 100%);
pointer-events: none;
}
::v-deep {
.fold-unfold {
margin-right: $base-padding;
}
}
.tabs-content {
position: relative;
width: calc(100% - 90px);
height: $base-tag-item-height;
::v-deep {
.el-tabs__nav-next,
.el-tabs__nav-prev {
height: $base-tag-item-height;
line-height: $base-tag-item-height;
color: rgba(0, 0, 0, 0.6);
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
&:hover {
color: rgba(0, 0, 0, 0.8);
background: rgba(255, 255, 255, 0.8);
border-color: rgba(255, 255, 255, 1);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.05);
}
}
.el-tabs__header {
border-bottom: 0;
.el-tabs__nav {
border: 0;
}
.el-tabs__item {
position: relative;
box-sizing: border-box;
height: $base-tag-item-height;
margin-right: 8px;
margin-top: 3px;
padding: 0 20px;
line-height: $base-tag-item-height;
border: 1px solid $base-color-default;
color: $base-color-default;
border-radius: 5px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
overflow: hidden;
.tab-icon {
margin-right: 6px;
font-size: 12px;
}
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
&:hover {
color: rgba(255, 255, 255, 0.95);
background: $base-color-default;
border-color: $base-color-default;
&::before {
left: 100%;
}
}
&.is-active {
color: rgba(255, 255, 255, 0.95);
background: $base-color-default;
border-color: $base-color-default;
}
.el-icon-close {
position: relative;
margin-left: 8px;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 5px;
padding: 2px;
&:hover {
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.2);
transform: scale(1.2);
}
}
}
}
}
}
.more {
position: relative;
display: flex;
align-content: center;
align-items: center;
padding: 8px 16px;
color: rgba(0, 0, 0, 0.7);
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 16px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
&:hover {
color: rgba(0, 0, 0, 0.9);
background: rgba(255, 255, 255, 0.8);
border-color: rgba(255, 255, 255, 1);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.05);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05);
}
}
}
</style>
<style lang="scss">
.tabs-more {
border-radius:15px;
width: 550px;
height: 550px;
position: relative;
overflow: auto;
.allmark{
font-size: 12px;
padding: 0 10px;
position: absolute;
right: 10px;
top: 20px;
// width: 60px;
height: 30px;
border: 1px solid #ccc;
font-weight: 600;
border-radius: 30px;
display: flex;
justify-content: center;
align-items: center;
.gg{
width: 15px;
height: 15px;
}
}
.message{
display: flex;
justify-content: center;
margin: 0;
// display: inline-block;
}
.order{
padding:0 20px 0 20px;
.od-item{
color:#9B939A;
margin-top: 30px;
}
.order-item{
border-radius: 5px;
margin-top: 10px;
background: #fbf9f9;
height: 50px;
padding: 5px;
position: relative;
.tick{
display: flex;
align-items: center;
justify-content: space-between;
.mark{
position: absolute;
right: 10px;
top: 10px;
font-size: 12px;
padding: 0 10px;
// width: 60px;
height: 30px;
border: 1px solid #ccc;
font-weight: 600;
border-radius: 30px;
display: flex;
justify-content: center;
align-items: center;
.gg{
width: 15px;
height: 15px;
margin-left: 4px;
}
}
.time{
display: flex;
justify-content: center;
font-size: 12px;
color:#887a86;
}
}
.od-item2{
color:#353535;
margin-top: 10px;
}
}
.order-item2{
margin-top: 10px;
height: 50px;
padding: 5px;
position: relative;
.tick{
display: flex;
align-items: center;
justify-content: space-between;
.mark{
font-size: 12px;
padding: 0 10px;
position: absolute;
right: 10px;
top: 10px;
// width: 60px;
height: 30px;
border: 1px solid #ccc;
font-weight: 600;
border-radius: 30px;
display: flex;
justify-content: center;
align-items: center;
.gg{
width: 15px;
height: 15px;
}
}
.time{
display: flex;
justify-content: center;
font-size: 12px;
color:#887a86;
}
}
.od-item2{
color:#353535;
margin-top: 10px;
}
}
}
.el-dropdown-menu__item {
padding: 8px 20px;
font-size: 14px;
line-height: 1.5;
&:not(:last-child) {
margin-bottom: 4px;
}
.vab-icon {
margin-right: 8px;
width: 16px;
text-align: center;
}
&:hover {
background: rgba(77, 138, 240, 0.1);
color: #4d8af0;
border-radius: 6px;
}
}
}
</style>

View File

@ -0,0 +1,600 @@
<template>
<span v-if="themeBar">
<vab-icon :icon="['fas', 'crown']" title="主题配置" @click="handleOpenTheme" />
<div class="theme-setting">
<div @click="handleOpenTheme">
<vab-icon :icon="['fas', 'crown']" />
<p>主题配置</p>
</div>
<div @click="handleGetCode">
<vab-icon :icon="['fas', 'laptop-code']"></vab-icon>
<p>拷贝源码</p>
</div>
</div>
<el-drawer :visible.sync="drawerVisible" append-to-body direction="rtl" size="300px" title="主题配置">
<el-scrollbar style="height: 80vh; overflow: hidden">
<div class="el-drawer__body">
<div class="theme-config-container">
<div class="config-section">
<div class="section-header">
<i class="el-icon-picture-outline"></i>
<span>主题风格</span>
</div>
<div class="theme-options">
<div
class="theme-option"
:class="{ active: theme.name === 'default' }"
@click="
theme.name = 'default'
handleSaveTheme()
"
>
<div class="theme-preview default-theme">
<div class="preview-header"></div>
<div class="preview-sidebar"></div>
<div class="preview-content"></div>
</div>
<span class="theme-name">默认主题</span>
</div>
<div
class="theme-option"
:class="{ active: theme.name === 'green' }"
@click="
theme.name = 'green'
handleSaveTheme()
"
>
<div class="theme-preview green-theme">
<div class="preview-header"></div>
<div class="preview-sidebar"></div>
<div class="preview-content"></div>
</div>
<span class="theme-name">绿荫草场</span>
</div>
<div
class="theme-option"
:class="{ active: theme.name === 'glory' }"
@click="
theme.name = 'glory'
handleSaveTheme()
"
>
<div class="theme-preview glory-theme">
<div class="preview-header"></div>
<div class="preview-sidebar"></div>
<div class="preview-content"></div>
</div>
<span class="theme-name">荣耀典藏</span>
</div>
</div>
</div>
<div class="config-section">
<div class="section-header">
<i class="el-icon-s-grid"></i>
<span>布局设置</span>
</div>
<div class="layout-options">
<div
class="layout-option"
:class="{ active: theme.layout === 'vertical' }"
@click="
theme.layout = 'vertical'
handleSaveTheme()
"
>
<div class="layout-preview vertical-layout">
<div class="preview-header"></div>
<div class="preview-sidebar"></div>
<div class="preview-content"></div>
</div>
<span class="layout-name">纵向布局</span>
</div>
<div
class="layout-option"
:class="{ active: theme.layout === 'horizontal' }"
@click="
theme.layout = 'horizontal'
handleSaveTheme()
"
>
<div class="layout-preview horizontal-layout">
<div class="preview-header"></div>
<div class="preview-main">
<div class="preview-sidebar"></div>
<div class="preview-content"></div>
</div>
</div>
<span class="layout-name">横向布局</span>
</div>
</div>
</div>
<div class="config-section">
<div class="section-header">
<i class="el-icon-setting"></i>
<span>功能设置</span>
</div>
<div class="feature-options">
<div class="feature-item">
<div class="feature-info">
<span class="feature-name">固定头部</span>
<span class="feature-desc">头部始终固定在页面顶部</span>
</div>
<el-switch v-model="theme.header" active-value="fixed" inactive-value="noFixed" @change="handleSaveTheme" />
</div>
<div class="feature-item">
<div class="feature-info">
<span class="feature-name">多标签页</span>
<span class="feature-desc">开启页面标签页功能</span>
</div>
<el-switch v-model="theme.tabsBar" active-value="true" inactive-value="false" @change="handleSaveTheme" />
</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
<div class="el-drawer__footer">
<el-button type="primary" @click="handleSaveTheme">保存设置</el-button>
<el-button @click="drawerVisible = false">取消</el-button>
</div>
</el-drawer>
</span>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { layout as defaultLayout } from '@/config'
export default {
name: 'VabTheme',
data() {
return {
drawerVisible: false,
theme: {
name: 'default',
layout: '',
header: 'fixed',
tabsBar: '',
},
}
},
computed: {
...mapGetters({
layout: 'settings/layout',
header: 'settings/header',
tabsBar: 'settings/tabsBar',
themeBar: 'settings/themeBar',
}),
},
created() {
const handleTheme = () => {
this.handleOpenTheme()
}
this.$baseEventBus.$on('theme', handleTheme)
const theme = localStorage.getItem('vue-admin-better-theme')
if (null !== theme) {
this.theme = JSON.parse(theme)
this.handleSaveTheme()
} else {
this.theme.layout = this.layout
this.theme.header = this.header
this.theme.tabsBar = this.tabsBar
}
this.$once('hook:beforeDestroy', () => {
this.$baseEventBus.$off('theme', handleTheme)
})
},
methods: {
...mapActions({
changeLayout: 'settings/changeLayout',
changeHeader: 'settings/changeHeader',
changeTabsBar: 'settings/changeTabsBar',
}),
handleIsMobile() {
return document.body.getBoundingClientRect().width - 1 < 992
},
handleOpenTheme() {
this.drawerVisible = true
},
handleSaveTheme() {
let { name, layout, header, tabsBar } = this.theme
localStorage.setItem(
'vue-admin-better-theme',
`{
"name":"${name}",
"layout":"${layout}",
"header":"${header}",
"tabsBar":"${tabsBar}"
}`
)
if (!this.handleIsMobile()) this.changeLayout(layout)
this.changeHeader(header)
this.changeTabsBar(tabsBar)
document.getElementsByTagName('body')[0].className = `vue-admin-better-theme-${name}`
this.drawerVisible = false
},
handleGetCode() {
const url = 'https://github.com/zxwk1998/vue-admin-better/tree/master/src/views'
let path = this.$route.path + '/index.vue'
if (path === '/vab/menu1/menu1-1/menu1-1-1/index.vue') {
path = '/vab/nested/menu1/menu1-1/menu1-1-1/index.vue'
}
if (path === '/vab/icon/awesomeIcon/index.vue') {
path = '/vab/icon/index.vue'
}
if (path === '/vab/icon/remixIcon/index.vue') {
path = '/vab/icon/remixIcon.vue'
}
if (path === '/vab/icon/colorfulIcon/index.vue') {
path = '/vab/icon/colorfulIcon.vue'
}
if (path === '/vab/table/comprehensiveTable/index.vue') {
path = '/vab/table/index.vue'
}
if (path === '/vab/table/inlineEditTable/index.vue') {
path = '/vab/table/inlineEditTable.vue'
}
window.open(url + path)
},
},
}
</script>
<style lang="scss" scoped>
@mixin right-bar {
position: fixed;
right: 0;
z-index: $base-z-index;
width: 60px;
min-height: 60px;
text-align: center;
cursor: pointer;
background: $base-color-blue;
border-radius: $base-border-radius;
> div {
padding-top: 10px;
border-bottom: 0 !important;
&:hover {
opacity: 0.9;
}
& + div {
border-top: 1px solid $base-color-white;
}
p {
padding: 0;
margin: 0;
font-size: $base-font-size-small;
line-height: 30px;
color: $base-color-white;
}
}
}
.theme-setting {
@include right-bar;
top: calc((100vh - 110px) / 2);
::v-deep {
svg:not(:root).svg-inline--fa {
display: block;
margin-right: auto;
margin-left: auto;
color: $base-color-white;
}
.svg-icon {
display: block;
margin-right: auto;
margin-left: auto;
font-size: 20px;
color: $base-color-white;
fill: $base-color-white;
}
}
}
.el-drawer__body {
padding: 20px;
}
.el-drawer__footer {
border-top: 1px solid #dedede;
position: fixed;
bottom: 0;
width: 100%;
padding: 10px 0 0 20px;
height: 50px;
}
.theme-config-container {
.config-section {
margin-bottom: 30px;
.section-header {
display: flex;
align-items: center;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
i {
margin-right: 8px;
font-size: 16px;
color: #677ae4;
}
span {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.theme-options {
display: flex;
flex-direction: column;
gap: 12px;
.theme-option {
display: flex;
align-items: center;
padding: 12px;
border: 2px solid #f0f0f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #677ae4;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
}
&.active {
border-color: #677ae4;
background: rgba(64, 158, 255, 0.05);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
.theme-preview {
width: 60px;
height: 40px;
margin-right: 12px;
border-radius: 4px;
overflow: hidden;
position: relative;
.preview-header {
height: 8px;
background: #f0f0f0;
}
.preview-sidebar {
position: absolute;
left: 0;
top: 8px;
width: 12px;
height: 32px;
background: #e0e0e0;
}
.preview-content {
position: absolute;
left: 12px;
top: 8px;
width: 48px;
height: 32px;
background: #fafafa;
}
&.default-theme {
.preview-header {
background: $base-color-default;
}
.preview-sidebar {
background: #2c3e50;
}
.preview-content {
background: #ffffff;
}
}
&.green-theme {
.preview-header {
background: #67c23a;
}
.preview-sidebar {
background: #2d5a27;
}
.preview-content {
background: #f0f9ff;
}
}
&.glory-theme {
.preview-header {
background: #e6a23c;
}
.preview-sidebar {
background: #8b4513;
}
.preview-content {
background: #fff8e1;
}
}
}
.theme-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
}
}
.layout-options {
display: flex;
flex-direction: column;
gap: 12px;
.layout-option {
display: flex;
align-items: center;
padding: 12px;
border: 2px solid #f0f0f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #677ae4;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
}
&.active {
border-color: #677ae4;
background: rgba(64, 158, 255, 0.05);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
.layout-preview {
width: 60px;
height: 40px;
margin-right: 12px;
border-radius: 4px;
overflow: hidden;
position: relative;
.preview-header {
height: 8px;
background: #677ae4;
}
.preview-main {
position: absolute;
left: 0;
top: 8px;
width: 100%;
height: 32px;
display: flex;
.preview-sidebar {
background: #2c3e50;
}
.preview-content {
background: #ffffff;
}
}
&.vertical-layout {
.preview-header {
height: 8px;
background: #677ae4;
}
.preview-sidebar {
position: absolute;
left: 0;
top: 8px;
width: 12px;
height: 32px;
background: #2c3e50;
}
.preview-content {
position: absolute;
left: 12px;
top: 8px;
width: 48px;
height: 32px;
background: #ffffff;
}
}
&.horizontal-layout {
.preview-main {
.preview-sidebar {
width: 100%;
height: 8px;
}
.preview-content {
width: 100%;
height: 24px;
margin-top: 8px;
}
}
}
}
.layout-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
}
}
.feature-options {
.feature-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.feature-info {
flex: 1;
margin-right: 16px;
.feature-name {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.feature-desc {
display: block;
font-size: 12px;
color: #999;
line-height: 1.4;
}
}
}
}
}
}
</style>
<style lang="scss">
.el-drawer__wrapper {
outline: none !important;
* {
outline: none !important;
}
}
.vab-color-picker {
.el-color-dropdown__link-btn {
display: none;
}
}
</style>

View File

@ -0,0 +1,221 @@
<template>
<div class="top-container">
<div class="vab-main">
<el-row>
<el-col :lg="7" :md="7" :sm="7" :xl="7" :xs="7">
<vab-logo />
</el-col>
<el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="12">
<el-menu
active-text-color=" hsla(0, 0%, 100%, 0.95)"
background-color="#191a23"
:default-active="activeMenu"
text-color=" hsla(0, 0%, 100%, 0.95)"
menu-trigger="hover"
mode="horizontal"
>
<template v-for="route in routes">
<vab-side-item v-if="!route.hidden" :key="route.path" :full-path="route.path" :item="route" />
</template>
</el-menu>
</el-col>
<el-col :lg="5" :md="5" :sm="5" :xl="5" :xs="5">
<div class="right-panel">
<vab-error-log />
<div class="right-menu">
<vab-full-screen @refresh="refreshRoute" />
<vab-theme class="hidden-md-and-down" />
</div>
<vab-icon :icon="['fas', 'sync-alt']" :pulse="pulse" title="重载路由" @click="refreshRoute" />
<vab-avatar />
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import variables from '@/styles/variables.scss'
import { mapGetters } from 'vuex'
export default {
name: 'VabTop',
data() {
return {
pulse: false,
menuTrigger: 'hover',
}
},
computed: {
...mapGetters({
routes: 'routes/routes',
visitedRoutes: 'tabsBar/visitedRoutes',
}),
activeMenu() {
const route = this.$route
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
variables() {
return variables
},
},
methods: {
async refreshRoute() {
this.$baseEventBus.$emit('reload-router-view')
this.pulse = true
this.timeOutID = setTimeout(() => {
this.pulse = false
}, 1000)
},
},
beforeDestroy() {
clearTimeout(this.timeOutID)
},
}
</script>
<style lang="scss" scoped>
.top-container {
display: flex;
align-items: center;
justify-items: flex-end;
height: $base-top-bar-height;
background: $base-menu-background;
.vab-main {
background: $base-menu-background;
::v-deep {
.el-menu {
&.el-menu--horizontal {
display: flex;
align-items: center;
justify-content: flex-end;
height: $base-top-bar-height;
border-bottom: 0 solid transparent !important;
.el-menu-item,
.el-submenu__title {
padding: 0 15px;
}
@media only screen and (max-width: 767px) {
.el-menu-item,
.el-submenu__title {
padding: 0 8px;
}
li:nth-child(4),
li:nth-child(5) {
display: none !important;
}
}
> .el-menu-item {
height: $base-top-bar-height;
line-height: $base-top-bar-height;
}
> .el-submenu {
.el-submenu__title {
height: $base-top-bar-height;
line-height: $base-top-bar-height;
}
}
}
svg {
width: 1rem;
margin-right: 3px;
}
&--horizontal {
.el-menu {
.el-menu-item,
.el-submenu__title {
height: $base-menu-item-height;
line-height: $base-menu-item-height;
}
}
.el-submenu,
.el-menu-item {
&.is-active {
background-color: $base-color-blue !important;
border-bottom: 0 solid transparent !important;
.el-submenu__title {
border-bottom: 0 solid transparent !important;
}
}
}
> .el-menu-item {
.el-tag {
margin-top: calc(#{$base-top-bar-height} / 2 - 7.5px);
margin-left: 5px;
}
@media only screen and (max-width: 1199px) {
.el-tag {
display: none;
}
}
&.is-active {
background-color: transparent !important;
border-bottom: 3px solid $base-color-blue !important;
}
}
}
}
}
}
.right-panel {
display: flex;
align-items: center;
justify-content: flex-end;
height: $base-top-bar-height;
::v-deep {
.username,
.user-role {
color: rgba($base-color-white, 0.9);
}
.username + i {
color: rgba($base-color-white, 0.9);
}
svg {
width: 1em;
height: 1em;
margin-right: 15px;
font-size: $base-font-size-big;
color: rgba($base-color-white, 0.9);
cursor: pointer;
fill: rgba($base-color-white, 0.9);
}
button {
svg {
margin-right: 0;
color: rgba($base-color-white, 0.9);
cursor: pointer;
fill: rgba($base-color-white, 0.9);
}
}
.el-badge {
margin-right: 15px;
}
}
}
}
</style>

20
src/layouts/export.js Normal file
View File

@ -0,0 +1,20 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 公共布局及样式自动引入
*/
import Vue from 'vue'
const requireComponents = require.context('./components', true, /\.vue$/)
requireComponents.keys().forEach((fileName) => {
const componentConfig = requireComponents(fileName)
const componentName = componentConfig.default.name
Vue.component(componentName, componentConfig.default || componentConfig)
})
// 使用 require.context 安全地导入主题文件
const requireThemes = require.context('@/styles/themes', true, /\.scss$/)
requireThemes.keys().forEach((fileName) => {
// 使用 require.context 直接引入,避免动态字符串拼接
requireThemes(fileName)
})

298
src/layouts/index.vue Normal file
View File

@ -0,0 +1,298 @@
<template>
<div :class="classObj" class="vue-admin-better-wrapper">
<div
v-if="'horizontal' === layout"
:class="{
fixed: header === 'fixed',
'no-tabs-bar': tabsBar === 'false' || tabsBar === false,
}"
class="layout-container-horizontal"
>
<div :class="header === 'fixed' ? 'fixed-header' : ''">
<vab-top />
<div v-if="tabsBar === 'true' || tabsBar === true" :class="{ 'tag-view-show': tabsBar }">
<el-scrollbar>
<div class="vab-main main-padding"><vab-tabs /></div>
</el-scrollbar>
</div>
</div>
<div class="vab-main main-padding">
<vab-ad />
<vab-app-main />
</div>
</div>
<div
v-else
:class="{
fixed: header === 'fixed',
'no-tabs-bar': tabsBar === 'false' || tabsBar === false,
}"
class="layout-container-vertical"
>
<div v-if="device === 'mobile' && collapse === false" class="mask" @click="handleFoldSideBar" />
<vab-side />
<div :class="collapse ? 'is-collapse-main' : ''" class="vab-main">
<div :class="header === 'fixed' ? 'fixed-header' : ''">
<vab-nav />
<vab-tabs v-if="tabsBar === 'true' || tabsBar === true" />
</div>
<vab-ad />
<vab-app-main />
</div>
</div>
<el-backtop />
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { tokenName } from '@/config'
export default {
name: 'Layout',
data() {
return {
oldLayout: '',
controller: new window.AbortController(),
timeOutID: null,
}
},
computed: {
...mapGetters({
layout: 'settings/layout',
tabsBar: 'settings/tabsBar',
collapse: 'settings/collapse',
header: 'settings/header',
device: 'settings/device',
}),
classObj() {
return {
mobile: this.device === 'mobile',
}
},
},
beforeMount() {
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
this.controller.abort()
clearTimeout(this.timeOutID)
},
mounted() {
this.oldLayout = this.layout
const userAgent = navigator.userAgent
const isMobile = this.handleIsMobile()
if (isMobile) {
if (isMobile) {
//横向布局时如果是手机端访问那么改成纵向版
this.$store.dispatch('settings/changeLayout', 'vertical')
} else {
this.$store.dispatch('settings/changeLayout', this.oldLayout)
}
this.$store.dispatch('settings/toggleDevice', 'mobile')
this.timeOutID = setTimeout(() => {
this.$store.dispatch('settings/foldSideBar')
}, 2000)
} else {
this.$store.dispatch('settings/openSideBar')
}
this.$nextTick(() => {
window.addEventListener(
'storage',
(e) => {
if (e.key === tokenName || e.key === null) window.location.reload()
if (e.key === tokenName && e.value === null) window.location.reload()
},
{
capture: false,
signal: this.controller?.signal,
}
)
})
},
methods: {
...mapActions({
handleFoldSideBar: 'settings/foldSideBar',
}),
handleIsMobile() {
return document.body.getBoundingClientRect().width - 1 < 992
},
handleResize() {
if (!document.hidden) {
const isMobile = this.handleIsMobile()
if (isMobile) {
//横向布局时如果是手机端访问那么改成纵向版
this.$store.dispatch('settings/changeLayout', 'vertical')
} else {
this.$store.dispatch('settings/changeLayout', this.oldLayout)
}
this.$store.dispatch('settings/toggleDevice', isMobile ? 'mobile' : 'desktop')
}
},
},
}
</script>
<style lang="scss" scoped>
@mixin fix-header {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: $base-z-index - 2;
width: 100%;
overflow: hidden;
}
.vue-admin-better-wrapper {
position: relative;
width: 100%;
height: 100%;
.layout-container-horizontal {
position: relative;
&.fixed {
padding-top: calc(#{$base-top-bar-height} + #{$base-tabs-bar-height});
}
&.fixed.no-tabs-bar {
padding-top: $base-top-bar-height;
}
::v-deep {
.vab-main {
width: 88%;
margin: auto;
}
.fixed-header {
@include fix-header;
}
.tag-view-show {
background: $base-color-white;
box-shadow: $base-box-shadow;
}
.nav-container {
.fold-unfold {
display: none;
}
}
.main-padding {
.app-main-container {
margin-top: $base-padding;
margin-bottom: $base-padding;
background: $base-color-white;
}
}
}
}
.layout-container-vertical {
position: relative;
.mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: $base-z-index - 1;
width: 100%;
height: 100vh;
overflow: hidden;
background: #000;
opacity: 0.5;
}
&.fixed {
padding-top: calc(#{$base-nav-bar-height} + #{$base-tabs-bar-height});
}
&.fixed.no-tabs-bar {
padding-top: $base-nav-bar-height;
}
.vab-main {
position: relative;
min-height: 100%;
margin-left: $base-left-menu-width;
background: #f6f8f9;
transition: $base-transition;
::v-deep {
.fixed-header {
@include fix-header;
left: $base-left-menu-width;
width: $base-right-content-width;
box-shadow: $base-box-shadow;
transition: $base-transition;
}
.nav-container {
position: relative;
box-sizing: border-box;
}
.tabs-container {
box-sizing: border-box;
}
.app-main-container {
width: calc(100% - #{$base-padding} - #{$base-padding});
margin: $base-padding auto;
background: $base-color-white;
border-radius: $base-border-radius;
}
}
&.is-collapse-main {
margin-left: $base-left-menu-width-min;
::v-deep {
.fixed-header {
left: $base-left-menu-width-min;
width: calc(100% - 65px);
}
}
}
}
}
/* 手机端开始 */
&.mobile {
::v-deep {
.el-pager,
.el-pagination__jump {
display: none;
}
.layout-container-vertical {
.el-scrollbar.side-container.is-collapse {
width: 0;
}
.vab-main {
width: 100%;
margin-left: 0;
}
}
.vab-main {
.fixed-header {
left: 0 !important;
width: 100% !important;
}
}
}
}
/* 手机端结束 */
}
</style>

47
src/main.js Normal file
View File

@ -0,0 +1,47 @@
import Vue from 'vue'
import App from './App'
import store from './store'
import router from './router'
import './plugins'
import '@/layouts/export'
import { printLayoutsInfo } from '@/utils/printInfo'
import JSEncrypt from 'jsencrypt';
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 生产环境默认都使用mock如果正式用于生产环境时记得去掉
*/
// 检测环境变量或默认使用mock
// 生产环境
// const useMock = process.env.VUE_APP_MOCK_ENABLE === 'true' || process.env.NODE_ENV === 'development'
// development
// production
// 测试环境
const useMock = process.env.VUE_APP_MOCK_ENABLE === 'true' || process.env.NODE_ENV === 'production'
if (useMock) {
// 使用动态import替代require
import('@/utils/static').then(({ mockXHR }) => {
mockXHR()
console.log('已启用Mock拦截所有接口请求将被Mock拦截')
// 打印layouts/index.js中的信息到控制台
printLayoutsInfo()
Vue.config.productionTip = false
Vue.prototype.$jsEncrypt = JSEncrypt;
new Vue({
el: '#vue-admin-better',
router,
store,
render: (h) => h(App),
})
})
} else {
// 未启用Mock时直接打印layouts/index.js中的信息到控制台
printLayoutsInfo()
new Vue({
el: '#vue-admin-better',
router,
store,
render: (h) => h(App),
})
}

4
src/plugins/echarts.js Normal file
View File

@ -0,0 +1,4 @@
import 'echarts'
import VabChart from 'vue-echarts'
export default VabChart

7
src/plugins/element.js Normal file
View File

@ -0,0 +1,7 @@
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI, {
size: 'small',
})

13
src/plugins/index.js Normal file
View File

@ -0,0 +1,13 @@
/* 公共引入,勿随意修改,修改时需经过确认 */
import Vue from 'vue'
import './element'
import './support'
import '@/styles/vab.scss'
import '@/config/permission'
import '@/utils/errorLog'
import './vabIcon'
import VabPermissions from 'layouts/Permissions'
import Vab from '@/utils/vab'
Vue.use(Vab)
Vue.use(VabPermissions)

16
src/plugins/support.js Normal file
View File

@ -0,0 +1,16 @@
import { MessageBox } from 'element-ui'
if (!!window.ActiveXObject || 'ActiveXObject' in window) {
MessageBox({
title: '温馨提示',
message:
'自2015年3月起微软已宣布弃用IE且不再对IE提供任何更新维护请<a target="_blank" style="color:blue" href="https://www.microsoft.com/zh-cn/edge/">点击此处</a>访问微软官网更新浏览器,如果您使用的是双核浏览器,请您切换浏览器内核为极速模式',
type: 'warning',
showClose: false,
showConfirmButton: false,
closeOnClickModal: false,
closeOnPressEscape: false,
closeOnHashChange: false,
dangerouslyUseHTMLString: true,
})
}

4
src/plugins/vabIcon.js Normal file
View File

@ -0,0 +1,4 @@
import Vue from 'vue'
import VabIcon from 'vab-icon'
Vue.component('VabIcon', VabIcon)

539
src/router/index.js Normal file
View File

@ -0,0 +1,539 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description router全局配置如有必要可分文件抽离其中asyncRoutes只有在intelligence模式下才会用到vip文档中已提供路由的基础图标与小清新图标的配置方案请仔细阅读
*/
import Vue from 'vue'
import VueRouter from 'vue-router'
import Layout from '@/layouts'
import EmptyLayout from '@/layouts/EmptyLayout'
import { publicPath, routerMode } from '@/config'
Vue.use(VueRouter)
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true,
},
{
path: '/register',
component: () => import('@/views/register/index'),
hidden: true,
},
{
path: '/401',
name: '401',
component: () => import('@/views/401'),
hidden: true,
},
{
path: '/404',
name: '404',
component: () => import('@/views/404'),
hidden: true,
},
]
export const asyncRoutes = [
{
path: '/',
component: Layout,
redirect: '/index',
children: [
{
path: 'index',
name: 'Index',
component: () => import('@/views/index/index'),
meta: {
title: '首页',
icon: 'home',
affix: true,
},
},
],
},
{
path: '/mall',
component: Layout,
redirect: 'noRedirect',
name: 'Mall',
meta: {
title: '预约数据',
icon: 'book',
permissions: ['admin'],
},
children: [
{
path: 'pay',
name: 'Pay',
component: () => import('@/views/mall/pay/index'),
meta: {
title: '洗护订单管理',
noKeepAlive: true,
},
children: null,
},
{
path: 'doorOrder',
name: 'doorOrder',
component: () => import('@/views/mall/doorOrder/index'),
meta: {
title: '上门订单管理',
noKeepAlive: true,
},
children: null,
},
{
path: 'registerUser',
name: 'registerUser',
component: () => import('@/views/mall/registerUser/index'),
meta: {
title: '注册用户',
noKeepAlive: true,
},
children: null,
},
{
path: 'goodsList',
name: 'GoodsList',
component: () => import('@/views/mall/goodsList/index'),
meta: {
title: '钱包管理',
},
},
{
path: 'points',
name: 'points',
component: () => import('@/views/mall/points/index'),
meta: {
title: '积分管理',
},
},
{
path: 'member',
name: 'Member',
component: () => import('@/views/mall/member/index'),
meta: {
title: '会员管理',
},
},
{
path: 'time',
name: 'time',
component: () => import('@/views/mall/time/index'),
meta: {
title: '时段管理',
},
},
{
path: 'additional',
name: 'Additional',
component: () => import('@/views/mall/additional/index'),
meta: {
title: '附加项管理',
},
},
{
path: 'archive',
name: 'Archive',
component: () => import('@/views/mall/archive/index'),
meta: {
title: '宠物档案管理',
},
},
],
},
{
path: '/coupon',
component: Layout,
redirect: 'noRedirect',
name: 'Coupon',
meta: {
title: '券管理',
icon: 'cog',
permissions: ['admin'],
},
children: [
// {
// path: 'couponlist',
// name: 'Couponlist',
// component: () => import('@/views/Coupon/couponlist/index'),
// meta: {
// title: '券包',
// },
// },
// {
// path: 'distribution',
// name: 'distribution',
// component: () => import('@/views/Coupon/distribution/index'),
// meta: {
// title: '优惠券',
// },
// },
{
path: 'redemption',
name: 'redemption',
component: () => import('@/views/Coupon/redemption/index'),
meta: {
title: '兑换码',
},
},
{
path: 'distribute',
name: 'Distribute',
component: () => import('@/views/Coupon/distribute/index'),
meta: {
title: '派券管理',
},
},
{
path: 'record',
name: 'Record',
component: () => import('@/views/Coupon/record/index'),
meta: {
title: '派券记录',
},
},
],
},
{
path: '/careManagement',
component: Layout,
redirect: 'careManagement',
name: 'Comment',
meta: {
title: '洗护管理',
icon: 'sleigh',
permissions: ['admin'],
},
children: [
{
path: 'therapist',
name: 'therapist',
component: () => import('@/views/careManagement/therapist/index'),
meta: {
title: '洗护师管理',
},
},
{
path: 'vehicle',
name: 'vehicle',
component: () => import('@/views/careManagement/vehicle/index'),
meta: {
title: '车辆洗护师关系绑定',
},
}
],
},
{
path: '/publicWelfare',
component: Layout,
redirect: 'publicWelfare',
name: 'Comment',
meta: {
title: '公益管理',
icon: 'bahai',
permissions: ['admin'],
},
children: [
{
path: 'adopt',
name: 'adopt',
component: () => import('@/views/publicWelfare/adopt/index'),
meta: {
title: '领养宠物管理',
},
},
{
path: 'points',
name: 'points',
component: () => import('@/views/publicWelfare/points/index'),
meta: {
title: '积分捐助管理',
},
},
{
path: 'public',
name: 'public',
component: () => import('@/views/publicWelfare/public/index'),
meta: {
title: '公益行管理',
},
}
],
},
{
path: '/franchisee',
component: Layout,
redirect: 'franchisee',
name: 'Comment',
meta: {
title: '加盟商管理',
icon: 'book-reader',
permissions: ['admin'],
},
children: [
{
path: 'xiaowa',
name: 'Xiaowa',
component: () => import('@/views/xiaowa/index'),
meta: {
title: '小哇管理',
},
},
{
path: 'vehicle',
name: 'Vehicle',
component: () => import('@/views/vehicle/index'),
meta: {
title: '车辆管理',
},
},
// {
// path: 'order',
// name: 'Order',
// component: () => import('@/views/order/index'),
// meta: {
// title: '订单管理',
// },
// },
{
path: 'information',
name: 'Information',
component: () => import('@/views/information/index'),
meta: {
title: '信息管理',
permissions: ['admin', ],
},
},
],
},
{
path: '/administrator',
component: Layout,
redirect: 'administrator',
name: 'Comment',
meta: {
title: '管理员权限管理',
icon: 'paper-plane',
permissions: ['admin'],
},
children: [
{
path: 'role',
name: 'role',
component: () => import('@/views/administrator/role/index'),
meta: {
title: '角色管理',
},
},
{
path: 'user',
name: 'user',
component: () => import('@/views/administrator/user/index'),
meta: {
title: '用户列表',
},
}
],
},
{
path: '/pricing',
component: Layout,
redirect: 'pricing',
name: 'Comment',
meta: {
title: '定价管理',
icon: 'pen-square',
permissions: ['admin'],
},
children: [
{
path: 'care',
name: 'care',
component: () => import('@/views/pricing/care/index'),
meta: {
title: '洗护价格列表',
},
},
{
path: 'commission',
name: 'commission',
component: () => import('@/views/pricing/commission/index'),
meta: {
title: '佣金列表',
},
}
],
},
{
path: '/commodity',
component: Layout,
redirect: 'noRedirect',
name: 'commodity',
meta: {
title: '商城管理',
icon: 'comment-dots',
permissions: ['admin'],
},
children: [
{
path: 'malllist',
name: 'malllist',
component: () => import('@/views/commodity/malllist/index'),
meta: {
title: '商品列表',
},
},
{
path: 'orderlist',
name: 'orderlist',
component: () => import('@/views/commodity/orderlist/index'),
meta: {
title: '商城订单',
},
},
],
},
{
path: '/comment',
component: Layout,
redirect: 'noRedirect',
name: 'Comment',
meta: {
title: '评论管理',
icon: 'comment-dots',
permissions: ['admin'],
},
children: [
{
path: 'comment',
name: 'Comment',
component: () => import('@/views/comment/index'),
meta: {
title: '评论列表',
},
},
],
},
{
path: '/payment',
component: Layout,
redirect: 'payment',
name: 'Comment',
meta: {
title: '支付管理',
icon: 'comment-dots',
permissions: ['admin'],
},
children: [
{
path: 'paymentList',
name: 'paymentList',
component: () => import('@/views/payment/paymentList/index'),
meta: {
title: '支付列表',
},
}
],
},
{
path: '/settlement',
component: Layout,
redirect: 'settlement',
name: 'Comment',
meta: {
title: '结算管理',
icon: 'comment-dots',
permissions: ['admin'],
},
children: [
{
path: 'sett',
name: 'sett',
component: () => import('@/views/settlement/sett/index'),
meta: {
title: '结算列表',
},
}
],
},
{
path: '/certificate',
component: Layout,
redirect: 'certificate',
name: 'Comment',
meta: {
title: '证书管理',
icon: 'comment-dots',
permissions: ['admin'],
},
children: [
{
path: 'ficate',
name: 'ficate',
component: () => import('@/views/certificate/ficate/index'),
meta: {
title: '证书列表',
},
}
],
},
{
path: '/holidays',
component: Layout,
redirect: 'holidays',
name: 'Comment',
meta: {
title: '节假日管理',
icon: 'comment-dots',
permissions: ['admin'],
},
children: [
{
path: 'day',
name: 'day',
component: () => import('@/views/holidays/day/index'),
meta: {
title: '节假日列表',
},
}
],
},
{
path: '*',
redirect: '/404',
hidden: true,
},
]
const router = new VueRouter({
base: publicPath,
mode: routerMode,
scrollBehavior: () => ({
y: 0,
}),
routes: constantRoutes,
})
export function resetRouter() {
location.reload()
}
export default router

22
src/store/index.js Normal file
View File

@ -0,0 +1,22 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 导入所有 vuex 模块自动加入namespaced:true用于解决vuex命名冲突请勿修改。
*/
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const files = require.context('./modules', false, /\.js$/)
const modules = {}
files.keys().forEach((key) => {
modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default
})
Object.keys(modules).forEach((key) => {
modules[key]['namespaced'] = true
})
const store = new Vuex.Store({
modules,
})
export default store

View File

@ -0,0 +1,28 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 异常捕获的状态拦截,请勿修改
*/
const state = () => ({
errorLogs: [],
})
const getters = {
errorLogs: (state) => state.errorLogs,
}
const mutations = {
addErrorLog(state, errorLog) {
state.errorLogs.push(errorLog)
},
clearErrorLog: (state) => {
state.errorLogs.splice(0)
},
}
const actions = {
addErrorLog({ commit }, errorLog) {
commit('addErrorLog', errorLog)
},
clearErrorLog({ commit }) {
commit('clearErrorLog')
},
}
export default { state, getters, mutations, actions }

View File

@ -0,0 +1,63 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 路由拦截状态管理目前两种模式all模式与intelligence模式
*/
import { asyncRoutes, constantRoutes } from '@/router'
import { getRouterList } from '@/api/router'
import { convertRouter, filterAsyncRoutes } from '@/utils/handleRoutes'
const state = () => ({
routes: [],
partialRoutes: [],
})
const getters = {
routes: (state) => state.routes,
partialRoutes: (state) => state.partialRoutes,
}
const mutations = {
setRoutes(state, routes) {
state.routes = constantRoutes.concat(routes)
},
setAllRoutes(state, routes) {
state.routes = constantRoutes.concat(routes)
},
}
const actions = {
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description intelligence模式设置路由
* @param {*} { commit }
* @param {*} permissions
* @returns
*/
async setRoutes({ commit }, permissions) {
//根据permissions做路由筛选
let accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
commit('setRoutes', accessedRoutes)
return accessedRoutes
},
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description all模式设置路由
* @param {*} { commit }
* @returns
*/
async setAllRoutes({ commit }) {
try {
let { data } = await getRouterList()
if (!data || !Array.isArray(data)) {
console.error('后端返回的路由数据格式不正确', data)
data = []
}
const accessedRoutes = convertRouter(data)
commit('setAllRoutes', accessedRoutes)
return accessedRoutes
} catch (error) {
console.error('获取路由列表失败', error)
commit('setAllRoutes', [])
return []
}
},
}
export default { state, getters, mutations, actions }

View File

@ -0,0 +1,74 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 所有全局配置的状态管理,如无必要请勿修改
*/
import defaultSettings from '@/config'
const { tabsBar, logo, layout, header, themeBar } = defaultSettings
const theme = JSON.parse(localStorage.getItem('vue-admin-better-theme')) || ''
const state = () => ({
tabsBar: theme.tabsBar || tabsBar,
logo,
collapse: false,
layout: theme.layout || layout,
header: theme.header || header,
device: 'desktop',
themeBar,
})
const getters = {
collapse: (state) => state.collapse,
device: (state) => state.device,
header: (state) => state.header,
layout: (state) => state.layout,
logo: (state) => state.logo,
tabsBar: (state) => state.tabsBar,
themeBar: (state) => state.themeBar,
}
const mutations = {
changeLayout: (state, layout) => {
if (layout) state.layout = layout
},
changeHeader: (state, header) => {
if (header) state.header = header
},
changeTabsBar: (state, tabsBar) => {
if (tabsBar) state.tabsBar = tabsBar
},
changeCollapse: (state) => {
state.collapse = !state.collapse
},
foldSideBar: (state) => {
state.collapse = true
},
openSideBar: (state) => {
state.collapse = false
},
toggleDevice: (state, device) => {
state.device = device
},
}
const actions = {
changeLayout({ commit }, layout) {
commit('changeLayout', layout)
},
changeHeader({ commit }, header) {
commit('changeHeader', header)
},
changeTabsBar({ commit }, tabsBar) {
commit('changeTabsBar', tabsBar)
},
changeCollapse({ commit }) {
commit('changeCollapse')
},
foldSideBar({ commit }) {
commit('foldSideBar')
},
openSideBar({ commit }) {
commit('openSideBar')
},
toggleDevice({ commit }, device) {
commit('toggleDevice', device)
},
}
export default { state, getters, mutations, actions }

View File

@ -0,0 +1,23 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 代码生成机状态管理
*/
const state = () => ({
srcCode: '',
})
const getters = {
srcTableCode: (state) => state.srcCode,
}
const mutations = {
setTableCode(state, srcCode) {
state.srcCode = srcCode
},
}
const actions = {
setTableCode({ commit }, srcCode) {
commit('setTableCode', srcCode)
},
}
export default { state, getters, mutations, actions }

View File

@ -0,0 +1,110 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description tabsBar多标签页逻辑前期借鉴了很多开源项目发现都有个共同的特点很繁琐并不符合框架设计的初衷后来在github用户hipi的启发下完成了重构请勿修改
*/
const state = () => ({
visitedRoutes: [],
})
const getters = {
visitedRoutes: (state) => state.visitedRoutes,
}
const mutations = {
addVisitedRoute(state, route) {
let target = state.visitedRoutes.find((item) => item.path === route.path)
if (target) {
if (route.fullPath !== target.fullPath) Object.assign(target, route)
return
}
state.visitedRoutes.push(Object.assign({}, route))
},
delVisitedRoute(state, route) {
state.visitedRoutes.forEach((item, index) => {
if (item.path === route.path) state.visitedRoutes.splice(index, 1)
})
},
delOthersVisitedRoute(state, route) {
state.visitedRoutes = state.visitedRoutes.filter((item) => item.meta.affix || item.path === route.path)
},
delLeftVisitedRoute(state, route) {
let index = state.visitedRoutes.length
state.visitedRoutes = state.visitedRoutes.filter((item) => {
if (item.name === route.name) index = state.visitedRoutes.indexOf(item)
return item.meta.affix || index <= state.visitedRoutes.indexOf(item)
})
},
delRightVisitedRoute(state, route) {
let index = state.visitedRoutes.length
state.visitedRoutes = state.visitedRoutes.filter((item) => {
if (item.name === route.name) index = state.visitedRoutes.indexOf(item)
return item.meta.affix || index >= state.visitedRoutes.indexOf(item)
})
},
delAllVisitedRoutes(state) {
state.visitedRoutes = state.visitedRoutes.filter((item) => item.meta.affix)
},
updateVisitedRoute(state, route) {
state.visitedRoutes.forEach((item) => {
if (item.path === route.path) item = Object.assign(item, route)
})
},
}
const actions = {
addVisitedRoute({ commit }, route) {
commit('addVisitedRoute', route)
},
async delRoute({ dispatch, state }, route) {
await dispatch('delVisitedRoute', route)
return {
visitedRoutes: [...state.visitedRoutes],
}
},
delVisitedRoute({ commit, state }, route) {
commit('delVisitedRoute', route)
return [...state.visitedRoutes]
},
async delOthersRoutes({ dispatch, state }, route) {
await dispatch('delOthersVisitedRoute', route)
return {
visitedRoutes: [...state.visitedRoutes],
}
},
async delLeftRoutes({ dispatch, state }, route) {
await dispatch('delLeftVisitedRoute', route)
return {
visitedRoutes: [...state.visitedRoutes],
}
},
async delRightRoutes({ dispatch, state }, route) {
await dispatch('delRightVisitedRoute', route)
return {
visitedRoutes: [...state.visitedRoutes],
}
},
delOthersVisitedRoute({ commit, state }, route) {
commit('delOthersVisitedRoute', route)
return [...state.visitedRoutes]
},
delLeftVisitedRoute({ commit, state }, route) {
commit('delLeftVisitedRoute', route)
return [...state.visitedRoutes]
},
delRightVisitedRoute({ commit, state }, route) {
commit('delRightVisitedRoute', route)
return [...state.visitedRoutes]
},
async delAllRoutes({ dispatch, state }, route) {
await dispatch('delAllVisitedRoutes', route)
return {
visitedRoutes: [...state.visitedRoutes],
}
},
delAllVisitedRoutes({ commit, state }) {
commit('delAllVisitedRoutes')
return [...state.visitedRoutes]
},
updateVisitedRoute({ commit }, route) {
commit('updateVisitedRoute', route)
},
}
export default { state, getters, mutations, actions }

85
src/store/modules/user.js Normal file
View File

@ -0,0 +1,85 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 登录、获取用户信息、退出登录、清除accessToken逻辑不建议修改
*/
import Vue from 'vue'
import { getUserInfo, login, logout } from '@/api/user'
import { getPublicKey } from '@/api/publicKey'
import { getAccessToken, removeAccessToken, setAccessToken } from '@/utils/accessToken'
import { resetRouter } from '@/router'
import { title, tokenName } from '@/config'
const state = () => ({
accessToken: getAccessToken(),
username: '',
avatar: '',
permissions: [],
})
const getters = {
accessToken: (state) => state.accessToken,
username: (state) => state.username,
avatar: (state) => state.avatar,
permissions: (state) => state.permissions,
}
const mutations = {
setAccessToken(state, accessToken) {
state.accessToken = accessToken
setAccessToken(accessToken)
},
setUsername(state, username) {
state.username = username
},
setAvatar(state, avatar) {
state.avatar = avatar
},
setPermissions(state, permissions) {
state.permissions = permissions
},
}
const actions = {
setPermissions({ commit }, permissions) {
commit('setPermissions', permissions)
},
async login({ commit }, userInfo) {
const { data } = await login(userInfo)
const accessToken = data.tokenName
console.log(data,'--/-----111')
if (accessToken) {
commit('setAccessToken', accessToken)
const hour = new Date().getHours()
const thisTime = hour < 8 ? '早上好' : hour <= 11 ? '上午好' : hour <= 13 ? '中午好' : hour < 18 ? '下午好' : '晚上好'
Vue.prototype.$baseNotify(`欢迎登录${title}`, `${thisTime}`)
} else {
Vue.prototype.$baseMessage(`登录接口异常,未正确返回${tokenName}...`, 'error')
}
},
async getUserInfo({ commit, state }) {
const { data } = await getUserInfo(state.accessToken)
if (!data) {
Vue.prototype.$baseMessage('验证失败,请重新登录...', 'error')
return false
}
let { permissions, username, avatar } = data
if (permissions && username && Array.isArray(permissions)) {
commit('setPermissions', permissions)
commit('setUsername', username)
commit('setAvatar', avatar)
return permissions
} else {
Vue.prototype.$baseMessage('用户信息接口异常', 'error')
return false
}
},
async logout({ dispatch }) {
await logout(state.accessToken)
await dispatch('resetAccessToken')
await resetRouter()
},
resetAccessToken({ commit }) {
commit('setPermissions', [])
commit('setAccessToken', '')
removeAccessToken()
},
}
export default { state, getters, mutations, actions }

346
src/styles/loading.scss Normal file
View File

@ -0,0 +1,346 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 全局加载动画
*/
@charset "utf-8";
@import './spinner/dots.css';
@import './spinner/gauge.css';
@import './spinner/inner-circles.css';
@import './spinner/plus.css';
$base-loading: '.vab-loading-type';
/* 自定义loading开始 */
#{$base-loading}1 {
display: flex;
width: 36px;
height: 36px;
margin: 0 auto 15px;
border: 3px solid transparent;
border-top-color: $base-color-blue;
border-bottom-color: $base-color-blue;
border-radius: 50%;
animation: vabLoading1-0 0.8s linear infinite;
}
#{$base-loading}1::before {
display: block;
width: 8px;
height: 8px;
margin: auto;
content: '';
border: 3px solid $base-color-blue;
border-radius: 50%;
animation: vabLoading1 0.5s alternate ease-in infinite;
}
@keyframes vabLoading1-0 {
to {
transform: rotate(360deg);
}
}
@keyframes vabLoading1 {
from {
transform: scale(0.5);
}
to {
transform: scale(1.2);
}
}
#{$base-loading}2 {
width: 20px;
height: 20px;
margin-top: -40px;
margin-left: -10px;
animation: vabLoading2 1s linear reverse infinite;
}
#{$base-loading}2::before {
display: block;
width: 36px;
height: 36px;
margin-top: -17px;
margin-left: -18px;
content: '';
animation: vabLoading2 0.4s linear infinite;
}
#{$base-loading}2::after {
display: block;
width: 8px;
height: 8px;
margin-top: -3px;
margin-left: -4px;
content: '';
animation: vabLoading2 0.4s linear infinite;
}
#{$base-loading}2::before,
#{$base-loading}2,
#{$base-loading}2::after {
position: absolute;
top: 40%;
left: 50%;
border: 3px solid transparent;
border-top-color: $base-color-blue;
border-right-color: $base-color-blue;
border-radius: 50%;
}
@keyframes vabLoading2 {
to {
transform: rotate(360deg);
}
}
#{$base-loading}3 {
display: inline-block;
width: 2.5em;
height: 3em;
margin-bottom: 15px;
border: 3px solid transparent;
border-top-color: $base-color-blue;
border-bottom-color: $base-color-blue;
border-radius: 50%;
animation: vabLoading3 2s ease infinite;
}
@keyframes vabLoading3 {
50% {
border-width: 8px;
transform: rotate(360deg) scale(0.4, 0.33);
}
100% {
border-width: 3px;
transform: rotate(720deg) scale(1, 1);
}
}
#{$base-loading}4 {
display: inline-block;
width: 30px;
height: 30px;
margin: 0 auto 10px;
border: 8px solid transparent;
border-bottom-color: $base-color-blue;
border-left-color: $base-color-blue;
border-radius: 50%;
animation: vabLoading4 1s linear infinite normal;
}
#{$base-loading}4::after {
display: block;
width: 15px;
height: 15px;
margin: 0;
content: ' ';
border: 6px solid $base-color-blue;
border-bottom-color: transparent;
border-left-color: transparent;
border-radius: 50%;
}
@keyframes vabLoading4 {
0% {
opacity: 0.2;
transform: rotate(0deg);
}
50% {
opacity: 1;
transform: rotate(180deg);
}
100% {
opacity: 0.2;
transform: rotate(360deg);
}
}
#{$base-loading}5 {
display: block;
width: 0;
height: 0;
margin: 0 auto 15px;
border: solid 1.5em $base-color-blue;
border-right: solid 1.5em transparent;
border-left: solid 1.5em transparent;
border-radius: 100%;
animation: vabLoading5 1s linear infinite;
}
@keyframes vabLoading5 {
0% {
transform: rotate(0deg);
}
50% {
transform: rotate(60deg);
}
100% {
transform: rotate(360deg);
}
}
#{$base-loading}6 {
display: block;
width: 0;
height: 0;
margin: 0 auto 25px auto;
perspective: 200px;
}
#{$base-loading}6::before,
#{$base-loading}6::after {
position: absolute;
width: 20px;
height: 20px;
content: '';
background: rgba(0, 0, 0, 0);
animation: vabLoading6 0.5s infinite alternate;
}
#{$base-loading}6::before {
left: 0;
}
#{$base-loading}6::after {
right: 0;
animation-delay: 0.15s;
}
@keyframes vabLoading6 {
0% {
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
transform: scale(1) translateY(0) rotateX(0deg);
}
100% {
background: $base-color-blue;
box-shadow: 0 25px 40px rgba($base-color-blue, 0.5);
transform: scale(1.2) translateY(-25px) rotateX(45deg);
}
}
#{$base-loading}7 {
display: block;
width: 25px;
height: 25px;
margin: 0 auto 15px auto;
border: 2px solid $base-color-blue;
border-top-color: rgba($base-color-blue, 0.2);
border-right-color: rgba($base-color-blue, 0.2);
border-bottom-color: rgba($base-color-blue, 0.2);
border-radius: 100%;
animation: vabLoading7 infinite 0.75s linear;
}
@keyframes vabLoading7 {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
#{$base-loading}8 {
position: relative;
box-sizing: border-box;
display: block;
width: 20px;
height: 20px;
margin: 0 auto 15px auto;
background-color: $base-color-blue;
border-radius: 50%;
box-shadow: 30px 0 0 0 $base-color-blue;
transform: translateX(-15px);
}
#{$base-loading}8::after {
position: absolute;
top: 8px;
left: 9px;
width: 10px;
height: 10px;
content: '';
background-color: $base-color-white;
border-radius: 50%;
box-shadow: 30px 0 0 0 $base-color-white;
animation: vabLoading8 2s ease-in-out infinite alternate;
}
@keyframes vabLoading8 {
0% {
left: 9px;
}
100% {
left: 1px;
}
}
#{$base-loading}9 {
position: relative;
box-sizing: border-box;
display: block;
width: 20px;
height: 20px;
margin: 0 auto 15px auto;
border: 1px $base-color-blue solid;
animation: vabLoading9 5s linear infinite;
}
#{$base-loading}9::after {
position: absolute;
top: -8px;
left: 0;
width: 4px;
height: 4px;
content: '';
background-color: $base-color-blue;
animation: vabLoading9_check 1s ease-in-out infinite;
}
@keyframes vabLoading9_check {
25% {
top: -8px;
left: 22px;
}
50% {
top: 22px;
left: 22px;
}
75% {
top: 22px;
left: -9px;
}
100% {
top: -7px;
left: -9px;
}
}
@keyframes vabLoading9 {
0% {
box-shadow: inset 0 0 0 0 rgba($base-color-blue, 0.5);
opacity: 0.5;
}
100% {
box-shadow: inset 0 -20px 0 0 $base-color-blue;
}
}
/* 自定义loading结束 */

353
src/styles/normalize.scss vendored Normal file
View File

@ -0,0 +1,353 @@
@charset "utf-8";
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
margin: 0.67em 0;
font-size: 2em;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
border-bottom: none; /* 1 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
margin: 0; /* 2 */
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
padding: 0;
border-style: none;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type='button']:-moz-focusring,
[type='reset']:-moz-focusring,
[type='submit']:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
color: inherit; /* 2 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type='checkbox'],
[type='radio'] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@ -0,0 +1,68 @@
.dots-loader:not(:required) {
position: relative;
display: inline-block;
width: 7px;
height: 7px;
margin-bottom: 30px;
overflow: hidden;
text-indent: -9999px;
background: transparent;
border-radius: 100%;
box-shadow: #f86 -14px -14px 0 7px, #fc6 14px -14px 0 7px, #6d7 14px 14px 0 7px, #4ae -14px 14px 0 7px;
transform-origin: 50% 50%;
animation: dots-loader 5s infinite ease-in-out;
}
@keyframes dots-loader {
0% {
box-shadow: #f86 -14px -14px 0 7px, #fc6 14px -14px 0 7px, #6d7 14px 14px 0 7px, #4ae -14px 14px 0 7px;
}
8.33% {
box-shadow: #f86 14px -14px 0 7px, #fc6 14px -14px 0 7px, #6d7 14px 14px 0 7px, #4ae -14px 14px 0 7px;
}
16.67% {
box-shadow: #f86 14px 14px 0 7px, #fc6 14px 14px 0 7px, #6d7 14px 14px 0 7px, #4ae -14px 14px 0 7px;
}
25% {
box-shadow: #f86 -14px 14px 0 7px, #fc6 -14px 14px 0 7px, #6d7 -14px 14px 0 7px, #4ae -14px 14px 0 7px;
}
33.33% {
box-shadow: #f86 -14px -14px 0 7px, #fc6 -14px 14px 0 7px, #6d7 -14px -14px 0 7px, #4ae -14px -14px 0 7px;
}
41.67% {
box-shadow: #f86 14px -14px 0 7px, #fc6 -14px 14px 0 7px, #6d7 -14px -14px 0 7px, #4ae 14px -14px 0 7px;
}
50% {
box-shadow: #f86 14px 14px 0 7px, #fc6 -14px 14px 0 7px, #6d7 -14px -14px 0 7px, #4ae 14px -14px 0 7px;
}
58.33% {
box-shadow: #f86 -14px 14px 0 7px, #fc6 -14px 14px 0 7px, #6d7 -14px -14px 0 7px, #4ae 14px -14px 0 7px;
}
66.67% {
box-shadow: #f86 -14px -14px 0 7px, #fc6 -14px -14px 0 7px, #6d7 -14px -14px 0 7px, #4ae 14px -14px 0 7px;
}
75% {
box-shadow: #f86 14px -14px 0 7px, #fc6 14px -14px 0 7px, #6d7 14px -14px 0 7px, #4ae 14px -14px 0 7px;
}
83.33% {
box-shadow: #f86 14px 14px 0 7px, #fc6 14px -14px 0 7px, #6d7 14px 14px 0 7px, #4ae 14px 14px 0 7px;
}
91.67% {
box-shadow: #f86 -14px 14px 0 7px, #fc6 14px -14px 0 7px, #6d7 14px 14px 0 7px, #4ae -14px 14px 0 7px;
}
100% {
box-shadow: #f86 -14px -14px 0 7px, #fc6 14px -14px 0 7px, #6d7 14px 14px 0 7px, #4ae -14px 14px 0 7px;
}
}

View File

@ -0,0 +1,104 @@
.gauge-loader:not(:required) {
position: relative;
display: inline-block;
width: 64px;
height: 32px;
margin-bottom: 10px;
overflow: hidden;
text-indent: -9999px;
background: #6ca;
border-top-left-radius: 32px;
border-top-right-radius: 32px;
}
.gauge-loader:not(:required)::before {
position: absolute;
top: 5px;
left: 30px;
width: 4px;
height: 27px;
content: '';
background: white;
border-radius: 2px;
transform-origin: 50% 100%;
animation: gauge-loader 4000ms infinite ease;
}
.gauge-loader:not(:required)::after {
position: absolute;
top: 26px;
left: 26px;
width: 13px;
height: 13px;
content: '';
background: white;
-moz-border-radius: 8px;
-webkit-border-radius: 8px;
border-radius: 8px;
}
@keyframes gauge-loader {
0% {
transform: rotate(-50deg);
}
10% {
transform: rotate(20deg);
}
20% {
transform: rotate(60deg);
}
24% {
transform: rotate(60deg);
}
40% {
transform: rotate(-20deg);
}
54% {
transform: rotate(70deg);
}
56% {
transform: rotate(78deg);
}
58% {
transform: rotate(73deg);
}
60% {
transform: rotate(75deg);
}
62% {
transform: rotate(70deg);
}
70% {
transform: rotate(-20deg);
}
80% {
transform: rotate(20deg);
}
83% {
transform: rotate(25deg);
}
86% {
transform: rotate(20deg);
}
89% {
transform: rotate(25deg);
}
100% {
transform: rotate(-50deg);
}
}

View File

@ -0,0 +1,51 @@
.inner-circles-loader:not(:required) {
position: relative;
display: inline-block;
width: 50px;
height: 50px;
margin-bottom: 10px;
overflow: hidden;
text-indent: -9999px;
background: rgba(25, 165, 152, 0.5);
border-radius: 50%;
transform: translate3d(0, 0, 0);
}
.inner-circles-loader:not(:required)::before,
.inner-circles-loader:not(:required)::after {
position: absolute;
top: 0;
display: inline-block;
width: 50px;
height: 50px;
content: '';
border-radius: 50%;
}
.inner-circles-loader:not(:required)::before {
left: 0;
background: #c7efcf;
transform-origin: 0 50%;
animation: inner-circles-loader 3s infinite;
}
.inner-circles-loader:not(:required)::after {
right: 0;
background: #eef5db;
transform-origin: 100% 50%;
animation: inner-circles-loader 3s 0.2s reverse infinite;
}
@keyframes inner-circles-loader {
0% {
transform: rotate(0deg);
}
50% {
transform: rotate(360deg);
}
100% {
transform: rotate(0deg);
}
}

341
src/styles/spinner/plus.css Normal file
View File

@ -0,0 +1,341 @@
.plus-loader:not(:required) {
position: relative;
display: inline-block;
width: 48px;
height: 48px;
margin-bottom: 10px;
overflow: hidden;
text-indent: -9999px;
background: #f86;
-moz-border-radius: 24px;
-webkit-border-radius: 24px;
border-radius: 24px;
-moz-transform: rotateZ(90deg);
-ms-transform: rotateZ(90deg);
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
-moz-transform-origin: 50% 50%;
-ms-transform-origin: 50% 50%;
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
-moz-animation: plus-loader-background 3s infinite ease-in-out;
-webkit-animation: plus-loader-background 3s infinite ease-in-out;
animation: plus-loader-background 3s infinite ease-in-out;
}
.plus-loader:not(:required)::after {
position: absolute;
top: 0;
right: 50%;
width: 50%;
height: 100%;
content: '';
background: #f86;
-moz-border-radius: 24px 0 0 24px;
-webkit-border-radius: 24px;
border-radius: 24px 0 0 24px;
-moz-transform-origin: 100% 50%;
-ms-transform-origin: 100% 50%;
-webkit-transform-origin: 100% 50%;
transform-origin: 100% 50%;
-moz-animation: plus-loader-top 3s infinite linear;
-webkit-animation: plus-loader-top 3s infinite linear;
animation: plus-loader-top 3s infinite linear;
}
.plus-loader:not(:required)::before {
position: absolute;
top: 0;
right: 50%;
width: 50%;
height: 100%;
content: '';
background: #fc6;
-moz-border-radius: 24px 0 0 24px;
-webkit-border-radius: 24px;
border-radius: 24px 0 0 24px;
-moz-transform-origin: 100% 50%;
-ms-transform-origin: 100% 50%;
-webkit-transform-origin: 100% 50%;
transform-origin: 100% 50%;
-moz-animation: plus-loader-bottom 3s infinite linear;
-webkit-animation: plus-loader-bottom 3s infinite linear;
animation: plus-loader-bottom 3s infinite linear;
}
@keyframes plus-loader-top {
2.5% {
background: #f86;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
13.75% {
background: #ff430d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
13.76% {
background: #ffae0d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-out;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
25% {
background: #fc6;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
27.5% {
background: #fc6;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
41.25% {
background: #ffae0d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
41.26% {
background: #2cc642;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-out;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
50% {
background: #6d7;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
}
52.5% {
background: #6d7;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
63.75% {
background: #2cc642;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
63.76% {
background: #1386d2;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-out;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
75% {
background: #4ae;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
77.5% {
background: #4ae;
-moz-transform: rotateY(180deg);
-ms-transform: rotateY(180deg);
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
91.25% {
background: #1386d2;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
91.26% {
background: #ff430d;
-moz-transform: rotateY(90deg);
-ms-transform: rotateY(90deg);
-webkit-transform: rotateY(90deg);
transform: rotateY(90deg);
-moz-animation-timing-function: ease-in;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
100% {
background: #f86;
-moz-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
}
@keyframes plus-loader-bottom {
0% {
background: #fc6;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
50% {
background: #fc6;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
75% {
background: #4ae;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
100% {
background: #4ae;
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
}
@keyframes plus-loader-background {
0% {
background: #f86;
-moz-transform: rotateZ(180deg);
-ms-transform: rotateZ(180deg);
-webkit-transform: rotateZ(180deg);
transform: rotateZ(180deg);
}
25% {
background: #f86;
-moz-transform: rotateZ(180deg);
-ms-transform: rotateZ(180deg);
-webkit-transform: rotateZ(180deg);
transform: rotateZ(180deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
27.5% {
background: #6d7;
-moz-transform: rotateZ(90deg);
-ms-transform: rotateZ(90deg);
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
}
50% {
background: #6d7;
-moz-transform: rotateZ(90deg);
-ms-transform: rotateZ(90deg);
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
52.5% {
background: #6d7;
-moz-transform: rotateZ(0deg);
-ms-transform: rotateZ(0deg);
-webkit-transform: rotateZ(0deg);
transform: rotateZ(0deg);
}
75% {
background: #6d7;
-moz-transform: rotateZ(0deg);
-ms-transform: rotateZ(0deg);
-webkit-transform: rotateZ(0deg);
transform: rotateZ(0deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
77.5% {
background: #f86;
-moz-transform: rotateZ(270deg);
-ms-transform: rotateZ(270deg);
-webkit-transform: rotateZ(270deg);
transform: rotateZ(270deg);
}
100% {
background: #f86;
-moz-transform: rotateZ(270deg);
-ms-transform: rotateZ(270deg);
-webkit-transform: rotateZ(270deg);
transform: rotateZ(270deg);
-moz-animation-timing-function: step-start;
-webkit-animation-timing-function: step-start;
animation-timing-function: step-start;
}
}

View File

@ -0,0 +1 @@
/* 绿荫草场主题、荣耀典藏主题、暗黑之子主题加QQ讨论群972435319、1139183756后私聊群主获取获取后将主题放到themes文件夹根目录即可 */

View File

@ -0,0 +1,19 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description vue过渡动画
*/
@charset "utf-8";
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: $base-transition;
}
.fade-transform-enter {
opacity: 0;
}
.fade-transform-leave-to {
opacity: 0;
}

532
src/styles/vab.scss Normal file
View File

@ -0,0 +1,532 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 全局样式
*/
@charset "utf-8";
@import './normalize.scss';
@import './transition.scss';
@import './loading.scss';
@import 'element-ui/lib/theme-chalk/display.css';
@mixin scrollbar {
max-height: 88vh;
margin-bottom: 0.5vh;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
height: 0;
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
border-radius: 10px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.3);
}
}
@mixin base-scrollbar {
&::-webkit-scrollbar {
width: 13px;
height: 13px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.4);
background-clip: padding-box;
border: 3px solid transparent;
border-radius: 7px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.5);
}
&::-webkit-scrollbar-track {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
&::-webkit-scrollbar-track:hover {
background-color: rgba(0, 0, 0, 0.08);
}
}
img {
object-fit: cover;
border-radius: 6px;
}
a {
color: $base-color-blue;
text-decoration: none;
cursor: pointer;
transition: color 0.3s ease;
&:hover {
color: darken($base-color-blue, 10%);
}
}
* {
transition: $base-transition;
}
svg {
transition: none;
* {
transition: none;
}
}
html {
body {
position: relative;
height: 100vh;
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: $base-font-size-default;
color: #2c3e50;
background: #f0f2f5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@include base-scrollbar;
div {
@include base-scrollbar;
}
svg,
i {
&:hover {
opacity: 0.8;
}
}
.v-modal {
backdrop-filter: blur(10px);
}
.el-tag + .el-tag {
margin-left: 10px;
}
.editor-toolbar {
.no-mobile,
.fa-question-circle {
display: none;
}
}
.el-divider--horizontal {
margin: 15px 0 25px 0;
.el-divider__text {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
}
.el-image-viewer {
&__close {
.el-icon-circle-close {
color: $base-color-white;
}
}
}
.vue-admin-better-wrapper {
.app-main-container {
@include base-scrollbar;
> [class*='-container'] {
* {
transition: none;
}
padding: $base-padding;
background: $base-color-white;
border-radius: 12px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.02);
}
}
}
/* 进度条开始 */
#nprogress {
position: fixed;
z-index: $base-z-index;
.bar {
background: linear-gradient(90deg, $base-color-blue, lighten($base-color-blue, 20%)) !important;
height: 3px !important;
}
.peg {
box-shadow: 0 0 14px $base-color-blue, 0 0 8px $base-color-blue !important;
}
}
.el-table {
.el-table__body-wrapper {
@include base-scrollbar;
}
th {
background: #f8fafc;
}
td,
th {
position: relative;
box-sizing: border-box;
padding: 10px 0;
.cell {
font-size: $base-font-size-default;
font-weight: normal;
color: #555;
.el-image {
width: 50px;
height: 50px;
border-radius: 8px;
}
}
}
&::before {
height: 0;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.08);
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.08);
}
}
.el-pagination {
padding: 2px 5px;
margin: 20px 0 0 0;
font-weight: normal;
color: $base-color-black;
text-align: center;
}
.el-menu.el-menu--popup.el-menu--popup-right-start {
@include scrollbar;
border-radius: 6px;
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.el-menu.el-menu--popup.el-menu--popup-bottom-start {
@include scrollbar;
border-radius: 6px;
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.el-submenu__title i {
color: $base-color-white;
}
.el-dialog,
.el-message-box {
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
&__body {
border-top: 1px solid $base-border-color;
.el-form {
padding-right: 30px;
}
}
&__footer {
padding: $base-padding;
text-align: right;
border-top: 1px solid $base-border-color;
}
&__content {
padding: 20px 20px 20px 20px;
}
}
.el-card {
margin-bottom: 16px;
border-radius: 12px;
border: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.09);
transform: translateY(-2px);
}
&__body {
padding: $base-padding;
}
}
/* VabPageHeader 全局样式 - 高优先级 */
.page-header {
background: linear-gradient(135deg, #518df1 0%, #667eea 100%) !important;
border-radius: 16px !important;
padding: 32px !important;
margin-bottom: 24px !important;
color: white !important;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3) !important;
border: none !important;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
transform: rotate(30deg);
}
.header-content {
position: relative;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
.header-left {
.page-title {
font-size: 2.2rem !important;
font-weight: 700 !important;
margin: 0 0 10px 0 !important;
display: flex !important;
align-items: center !important;
gap: 14px !important;
color: white !important;
.vab-icon {
font-size: 2rem !important;
color: white !important;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 8px;
}
}
.page-description {
font-size: 1.1rem !important;
opacity: 0.95 !important;
margin: 0 !important;
color: white !important;
font-weight: 300;
}
}
.header-right {
display: flex !important;
align-items: center !important;
gap: 10px !important;
font-size: 1.2rem !important;
font-weight: 500 !important;
color: white !important;
background: rgba(255, 255, 255, 0.2);
padding: 12px 20px;
border-radius: 12px;
backdrop-filter: blur(10px);
.vab-icon {
font-size: 1.4rem !important;
color: white !important;
}
}
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-header {
padding: 22px !important;
border-radius: 12px !important;
.header-content {
flex-direction: column !important;
gap: 18px !important;
text-align: center !important;
.header-left {
.page-title {
font-size: 1.7rem !important;
}
}
.header-right {
width: 100%;
justify-content: center;
}
}
}
}
.select-tree-popper {
.el-scrollbar {
.el-scrollbar__view {
.el-select-dropdown__item {
height: auto;
max-height: 274px;
padding: 0;
overflow-y: auto;
line-height: 26px;
}
}
}
}
/* 扩展 el-divider 样式 */
.el-divider {
position: relative;
border: none;
background: linear-gradient(90deg, transparent, #e4e7ed, transparent);
&--horizontal {
height: 1px;
margin: 24px 0 32px 0;
.el-divider__text {
position: relative;
padding: 0 18px;
font-size: 14px;
font-weight: 500;
color: #606266;
background: #fff;
border-radius: 6px;
&::before {
content: '';
position: absolute;
top: 50%;
left: -8px;
width: 4px;
height: 4px;
margin-top: -2px;
background: #409eff;
border-radius: 50%;
}
}
}
&--vertical {
width: 1px;
height: 1em;
margin: 0 16px;
background: #e4e7ed;
}
/* 主题变体 */
&--primary {
background: linear-gradient(90deg, transparent, #409eff, transparent);
.el-divider__text {
color: #409eff;
&::before {
background: #409eff;
}
}
}
&--success {
background: linear-gradient(90deg, transparent, #67c23a, transparent);
.el-divider__text {
color: #67c23a;
&::before {
background: #67c23a;
}
}
}
&--warning {
background: linear-gradient(90deg, transparent, #e6a23c, transparent);
.el-divider__text {
color: #e6a23c;
&::before {
background: #e6a23c;
}
}
}
&--danger {
background: linear-gradient(90deg, transparent, #f56c6c, transparent);
.el-divider__text {
color: #f56c6c;
&::before {
background: #f56c6c;
}
}
}
/* 虚线样式 */
&--dashed {
background: none;
border-top: 1px dashed #e4e7ed;
.el-divider__text {
background: #fff;
}
}
/* 粗线样式 */
&--thick {
height: 2px;
background: linear-gradient(90deg, transparent, #409eff, transparent);
}
}
}
.side-container {
.el-menu-item,
.el-submenu {
margin: 7px !important;
border-radius: 5px !important;
transition: all 0.3s ease;
&:hover {
border-radius: 5px !important;
}
&.is-active {
background: linear-gradient(90deg, $base-color-default, lighten($base-color-default, 10%)) !important;
}
}
}
}

70
src/styles/variables.scss Normal file
View File

@ -0,0 +1,70 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 全局主题变量配置
*/
/* stylelint-disable */
@charset "utf-8";
//框架默认主题色
$base-color-default: #4d8af0;
//默认层级
$base-z-index: 1000;
//横向布局纵向布局时菜单背景色
$base-menu-background: #191a23;
//菜单文字颜色
$base-menu-color: hsla(0, 0%, 100%, 0.95);
//菜单选中文字颜色
$base-menu-color-active: hsla(0, 0%, 100%, 0.95);
//菜单选中背景色
$base-menu-background-active: $base-color-default;
//标题颜色
$base-title-color: #fff;
//字体大小配置
$base-font-size-small: 12px;
$base-font-size-default: 14px;
$base-font-size-big: 16px;
$base-font-size-bigger: 18px;
$base-font-size-max: 22px;
$base-font-color: #606266;
$base-color-blue: $base-color-default;
$base-color-green: #52c41a;
$base-color-white: #fff;
$base-color-black: #000;
$base-color-yellow: #faad14;
$base-color-orange: #fa8c16;
$base-color-red: #f5222d;
$base-color-gray: rgba(0, 0, 0, 0.65);
$base-main-width: 1279px;
$base-border-radius: 6px;
$base-border-color: #ebeef5;
//输入框高度
$base-input-height: 36px;
//默认paddiing
$base-padding: 22px;
//默认阴影
$base-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
//横向布局时top-bar、logo、一级菜单的高度
$base-top-bar-height: 64px;
//纵向布局时logo的高度
$base-logo-height: 70px;
//顶部nav-bar的高度
$base-nav-bar-height: 60px;
//顶部多标签页tabs-bar的高度
$base-tabs-bar-height: 50px;
//顶部多标签页tabs-bar中每一个item的高度
$base-tag-item-height: 32px;
//菜单li标签的高度
$base-menu-item-height: 50px;
//app-main的高度
$base-app-main-height: calc(100vh - #{$base-nav-bar-height} - #{$base-tabs-bar-height} - #{$base-padding} - #{$base-padding} - 55px - 55px);
//纵向布局时左侧导航未折叠时的宽度
$base-left-menu-width: 240px;
//纵向布局时左侧导航未折叠时右侧内容的宽度
$base-right-content-width: calc(100% - #{$base-left-menu-width});
//纵向布局时左侧导航已折叠时的宽度
$base-left-menu-width-min: 64px;
//纵向布局时左侧导航已折叠时右侧内容的宽度
$base-right-content-width-min: calc(100% - #{$base-left-menu-width-min});
//默认动画
$base-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border 0s, background 0s, color 0s, font-size 0s;
//默认动画长
$base-transition-time: 0.3s;

59
src/utils/accessToken.js Normal file
View File

@ -0,0 +1,59 @@
import { storage, tokenTableName } from '@/config'
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 获取accessToken
* @returns {string|ActiveX.IXMLDOMNode|Promise<any>|any|IDBRequest<any>|MediaKeyStatus|FormDataEntryValue|Function|Promise<Credential | null>}
*/
export function getAccessToken() {
if (storage) {
if ('localStorage' === storage) {
return localStorage.getItem(tokenTableName)
} else if ('sessionStorage' === storage) {
return sessionStorage.getItem(tokenTableName)
} else {
return localStorage.getItem(tokenTableName)
}
} else {
return localStorage.getItem(tokenTableName)
}
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 存储accessToken
* @param accessToken
* @returns {void|*}
*/
export function setAccessToken(accessToken) {
if (storage) {
if ('localStorage' === storage) {
return localStorage.setItem(tokenTableName, accessToken)
} else if ('sessionStorage' === storage) {
return sessionStorage.setItem(tokenTableName, accessToken)
} else {
return localStorage.setItem(tokenTableName, accessToken)
}
} else {
return localStorage.setItem(tokenTableName, accessToken)
}
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 移除accessToken
* @returns {void|Promise<void>}
*/
export function removeAccessToken() {
if (storage) {
if ('localStorage' === storage) {
return localStorage.removeItem(tokenTableName)
} else if ('sessionStorage' === storage) {
return sessionStorage.clear()
} else {
return localStorage.removeItem(tokenTableName)
}
} else {
return localStorage.removeItem(tokenTableName)
}
}

31
src/utils/clipboard.js Normal file
View File

@ -0,0 +1,31 @@
import Vue from 'vue'
import Clipboard from 'clipboard'
function clipboardSuccess() {
Vue.prototype.$baseMessage('复制成功', 'success')
}
function clipboardError() {
Vue.prototype.$baseMessage('复制失败', 'error')
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 复制数据
* @param text
* @param event
*/
export default function handleClipboard(text, event) {
const clipboard = new Clipboard(event.target, {
text: () => text,
})
clipboard.on('success', () => {
clipboardSuccess()
clipboard.destroy()
})
clipboard.on('error', () => {
clipboardError()
clipboard.destroy()
})
clipboard.onClick(event)
}

42
src/utils/encrypt.js Normal file
View File

@ -0,0 +1,42 @@
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='
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description RSA加密
* @param data
* @returns {Promise<{param: PromiseLike<ArrayBuffer>}|*>}
*/
export async function encryptedData(data) {
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-----`)
data = encrypt.encrypt(JSON.stringify(data))
return {
param: data,
}
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description RSA解密
* @param data
* @returns {PromiseLike<ArrayBuffer>}
*/
export function decryptedData(data) {
const decrypt = new JSEncrypt()
decrypt.setPrivateKey(`-----BEGIN RSA PRIVATE KEY-----${privateKey}-----END RSA PRIVATE KEY-----`)
data = decrypt.decrypt(JSON.stringify(data))
return data
}

52
src/utils/errorLog.js Normal file
View File

@ -0,0 +1,52 @@
import Vue from 'vue'
import store from '@/store'
import { isArray, isString } from '@/utils/validate'
import { errorLog } from '@/config'
const needErrorLog = errorLog
const checkNeed = () => {
const env = process.env.NODE_ENV
if (isString(needErrorLog)) {
return env === needErrorLog
}
if (isArray(needErrorLog)) {
return needErrorLog.includes(env)
}
return false
}
// 检查是否是Chrome扩展相关错误
const isChromeExtensionError = (err) => {
if (!err) return false
// 错误消息是字符串
if (typeof err.message === 'string') {
return (
err.message.includes('runtime.lastError') ||
err.message.includes('message port closed') ||
err.message.includes('The message port closed')
)
}
// 错误本身是字符串
if (typeof err === 'string') {
return err.includes('runtime.lastError') || err.includes('message port closed') || err.includes('The message port closed')
}
return false
}
if (checkNeed()) {
Vue.config.errorHandler = (err, vm, info) => {
// 过滤Chrome扩展相关错误
if (isChromeExtensionError(err)) {
return
}
console.error('vue-admin-better错误拦截:', err, vm, info)
const url = window.location.href
Vue.nextTick(() => {
store.dispatch('errorLog/addErrorLog', { err, vm, info, url })
})
}
}

110
src/utils/handleRoutes.js Normal file
View File

@ -0,0 +1,110 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description all模式渲染后端返回路由
* @param constantRoutes
* @returns {*}
*/
// import { pcaData } from "@/utils/rouList";
// console.log(pcaData,'--=')
export function convertRouter(asyncRoutes) {
console.log(asyncRoutes,'--=11?')
// 处理空值情况
if (!asyncRoutes || !Array.isArray(asyncRoutes)) {
console.warn('后端返回的路由格式不正确或为空')
return []
}
return asyncRoutes
.map((route) => {
if (!route) return null
if (route.component) {
if (route.component === 'Layout') {
route.component = () => import('@/layouts')
} else if (route.component === 'EmptyLayout') {
route.component = () => import('@/layouts/EmptyLayout')
} else {
try {
const index = route.component.indexOf('views')
const path = index > 0 ? route.component.slice(index) : `views/${route.component}`
route.component = () =>
import(`@/${path}`).catch((err) => {
console.error(`路由组件加载失败: @/${path}`, err)
return import('@/views/404')
})
} catch (err) {
console.error(`路由组件解析失败: ${route.component}`, err)
route.component = () => import('@/views/404')
}
}
}
if (route.children) {
if (Array.isArray(route.children) && route.children.length) {
route.children = convertRouter(route.children)
// 过滤掉空路由
route.children = route.children.filter((child) => child !== null)
}
if (!route.children || route.children.length === 0) delete route.children
}
return route
})
.filter((route) => route !== null) // 过滤掉无效路由
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 判断当前路由是否包含权限
* @param permissions
* @param route
* @returns {boolean|*}
*/
function hasPermission(permissions, route) {
// 确保permissions是数组
if (!permissions || !Array.isArray(permissions)) {
return false
}
if (route.meta && route.meta.permissions) {
return permissions.some((role) => route.meta.permissions.includes(role))
} else {
return true
}
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description intelligence模式根据permissions数组拦截路由
* @param routes
* @param permissions
* @returns {[]}
*/
export function filterAsyncRoutes(routes, permissions) {
// 处理无效参数
if (!routes || !Array.isArray(routes)) {
return []
}
if (!permissions || !Array.isArray(permissions)) {
return []
}
const finallyRoutes = []
routes.forEach((route) => {
if (!route) return
const item = { ...route }
if (hasPermission(permissions, item)) {
if (item.children && Array.isArray(item.children)) {
item.children = filterAsyncRoutes(item.children, permissions)
}
finallyRoutes.push(item)
}
})
return finallyRoutes
}

242
src/utils/index.js Normal file
View File

@ -0,0 +1,242 @@
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 格式化时间
* @param time
* @param cFormat
* @returns {string|null}
*/
export function parseTime(time, cFormat) {
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' && /^[0-9]+$/.test(time)) {
time = parseInt(time)
}
if (typeof time === 'number' && time.toString().length === 10) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay(),
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
if (key === 'a') {
return ['日', '一', '二', '三', '四', '五', '六'][value]
}
if (result.length > 0 && value < 10) {
value = `0${value}`
}
return value || 0
})
return time_str
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 格式化时间
* @param time
* @param option
* @returns {string}
*/
export function formatTime(time, option) {
if (`${time}`.length === 10) {
time = parseInt(time) * 1000
} else {
time = +time
}
const d = 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()}`
}
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 将url请求参数转为json格式
* @param url
* @returns {{}|any}
*/
export function paramObj(url) {
const search = url.split('?')[1]
if (!search) {
return {}
}
return JSON.parse(`{"${decodeURIComponent(search).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"').replace(/\+/g, ' ')}"}`)
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 父子关系的数组转换成树形结构数据
* @param data
* @returns {*}
*/
export function translateDataToTree(data) {
const parent = data.filter((value) => value.parentId === 'undefined' || value.parentId == null)
const children = data.filter((value) => value.parentId !== 'undefined' && value.parentId != null)
const translator = (parent, children) => {
parent.forEach((parent) => {
children.forEach((current, index) => {
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
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 树形结构数据转换成父子关系的数组
* @param data
* @returns {[]}
*/
export function translateTreeToData(data) {
const result = []
data.forEach((item) => {
const loop = (data) => {
result.push({
id: data.id,
name: data.name,
parentId: data.parentId,
})
const child = data.children
if (child) {
for (let i = 0; i < child.length; i++) {
loop(child[i])
}
}
}
loop(item)
})
return result
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 10位时间戳转换
* @param time
* @returns {string}
*/
export function tenBitTimestamp(time) {
const date = new Date(time * 1000)
const y = date.getFullYear()
let m = date.getMonth() + 1
m = m < 10 ? `${m}` : m
let d = date.getDate()
d = d < 10 ? `${d}` : d
let h = date.getHours()
h = h < 10 ? `0${h}` : h
let minute = date.getMinutes()
let second = date.getSeconds()
minute = minute < 10 ? `0${minute}` : minute
second = second < 10 ? `0${second}` : second
return `${y}${m}${d}${h}:${minute}:${second}` //组合
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 13位时间戳转换
* @param time
* @returns {string}
*/
export function thirteenBitTimestamp(time) {
const date = new Date(time / 1)
const y = date.getFullYear()
let m = date.getMonth() + 1
m = m < 10 ? `${m}` : m
let d = date.getDate()
d = d < 10 ? `${d}` : d
let h = date.getHours()
h = h < 10 ? `0${h}` : h
let minute = date.getMinutes()
let second = date.getSeconds()
minute = minute < 10 ? `0${minute}` : minute
second = second < 10 ? `0${second}` : second
return `${y}${m}${d}${h}:${minute}:${second}` //组合
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @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
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description m到n的随机数
* @param m
* @param n
* @returns {number}
*/
export function random(m, n) {
return Math.floor(Math.random() * (m - n) + n)
}
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description addEventListener
* @type {function(...[*]=)}
*/
export const on = (function () {
return function (element, event, handler, useCapture = false) {
if (element && event && handler) {
element.addEventListener(event, handler, useCapture)
}
}
})()
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description removeEventListener
* @type {function(...[*]=)}
*/
export const off = (function () {
return function (element, event, handler, useCapture = false) {
if (element && event) {
element.removeEventListener(event, handler, useCapture)
}
}
})()

12
src/utils/pageTitle.js Normal file
View File

@ -0,0 +1,12 @@
import { title } from '@/config'
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 设置标题
* @param pageTitle
* @returns {string}
*/
export default function getPageTitle(pageTitle) {
if (pageTitle) return `${pageTitle}-${title}`
return `${title}`
}

20
src/utils/permission.js Normal file
View File

@ -0,0 +1,20 @@
import store from '@/store'
/**
* @author https://github.com/zxwk1998/vue-admin-better 不想保留author可删除
* @description 检查权限
* @param value
* @returns {boolean}
*/
export default function checkPermission(value) {
if (value && value instanceof Array && value.length > 0) {
const permissions = store.getters['user/permissions']
const permissionPermissions = value
return permissions.some((role) => {
return permissionPermissions.includes(role)
})
} else {
return false
}
}

9
src/utils/printInfo.js Normal file
View File

@ -0,0 +1,9 @@
/**
* @description 只在控制台打印layouts/index.js中的内容
*/
import { donationConsole } from 'layouts'
export function printLayoutsInfo() {
// 只在控制台打印
donationConsole()
}

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