全局指令

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>
<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>
<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>
<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>
<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 setup name="NavMenu">
import {defineProps} from 'vue';
const props = defineProps(['navMenus']);
function hasChildren(item) {
return item.children && !item.children.every(item => item.meta.hidden)
}
</script>

View File

@ -1,14 +1,20 @@
<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-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-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">
<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>
@ -18,119 +24,107 @@
</template>
<script>
import NavMenu from './NavMenu.vue';
<script setup>
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 {
components: {
NavMenu
},
data() {
return {
nav: false,
menu: []
}
},
computed:{
const {proxy} = getCurrentInstance()
const route = useRoute();
},
created() {
var menu = this.$router.getMenu()
this.menu = this.filterUrl(menu)
},
let nav = ref(false)
let menu = getMenu()
watch: {
menu = filterUrl(menu)
},
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;
};
}
}
function showMobileNav(e) {
var isdrag = e.currentTarget.getAttribute('drag-flag')
if (isdrag == 'true') {
return false;
} else {
nav.value = true;
}
}
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>
<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-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 {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;}
.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

@ -83,6 +83,7 @@ onMounted(() => {
})
watch(route, (e) => {
console.log(111)
addViewTags(e);
//
nextTick(() => {
@ -92,7 +93,7 @@ watch(route, (e) => {
let targetTag = tags.querySelector(".active")
targetTag.scrollIntoView()
//
if (!tipDisplayed) {
if (!tipDisplayed.value) {
proxy.$msgbox({
type: 'warning',
center: true,
@ -100,14 +101,13 @@ watch(route, (e) => {
message: '当前标签数量过多,可通过鼠标滚轴滚动标签栏。关闭标签数量可减少系统性能消耗。',
confirmButtonText: '知道了'
})
tipDisplayed = true
tipDisplayed.value = true
}
}
})
})
watch(() => contextMenuVisible, (value) => {
watch(contextMenuVisible, (value) => {
var cm = function (e) {
let sp = document.getElementById("contextmenu");
if (sp && !sp.contains(e.target)) {
@ -173,7 +173,7 @@ function closeSelectedTag(tag, autoPushLatestView = true) {
//tag
function openContextMenu(e, tag) {
contextMenuItem = tag;
contextMenuVisible = true;
contextMenuVisible.value = true;
left.value = e.clientX + 1;
top.value = e.clientY + 1;
@ -267,7 +267,7 @@ function openWindow() {
closeSelectedTag(nowTag)
}
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">
<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>
<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>
@ -16,34 +22,46 @@
</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 setup>
import {ref, watch} from "vue";
import {useRoute} from "vue-router";
const route = useRoute()
let breadList = ref([])
getBreadcrumb();
watch(route, () => {
getBreadcrumb()
})
function getBreadcrumb() {
breadList.value = route.meta.breadcrumb;
}
</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;}
.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

@ -1,10 +1,24 @@
import * as elIcons from '@element-plus/icons-vue'
import {App} from "vue";
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";
export default {
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图标
for (let icon in elIcons) {
app.component(`ElIcon${icon}`, elIcons[icon])

View File

@ -2,32 +2,29 @@
*
* 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] || "未知错误"
var errorMap = {
InternalError: "Javascript引擎内部错误",
ReferenceError: "未找到对象",
TypeError: "使用了错误的类型或对象",
RangeError: "使用内置对象时,参数超范围",
SyntaxError: "语法错误",
EvalError: "错误的使用了Eval",
URIError: "URI错误"
}
var errorName = errorMap[error.name] || "未知错误"
console.error(`[PI error]: ${error}`);
//throw error;
vm.$nextTick(() => {
// vm.$notify.error({
// title: errorName,
// message: error
// });
vm.$notify.error({
title: errorName,
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();
}
}
},
/* 复制对象 */
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;
}
}