基本框架
This commit is contained in:
parent
a474d7a8b2
commit
be9750b9eb
|
|
@ -6,3 +6,4 @@ VITE_APP_ENV='development'
|
|||
|
||||
# 开发环境
|
||||
VITE_API_BASE='/dev-api'
|
||||
VITE_WS_URL='/dev-api'
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ VITE_APP_ENV='production'
|
|||
|
||||
# 生产环境
|
||||
VITE_API_BASE='/prod-api'
|
||||
VITE_WS_URL='/prod-api'
|
||||
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS=gzip
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ VITE_APP_ENV='staging'
|
|||
|
||||
# 生产环境
|
||||
VITE_API_BASE='/staging-api'
|
||||
VITE_WS_URL='/staging-api'
|
||||
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS=gzip
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
document.documentElement.classList.add("dark")
|
||||
}
|
||||
</script>
|
||||
<div id="app">
|
||||
<div id="app" class="pi">
|
||||
<div class="app-loading">
|
||||
<div class="app-loading__logo">
|
||||
<img src="images/logo.png"/>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
import http from "@/utils/request"
|
||||
|
||||
export default {
|
||||
captcha: async function (data = {}) {
|
||||
return await http.get("v1/captcha", data);
|
||||
},
|
||||
login: async function (data = {}) {
|
||||
return await http.post("v1/login", data);
|
||||
},
|
||||
info: async function () {
|
||||
return await http.get("v1/info");
|
||||
},
|
||||
menu: async function (data = {}) {
|
||||
return await http.get("v1/menu", data)
|
||||
},
|
||||
captcha: async function(data = {}){
|
||||
return await http.get("v1/captcha", data);
|
||||
logout: async function(){
|
||||
return await http.get("v1/logout");
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<svg t="1715052304862" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6490" width="256" height="256"><path d="M157.2 940.1c-34.7 0-65.8-17.3-91.5-52-23.9-31.7-32.9-62.8-26.9-93.9 12-61 78.9-101 132.7-133.3l3-1.8c14.9-9 28.7-17.3 39.5-25.7 59.8-44.8 78.9-96.3 65.2-177.6-13.8-81.3 43-149.5 77.7-182.9l3-3-1.2-1.2 20.3-15.5L603.2 83.9 987 592.7 741.3 775.1l-0.6-0.6-4.2 2.4c-37.1 21.5-93.9 38.9-129.1 38.9-24.5 0-47.2-4.8-68.2-14.9-34.1-16.1-62.8-23.9-89.7-23.9-33.5 0-64.6 12-99.2 37.7-10.2 7.8-22.1 18.5-34.1 29.9L312 848c-41.8 41.2-96.2 92.1-154.8 92.1z m230.7-624.8c-25.1 25.7-66.4 76.5-57.4 132.1 16.7 99.8-9.6 169.8-84.9 226.6-12.6 9.6-28.7 19.1-47.2 30.5-43 25.7-101.6 61-108.8 99.2-3 16.1 2.4 33.5 17.3 53.2 23.9 31.7 44.2 31.7 50.8 31.7 38.9 0 84.3-43 121.4-77.1 17.3-16.1 30.5-28.1 42.4-37.1 43-32.3 84.9-47.8 129.1-47.8 34.7 0 71.1 9.6 111.8 28.7 12.6 6 26.9 9 43.6 9 35.9 0 74.7-14.3 99.2-27.5l5.4-3-318.5-422.7-4.2 4.2z m364.7 388.6l163.8-121.4-321.6-426.8-162.6 122.5 320.4 425.7z" p-id="6491"></path></svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<svg t="1722497355948" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6255" width="200" height="200"><path d="M902.8 192.9c-36.8-39-86.9-60.8-139.1-60.7h-0.2c-52-0.1-102 21.6-138.7 60.4l-0.1 0.1-362.3 381.2c-33.7 35.6-43.7 89-25.4 135.4 18.3 46.4 61.4 76.6 109.2 76.5 30.2 0 60.3-12 83.2-36.2l362.3-381.2c15.4-16.2 15.4-42.4 0-58.6-15.4-16.2-40.3-16.2-55.7 0L373.8 691c-7.4 7.7-17.4 12-27.8 12-10.4 0-20.4-4.4-27.7-12.2-7.5-7.6-11.7-18.1-11.8-29.1 0-11 4.1-21.5 11.5-29.2l362.3-381.2c22-23.3 52-36.3 83.2-36.2h0.1c31.3-0.1 61.4 13.1 83.5 36.4 22.3 23.4 34.6 54.7 34.6 87.8 0.1 32.9-12.3 64.5-34.4 87.7l-97.8 102.9-0.6 0.6-271.1 285.4c-76.9 80.8-201.5 80.9-278.5 0.1-37-38.8-57.7-91.5-57.5-146.4 0-55.4 20.5-107.5 57.7-146.6l369.4-388.7c15.4-16.2 15.4-42.4 0-58.6-15.4-16.2-40.3-16.2-55.7 0L143.8 464.4c-51.9 54.3-81 128.2-80.8 205.2 0 77.5 28.6 150.4 80.6 205 51.6 54.4 121.7 84.9 194.8 84.7 73.2 0.1 143.4-30.4 195.1-84.9l383.1-403.1c3.8-4.1 6.8-9 8.7-14.3 22.8-34.4 35.2-75 35.2-117.8 0.1-54.9-20.7-107.6-57.7-146.3" p-id="6256"></path></svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// 导入当前目录下的所有图标组件
|
||||
const files = import.meta.glob('./*.vue', {
|
||||
eager: true,
|
||||
import: 'default' // 可选,指定要导入的 export
|
||||
})
|
||||
const modules = {}
|
||||
|
||||
for (const path in files) {
|
||||
// 获取组件名,可以根据需要修改这里的逻辑
|
||||
const componentName = path.replace(/^\.\/(.*)\.\w+$/, '$1');
|
||||
// 全局注册组件
|
||||
modules[componentName] = files[path];
|
||||
}
|
||||
|
||||
export default {
|
||||
...modules
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
export default {
|
||||
//
|
||||
// 应用名称
|
||||
APP_NAME: import.meta.env.VITE_APP_TITLE,
|
||||
//接口地址
|
||||
API_URL: import.meta.env.VITE_API_BASE,
|
||||
// websocket
|
||||
WS_URL: import.meta.env.VITE_WS_URL,
|
||||
//请求超时
|
||||
TIMEOUT: 10000,
|
||||
//请求是否开启缓存
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
<template>
|
||||
<div class="router-err">
|
||||
<div class="router-err__icon">
|
||||
<img src="./img/404.png"/>
|
||||
<img src="/images/404.png"/>
|
||||
</div>
|
||||
<div class="router-err__content">
|
||||
<h2>无权限或找不到页面</h2>
|
||||
<p>当前页面无权限访问或者打开了一个不存在的链接,请检查当前账户权限和链接的可访问性。</p>
|
||||
<el-button type="primary" plain round @click="gohome">返回首页</el-button>
|
||||
<el-button type="primary" plain round @click="gologin">重新登录</el-button>
|
||||
<el-button type="primary" round @click="goback">返回上一页</el-button>
|
||||
<el-button type="primary" plain round @click="goHome">返回首页</el-button>
|
||||
<el-button type="primary" plain round @click="goLogin">重新登录</el-button>
|
||||
<el-button type="primary" round @click="goBack">返回上一页</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup>
|
||||
import {useRouter} from 'vue-router';
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const gohome = () => {
|
||||
const goHome = () => {
|
||||
location.href = "#/"
|
||||
}
|
||||
|
||||
const goback = () => {
|
||||
const goBack = () => {
|
||||
router.go(-1);
|
||||
}
|
||||
const gologin = () => {
|
||||
const goLogin = () => {
|
||||
router.push("/login");
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div v-if="navMenus.length<=0" style="padding:20px;">
|
||||
<el-alert title="无子集菜单" center type="info" :closable="false"></el-alert>
|
||||
</div>
|
||||
<template v-for="navMenu in navMenus" v-bind:key="navMenu">
|
||||
<el-menu-item v-if="!hasChildren(navMenu)" :index="navMenu.path">
|
||||
<a v-if="navMenu.meta&&navMenu.meta.type=='link'" :href="navMenu.path" target="_blank" @click.stop='()=>{}'></a>
|
||||
<el-icon v-if="navMenu.meta&&navMenu.meta.icon"><component :is="navMenu.meta.icon || 'el-icon-menu'"/></el-icon>
|
||||
<template #title>
|
||||
<span>{{navMenu.meta.title}}</span>
|
||||
<span v-if="navMenu.meta.tag" class="menu-tag">{{navMenu.meta.tag}}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-sub-menu v-else :index="navMenu.path">
|
||||
<template #title>
|
||||
<el-icon v-if="navMenu.meta&&navMenu.meta.icon"><component :is="navMenu.meta.icon || 'el-icon-menu'"/></el-icon>
|
||||
<span>{{navMenu.meta.title}}</span>
|
||||
<span v-if="navMenu.meta.tag" class="menu-tag">{{navMenu.meta.tag}}</span>
|
||||
</template>
|
||||
<NavMenu :navMenus="navMenu.children"></NavMenu>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavMenu',
|
||||
props: ['navMenus'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
hasChildren(item) {
|
||||
return item.children && !item.children.every(item => item.meta.hidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div v-show="$route.meta.type=='iframe'" class="iframe-pages">
|
||||
<iframe v-for="item in iframeList" :key="item.meta.url" v-show="$route.meta.url==item.meta.url" :src="item.meta.url" frameborder='0'></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(e) {
|
||||
this.push(e)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.push(this.$route);
|
||||
},
|
||||
computed:{
|
||||
iframeList(){
|
||||
return this.$store.state.iframe.iframeList
|
||||
},
|
||||
ismobile(){
|
||||
return this.$store.state.global.ismobile
|
||||
},
|
||||
layoutTags(){
|
||||
return this.$store.state.global.layoutTags
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
push(route){
|
||||
if(route.meta.type == 'iframe'){
|
||||
if(this.ismobile || !this.layoutTags){
|
||||
this.$store.commit("setIframeList", route)
|
||||
}else{
|
||||
this.$store.commit("pushIframeList", route)
|
||||
}
|
||||
}else{
|
||||
if(this.ismobile || !this.layoutTags){
|
||||
this.$store.commit("clearIframeList")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iframe-pages {width:100%;height:100%;background: #fff;}
|
||||
iframe {border:0;width:100%;height:100%;display: block;}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<el-container>
|
||||
<el-main class="nopadding">
|
||||
<ul class="msg-list">
|
||||
<li v-for="item in msgList" v-bind:key="item.accept_id" @click="toMessage(item.accept_id)">
|
||||
<a :href="item.link" target="_blank">
|
||||
<div class="msg-list__main">
|
||||
<p :class="item.status == 0 ? 'unread' : ''">{{item.title}}</p>
|
||||
</div>
|
||||
<div class="msg-list__time">
|
||||
<p v-time.tip="item.create_time" :class="item.status == 0 ? 'unread' : ''"></p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<el-empty v-if="msgList.length==0" description="暂无新消息" :image-size="100"></el-empty>
|
||||
</ul>
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<el-button type="primary" @click="toMessage(0)">消息中心</el-button>
|
||||
<el-button @click="markRead">全部已读</el-button>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['closed'],
|
||||
data() {
|
||||
return {
|
||||
msgList: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
let res = await this.$API.home.message.newMsg()
|
||||
this.msgList = res.data
|
||||
},
|
||||
//标记已读
|
||||
async markRead(){
|
||||
let res = await this.$API.home.message.read()
|
||||
this.msgList.forEach((item) => {
|
||||
item.status = 1
|
||||
})
|
||||
this.$message.success(res.msg)
|
||||
this.$emit('closed', 0)
|
||||
},
|
||||
toMessage(accept_id) {
|
||||
this.$router.push({name: 'messageCenter', params: { accept_id: accept_id}})
|
||||
this.$emit('closed', -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.msg-list li {border-top:1px solid #eee;}
|
||||
.msg-list li a {display: flex;padding:20px;}
|
||||
.msg-list li a:hover {background: #ecf5ff;}
|
||||
.msg-list__main {flex: 1;}
|
||||
.msg-list__main h2 {font-size: 15px;font-weight: normal;color: #333;}
|
||||
.msg-list__main p {font-size: 12px;color: #999;line-height: 1.8;margin-top: 5px;}
|
||||
.msg-list__time {width: 100px;text-align: right;color: #999;}
|
||||
.msg-list__time p {font-size: 12px;color: #999;line-height: 1.8;margin-top: 5px;}
|
||||
.unread {color: #333 !important;}
|
||||
.dark .msg-list__main h2 {color: #d0d0d0;}
|
||||
.dark .msg-list li {border-top:1px solid #363636;}
|
||||
.dark .msg-list li a:hover {background: #383838;}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
<template>
|
||||
<div class="sc-search">
|
||||
<el-input ref="input" v-model="input" placeholder="搜索" size="large" clearable prefix-icon="el-icon-search" :trigger-on-focus="false" @input="inputChange"/>
|
||||
<div class="sc-search-history" v-if="history.length>0">
|
||||
<el-tag closable effect="dark" type="info" v-for="(item, index) in history" :key="item" @click="historyClick(item)" @close="historyClose(index)">{{item}}</el-tag>
|
||||
</div>
|
||||
<div class="sc-search-result">
|
||||
<div class="sc-search-no-result" v-if="result.length<=0">暂无搜索结果</div>
|
||||
<ul v-else>
|
||||
<el-scrollbar max-height="366px">
|
||||
<li v-for="item in result" :key="item.path" @click="to(item)">
|
||||
<el-icon><component :is="item.icon || 'el-icon-menu'" /></el-icon>
|
||||
<span class="title">{{ item.breadcrumb }}</span>
|
||||
</li>
|
||||
</el-scrollbar>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
input: "",
|
||||
menu: [],
|
||||
result: [],
|
||||
history: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var searchHistory = this.$TOOL.data.get("SEARCH_HISTORY") || []
|
||||
this.history = searchHistory
|
||||
var menuTree = this.$TOOL.data.get("MENU")
|
||||
this.filterMenu(menuTree)
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
methods: {
|
||||
inputChange(value){
|
||||
if(value){
|
||||
this.result = this.menuFilter(value)
|
||||
}else{
|
||||
this.result = []
|
||||
}
|
||||
},
|
||||
filterMenu(map){
|
||||
map.forEach(item => {
|
||||
if(item.meta.hidden || item.meta.type=="button"){
|
||||
return false
|
||||
}
|
||||
if(item.meta.type=='iframe'){
|
||||
item.path = `/i/${item.name}`
|
||||
}
|
||||
if(item.children&&item.children.length > 0&&!item.component){
|
||||
this.filterMenu(item.children)
|
||||
}else{
|
||||
this.menu.push(item)
|
||||
}
|
||||
})
|
||||
},
|
||||
menuFilter(queryString){
|
||||
var res = []
|
||||
//过滤菜单树
|
||||
var filterMenu = []
|
||||
this.menu.forEach((item) => {
|
||||
filterMenu = filterMenu.concat(item.children.filter((v) => {
|
||||
if((v.meta.title).toLowerCase().indexOf(queryString.toLowerCase()) >= 0){
|
||||
return true
|
||||
}
|
||||
if((v.name).toLowerCase().indexOf(queryString.toLowerCase()) >= 0){
|
||||
return true
|
||||
}
|
||||
}))
|
||||
})
|
||||
//匹配系统路由
|
||||
var router = this.$router.getRoutes()
|
||||
var filterRouter= filterMenu.map((m) => {
|
||||
if(m.meta.type == "link"){
|
||||
return router.find(r => r.path == '/'+m.path)
|
||||
}else{
|
||||
return router.find(r => r.path == m.path)
|
||||
}
|
||||
})
|
||||
//重组对象
|
||||
filterRouter.forEach(item => {
|
||||
res.push({
|
||||
name: item.name,
|
||||
type: item.meta.type,
|
||||
path: item.meta.type=="link"?item.path.slice(1):item.path,
|
||||
icon: item.meta.icon,
|
||||
title: item.meta.title,
|
||||
breadcrumb: item.meta.breadcrumb.map(v => v.meta.title).join(' - ')
|
||||
})
|
||||
})
|
||||
return res
|
||||
},
|
||||
to(item){
|
||||
if(!this.history.includes(this.input)){
|
||||
this.history.push(this.input)
|
||||
this.$TOOL.data.set("SEARCH_HISTORY", this.history)
|
||||
}
|
||||
if(item.type=="link"){
|
||||
setTimeout(()=>{
|
||||
let a = document.createElement("a")
|
||||
a.style = "display: none"
|
||||
a.target = "_blank"
|
||||
a.href = item.path
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}, 10);
|
||||
}else{
|
||||
this.$router.push({path: item.path})
|
||||
}
|
||||
this.$emit('success', true)
|
||||
},
|
||||
historyClick(text){
|
||||
this.input = text
|
||||
this.inputChange(text)
|
||||
},
|
||||
historyClose(index){
|
||||
this.history.splice(index, 1);
|
||||
if(this.history.length <= 0){
|
||||
this.$TOOL.data.remove("SEARCH_HISTORY")
|
||||
}else{
|
||||
this.$TOOL.data.set("SEARCH_HISTORY", this.history)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sc-search {}
|
||||
.sc-search-no-result {text-align: center;margin: 40px 0;color: #999;}
|
||||
.sc-search-history {margin-top: 10px;}
|
||||
.sc-search-history .el-tag {cursor: pointer;}
|
||||
.sc-search-result {margin-top: 15px;}
|
||||
.sc-search-result li {height:56px;padding:0 15px;background: var(--el-bg-color-overlay);border: 1px solid var(--el-border-color-light);list-style:none;border-radius: 4px;margin-bottom: 5px;font-size: 14px;display: flex;align-items: center;cursor: pointer;}
|
||||
.sc-search-result li i {font-size: 20px;margin-right: 15px;}
|
||||
.sc-search-result li:hover {background: var(--el-color-primary);color: #fff;border-color: var(--el-color-primary);}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<el-form ref="form" label-width="120px" label-position="left" style="padding:0 20px;">
|
||||
<el-divider></el-divider>
|
||||
<el-form-item :label="$t('user.nightmode')">
|
||||
<el-switch v-model="dark"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('user.language')">
|
||||
<el-select v-model="lang">
|
||||
<el-option label="简体中文" value="zh-cn"></el-option>
|
||||
<el-option label="English" value="en"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-divider></el-divider>
|
||||
<el-form-item label="主题颜色">
|
||||
<el-color-picker v-model="colorPrimary" :predefine="colorList">></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-divider></el-divider>
|
||||
<el-form-item label="框架布局">
|
||||
<el-select v-model="layout" placeholder="请选择">
|
||||
<el-option label="默认" value="default"></el-option>
|
||||
<el-option label="通栏" value="header"></el-option>
|
||||
<el-option label="经典" value="menu"></el-option>
|
||||
<el-option label="功能坞" value="dock"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="折叠菜单">
|
||||
<el-switch v-model="menuIsCollapse"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签栏">
|
||||
<el-switch v-model="layoutTags"></el-switch>
|
||||
</el-form-item>
|
||||
<el-divider></el-divider>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import colorTool from '@/utils/color'
|
||||
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
layout: this.$TOOL.data.get('APP_LAYOUT') || this.$store.state.global.layout,
|
||||
menuIsCollapse: this.$store.state.global.menuIsCollapse,
|
||||
layoutTags: this.$store.state.global.layoutTags,
|
||||
lang: this.$TOOL.data.get('APP_LANG') || this.$CONFIG.LANG,
|
||||
dark: this.$TOOL.data.get('APP_DARK') || false,
|
||||
colorList: ['#409EFF', '#009688', '#536dfe', '#ff5c93', '#c62f2f', '#fd726d'],
|
||||
colorPrimary: this.$TOOL.data.get('APP_COLOR') || this.$CONFIG.COLOR || '#409EFF'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
layout(val) {
|
||||
this.$TOOL.data.set("APP_LAYOUT", val);
|
||||
this.$store.commit("SET_layout", val)
|
||||
},
|
||||
menuIsCollapse(){
|
||||
this.$store.commit("TOGGLE_menuIsCollapse")
|
||||
},
|
||||
layoutTags(){
|
||||
this.$store.commit("TOGGLE_layoutTags")
|
||||
},
|
||||
dark(val){
|
||||
if(val){
|
||||
document.documentElement.classList.add("dark")
|
||||
this.$TOOL.data.set("APP_DARK", val)
|
||||
}else{
|
||||
document.documentElement.classList.remove("dark")
|
||||
this.$TOOL.data.remove("APP_DARK")
|
||||
}
|
||||
},
|
||||
lang(val){
|
||||
this.$i18n.locale = val
|
||||
this.$TOOL.data.set("APP_LANG", val);
|
||||
},
|
||||
colorPrimary(val){
|
||||
if(!val){
|
||||
val = '#409EFF'
|
||||
this.colorPrimary = '#409EFF'
|
||||
}
|
||||
document.documentElement.style.setProperty('--el-color-primary', val);
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, colorTool.lighten(val,i/10));
|
||||
}
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, colorTool.darken(val,i/10));
|
||||
}
|
||||
this.$TOOL.data.set("APP_COLOR", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
<template>
|
||||
<div ref="" class="mobile-nav-button" @click="showMobileNav($event)" v-drag draggable="false"><el-icon><el-icon-menu /></el-icon></div>
|
||||
|
||||
<el-drawer ref="mobileNavBox" title="移动端菜单" :size="240" v-model="nav" direction="ltr" :with-header="false" destroy-on-close>
|
||||
<el-container class="mobile-nav">
|
||||
<el-header>
|
||||
<div class="logo-bar"><img class="logo" src="/images/logo.png"><span>{{ $CONFIG.APP_NAME }}</span></div>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="$route.meta.active || $route.fullPath" @select="select" router background-color="#212d3d" text-color="#fff" active-text-color="#409EFF">
|
||||
<NavMenu :navMenus="menu"></NavMenu>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavMenu from './NavMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavMenu
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nav: false,
|
||||
menu: []
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
|
||||
},
|
||||
created() {
|
||||
var menu = this.$router.getMenu()
|
||||
this.menu = this.filterUrl(menu)
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
showMobileNav(e){
|
||||
var isdrag = e.currentTarget.getAttribute('drag-flag')
|
||||
if (isdrag == 'true') {
|
||||
return false;
|
||||
}else{
|
||||
this.nav = true;
|
||||
}
|
||||
|
||||
},
|
||||
select(){
|
||||
this.$refs.mobileNavBox.handleClose()
|
||||
},
|
||||
//转换外部链接的路由
|
||||
filterUrl(map){
|
||||
var newMap = []
|
||||
map && map.forEach(item => {
|
||||
item.meta = item.meta?item.meta:{};
|
||||
//处理隐藏
|
||||
if(item.meta.hidden || item.meta.type=="button"){
|
||||
return false
|
||||
}
|
||||
//处理http
|
||||
if(item.meta.type=='iframe'){
|
||||
item.path = `/i/${item.name}`;
|
||||
}
|
||||
//递归循环
|
||||
if(item.children&&item.children.length > 0){
|
||||
item.children = this.filterUrl(item.children);
|
||||
}
|
||||
newMap.push(item)
|
||||
})
|
||||
return newMap;
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
drag(el){
|
||||
let oDiv = el; //当前元素
|
||||
let firstTime='',lastTime='';
|
||||
//禁止选择网页上的文字
|
||||
// document.onselectstart = function() {
|
||||
// return false;
|
||||
// };
|
||||
oDiv.onmousedown = function(e){
|
||||
//鼠标按下,计算当前元素距离可视区的距离
|
||||
let disX = e.clientX - oDiv.offsetLeft;
|
||||
let disY = e.clientY - oDiv.offsetTop;
|
||||
document.onmousemove = function(e){
|
||||
oDiv.setAttribute('drag-flag', true);
|
||||
firstTime = new Date().getTime();
|
||||
//通过事件委托,计算移动的距离
|
||||
let l = e.clientX - disX;
|
||||
let t = e.clientY - disY;
|
||||
|
||||
//移动当前元素
|
||||
|
||||
if(t > 0 && t < document.body.clientHeight - 50){
|
||||
oDiv.style.top = t + "px";
|
||||
}
|
||||
if(l > 0 && l < document.body.clientWidth - 50){
|
||||
oDiv.style.left = l + "px";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
document.onmouseup = function(){
|
||||
lastTime = new Date().getTime();
|
||||
if( (lastTime - firstTime)>200 ){
|
||||
oDiv.setAttribute('drag-flag', false);
|
||||
}
|
||||
document.onmousemove = null;
|
||||
document.onmouseup = null;
|
||||
};
|
||||
//return false不加的话可能导致黏连,就是拖到一个地方时div粘在鼠标上不下来,相当于onmouseup失效
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-nav-button {position: fixed;bottom:10px;left:10px;z-index: 10;width: 50px;height: 50px;background: #409EFF;box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 1);border-radius: 50%;display: flex;align-items: center;justify-content: center;}
|
||||
.mobile-nav-button i {color: #fff;font-size: 20px;}
|
||||
|
||||
.mobile-nav {background: #212d3d;}
|
||||
.mobile-nav .el-header {background: transparent;border: 0;}
|
||||
.mobile-nav .el-main {padding:0;}
|
||||
.mobile-nav .logo-bar {display: flex;align-items: center;font-weight: bold;font-size: 20px;color: #fff;}
|
||||
.mobile-nav .logo-bar img {width: 30px;margin-right: 10px;}
|
||||
.mobile-nav .el-submenu__title:hover {background: #fff!important;}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
<template>
|
||||
<div class="pi-tags">
|
||||
<ul ref="tags">
|
||||
<li v-for="tag in tagList" v-bind:key="tag" :class="[isActive(tag)?'active':'',tag.meta.affix?'affix':'' ]"
|
||||
@contextmenu.prevent="openContextMenu($event, tag)">
|
||||
<router-link :to="tag">
|
||||
<span>{{ tag.meta.title }}</span>
|
||||
<el-icon v-if="!tag.meta.affix" @click.prevent.stop='closeSelectedTag(tag)'>
|
||||
<el-icon-close/>
|
||||
</el-icon>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<transition name="el-zoom-in-top">
|
||||
<ul v-if="contextMenuVisible" :style="{left:left+'px',top:top+'px'}" class="contextmenu" id="contextmenu">
|
||||
<li @click="refreshTab()">
|
||||
<el-icon>
|
||||
<el-icon-refresh/>
|
||||
</el-icon>
|
||||
刷新
|
||||
</li>
|
||||
<hr>
|
||||
<li @click="closeTabs()" :class="contextMenuItem.meta.affix?'disabled':''">
|
||||
<el-icon>
|
||||
<el-icon-close/>
|
||||
</el-icon>
|
||||
关闭标签
|
||||
</li>
|
||||
<li @click="closeOtherTabs()">
|
||||
<el-icon>
|
||||
<el-icon-folder-delete/>
|
||||
</el-icon>
|
||||
关闭其他标签
|
||||
</li>
|
||||
<hr>
|
||||
<li @click="maximize()">
|
||||
<el-icon>
|
||||
<el-icon-full-screen/>
|
||||
</el-icon>
|
||||
最大化
|
||||
</li>
|
||||
<li @click="openWindow()">
|
||||
<el-icon>
|
||||
<el-icon-copy-document/>
|
||||
</el-icon>
|
||||
在新的窗口中打开
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup name="tags">
|
||||
import Sortable from 'sortablejs'
|
||||
import {getCurrentInstance, nextTick, ref, watch, onMounted} from "vue";
|
||||
import store from "@/store/index";
|
||||
import {useRouter, useRoute} from "vue-router";
|
||||
import {getMenu} from "@/utils/route"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const {proxy} = getCurrentInstance();
|
||||
const menu = getMenu();
|
||||
const dashboardRoute = treeFind(menu, node => node.path === "/dashboard");
|
||||
|
||||
let contextMenuVisible = ref(false)
|
||||
let contextMenuItem = ref(null)
|
||||
let left = ref(0)
|
||||
let top = ref(0)
|
||||
let tagList = store.state.viewTags.viewTags
|
||||
let tipDisplayed = ref(false)
|
||||
|
||||
if (dashboardRoute) {
|
||||
dashboardRoute.fullPath = dashboardRoute.path
|
||||
addViewTags(dashboardRoute)
|
||||
addViewTags(route)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
tagDrop();
|
||||
scrollInit()
|
||||
})
|
||||
|
||||
watch(route, (e) => {
|
||||
addViewTags(e);
|
||||
//判断标签容器是否出现滚动条
|
||||
nextTick(() => {
|
||||
const tags = proxy.$refs.tags
|
||||
if (tags && tags.scrollWidth > tags.clientWidth) {
|
||||
//确保当前标签在可视范围内
|
||||
let targetTag = tags.querySelector(".active")
|
||||
targetTag.scrollIntoView()
|
||||
//显示提示
|
||||
if (!tipDisplayed) {
|
||||
proxy.$msgbox({
|
||||
type: 'warning',
|
||||
center: true,
|
||||
title: '提示',
|
||||
message: '当前标签数量过多,可通过鼠标滚轴滚动标签栏。关闭标签数量可减少系统性能消耗。',
|
||||
confirmButtonText: '知道了'
|
||||
})
|
||||
tipDisplayed = true
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
watch(() => contextMenuVisible, (value) => {
|
||||
var cm = function (e) {
|
||||
let sp = document.getElementById("contextmenu");
|
||||
if (sp && !sp.contains(e.target)) {
|
||||
closeMenu()
|
||||
}
|
||||
}
|
||||
if (value) {
|
||||
document.body.addEventListener('click', e => cm(e))
|
||||
} else {
|
||||
document.body.removeEventListener('click', e => cm(e))
|
||||
}
|
||||
})
|
||||
|
||||
//查找树
|
||||
function treeFind(tree, func) {
|
||||
for (const data of tree) {
|
||||
if (func(data)) return data
|
||||
if (data.children) {
|
||||
const res = treeFind(data.children, func)
|
||||
if (res) return res
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
//标签拖拽排序
|
||||
function tagDrop() {
|
||||
Sortable.create(proxy.$refs.tags, {
|
||||
draggable: 'li',
|
||||
animation: 300
|
||||
})
|
||||
}
|
||||
|
||||
//增加tag
|
||||
function addViewTags(route) {
|
||||
if (route.name && !route.meta.fullpage) {
|
||||
store.commit("pushViewTags", route)
|
||||
store.commit("pushKeepLive", route.name)
|
||||
}
|
||||
}
|
||||
|
||||
//高亮tag
|
||||
function isActive(r) {
|
||||
return r.fullPath === route.fullPath
|
||||
}
|
||||
|
||||
//关闭tag
|
||||
function closeSelectedTag(tag, autoPushLatestView = true) {
|
||||
const nowTagIndex = tagList.findIndex(item => item.fullPath == tag.fullPath)
|
||||
store.commit("removeViewTags", tag)
|
||||
store.commit("removeIframeList", tag)
|
||||
store.commit("removeKeepLive", tag.name)
|
||||
if (autoPushLatestView && isActive(tag)) {
|
||||
const leftView = tagList[nowTagIndex - 1]
|
||||
if (leftView) {
|
||||
router.push(leftView)
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//tag右键
|
||||
function openContextMenu(e, tag) {
|
||||
contextMenuItem = tag;
|
||||
contextMenuVisible = true;
|
||||
left = e.clientX + 1;
|
||||
top = e.clientY + 1;
|
||||
|
||||
//FIX 右键菜单边缘化位置处理
|
||||
nextTick(() => {
|
||||
let sp = document.getElementById("contextmenu");
|
||||
if (document.body.offsetWidth - e.clientX < sp.offsetWidth) {
|
||||
left = document.body.offsetWidth - sp.offsetWidth + 1;
|
||||
top = e.clientY + 1;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//关闭右键菜单
|
||||
function closeMenu() {
|
||||
contextMenuItem = null;
|
||||
contextMenuVisible = false
|
||||
}
|
||||
|
||||
//TAB 刷新
|
||||
function refreshTab() {
|
||||
const nowTag = contextMenuItem;
|
||||
contextMenuVisible = false
|
||||
//判断是否当前路由,否的话跳转
|
||||
if (route.fullPath != nowTag.fullPath) {
|
||||
router.push({
|
||||
path: nowTag.fullPath,
|
||||
query: nowTag.query
|
||||
})
|
||||
}
|
||||
store.commit("refreshIframe", nowTag)
|
||||
setTimeout(function () {
|
||||
store.commit("removeKeepLive", nowTag.name)
|
||||
store.commit("setRouteShow", false)
|
||||
nextTick(() => {
|
||||
store.commit("pushKeepLive", nowTag.name)
|
||||
store.commit("setRouteShow", true)
|
||||
})
|
||||
}, 0);
|
||||
}
|
||||
|
||||
//TAB 关闭
|
||||
function closeTabs() {
|
||||
var nowTag = contextMenuItem;
|
||||
if (!nowTag.meta.affix) {
|
||||
closeSelectedTag(nowTag)
|
||||
contextMenuVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
//TAB 关闭其他
|
||||
function closeOtherTabs() {
|
||||
var nowTag = contextMenuItem;
|
||||
//判断是否当前路由,否的话跳转
|
||||
if (route.fullPath != nowTag.fullPath) {
|
||||
router.push({
|
||||
path: nowTag.fullPath,
|
||||
query: nowTag.query
|
||||
})
|
||||
}
|
||||
var tags = [...tagList];
|
||||
tags.forEach(tag => {
|
||||
if (tag.meta && tag.meta.affix || nowTag.fullPath == tag.fullPath) {
|
||||
return true
|
||||
} else {
|
||||
closeSelectedTag(tag, false)
|
||||
}
|
||||
})
|
||||
contextMenuVisible = false
|
||||
}
|
||||
|
||||
//TAB 最大化
|
||||
function maximize() {
|
||||
var nowTag = contextMenuItem;
|
||||
contextMenuVisible = false
|
||||
//判断是否当前路由,否的话跳转
|
||||
if (route.fullPath != nowTag.fullPath) {
|
||||
router.push({
|
||||
path: nowTag.fullPath,
|
||||
query: nowTag.query
|
||||
})
|
||||
}
|
||||
document.getElementById('app').classList.add('main-maximize')
|
||||
}
|
||||
|
||||
//新窗口打开
|
||||
function openWindow() {
|
||||
var nowTag = contextMenuItem;
|
||||
var url = nowTag.href || '/';
|
||||
if (!nowTag.meta.affix) {
|
||||
closeSelectedTag(nowTag)
|
||||
}
|
||||
window.open(url);
|
||||
contextMenuVisible = false
|
||||
}
|
||||
|
||||
//横向滚动
|
||||
function scrollInit() {
|
||||
const scrollDiv = proxy.$refs.tags;
|
||||
scrollDiv.addEventListener('mousewheel', handler, false) || scrollDiv.addEventListener("DOMMouseScroll", handler, false)
|
||||
|
||||
function handler(event) {
|
||||
const detail = event.wheelDelta || event.detail;
|
||||
//火狐上滚键值-3 下滚键值3,其他内核上滚键值120 下滚键值-120
|
||||
const moveForwardStep = 1;
|
||||
const moveBackStep = -1;
|
||||
let step = 0;
|
||||
if (detail == 3 || detail < 0 && detail != -3) {
|
||||
step = moveForwardStep * 50;
|
||||
} else {
|
||||
step = moveBackStep * 50;
|
||||
}
|
||||
scrollDiv.scrollLeft += step;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.contextmenu {
|
||||
position: fixed;
|
||||
width: 200px;
|
||||
margin: 0;
|
||||
border-radius: 0px;
|
||||
background: var(--el-bg-color-overlay);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
|
||||
z-index: 3000;
|
||||
list-style-type: none;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.contextmenu hr {
|
||||
margin: 5px 0;
|
||||
border: none;
|
||||
height: 1px;
|
||||
font-size: 0px;
|
||||
background-color: var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.contextmenu li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
line-height: 30px;
|
||||
padding: 0 17px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.contextmenu li i {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.contextmenu li:hover {
|
||||
background-color: #ecf5ff;
|
||||
color: #66b1ff;
|
||||
}
|
||||
|
||||
.contextmenu li.disabled {
|
||||
cursor: not-allowed;
|
||||
color: #bbb;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tags-tip {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.tags-tip p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dark .contextmenu li {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<el-container v-loading="loading">
|
||||
<el-main>
|
||||
<el-empty v-if="tasks.length==0" :image-size="120" >
|
||||
<template #description>
|
||||
<h2>没有正在执行的任务</h2>
|
||||
</template>
|
||||
</el-empty>
|
||||
<el-card v-for="task in tasks" :key="task.id" shadow="hover" class="user-bar-tasks-item">
|
||||
<div class="user-bar-tasks-item-body">
|
||||
<div class="taskIcon">
|
||||
<el-icon v-if="task.type=='1'" :size="20"><el-icon-paperclip /></el-icon>
|
||||
</div>
|
||||
<div class="taskMain">
|
||||
<div class="title">
|
||||
<h2>{{ task.task_name }}</h2>
|
||||
<p><span v-time.tip="task.create_time"></span> 创建</p>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="state">
|
||||
<el-tag type="info" v-if="task.status=='0'">待处理</el-tag>
|
||||
<el-tag v-if="task.status=='3'">处理中</el-tag>
|
||||
<el-tag type="info" v-if="task.status=='1'">完成</el-tag>
|
||||
<el-tag type="danger" v-if="task.status=='0'">失败</el-tag>
|
||||
</div>
|
||||
<div class="handler">
|
||||
<el-button v-if="task.status=='1'" type="primary" circle icon="el-icon-download" @click="download(task)"></el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-main>
|
||||
<el-footer style="padding:10px;text-align: right;">
|
||||
<el-button circle icon="el-icon-refresh" @click="refresh"></el-button>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
tasks: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getData()
|
||||
},
|
||||
methods: {
|
||||
async getData(){
|
||||
this.loading = true
|
||||
var res = await this.$API.system.tasks.list()
|
||||
this.tasks = res.data
|
||||
this.loading = false
|
||||
},
|
||||
refresh(){
|
||||
this.getData()
|
||||
},
|
||||
download(row){
|
||||
let a = document.createElement("a")
|
||||
a.style = "display: none"
|
||||
a.target = "_blank"
|
||||
a.href = row.attach
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-bar-tasks-item {margin-bottom: 10px;}
|
||||
.user-bar-tasks-item:hover {border-color: var(--el-color-primary);}
|
||||
.user-bar-tasks-item-body {display: flex;}
|
||||
.user-bar-tasks-item-body .taskIcon {width: 45px;height: 45px;background: var(--el-color-primary-light-9);margin-right: 20px;display: flex;justify-content:center;align-items: center;color: var(--el-color-primary);border-radius:20px;}
|
||||
.user-bar-tasks-item-body .taskMain {flex: 1;}
|
||||
.user-bar-tasks-item-body .title h2 {font-size: 15px;}
|
||||
.user-bar-tasks-item-body .title p {font-size: 12px;color: #999;margin-top: 5px;}
|
||||
.user-bar-tasks-item-body .bottom {display: flex;justify-content: space-between;align-items: center;padding-top: 20px;}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<div class="pi-topbar">
|
||||
<div class="left-panel">
|
||||
<el-breadcrumb separator-icon="el-icon-arrow-right" class="hidden-sm-and-down">
|
||||
<transition-group name="breadcrumb">
|
||||
<template v-for="item in breadList" :key="item.title">
|
||||
<el-breadcrumb-item v-if="item.path!='/' && !item.meta.hiddenBreadcrumb" :key="item.meta.title"><el-icon class="icon" v-if="item.meta.icon"><component :is="item.meta.icon" /></el-icon>{{item.meta.title}}</el-breadcrumb-item>
|
||||
</template>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="center-panel"></div>
|
||||
<div class="right-panel">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
breadList: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getBreadcrumb();
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.getBreadcrumb();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getBreadcrumb(){
|
||||
let matched = this.$route.meta.breadcrumb;
|
||||
this.breadList = matched;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-breadcrumb {margin-left: 15px;}
|
||||
.el-breadcrumb .el-breadcrumb__inner .icon {font-size: 14px;margin-right: 5px;float: left;}
|
||||
.breadcrumb-enter-active,.breadcrumb-leave-active {transition: all 0.3s;}
|
||||
.breadcrumb-enter-from,.breadcrumb-leave-active {opacity: 0;transform: translateX(20px);}
|
||||
.breadcrumb-leave-active {position: absolute;}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
<template>
|
||||
<div class="user-bar">
|
||||
<div class="panel-item hidden-sm-and-down" @click="searchVisible = true">
|
||||
<el-badge :hidden="true">
|
||||
<el-icon>
|
||||
<el-icon-search/>
|
||||
</el-icon>
|
||||
</el-badge>
|
||||
</div>
|
||||
<div class="screen panel-item hidden-sm-and-down" @click="screen">
|
||||
<el-badge :hidden="true">
|
||||
<el-icon>
|
||||
<el-icon-full-screen/>
|
||||
</el-icon>
|
||||
</el-badge>
|
||||
</div>
|
||||
<div class="tasks panel-item" @click="tasksVisible=true">
|
||||
<el-badge :hidden="taskNum==0" :value="taskNum" class="badge" type="danger">
|
||||
<el-icon>
|
||||
<pi-icon-task/>
|
||||
</el-icon>
|
||||
</el-badge>
|
||||
</div>
|
||||
<div class="msg panel-item" @click="showMsg">
|
||||
<el-badge :hidden="msgNum==0" :value="msgNum" class="badge" type="danger">
|
||||
<el-icon>
|
||||
<el-icon-chat-dot-round/>
|
||||
</el-icon>
|
||||
</el-badge>
|
||||
<el-drawer title="站内信息" v-model="msg" :size="400" append-to-body destroy-on-close>
|
||||
<message @closed="closeMsg"></message>
|
||||
</el-drawer>
|
||||
</div>
|
||||
<el-dropdown class="user panel-item" trigger="click" @command="handleUser">
|
||||
<div class="user-avatar">
|
||||
<el-avatar :size="30" :src="avatar">{{ realnameF }}</el-avatar>
|
||||
<label>{{ realname }}</label>
|
||||
<el-icon class="el-icon--right">
|
||||
<el-icon-arrow-down/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="uc">帐号信息</el-dropdown-item>
|
||||
<el-dropdown-item command="clearCache">清除缓存</el-dropdown-item>
|
||||
<el-dropdown-item divided command="outLogin">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<div class="setting panel-item hidden-sm-and-down" @click="settingDialog=true">
|
||||
<el-icon>
|
||||
<pi-icon-brush/>
|
||||
</el-icon>
|
||||
<el-drawer title="页面布局" v-model="settingDialog" :size="400" append-to-body destroy-on-close>
|
||||
<setting></setting>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="searchVisible" :width="700" title="菜单搜索" class="drawerBG" center destroy-on-close>
|
||||
<search @success="searchVisible=false"></search>
|
||||
</el-dialog>
|
||||
<el-drawer v-model="tasksVisible" :size="450" title="任务中心" destroy-on-close>
|
||||
<tasks></tasks>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted} from "vue";
|
||||
import search from './search.vue'
|
||||
import message from './message.vue'
|
||||
import setting from './setting.vue'
|
||||
import tasks from './tasks.vue'
|
||||
import websocket from "@/utils/websocket";
|
||||
import {ElNotification} from 'element-plus';
|
||||
import tools from "@/utils/tools.js";
|
||||
import api from "@/api/index.js";
|
||||
|
||||
let searchVisible = ref(false)
|
||||
let msg = ref(false)
|
||||
let msgNum = ref(0)
|
||||
const tasksVisible = ref(false)
|
||||
const taskNum = ref(0)
|
||||
const settingDialog = ref(false)
|
||||
const userInfo = tools.data.get("USER_INFO");
|
||||
|
||||
let realname = userInfo.realname;
|
||||
let realnameF = realname.substring(0, 1);
|
||||
let avatar = userInfo.avatar
|
||||
|
||||
// mounted
|
||||
onMounted(() => {
|
||||
initWebSocket()
|
||||
loadData()
|
||||
})
|
||||
|
||||
function initWebSocket() {
|
||||
const ws = websocket.init();
|
||||
ws.websocket.onmessage = messageHandle
|
||||
ws.websocket.onclose = closeHandle
|
||||
}
|
||||
|
||||
function messageHandle(message) {
|
||||
if (message.data == "connection successful" || message.data == "pong") {
|
||||
return;
|
||||
}
|
||||
const data = JSON.parse(message.data)
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.code == 200) {
|
||||
ElNotification.success({
|
||||
title: 'Success',
|
||||
message: data['msg']
|
||||
})
|
||||
} else {
|
||||
ElNotification.warning({
|
||||
title: 'Warning',
|
||||
message: data['msg']
|
||||
})
|
||||
}
|
||||
if (data.type == "message") {
|
||||
this.msgNum++
|
||||
} else if (data.type == "task") {
|
||||
this.taskNum++
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function closeHandle() {
|
||||
setTimeout(function () {
|
||||
initWebSocket()
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
//个人信息
|
||||
function handleUser(command) {
|
||||
if (command === "uc") {
|
||||
this.$router.push({path: '/usercenter'});
|
||||
}
|
||||
if (command === "cmd") {
|
||||
this.$router.push({path: '/cmd'});
|
||||
}
|
||||
if (command === "clearCache") {
|
||||
this.$confirm('清除缓存会将系统初始化,包括登录状态、主题、语言设置等,是否继续?', '提示', {
|
||||
type: 'info',
|
||||
}).then(async () => {
|
||||
const loading = this.$loading()
|
||||
tools.data.clear()
|
||||
const [res, err] = await tools.go(api.auth.logout())
|
||||
if (!err) {
|
||||
loading.close()
|
||||
location.reload()
|
||||
this.$message.success(res.msg)
|
||||
this.$router.replace({path: '/login'});
|
||||
}
|
||||
})
|
||||
}
|
||||
if (command === "outLogin") {
|
||||
this.$confirm('确认是否退出当前用户?', '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '退出',
|
||||
confirmButtonClass: 'el-button--danger'
|
||||
}).then(async () => {
|
||||
const [res, err] = await tools.go(api.auth.logout())
|
||||
if (!err) {
|
||||
this.$message.success(res.msg)
|
||||
this.$router.replace({path: '/login'});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//全屏
|
||||
function screen() {
|
||||
tools.screen(document.documentElement)
|
||||
}
|
||||
|
||||
//显示短消息
|
||||
function showMsg() {
|
||||
msg = true
|
||||
}
|
||||
|
||||
function closeMsg(num) {
|
||||
if (num > -1) {
|
||||
msgNum = num
|
||||
}
|
||||
msg = false
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
// let res = await this.$API.home.message.newCount()
|
||||
// this.msgNum = res.data.count
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.user-bar .panel-item {
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-bar .panel-item i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.user-bar .panel-item:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-bar .user-avatar {
|
||||
height: 49px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-bar .user-avatar label {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-bar .store-name {
|
||||
height: 49px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-bar .store-name label {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.msg-list li {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.msg-list li a {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.msg-list li a:hover {
|
||||
background: #ecf5ff;
|
||||
}
|
||||
|
||||
.msg-list__icon {
|
||||
width: 40px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.msg-list__main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.msg-list__main h2 {
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.msg-list__main p {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.8;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.msg-list__time {
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.dark .msg-list__main h2 {
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
.dark .msg-list li {
|
||||
border-top: 1px solid #363636;
|
||||
}
|
||||
|
||||
.dark .msg-list li a:hover {
|
||||
background: #383838;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,11 +1,291 @@
|
|||
<template>
|
||||
<div></div>
|
||||
<!-- 通栏布局 -->
|
||||
<template v-if="layout=='header'">
|
||||
<header class="pi-header">
|
||||
<div class="pi-header-left">
|
||||
<div class="logo-bar">
|
||||
<img class="logo" src="/images/logo.png">
|
||||
<span>{{ config.APP_NAME }}</span>
|
||||
</div>
|
||||
<ul v-if="!ismobile" class="nav">
|
||||
<li v-for="item in menu" :key="item" :class="pmenu.path==item.path?'active':''"
|
||||
@click="showMenu(item)">
|
||||
<el-icon>
|
||||
<component :is="item.meta.icon || 'el-icon-menu'"/>
|
||||
</el-icon>
|
||||
<span>{{ item.meta.title }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pi-header-right">
|
||||
<userbar></userbar>
|
||||
</div>
|
||||
</header>
|
||||
<section class="pi-wrapper">
|
||||
<div v-if="!ismobile && nextMenu.length>0 || !pmenu.component"
|
||||
:class="menuIsCollapse?'pi-side isCollapse':'pi-side'">
|
||||
<div v-if="!menuIsCollapse" class="pi-side-top">
|
||||
<h2>{{ pmenu.meta.title }}</h2>
|
||||
</div>
|
||||
<div class="pi-side-scroll">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="active" router :collapse="menuIsCollapse"
|
||||
:unique-opened="false">
|
||||
<NavMenu :navMenus="nextMenu"></NavMenu>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="pi-side-bottom" @click="store.commit('TOGGLE_menuIsCollapse')">
|
||||
<el-icon>
|
||||
<el-icon-expand v-if="menuIsCollapse"/>
|
||||
<el-icon-fold v-else/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<Side-m v-if="ismobile"></Side-m>
|
||||
<div class="pi-body el-container">
|
||||
<Topbar v-if="!ismobile"></Topbar>
|
||||
<Tags v-if="!ismobile && layoutTags"></Tags>
|
||||
<div class="pi-main" id="pi-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="store.state.keepAlive.keepLiveRoute">
|
||||
<component :is="Component" :key="route.fullPath" v-if="store.state.keepAlive.routeShow"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
<iframe-view></iframe-view>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- 经典布局 -->
|
||||
<template v-else-if="layout=='menu'">
|
||||
<header class="pi-header">
|
||||
<div class="pi-header-left">
|
||||
<div class="logo-bar">
|
||||
<img class="logo" src="/images/logo.png">
|
||||
<span>{{ config.APP_NAME }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pi-header-right">
|
||||
<userbar></userbar>
|
||||
</div>
|
||||
</header>
|
||||
<section class="pi-wrapper">
|
||||
<div v-if="!ismobile" :class="menuIsCollapse?'pi-side isCollapse':'pi-side'">
|
||||
<div class="pi-side-scroll">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="active" router :collapse="menuIsCollapse"
|
||||
:unique-opened="false">
|
||||
<NavMenu :navMenus="menu"></NavMenu>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="pi-side-bottom" @click="store.commit('TOGGLE_menuIsCollapse')">
|
||||
<el-icon>
|
||||
<el-icon-expand v-if="menuIsCollapse"/>
|
||||
<el-icon-fold v-else/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<Side-m v-if="ismobile"></Side-m>
|
||||
<div class="pi-body el-container">
|
||||
<Topbar v-if="!ismobile"></Topbar>
|
||||
<Tags v-if="!ismobile && layoutTags"></Tags>
|
||||
<div class="pi-main" id="pi-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="store.state.keepAlive.keepLiveRoute">
|
||||
<component :is="Component" :key="route.fullPath" v-if="store.state.keepAlive.routeShow"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
<iframe-view></iframe-view>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- 功能坞布局 -->
|
||||
<template v-else-if="layout=='dock'">
|
||||
<header class="pi-header">
|
||||
<div class="pi-header-left">
|
||||
<div class="logo-bar">
|
||||
<img class="logo" src="/images/logo.png">
|
||||
<span>{{ config.APP_NAME }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pi-header-right">
|
||||
<div v-if="!ismobile" class="pi-header-menu">
|
||||
<el-menu mode="horizontal" :default-active="active" router background-color="#222b45"
|
||||
text-color="#fff" active-text-color="var(--el-color-primary)">
|
||||
<NavMenu :navMenus="menu"></NavMenu>
|
||||
</el-menu>
|
||||
</div>
|
||||
<Side-m v-if="ismobile"></Side-m>
|
||||
<userbar></userbar>
|
||||
</div>
|
||||
</header>
|
||||
<section class="pi-wrapper">
|
||||
<div class="pi-body el-container">
|
||||
<Tags v-if="!ismobile && layoutTags"></Tags>
|
||||
<div class="pi-main" id="pi-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="store.state.keepAlive.keepLiveRoute">
|
||||
<component :is="Component" :key="route.fullPath" v-if="store.state.keepAlive.routeShow"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
<iframe-view></iframe-view>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- 默认布局 -->
|
||||
<template v-else>
|
||||
<section class="pi-wrapper">
|
||||
<div v-if="!ismobile" class="pi-side-split">
|
||||
<div class="pi-side-split-top">
|
||||
<router-link to="/dashboard">
|
||||
<img class="logo" :title="config.APP_NAME" src="/images/logo.png">
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="pi-side-split-scroll">
|
||||
<el-scrollbar>
|
||||
<ul>
|
||||
<li v-for="item in menu" :key="item" :class="pmenu.path==item.path?'active':''"
|
||||
@click="showMenu(item)">
|
||||
<el-icon>
|
||||
<component :is="item.meta.icon || el-icon-menu"/>
|
||||
</el-icon>
|
||||
<p>{{ item.meta.title }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!ismobile && nextMenu.length>0 || !pmenu.component"
|
||||
:class="menuIsCollapse?'pi-side isCollapse':'pi-side'">
|
||||
<div v-if="!menuIsCollapse" class="pi-side-top">
|
||||
<h2>{{ pmenu.meta.title }}</h2>
|
||||
</div>
|
||||
<div class="pi-side-scroll">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="active" router :collapse="menuIsCollapse"
|
||||
:unique-opened="false">
|
||||
<NavMenu :navMenus="nextMenu"></NavMenu>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="pi-side-bottom" @click="store.commit('TOGGLE_menuIsCollapse')">
|
||||
<el-icon>
|
||||
<el-icon-expand v-if="menuIsCollapse"/>
|
||||
<el-icon-fold v-else/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<Side-m v-if="ismobile"></Side-m>
|
||||
<div class="pi-body el-container">
|
||||
<Topbar>
|
||||
<userbar></userbar>
|
||||
</Topbar>
|
||||
<Tags v-if="!ismobile && layoutTags"></Tags>
|
||||
<div class="pi-main" id="pi-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="store.state.keepAlive.keepLiveRoute">
|
||||
<component :is="Component" :key="route.fullPath" v-if="store.state.keepAlive.routeShow"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
<iframe-view></iframe-view>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<div class="main-maximize-exit" @click="exitMaximize">
|
||||
<el-icon>
|
||||
<el-icon-close/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup name="index">
|
||||
import {ref, watch, computed, nextTick} from 'vue'
|
||||
import SideM from './components/sideM.vue';
|
||||
import Topbar from './components/topbar.vue';
|
||||
import Tags from './components/tags.vue';
|
||||
import NavMenu from './components/NavMenu.vue';
|
||||
import userbar from './components/userbar.vue';
|
||||
import iframeView from './components/iframeView.vue';
|
||||
import store from '@/store'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import config from "@/config";
|
||||
import {getMenu} from '@/utils/route'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
let nextMenu = ref([])
|
||||
let pmenu = ref([])
|
||||
let active = ref("")
|
||||
let menu = filterUrl(getMenu());
|
||||
|
||||
const ismobile = computed(() => store.state.global.ismobile)
|
||||
const layout = computed(() => store.state.global.layout)
|
||||
const layoutTags = computed(() => store.state.global.layoutTags)
|
||||
const menuIsCollapse = computed(() => store.state.global.menuIsCollapse)
|
||||
|
||||
onLayoutResize();
|
||||
window.addEventListener('resize', onLayoutResize);
|
||||
showThis()
|
||||
|
||||
watch(route, () => {
|
||||
showThis()
|
||||
})
|
||||
|
||||
watch(layout, (val) => {
|
||||
document.body.setAttribute('data-layout', val)
|
||||
}, {immediate: true})
|
||||
|
||||
function exitMaximize() {
|
||||
document.getElementById('app').classList.remove('main-maximize')
|
||||
}
|
||||
|
||||
function filterUrl(map) {
|
||||
const newMap = [];
|
||||
map && map.forEach(item => {
|
||||
item.meta = item.meta ? item.meta : {};
|
||||
//处理隐藏
|
||||
if (item.meta.hidden || item.meta.type == "button") {
|
||||
return false
|
||||
}
|
||||
//处理http
|
||||
if (item.meta.type == 'iframe') {
|
||||
item.path = `/i/${item.name}`;
|
||||
}
|
||||
//递归循环
|
||||
if (item.children && item.children.length > 0) {
|
||||
item.children = filterUrl(item.children)
|
||||
}
|
||||
newMap.push(item)
|
||||
})
|
||||
return newMap;
|
||||
}
|
||||
|
||||
function showMenu(route) {
|
||||
pmenu.value = route;
|
||||
nextMenu.value = filterUrl(route.children);
|
||||
if ((!route.children || route.children.length == 0) && route.component) {
|
||||
router.push({path: route.path})
|
||||
}
|
||||
}
|
||||
|
||||
function showThis() {
|
||||
pmenu.value = route.meta.breadcrumb ? route.meta.breadcrumb[0] : {}
|
||||
nextMenu.value = filterUrl(pmenu.value.children);
|
||||
nextTick(() => {
|
||||
active.value = route.meta.active || route.fullPath;
|
||||
})
|
||||
}
|
||||
|
||||
function onLayoutResize() {
|
||||
store.commit("SET_ismobile", document.body.clientWidth < 992)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import * as elIcons from '@element-plus/icons-vue'
|
||||
import {App} from "vue";
|
||||
import * as piIcons from '@assets/icons'
|
||||
import errorHandler from "@/utils/errorHandler";
|
||||
|
||||
export default {
|
||||
install(app: App) {
|
||||
|
|
@ -7,5 +9,12 @@ export default {
|
|||
for (let icon in elIcons) {
|
||||
app.component(`ElIcon${icon}`, elIcons[icon])
|
||||
}
|
||||
//统一注册sc-icon图标
|
||||
for(let icon in piIcons.default){
|
||||
app.component(`PiIcon${icon}`, piIcons.default[icon])
|
||||
}
|
||||
|
||||
//全局代码错误捕捉
|
||||
app.config.errorHandler = errorHandler
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'nprogress/nprogress.css'
|
|||
import tools from '@/utils/tools';
|
||||
import api from "@/api";
|
||||
import sRouter from './system';
|
||||
import {treeFilter, filterAsyncRouter, flatAsyncRoutes} from '@/utils/route'
|
||||
import {beforeEach, afterEach} from '@/utils/route';
|
||||
|
||||
//系统路由
|
||||
|
|
@ -68,20 +69,19 @@ router.beforeEach(async (to, from, next) => {
|
|||
//加载动态/静态路由
|
||||
if (!isGetRouter) {
|
||||
// 动态加载菜单
|
||||
// const [res, err] = await tools.go(api.auth.menu())
|
||||
// console.log(res)
|
||||
// if (err) {
|
||||
// return false
|
||||
// }
|
||||
// tools.data.set("MENU", tools.makeMenu(res.data.menus, 0))
|
||||
// tools.data.set("PERMISSIONS", res.data.buttons)
|
||||
// tools.data.set("ROLE", res.data.roles)
|
||||
/* @ts-ignore */
|
||||
const res = await api.auth.menu()
|
||||
|
||||
tools.data.set("MENU", tools.makeMenu(res.data.menus, 0))
|
||||
tools.data.set("PERMISSIONS", res.data.buttons)
|
||||
tools.data.set("ROLE", res.data.roles)
|
||||
|
||||
let apiMenu = tools.data.get("MENU") || []
|
||||
let userInfo = tools.data.get("USER_INFO")
|
||||
let userMenu = treeFilter([], node => {
|
||||
return node.meta.role ? node.meta.role.filter(item => userInfo.role.indexOf(item) > -1).length > 0 : true
|
||||
})
|
||||
|
||||
let menu = [...userMenu, ...apiMenu]
|
||||
var menuRouter = filterAsyncRouter(menu)
|
||||
menuRouter = flatAsyncRoutes(menuRouter)
|
||||
|
|
@ -111,82 +111,4 @@ router.onError((error) => {
|
|||
});
|
||||
});
|
||||
|
||||
//入侵追加自定义方法、对象
|
||||
// router.sc_getMenu = () => {
|
||||
// var apiMenu = tools.data.get("MENU") || []
|
||||
// let userInfo = tools.data.get("USER_INFO")
|
||||
// let userMenu = treeFilter([], node => {
|
||||
// return node.meta.role ? node.meta.role.filter(item=>userInfo.role.indexOf(item)>-1).length > 0 : true
|
||||
// })
|
||||
// var menu = [...userMenu, ...apiMenu]
|
||||
// return menu
|
||||
// }
|
||||
|
||||
//转换
|
||||
function filterAsyncRouter(routerMap) {
|
||||
const accessedRouters = []
|
||||
routerMap.forEach(item => {
|
||||
item.meta = item.meta ? item.meta : {};
|
||||
//处理外部链接特殊路由
|
||||
if (item.meta.type == 'iframe') {
|
||||
item.meta.url = item.path;
|
||||
item.path = `/i/${item.name}`;
|
||||
}
|
||||
//MAP转路由对象
|
||||
var route = {
|
||||
path: item.path,
|
||||
name: item.name,
|
||||
meta: item.meta,
|
||||
redirect: item.redirect,
|
||||
children: item.children ? filterAsyncRouter(item.children) : null,
|
||||
component: loadComponent(item.component)
|
||||
}
|
||||
accessedRouters.push(route)
|
||||
})
|
||||
return accessedRouters
|
||||
}
|
||||
|
||||
function loadComponent(component: string) {
|
||||
if (component) {
|
||||
return () => import(/* @vite-ignore */`@/views/${component}`)
|
||||
} else {
|
||||
return () => import(`@/layout/empty`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//路由扁平化
|
||||
function flatAsyncRoutes(routes, breadcrumb = []) {
|
||||
let res = []
|
||||
routes.forEach(route => {
|
||||
const tmp = {...route}
|
||||
if (tmp.children) {
|
||||
let childrenBreadcrumb = [...breadcrumb]
|
||||
childrenBreadcrumb.push(route)
|
||||
let tmpRoute = {...route}
|
||||
tmpRoute.meta.breadcrumb = childrenBreadcrumb
|
||||
delete tmpRoute.children
|
||||
res.push(tmpRoute)
|
||||
let childrenRoutes = flatAsyncRoutes(tmp.children, childrenBreadcrumb)
|
||||
childrenRoutes.map(item => {
|
||||
res.push(item)
|
||||
})
|
||||
} else {
|
||||
let tmpBreadcrumb = [...breadcrumb]
|
||||
tmpBreadcrumb.push(tmp)
|
||||
tmp.meta.breadcrumb = tmpBreadcrumb
|
||||
res.push(tmp)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
//过滤树
|
||||
function treeFilter(tree, func) {
|
||||
return tree.map(node => ({...node})).filter(node => {
|
||||
node.children = node.children && treeFilter(node.children, func)
|
||||
return func(node) || (node.children && node.children.length)
|
||||
})
|
||||
}
|
||||
|
||||
export default router
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import tools from "@/utils/tools";
|
||||
|
||||
export default {
|
||||
state: {
|
||||
//移动端布局
|
||||
ismobile: false,
|
||||
//布局
|
||||
layout: tools.data.get('APP_LAYOUT') || 'default',
|
||||
//菜单是否折叠 toggle
|
||||
menuIsCollapse: false,
|
||||
//多标签栏
|
||||
layoutTags: true
|
||||
},
|
||||
mutations: {
|
||||
SET_ismobile(state, key){
|
||||
state.ismobile = key
|
||||
},
|
||||
SET_layout(state, key){
|
||||
state.layout = key
|
||||
},
|
||||
TOGGLE_menuIsCollapse(state){
|
||||
state.menuIsCollapse = !state.menuIsCollapse
|
||||
},
|
||||
TOGGLE_layoutTags(state){
|
||||
state.layoutTags = !state.layoutTags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
export default {
|
||||
state: {
|
||||
iframeList: []
|
||||
},
|
||||
mutations: {
|
||||
setIframeList(state, route){
|
||||
state.iframeList = []
|
||||
state.iframeList.push(route)
|
||||
},
|
||||
pushIframeList(state, route){
|
||||
let target = state.iframeList.find((item) => item.path === route.path)
|
||||
if(!target){
|
||||
state.iframeList.push(route)
|
||||
}
|
||||
},
|
||||
removeIframeList(state, route){
|
||||
state.iframeList.forEach((item, index) => {
|
||||
if (item.path === route.path){
|
||||
state.iframeList.splice(index, 1)
|
||||
}
|
||||
})
|
||||
},
|
||||
refreshIframe(state, route){
|
||||
state.iframeList.forEach((item) => {
|
||||
if (item.path == route.path){
|
||||
var url = route.meta.url;
|
||||
item.meta.url = '';
|
||||
setTimeout(function() {
|
||||
item.meta.url = url
|
||||
}, 200);
|
||||
}
|
||||
})
|
||||
},
|
||||
clearIframeList(state){
|
||||
state.iframeList = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
export default {
|
||||
state: {
|
||||
keepLiveRoute: [],
|
||||
routeKey: null,
|
||||
routeShow: true
|
||||
},
|
||||
mutations: {
|
||||
pushKeepLive(state, component){
|
||||
if(!state.keepLiveRoute.includes(component)){
|
||||
state.keepLiveRoute.push(component)
|
||||
}
|
||||
},
|
||||
removeKeepLive(state, component){
|
||||
var index = state.keepLiveRoute.indexOf(component);
|
||||
if(index !== -1){
|
||||
state.keepLiveRoute.splice(index, 1);
|
||||
}
|
||||
},
|
||||
clearKeepLive(state){
|
||||
state.keepLiveRoute = []
|
||||
},
|
||||
setRouteKey(state, key){
|
||||
state.routeKey = key
|
||||
},
|
||||
setRouteShow(state, key){
|
||||
state.routeShow = key
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setRouteKey({ commit }, key) {
|
||||
commit('setRouteKey', key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,10 @@ export default {
|
|||
})
|
||||
},
|
||||
updateViewTags(state, route){
|
||||
// state.viewTags = state.viewTags.map(item =>
|
||||
// item.fullPath === route.fullPath ? { ...item, ...route } : item
|
||||
// );
|
||||
|
||||
state.viewTags.forEach((item) => {
|
||||
if (item.fullPath == route.fullPath){
|
||||
item = Object.assign(item, route)
|
||||
|
|
@ -8,8 +8,8 @@ a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: bo
|
|||
* {margin: 0;padding: 0;box-sizing: border-box;outline: none;}
|
||||
|
||||
/* 大布局样式 */
|
||||
.aminui {display: flex;flex-flow: column;}
|
||||
.aminui-wrapper {display: flex;flex:1;overflow: auto;}
|
||||
.pi {display: flex;flex-flow: column;}
|
||||
.pi-wrapper {display: flex;flex:1;overflow: auto;}
|
||||
|
||||
/* 全局滚动条样式 */
|
||||
.scrollable {-webkit-overflow-scrolling: touch;}
|
||||
|
|
@ -24,80 +24,80 @@ a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: bo
|
|||
.layout-setting i {font-size: 18px;color: #fff;}
|
||||
|
||||
/* 头部 */
|
||||
.adminui-header {height: 58px;background: #222b45;color: #fff;display: flex;justify-content:space-between;}
|
||||
.adminui-header-left {display: flex;align-items: center;padding-left:20px;}
|
||||
.adminui-header-right {display: flex;align-items: center;}
|
||||
.adminui-header .logo-bar {font-size: 20px;font-weight: bold;display: flex;align-items: center;}
|
||||
.adminui-header .logo-bar .logo {margin-right: 10px;width: 35px;height: 35px;}
|
||||
.adminui-header .nav {display: flex;height: 100%;margin-left: 40px;}
|
||||
.adminui-header .nav li {padding:0 10px;margin: 0 10px 0 0;font-size: 14px;color: rgba(255, 255, 255, 0.6);list-style: none;height: 100%;display: flex;align-items: center;cursor: pointer;}
|
||||
.adminui-header .nav li i {margin-right: 5px;}
|
||||
.adminui-header .nav li:hover {color: #fff;}
|
||||
.adminui-header .nav li.active {background: rgba(255, 255, 255, 0.1);color: #fff;}
|
||||
.adminui-header .user-bar .panel-item:hover {background: rgba(255, 255, 255, 0.1)!important;}
|
||||
.adminui-header .user-bar .user label{color: #fff;}
|
||||
.pi-header {height: 58px;background: #222b45;color: #fff;display: flex;justify-content:space-between;}
|
||||
.pi-header-left {display: flex;align-items: center;padding-left:20px;}
|
||||
.pi-header-right {display: flex;align-items: center;}
|
||||
.pi-header .logo-bar {font-size: 20px;font-weight: bold;display: flex;align-items: center;}
|
||||
.pi-header .logo-bar .logo {margin-right: 10px;width: 35px;height: 35px;}
|
||||
.pi-header .nav {display: flex;height: 100%;margin-left: 40px;}
|
||||
.pi-header .nav li {padding:0 10px;margin: 0 10px 0 0;font-size: 14px;color: rgba(255, 255, 255, 0.6);list-style: none;height: 100%;display: flex;align-items: center;cursor: pointer;}
|
||||
.pi-header .nav li i {margin-right: 5px;}
|
||||
.pi-header .nav li:hover {color: #fff;}
|
||||
.pi-header .nav li.active {background: rgba(255, 255, 255, 0.1);color: #fff;}
|
||||
.pi-header .user-bar .panel-item:hover {background: rgba(255, 255, 255, 0.1)!important;}
|
||||
.pi-header .user-bar .user label{color: #fff;}
|
||||
|
||||
/* 左侧菜单 */
|
||||
.aminui-side-split {width:65px;flex-shrink:0;background: #222b45;display: flex;flex-flow: column;}
|
||||
.aminui-side-split-top {height: 49px;}
|
||||
.aminui-side-split-top a {display: inline-block;width: 100%;height: 100%;display: flex;align-items: center;justify-content: center;}
|
||||
.aminui-side-split-top .logo {height:30px;vertical-align: bottom;}
|
||||
.adminui-side-split-scroll {overflow: auto;overflow-x:hidden;height: 100%;flex: 1;}
|
||||
.aminui-side-split li {cursor: pointer;width: 65px;height: 65px;color: #fff;text-align: center;display: flex;flex-direction: column;align-items: center;justify-content: center;}
|
||||
.aminui-side-split li i {font-size: 18px;}
|
||||
.aminui-side-split li p {margin-top:5px;}
|
||||
.aminui-side-split li:hover {background: rgba(255, 255, 255, 0.1);}
|
||||
.aminui-side-split li.active {background: #409EFF;}
|
||||
.pi-side-split {width:65px;flex-shrink:0;background: #222b45;display: flex;flex-flow: column;}
|
||||
.pi-side-split-top {height: 49px;}
|
||||
.pi-side-split-top a {display: inline-block;width: 100%;height: 100%;display: flex;align-items: center;justify-content: center;}
|
||||
.pi-side-split-top .logo {height:30px;vertical-align: bottom;}
|
||||
.pi-side-split-scroll {overflow: auto;overflow-x:hidden;height: 100%;flex: 1;}
|
||||
.pi-side-split li {cursor: pointer;width: 65px;height: 65px;color: #fff;text-align: center;display: flex;flex-direction: column;align-items: center;justify-content: center;}
|
||||
.pi-side-split li i {font-size: 18px;}
|
||||
.pi-side-split li p {margin-top:5px;}
|
||||
.pi-side-split li:hover {background: rgba(255, 255, 255, 0.1);}
|
||||
.pi-side-split li.active {background: #409EFF;}
|
||||
|
||||
.adminui-side-split-scroll::-webkit-scrollbar-thumb {background-color: rgba(255, 255, 255, 0.4);border-radius:5px;}
|
||||
.adminui-side-split-scroll::-webkit-scrollbar-thumb:hover {background-color: rgba(255, 255, 255, 0.5);}
|
||||
.adminui-side-split-scroll::-webkit-scrollbar-track {background-color: rgba(255, 255, 255, 0);}
|
||||
.adminui-side-split-scroll::-webkit-scrollbar-track:hover {background-color: rgba(255, 255, 255, 0);}
|
||||
.pi-side-split-scroll::-webkit-scrollbar-thumb {background-color: rgba(255, 255, 255, 0.4);border-radius:5px;}
|
||||
.pi-side-split-scroll::-webkit-scrollbar-thumb:hover {background-color: rgba(255, 255, 255, 0.5);}
|
||||
.pi-side-split-scroll::-webkit-scrollbar-track {background-color: rgba(255, 255, 255, 0);}
|
||||
.pi-side-split-scroll::-webkit-scrollbar-track:hover {background-color: rgba(255, 255, 255, 0);}
|
||||
|
||||
.aminui-side {display: flex;flex-flow: column;flex-shrink:0;width:210px;background: #fff;box-shadow: 2px 0 8px 0 rgba(29,35,41,.05);border-right: 1px solid #e6e6e6;transition:width 0.3s;}
|
||||
.adminui-side-top {border-bottom: 1px solid #ebeef5;height:50px;line-height: 50px;}
|
||||
.adminui-side-top h2 {padding:0 20px;font-size: 17px;color: #3c4a54;}
|
||||
.adminui-side-scroll {overflow: auto;overflow-x:hidden;flex: 1;}
|
||||
.adminui-side-bottom {border-top: 1px solid #ebeef5;height:51px;cursor: pointer;display: flex;align-items: center;justify-content: center;}
|
||||
.adminui-side-bottom i {font-size: 16px;}
|
||||
.adminui-side-bottom:hover {color: var(--el-color-primary);}
|
||||
.aminui-side.isCollapse {width: 65px;}
|
||||
.pi-side {display: flex;flex-flow: column;flex-shrink:0;width:210px;background: #fff;box-shadow: 2px 0 8px 0 rgba(29,35,41,.05);border-right: 1px solid #e6e6e6;transition:width 0.3s;}
|
||||
.pi-side-top {border-bottom: 1px solid #ebeef5;height:50px;line-height: 50px;}
|
||||
.pi-side-top h2 {padding:0 20px;font-size: 17px;color: #3c4a54;}
|
||||
.pi-side-scroll {overflow: auto;overflow-x:hidden;flex: 1;}
|
||||
.pi-side-bottom {border-top: 1px solid #ebeef5;height:51px;cursor: pointer;display: flex;align-items: center;justify-content: center;}
|
||||
.pi-side-bottom i {font-size: 16px;}
|
||||
.pi-side-bottom:hover {color: var(--el-color-primary);}
|
||||
.pi-side.isCollapse {width: 65px;}
|
||||
.el-menu .menu-tag {position: absolute;height: 18px;line-height: 18px;background: var(--el-color-danger);font-size: 12px;color: #fff;right: 20px;border-radius:18px;padding:0 6px;}
|
||||
.el-menu .el-sub-menu__title .menu-tag {right: 40px;}
|
||||
.el-menu--horizontal > li .menu-tag {display: none;}
|
||||
|
||||
/* 右侧内容 */
|
||||
.aminui-body {flex: 1;display: flex;flex-flow: column;}
|
||||
.pi-body {flex: 1;display: flex;flex-flow: column;}
|
||||
|
||||
.adminui-topbar {height: 50px;border-bottom: 1px solid #ebeef5;background: #fff;box-shadow: 0 1px 4px rgba(0,21,41,.08);display: flex;justify-content:space-between;}
|
||||
.adminui-topbar .left-panel {display: flex;align-items: center;}
|
||||
.adminui-topbar .right-panel {display: flex;align-items: center;}
|
||||
.pi-topbar {height: 50px;border-bottom: 1px solid #ebeef5;background: #fff;box-shadow: 0 1px 4px rgba(0,21,41,.08);display: flex;justify-content:space-between;}
|
||||
.pi-topbar .left-panel {display: flex;align-items: center;}
|
||||
.pi-topbar .right-panel {display: flex;align-items: center;}
|
||||
|
||||
.right-panel-search {display: flex;align-items: center;}
|
||||
.right-panel-search > * + * {margin-left:10px;}
|
||||
|
||||
.adminui-tags {height:35px;background: #fff;border-bottom: 1px solid #e6e6e6;}
|
||||
.adminui-tags ul {display: flex;overflow: hidden;}
|
||||
.adminui-tags li {cursor: pointer;display: inline-block;float: left;height:34px;line-height: 34px;position: relative;flex-shrink: 0;}
|
||||
.adminui-tags li::after {content: " ";width:1px;height:100%;position: absolute;right:0px;background-image: linear-gradient(#fff, #e6e6e6);}
|
||||
.adminui-tags li a {display: inline-block;padding:0 10px;width:100%;height:100%;color: #999;text-decoration:none;display: flex;align-items: center;}
|
||||
.adminui-tags li i {margin-left:10px;border-radius: 3px;width:18px;height:18px;display: flex;align-items: center;justify-content: center;}
|
||||
.adminui-tags li i:hover {background: rgba(0,0,0,.2);color: #fff;}
|
||||
.adminui-tags li:hover {background: #ecf5ff;}
|
||||
.adminui-tags li.active {background: #409EFF;}
|
||||
.adminui-tags li.active a {color: #fff;}
|
||||
.adminui-tags li.sortable-ghost {opacity: 0;}
|
||||
.pi-tags {height:35px;background: #fff;border-bottom: 1px solid #e6e6e6;}
|
||||
.pi-tags ul {display: flex;overflow: hidden;}
|
||||
.pi-tags li {cursor: pointer;display: inline-block;float: left;height:34px;line-height: 34px;position: relative;flex-shrink: 0;}
|
||||
.pi-tags li::after {content: " ";width:1px;height:100%;position: absolute;right:0px;background-image: linear-gradient(#fff, #e6e6e6);}
|
||||
.pi-tags li a {display: inline-block;padding:0 10px;width:100%;height:100%;color: #999;text-decoration:none;display: flex;align-items: center;}
|
||||
.pi-tags li i {margin-left:10px;border-radius: 3px;width:18px;height:18px;display: flex;align-items: center;justify-content: center;}
|
||||
.pi-tags li i:hover {background: rgba(0,0,0,.2);color: #fff;}
|
||||
.pi-tags li:hover {background: #ecf5ff;}
|
||||
.pi-tags li.active {background: #409EFF;}
|
||||
.pi-tags li.active a {color: #fff;}
|
||||
.pi-tags li.sortable-ghost {opacity: 0;}
|
||||
|
||||
.adminui-main {overflow: auto;background-color: #f6f8f9;flex: 1;}
|
||||
.pi-main {overflow: auto;background-color: #f6f8f9;flex: 1;}
|
||||
|
||||
/*页面最大化*/
|
||||
.aminui.main-maximize {
|
||||
.pi.main-maximize {
|
||||
.main-maximize-exit {display: block;}
|
||||
.aminui-side-split, .aminui-side, .adminui-header, .adminui-topbar, .adminui-tags {display: none;}
|
||||
.pi-side-split, .pi-side, .pi-header, .pi-topbar, .pi-tags {display: none;}
|
||||
}
|
||||
.main-maximize-exit {display: none;position: fixed;z-index: 3000;top:-20px;left:50%;margin-left: -20px;border-radius: 50%;width: 40px;height: 40px;cursor: pointer;background: rgba(0,0,0,0.2);text-align: center;}
|
||||
.main-maximize-exit i {font-size: 14px;margin-top: 22px;color: #fff;}
|
||||
.main-maximize-exit:hover {background: rgba(0,0,0,0.4);}
|
||||
|
||||
/*定宽页面*/
|
||||
.sc-page {width: 1230px;margin: 0 auto;}
|
||||
.pi-page {width: 1230px;margin: 0 auto;}
|
||||
|
|
|
|||
|
|
@ -17,16 +17,16 @@ html.dark {
|
|||
.login_bg {background: var(--el-bg-color);}
|
||||
|
||||
//框架
|
||||
.adminui-header {background: var(--el-bg-color-overlay);border-bottom: 1px solid var(--el-border-color-light);height:59px;}
|
||||
.aminui-side-split {background: var(--el-bg-color);}
|
||||
.aminui-side-split li {color: var(--el-text-color-primary);}
|
||||
.aminui-side {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
|
||||
.adminui-side-top, .adminui-side-bottom {border-color: var(--el-border-color-light);}
|
||||
.adminui-side-top h2 {color: var(--el-text-color-primary);}
|
||||
.adminui-topbar, .adminui-tags {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
|
||||
.adminui-main {background: var(--el-bg-color);}
|
||||
.pi-header {background: var(--el-bg-color-overlay);border-bottom: 1px solid var(--el-border-color-light);height:59px;}
|
||||
.pi-side-split {background: var(--el-bg-color);}
|
||||
.pi-side-split li {color: var(--el-text-color-primary);}
|
||||
.pi-side {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
|
||||
.pi-side-top, .pi-side-bottom {border-color: var(--el-border-color-light);}
|
||||
.pi-side-top h2 {color: var(--el-text-color-primary);}
|
||||
.pi-topbar, .pi-tags {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
|
||||
.pi-main {background: var(--el-bg-color);}
|
||||
.drawerBG {background: var(--el-bg-color);}
|
||||
.adminui-header-menu .el-menu {--el-menu-bg-color:var(--el-bg-color-overlay) !important;--el-menu-hover-bg-color: #171819 !important;}
|
||||
.pi-header-menu .el-menu {--el-menu-bg-color:var(--el-bg-color-overlay) !important;--el-menu-hover-bg-color: #171819 !important;}
|
||||
|
||||
//组件
|
||||
.el-header, .el-main.nopadding, .el-footer {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@
|
|||
|
||||
.el-dropdown-link {cursor: pointer;color: var(--el-color-primary);line-height: 22px;font-size: 12px;}
|
||||
|
||||
.aminui-side-split li.active {background-color: var(--el-color-primary);}
|
||||
.adminui-tags li:hover {background-color: var(--el-color-primary-light-9);}
|
||||
.adminui-tags li.active {background-color: var(--el-color-primary)!important;}
|
||||
.pi-side-split li.active {background-color: var(--el-color-primary);}
|
||||
.pi-tags li:hover {background-color: var(--el-color-primary-light-9);}
|
||||
.pi-tags li.active {background-color: var(--el-color-primary)!important;}
|
||||
.contextmenu li:hover {background-color: var(--el-color-primary-light-9)!important;color: var(--el-color-primary-light-2)!important;}
|
||||
.data-box .item-background {background-color: var(--el-color-primary)!important;}
|
||||
.layout-setting,.diy-grid-setting {background-color: var(--el-color-primary)!important;}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
.el-drawer.rtl {width: 90%!important;}
|
||||
.el-form-item__content {margin-left: 0px!important;}
|
||||
|
||||
.adminui-main {
|
||||
.pi-main {
|
||||
>.el-container {display: block;height:auto;}
|
||||
>.el-container > .el-aside {width: 100%!important;border: 0}
|
||||
}
|
||||
|
|
@ -29,21 +29,21 @@
|
|||
.right-panel .right-panel-search {display: block;}
|
||||
.right-panel .right-panel-search >* {width: 100%;margin: 0;margin-top: 15px;}
|
||||
}
|
||||
.adminui-main > .el-container >*:first-child:not(.el-aside):not(.el-header) {border: 0;margin-top: 0;}
|
||||
.adminui-main > .el-container >*:first-child:not(.el-aside):not(.el-header) + .el-aside {margin-top: 0;}
|
||||
.adminui-main > .el-container > .el-aside {border-bottom: 1px solid var(--el-border-color-light)!important;}
|
||||
.adminui-main > .el-container > .el-container {border-top: 1px solid var(--el-border-color-light);border-bottom: 1px solid var(--el-border-color-light);margin-top: 15px;}
|
||||
.adminui-main > .el-container > .el-container + .el-aside {border-top: 1px solid var(--el-border-color-light);margin-top: 15px;}
|
||||
.adminui-main > .el-container > .el-header {@extend .headerPublic;}
|
||||
.adminui-main > .el-container > .el-main.nopadding {border-top: 1px solid var(--el-border-color-light);border-bottom: 1px solid var(--el-border-color-light);margin-top: 15px;}
|
||||
.adminui-main > .el-container > .el-main + .el-aside {border-left: 0!important;border-top: 1px solid var(--el-border-color-light);margin-top: 15px;}
|
||||
.adminui-main > .el-container > .el-footer {margin-top: 15px;border-bottom: 1px solid var(--el-border-color-light);}
|
||||
.adminui-main > .el-container > .el-container > .el-header {@extend .headerPublic}
|
||||
.adminui-main > .el-container > .el-container > .el-header .left-panel {display: block;}
|
||||
.adminui-main > .el-container > .el-container > .el-header .right-panel {display: block;margin-top: 15px;}
|
||||
.pi-main > .el-container >*:first-child:not(.el-aside):not(.el-header) {border: 0;margin-top: 0;}
|
||||
.pi-main > .el-container >*:first-child:not(.el-aside):not(.el-header) + .el-aside {margin-top: 0;}
|
||||
.pi-main > .el-container > .el-aside {border-bottom: 1px solid var(--el-border-color-light)!important;}
|
||||
.pi-main > .el-container > .el-container {border-top: 1px solid var(--el-border-color-light);border-bottom: 1px solid var(--el-border-color-light);margin-top: 15px;}
|
||||
.pi-main > .el-container > .el-container + .el-aside {border-top: 1px solid var(--el-border-color-light);margin-top: 15px;}
|
||||
.pi-main > .el-container > .el-header {@extend .headerPublic;}
|
||||
.pi-main > .el-container > .el-main.nopadding {border-top: 1px solid var(--el-border-color-light);border-bottom: 1px solid var(--el-border-color-light);margin-top: 15px;}
|
||||
.pi-main > .el-container > .el-main + .el-aside {border-left: 0!important;border-top: 1px solid var(--el-border-color-light);margin-top: 15px;}
|
||||
.pi-main > .el-container > .el-footer {margin-top: 15px;border-bottom: 1px solid var(--el-border-color-light);}
|
||||
.pi-main > .el-container > .el-container > .el-header {@extend .headerPublic}
|
||||
.pi-main > .el-container > .el-container > .el-header .left-panel {display: block;}
|
||||
.pi-main > .el-container > .el-container > .el-header .right-panel {display: block;margin-top: 15px;}
|
||||
|
||||
.sc-page {width: 100%;margin: 0;}
|
||||
|
||||
|
||||
.common-main .el-form {width: 100% !important;}
|
||||
.common-header-logo label {display: none;}
|
||||
.common-header-title {display: none;}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
export default {
|
||||
//hex颜色转rgb颜色
|
||||
HexToRgb(str) {
|
||||
str = str.replace("#", "")
|
||||
var hxs = str.match(/../g)
|
||||
for (var i = 0; i < 3; i++) hxs[i] = parseInt(hxs[i], 16)
|
||||
return hxs
|
||||
},
|
||||
//rgb颜色转hex颜色
|
||||
RgbToHex(a, b, c) {
|
||||
var hexs = [a.toString(16), b.toString(16), c.toString(16)]
|
||||
for (var i = 0; i < 3; i++) {
|
||||
if (hexs[i].length == 1) hexs[i] = "0" + hexs[i]
|
||||
}
|
||||
return "#" + hexs.join("");
|
||||
},
|
||||
//加深
|
||||
darken(color, level) {
|
||||
var rgbc = this.HexToRgb(color)
|
||||
for (var i = 0; i < 3; i++) rgbc[i] = Math.floor(rgbc[i] * (1 - level))
|
||||
return this.RgbToHex(rgbc[0], rgbc[1], rgbc[2])
|
||||
},
|
||||
//变淡
|
||||
lighten(color, level) {
|
||||
var rgbc = this.HexToRgb(color)
|
||||
for (var i = 0; i < 3; i++) rgbc[i] = Math.floor((255 - rgbc[i]) * level + rgbc[i])
|
||||
return this.RgbToHex(rgbc[0], rgbc[1], rgbc[2])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* 全局代码错误捕捉
|
||||
* 比如 null.length 就会被捕捉到
|
||||
*/
|
||||
import {nextTick} from "vue";
|
||||
|
||||
export default (error, vm)=>{
|
||||
//过滤HTTP请求错误
|
||||
if(error.status || error.status==0){
|
||||
return false
|
||||
}
|
||||
|
||||
// var errorMap = {
|
||||
// InternalError: "Javascript引擎内部错误",
|
||||
// ReferenceError: "未找到对象",
|
||||
// TypeError: "使用了错误的类型或对象",
|
||||
// RangeError: "使用内置对象时,参数超范围",
|
||||
// SyntaxError: "语法错误",
|
||||
// EvalError: "错误的使用了Eval",
|
||||
// URIError: "URI错误"
|
||||
// }
|
||||
// var errorName = errorMap[error.name] || "未知错误"
|
||||
|
||||
console.warn(`[PI error]: ${error}`);
|
||||
console.error(error);
|
||||
//throw error;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
// vm.$notify.error({
|
||||
// title: errorName,
|
||||
// message: error
|
||||
// });
|
||||
})
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
// import store from '@/store'
|
||||
import store from '@/store'
|
||||
import {nextTick} from 'vue'
|
||||
import {RouteLocationNormalized, RouteLocationNormalizedLoaded} from "vue-router";
|
||||
import tools from "@/utils/tools";
|
||||
|
||||
export function beforeEach(to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded) {
|
||||
const adminMain = document.querySelector('#pi-main');
|
||||
if (!adminMain) {
|
||||
return false
|
||||
}
|
||||
// store.commit("updateViewTags", {
|
||||
// fullPath: from.fullPath,
|
||||
// scrollTop: adminMain.scrollTop
|
||||
// })
|
||||
store.commit("updateViewTags", {
|
||||
fullPath: from.fullPath,
|
||||
scrollTop: adminMain.scrollTop
|
||||
})
|
||||
}
|
||||
|
||||
export function afterEach(to: RouteLocationNormalized) {
|
||||
|
|
@ -18,10 +19,87 @@ export function afterEach(to: RouteLocationNormalized) {
|
|||
if (!adminMain) {
|
||||
return false
|
||||
}
|
||||
// nextTick(() => {
|
||||
// const beforeRoute = store.state.views.viewTags.filter(v => v.fullPath == to.fullPath)[0];
|
||||
// if (beforeRoute) {
|
||||
// adminMain.scrollTop = beforeRoute.scrollTop || 0
|
||||
// }
|
||||
// }).then(r => {})
|
||||
nextTick(() => {
|
||||
// @ts-ignore
|
||||
const beforeRoute = store.state.viewTags.viewTags.filter(v => v.fullPath == to.fullPath)[0];
|
||||
if (beforeRoute) {
|
||||
adminMain.scrollTop = beforeRoute.scrollTop || 0
|
||||
}
|
||||
}).then(r => {
|
||||
})
|
||||
}
|
||||
|
||||
//转换
|
||||
export function filterAsyncRouter(routerMap) {
|
||||
const accessedRouters = []
|
||||
routerMap.forEach(item => {
|
||||
item.meta = item.meta ? item.meta : {};
|
||||
//处理外部链接特殊路由
|
||||
if (item.meta.type == 'iframe') {
|
||||
item.meta.url = item.path;
|
||||
item.path = `/i/${item.name}`;
|
||||
}
|
||||
//MAP转路由对象
|
||||
var route = {
|
||||
path: item.path,
|
||||
name: item.name,
|
||||
meta: item.meta,
|
||||
redirect: item.redirect,
|
||||
children: item.children ? filterAsyncRouter(item.children) : null,
|
||||
component: loadComponent(item.component)
|
||||
}
|
||||
accessedRouters.push(route)
|
||||
})
|
||||
return accessedRouters
|
||||
}
|
||||
|
||||
export function loadComponent(component: string) {
|
||||
if (component) {
|
||||
return () => import(/* @vite-ignore */`/src/views/${component}`)
|
||||
} else {
|
||||
return () => import(`@/layout/empty`)
|
||||
}
|
||||
}
|
||||
|
||||
//路由扁平化
|
||||
export function flatAsyncRoutes(routes, breadcrumb = []) {
|
||||
let res = []
|
||||
routes.forEach(route => {
|
||||
const tmp = {...route}
|
||||
if (tmp.children) {
|
||||
let childrenBreadcrumb = [...breadcrumb]
|
||||
childrenBreadcrumb.push(route)
|
||||
let tmpRoute = {...route}
|
||||
tmpRoute.meta.breadcrumb = childrenBreadcrumb
|
||||
delete tmpRoute.children
|
||||
res.push(tmpRoute)
|
||||
let childrenRoutes = flatAsyncRoutes(tmp.children, childrenBreadcrumb)
|
||||
childrenRoutes.map(item => {
|
||||
res.push(item)
|
||||
})
|
||||
} else {
|
||||
let tmpBreadcrumb = [...breadcrumb]
|
||||
tmpBreadcrumb.push(tmp)
|
||||
tmp.meta.breadcrumb = tmpBreadcrumb
|
||||
res.push(tmp)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
//过滤树
|
||||
export function treeFilter(tree, func) {
|
||||
return tree.map(node => ({...node})).filter(node => {
|
||||
node.children = node.children && treeFilter(node.children, func)
|
||||
return func(node) || (node.children && node.children.length)
|
||||
})
|
||||
}
|
||||
|
||||
export function getMenu() {
|
||||
const apiMenu = tools.data.get("MENU") || [];
|
||||
let userInfo = tools.data.get("USER_INFO")
|
||||
let userMenu = treeFilter([], node => {
|
||||
return node.meta.role ? node.meta.role.filter(item => userInfo.role.indexOf(item) > -1).length > 0 : true
|
||||
})
|
||||
return [...userMenu, ...apiMenu]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import CryptoJS from 'crypto-js';
|
|||
|
||||
const tools = {
|
||||
data: {
|
||||
set(cacheKey, data, expireIn = 0) {
|
||||
set(cacheKey: string, data: any, expireIn: number = 0) {
|
||||
let cacheValue = {
|
||||
content: data,
|
||||
expireIn: expireIn === 0 ? 0 : new Date().getTime() + expireIn * 1000
|
||||
}
|
||||
return localStorage.setItem(cacheKey, tools.base64.encrypt(JSON.stringify(cacheValue)))
|
||||
},
|
||||
get(cacheKey) {
|
||||
get(cacheKey: string) {
|
||||
try {
|
||||
const cacheValue = JSON.parse(tools.base64.decrypt(localStorage.getItem(cacheKey)))
|
||||
if (cacheValue) {
|
||||
|
|
@ -25,7 +25,7 @@ const tools = {
|
|||
return null
|
||||
}
|
||||
},
|
||||
remove(cacheKey) {
|
||||
remove(cacheKey: string) {
|
||||
return localStorage.removeItem(cacheKey)
|
||||
},
|
||||
clear() {
|
||||
|
|
@ -33,10 +33,10 @@ const tools = {
|
|||
}
|
||||
},
|
||||
base64: {
|
||||
encrypt(data) {
|
||||
encrypt(data: string) {
|
||||
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data))
|
||||
},
|
||||
decrypt(cipher) {
|
||||
decrypt(cipher: string) {
|
||||
return CryptoJS.enc.Base64.parse(cipher).toString(CryptoJS.enc.Utf8)
|
||||
}
|
||||
},
|
||||
|
|
@ -50,9 +50,58 @@ const tools = {
|
|||
},
|
||||
crypto: {
|
||||
//MD5加密
|
||||
MD5(data){
|
||||
MD5(data: string) {
|
||||
return CryptoJS.MD5(data).toString()
|
||||
},
|
||||
},
|
||||
makeMenu: function (menus, parent_id = 0) {
|
||||
const arr = [];
|
||||
for (let item of menus) {
|
||||
if (item.parent_id === parent_id) {
|
||||
// 数据格式处理
|
||||
const tmp = {
|
||||
name: item['name'],
|
||||
path: '/' + item['path'],
|
||||
component: item['path'],
|
||||
meta: {
|
||||
'title': item['title'],
|
||||
'icon': item['icon'],
|
||||
'hidden': item['hidden'],
|
||||
'type': 'menu'
|
||||
}
|
||||
};
|
||||
const children = this.makeMenu(menus, item.menu_id);
|
||||
if (children.length > 0) {
|
||||
tmp['children'] = children
|
||||
}
|
||||
arr.push(tmp)
|
||||
}
|
||||
}
|
||||
return arr
|
||||
},
|
||||
screen: function (element) {
|
||||
var isFull = !!(document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement || document.fullscreenElement);
|
||||
if (isFull) {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (element.requestFullscreen) {
|
||||
element.requestFullscreen();
|
||||
} else if (element.msRequestFullscreen) {
|
||||
element.msRequestFullscreen();
|
||||
} else if (element.mozRequestFullScreen) {
|
||||
element.mozRequestFullScreen();
|
||||
} else if (element.webkitRequestFullscreen) {
|
||||
element.webkitRequestFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import sysConfig from "@/config";
|
||||
import tools from "@/utils/tools";
|
||||
|
||||
const ws = {
|
||||
websocket: null,
|
||||
timer: null,
|
||||
init: function () {
|
||||
let token = tools.data.get("TOKEN");
|
||||
ws.websocket = new WebSocket(sysConfig.WS_URL + "?Authorization=" + token);
|
||||
ws.websocket.onopen = ws.open;
|
||||
return ws
|
||||
},
|
||||
open: function () {
|
||||
if (ws.timer) {
|
||||
clearInterval(ws.timer); // 停止心跳
|
||||
}
|
||||
ws.startBeat()
|
||||
},
|
||||
startBeat: function (){
|
||||
ws.timer = setInterval(() => {
|
||||
if (ws.websocket && ws.websocket.readyState === WebSocket.OPEN) {
|
||||
ws.websocket.send("ping");
|
||||
}
|
||||
}, 30000);
|
||||
},
|
||||
send: function (message){
|
||||
ws.websocket.send(JSON.stringify(message))
|
||||
}
|
||||
}
|
||||
|
||||
export default ws;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div v-if="pageLoading">
|
||||
<el-main>
|
||||
<el-card shadow="never">
|
||||
<el-skeleton :rows="1"></el-skeleton>
|
||||
</el-card>
|
||||
<el-card shadow="never" style="margin-top: 15px;">
|
||||
<el-skeleton></el-skeleton>
|
||||
</el-card>
|
||||
</el-main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "dashboard",
|
||||
components: {
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
pageLoading: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onMounted(){
|
||||
this.pageLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
<div class="login-form">
|
||||
<div class="login-header">
|
||||
<div class="logo">
|
||||
<img :alt="sysConfig.APP_NAME" :src="'./images/logo.png'">
|
||||
<img :alt="sysConfig.APP_NAME" src="/images/logo.png">
|
||||
<label>{{ sysConfig.APP_NAME }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Login">
|
||||
<script setup name="login">
|
||||
import {getCurrentInstance, ref, onMounted, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRouter} from 'vue-router';
|
||||
|
|
@ -191,7 +191,7 @@ async function login() {
|
|||
}
|
||||
|
||||
async function getInfo() {
|
||||
const [res, err] = await tools.go(api.auth.getAccountInfo())
|
||||
const [res, err] = await tools.go(api.auth.info())
|
||||
if (err) {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,17 @@ interface ImportMeta {
|
|||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE: string
|
||||
readonly VITE_APP_TITLE: string
|
||||
readonly VITE_WS_URL: string
|
||||
}
|
||||
|
||||
interface Document {
|
||||
webkitIsFullScreen: boolean;
|
||||
webkitFullscreenElement: Element | null;
|
||||
mozFullScreen: boolean;
|
||||
msFullscreenElement: Element | null;
|
||||
msExitFullscreen: () => void;
|
||||
mozCancelFullScreen: () => void;
|
||||
webkitExitFullscreen: () => void;
|
||||
}
|
||||
|
||||
declare module '*.vue' {
|
||||
|
|
@ -23,3 +34,9 @@ declare module '@/*' {
|
|||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module '@assets/*' {
|
||||
import { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +1,48 @@
|
|||
import {defineConfig, loadEnv} from 'vite'
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig(({mode, command}) => {
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
const {VITE_APP_ENV} = env
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
// 设置路径
|
||||
'~': path.resolve(__dirname, './'),
|
||||
// 设置别名
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
// 资源地址
|
||||
'@assets': path.resolve(__dirname, './src/assets')
|
||||
},
|
||||
extensions: ['.ts', '.json', '.vue', '.js']
|
||||
},
|
||||
base: '/',
|
||||
// vite 相关配置
|
||||
server: {
|
||||
port: 8611,
|
||||
host: true,
|
||||
// open: true,
|
||||
proxy: {
|
||||
'/dev-api': {
|
||||
target: 'https://dev.api.leapy.cn/merchant/',
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/dev-api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'static',
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
return 'vendor'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [vue()],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
// additionalData: `@use "@/styles/variables.scss" as *;`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export default defineConfig(({}) => {
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
// 设置路径
|
||||
'~': path.resolve(__dirname, './'),
|
||||
// 设置别名
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
// 资源地址
|
||||
'@assets': path.resolve(__dirname, './src/assets')
|
||||
},
|
||||
extensions: ['.ts', '.json', '.vue', '.js']
|
||||
},
|
||||
base: '/',
|
||||
// vite 相关配置
|
||||
server: {
|
||||
port: 8611,
|
||||
host: true,
|
||||
// open: true,
|
||||
proxy: {
|
||||
'/dev-api': {
|
||||
target: 'https://dev.api.leapy.cn/merchant/',
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/dev-api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'static',
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
return 'vendor'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [vue()]
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue