全局指令

This commit is contained in:
zhang zhuo 2025-06-05 13:56:07 +08:00
parent b1695084f2
commit d953f5d7ac
14 changed files with 474 additions and 181 deletions

22
src/directives/auth.ts Normal file
View File

@ -0,0 +1,22 @@
import {permission} from '@/utils/permission'
export default {
mounted(el, binding) {
const {value} = binding
if (Array.isArray(value)) {
let ishas = false;
value.forEach(item => {
if (permission(item)) {
ishas = true;
}
})
if (!ishas) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
if (!permission(value)) {
el.parentNode && el.parentNode.removeChild(el);
}
}
}
};

29
src/directives/copy.ts Normal file
View File

@ -0,0 +1,29 @@
import {ElMessage} from 'element-plus'
export default {
mounted(el, binding) {
el.$value = binding.value
el.handler = () => {
const textarea = document.createElement('textarea')
textarea.readOnly = true
textarea.style.position = 'absolute'
textarea.style.left = '-9999px'
textarea.value = el.$value
document.body.appendChild(textarea)
textarea.select()
textarea.setSelectionRange(0, textarea.value.length)
const result = document.execCommand('Copy')
if (result) {
ElMessage.success("复制成功")
}
document.body.removeChild(textarea)
}
el.addEventListener('click', el.handler)
},
updated(el, binding) {
el.$value = binding.value
},
unmounted(el) {
el.removeEventListener('click', el.handler)
}
}

44
src/directives/drag.ts Normal file
View File

@ -0,0 +1,44 @@
export default {
mounted(el, binding) {
let oDiv = el; //当前元素
let firstTime: number;
let lastTime: number;
//禁止选择网页上的文字
// 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;
};
}
}

View File

@ -0,0 +1,22 @@
import {rolePermission} from '@/utils/permission'
export default {
mounted(el, binding) {
const {value} = binding
if (Array.isArray(value)) {
let ishas = false
value.forEach(item => {
if (rolePermission(item)) {
ishas = true
}
})
if (ishas) {
el.parentNode.removeChild(el)
}
} else {
if (rolePermission(value)) {
el.parentNode.removeChild(el)
}
}
}
}

22
src/directives/role.ts Normal file
View File

@ -0,0 +1,22 @@
import {rolePermission} from '@/utils/permission'
export default {
mounted(el, binding) {
const {value} = binding
if (Array.isArray(value)) {
let ishas = false;
value.forEach(item => {
if (rolePermission(item)) {
ishas = true;
}
})
if (!ishas) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
if (!rolePermission(value)) {
el.parentNode && el.parentNode.removeChild(el);
}
}
}
};

79
src/directives/time.ts Normal file
View File

@ -0,0 +1,79 @@
import tools from '@/utils/tools'
var Time = {
//获取当前时间戳
getUnix: function () {
var date = new Date();
return date.getTime();
},
//获取今天0点0分0秒的时间戳
getTodayUnix: function () {
var date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date.getTime();
},
//获取今年1月1日0点0秒的时间戳
getYearUnix: function () {
var date = new Date();
date.setMonth(0);
date.setDate(1);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date.getTime();
},
//获取标准年月日
getLastDate: function (time) {
var date = new Date(time);
var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1;
var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
return date.getFullYear() + '-' + month + '-' + day;
},
//转换时间
getFormateTime: function (timestamp) {
timestamp = new Date(timestamp)
var now = this.getUnix();
var today = this.getTodayUnix();
//var year = this.getYearUnix();
var timer = (now - timestamp) / 1000;
var tip = '';
if (timer <= 0) {
tip = '刚刚';
} else if (Math.floor(timer / 60) <= 0) {
tip = '刚刚';
} else if (timer < 3600) {
tip = Math.floor(timer / 60) + '分钟前';
} else if (timer >= 3600 && (timestamp - today >= 0)) {
tip = Math.floor(timer / 3600) + '小时前';
} else if (timer / 86400 <= 31) {
tip = Math.ceil(timer / 86400) + '天前';
} else {
tip = this.getLastDate(timestamp);
}
return tip;
}
}
export default (el, binding) => {
let {value, modifiers} = binding
if (!value) {
return false
}
if (value.toString().length == 10) {
value = value * 1000
}
if (modifiers.tip) {
el.innerHTML = Time.getFormateTime(value)
el.__timeout__ = setInterval(() => {
el.innerHTML = Time.getFormateTime(value)
}, 60000)
} else {
const format = el.getAttribute('format') || undefined
el.innerHTML = tools.dateFormat(value, format)
}
}

View File

@ -4,35 +4,36 @@
</div> </div>
<template v-for="navMenu in navMenus" v-bind:key="navMenu"> <template v-for="navMenu in navMenus" v-bind:key="navMenu">
<el-menu-item v-if="!hasChildren(navMenu)" :index="navMenu.path"> <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> <a v-if="navMenu.meta&&navMenu.meta.type=='link'" :href="navMenu.path" target="_blank"
<el-icon v-if="navMenu.meta&&navMenu.meta.icon"><component :is="navMenu.meta.icon || 'el-icon-menu'"/></el-icon> @click.stop='()=>{}'></a>
<el-icon v-if="navMenu.meta&&navMenu.meta.icon">
<component :is="navMenu.meta.icon || 'el-icon-menu'"/>
</el-icon>
<template #title> <template #title>
<span>{{navMenu.meta.title}}</span> <span>{{ navMenu.meta.title }}</span>
<span v-if="navMenu.meta.tag" class="menu-tag">{{navMenu.meta.tag}}</span> <span v-if="navMenu.meta.tag" class="menu-tag">{{ navMenu.meta.tag }}</span>
</template> </template>
</el-menu-item> </el-menu-item>
<el-sub-menu v-else :index="navMenu.path"> <el-sub-menu v-else :index="navMenu.path">
<template #title> <template #title>
<el-icon v-if="navMenu.meta&&navMenu.meta.icon"><component :is="navMenu.meta.icon || 'el-icon-menu'"/></el-icon> <el-icon v-if="navMenu.meta&&navMenu.meta.icon">
<span>{{navMenu.meta.title}}</span> <component :is="navMenu.meta.icon || 'el-icon-menu'"/>
<span v-if="navMenu.meta.tag" class="menu-tag">{{navMenu.meta.tag}}</span> </el-icon>
<span>{{ navMenu.meta.title }}</span>
<span v-if="navMenu.meta.tag" class="menu-tag">{{ navMenu.meta.tag }}</span>
</template> </template>
<NavMenu :navMenus="navMenu.children"></NavMenu> <NavMenu :navMenus="navMenu.children"></NavMenu>
</el-sub-menu> </el-sub-menu>
</template> </template>
</template> </template>
<script> <script setup name="NavMenu">
export default { import {defineProps} from 'vue';
name: 'NavMenu',
props: ['navMenus'], const props = defineProps(['navMenus']);
data() {
return {} function hasChildren(item) {
}, return item.children && !item.children.every(item => item.meta.hidden)
methods: { }
hasChildren(item) {
return item.children && !item.children.every(item => item.meta.hidden)
}
}
}
</script> </script>

View File

@ -1,14 +1,20 @@
<template> <template>
<div ref="" class="mobile-nav-button" @click="showMobileNav($event)" v-drag draggable="false"><el-icon><el-icon-menu /></el-icon></div> <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-drawer ref="mobileNavBox" title="移动端菜单" :size="240" v-model="nav" direction="ltr" :with-header="false"
destroy-on-close>
<el-container class="mobile-nav"> <el-container class="mobile-nav">
<el-header> <el-header>
<div class="logo-bar"><img class="logo" src="/images/logo.png"><span>{{ $CONFIG.APP_NAME }}</span></div> <div class="logo-bar"><img class="logo" src="/images/logo.png"><span>{{ config.APP_NAME }}</span></div>
</el-header> </el-header>
<el-main> <el-main>
<el-scrollbar> <el-scrollbar>
<el-menu :default-active="$route.meta.active || $route.fullPath" @select="select" router background-color="#212d3d" text-color="#fff" active-text-color="#409EFF"> <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> <NavMenu :navMenus="menu"></NavMenu>
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
@ -18,119 +24,107 @@
</template> </template>
<script> <script setup>
import NavMenu from './NavMenu.vue'; import NavMenu from './NavMenu.vue';
import {ref, getCurrentInstance} from "vue";
import {getMenu} from "@/utils/route"
import config from "@/config/index";
import {useRoute} from "vue-router";
export default { const {proxy} = getCurrentInstance()
components: { const route = useRoute();
NavMenu
},
data() {
return {
nav: false,
menu: []
}
},
computed:{
}, let nav = ref(false)
created() { let menu = getMenu()
var menu = this.$router.getMenu()
this.menu = this.filterUrl(menu)
},
watch: { menu = filterUrl(menu)
}, function showMobileNav(e) {
methods: { var isdrag = e.currentTarget.getAttribute('drag-flag')
showMobileNav(e){ if (isdrag == 'true') {
var isdrag = e.currentTarget.getAttribute('drag-flag') return false;
if (isdrag == 'true') { } else {
return false; nav.value = true;
}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;
};
}
}
} }
}
function select() {
proxy.$refs.mobileNavBox.handleClose()
}
//
function 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 = filterUrl(item.children);
}
newMap.push(item)
})
return newMap;
}
</script> </script>
<style scoped> <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 {
.mobile-nav-button i {color: #fff;font-size: 20px;} 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 {background: #212d3d;} .mobile-nav-button i {
.mobile-nav .el-header {background: transparent;border: 0;} color: #fff;
.mobile-nav .el-main {padding:0;} font-size: 20px;
.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;} .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> </style>

View File

@ -83,6 +83,7 @@ onMounted(() => {
}) })
watch(route, (e) => { watch(route, (e) => {
console.log(111)
addViewTags(e); addViewTags(e);
// //
nextTick(() => { nextTick(() => {
@ -92,7 +93,7 @@ watch(route, (e) => {
let targetTag = tags.querySelector(".active") let targetTag = tags.querySelector(".active")
targetTag.scrollIntoView() targetTag.scrollIntoView()
// //
if (!tipDisplayed) { if (!tipDisplayed.value) {
proxy.$msgbox({ proxy.$msgbox({
type: 'warning', type: 'warning',
center: true, center: true,
@ -100,14 +101,13 @@ watch(route, (e) => {
message: '当前标签数量过多,可通过鼠标滚轴滚动标签栏。关闭标签数量可减少系统性能消耗。', message: '当前标签数量过多,可通过鼠标滚轴滚动标签栏。关闭标签数量可减少系统性能消耗。',
confirmButtonText: '知道了' confirmButtonText: '知道了'
}) })
tipDisplayed = true tipDisplayed.value = true
} }
} }
}) })
}) })
watch(() => contextMenuVisible, (value) => { watch(contextMenuVisible, (value) => {
var cm = function (e) { var cm = function (e) {
let sp = document.getElementById("contextmenu"); let sp = document.getElementById("contextmenu");
if (sp && !sp.contains(e.target)) { if (sp && !sp.contains(e.target)) {
@ -173,7 +173,7 @@ function closeSelectedTag(tag, autoPushLatestView = true) {
//tag //tag
function openContextMenu(e, tag) { function openContextMenu(e, tag) {
contextMenuItem = tag; contextMenuItem = tag;
contextMenuVisible = true; contextMenuVisible.value = true;
left.value = e.clientX + 1; left.value = e.clientX + 1;
top.value = e.clientY + 1; top.value = e.clientY + 1;
@ -267,7 +267,7 @@ function openWindow() {
closeSelectedTag(nowTag) closeSelectedTag(nowTag)
} }
window.open(url); window.open(url);
contextMenuVisible = false contextMenuVisible.value = false
} }
// //

View File

@ -4,7 +4,13 @@
<el-breadcrumb separator-icon="el-icon-arrow-right" class="hidden-sm-and-down"> <el-breadcrumb separator-icon="el-icon-arrow-right" class="hidden-sm-and-down">
<transition-group name="breadcrumb"> <transition-group name="breadcrumb">
<template v-for="item in breadList" :key="item.title"> <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> <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> </template>
</transition-group> </transition-group>
</el-breadcrumb> </el-breadcrumb>
@ -16,34 +22,46 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { import {ref, watch} from "vue";
data() { import {useRoute} from "vue-router";
return {
breadList: [] const route = useRoute()
}
}, let breadList = ref([])
created() {
this.getBreadcrumb(); getBreadcrumb();
},
watch: { watch(route, () => {
$route() { getBreadcrumb()
this.getBreadcrumb(); })
}
}, function getBreadcrumb() {
methods: { breadList.value = route.meta.breadcrumb;
getBreadcrumb(){ }
let matched = this.$route.meta.breadcrumb;
this.breadList = matched;
}
}
}
</script> </script>
<style scoped> <style scoped>
.el-breadcrumb {margin-left: 15px;} .el-breadcrumb {
.el-breadcrumb .el-breadcrumb__inner .icon {font-size: 14px;margin-right: 5px;float: left;} margin-left: 15px;
.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;} .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> </style>

View File

@ -1,10 +1,24 @@
import * as elIcons from '@element-plus/icons-vue' import * as elIcons from '@element-plus/icons-vue'
import {App} from "vue"; import {App} from "vue";
import * as piIcons from '@assets/icons' import * as piIcons from '@assets/icons'
import auth from './directives/auth'
import role from './directives/role'
import excludeRole from './directives/excludeRole'
import time from './directives/time'
import copy from './directives/copy'
import drag from './directives/drag'
import errorHandler from "@/utils/errorHandler"; import errorHandler from "@/utils/errorHandler";
export default { export default {
install(app: App) { install(app: App) {
//注册全局指令
app.directive('auth', auth)
app.directive('role', role)
app.directive('exclude-role', excludeRole)
app.directive('time', time)
app.directive('copy', copy)
app.directive('drag', drag)
//统一注册el-icon图标 //统一注册el-icon图标
for (let icon in elIcons) { for (let icon in elIcons) {
app.component(`ElIcon${icon}`, elIcons[icon]) app.component(`ElIcon${icon}`, elIcons[icon])

View File

@ -2,32 +2,29 @@
* *
* null.length * null.length
*/ */
import {nextTick} from "vue";
export default (error, vm)=>{ export default (error, vm)=>{
//过滤HTTP请求错误 //过滤HTTP请求错误
if(error.status || error.status==0){ if(error.status || error.status==0){
return false return false
} }
// var errorMap = { var errorMap = {
// InternalError: "Javascript引擎内部错误", InternalError: "Javascript引擎内部错误",
// ReferenceError: "未找到对象", ReferenceError: "未找到对象",
// TypeError: "使用了错误的类型或对象", TypeError: "使用了错误的类型或对象",
// RangeError: "使用内置对象时,参数超范围", RangeError: "使用内置对象时,参数超范围",
// SyntaxError: "语法错误", SyntaxError: "语法错误",
// EvalError: "错误的使用了Eval", EvalError: "错误的使用了Eval",
// URIError: "URI错误" URIError: "URI错误"
// } }
// var errorName = errorMap[error.name] || "未知错误" var errorName = errorMap[error.name] || "未知错误"
console.error(`[PI error]: ${error}`); console.error(`[PI error]: ${error}`);
//throw error;
vm.$nextTick(() => { vm.$nextTick(() => {
// vm.$notify.error({ vm.$notify.error({
// title: errorName, title: errorName,
// message: error message: error
// }); });
}) })
} }

25
src/utils/permission.ts Normal file
View File

@ -0,0 +1,25 @@
import tools from '@/utils/tools';
export function permission(data) {
let permissions = tools.data.get("PERMISSIONS");
if (!permissions) {
return false;
}
return permissions.includes(data);
}
export function noPermission(data) {
return !permission(data);
}
export function rolePermission(data) {
let roles = tools.data.get("ROLE");
if (!roles) {
return false;
}
return roles.includes(data);
}
export function noRolePermission(data) {
return !rolePermission(data);
}

View File

@ -102,6 +102,32 @@ const tools = {
element.webkitRequestFullscreen(); element.webkitRequestFullscreen();
} }
} }
},
/* 复制对象 */
objCopy: function (obj) {
return JSON.parse(JSON.stringify(obj));
},
/* 日期格式化 */
dateFormat: function (date, fmt='yyyy-MM-dd hh:mm:ss') {
date = new Date(date)
var o = {
"M+" : date.getMonth()+1, //月份
"d+" : date.getDate(), //日
"h+" : date.getHours(), //小时
"m+" : date.getMinutes(), //分
"s+" : date.getSeconds(), //秒
"q+" : Math.floor((date.getMonth()+3)/3), //季度
"S" : date.getMilliseconds() //毫秒
};
if(/(y+)/.test(fmt)) {
fmt=fmt.replace(RegExp.$1, (date.getFullYear()+"").substr(4 - RegExp.$1.length));
}
for(var k in o) {
if(new RegExp("("+ k +")").test(fmt)){
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));
}
}
return fmt;
} }
} }