基本框架

This commit is contained in:
zhang zhuo 2025-06-04 14:43:52 +08:00
parent a474d7a8b2
commit be9750b9eb
41 changed files with 2204 additions and 258 deletions

View File

@ -6,3 +6,4 @@ VITE_APP_ENV='development'
# 开发环境
VITE_API_BASE='/dev-api'
VITE_WS_URL='/dev-api'

View File

@ -6,6 +6,7 @@ VITE_APP_ENV='production'
# 生产环境
VITE_API_BASE='/prod-api'
VITE_WS_URL='/prod-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS=gzip

View File

@ -6,6 +6,7 @@ VITE_APP_ENV='staging'
# 生产环境
VITE_API_BASE='/staging-api'
VITE_WS_URL='/staging-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS=gzip

View File

@ -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"/>

View File

@ -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");
},
}

View File

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

View File

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

17
src/assets/icons/index.js Normal file
View File

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

View File

@ -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,
//请求是否开启缓存

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 falsedivonmouseup
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>

View File

@ -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 3120 -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

28
src/store/model/global.ts Normal file
View File

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

38
src/store/model/iframe.ts Normal file
View File

@ -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 = []
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)

View File

@ -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;}

View File

@ -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);}

View File

@ -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;}

View File

@ -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;}

29
src/utils/color.ts Normal file
View File

@ -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])
}
}

34
src/utils/errorHandler.ts Normal file
View File

@ -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
// });
})
}

View File

@ -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]
}

View File

@ -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();
}
}
}
}

31
src/utils/websocket.ts Normal file
View File

@ -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;

View File

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

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>

View File

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

17
vite-env.d.ts vendored
View File

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

View File

@ -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()]
}
})