mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
refactor: 消息模块调整 & 样式优化
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@logicflow/core": "^2.0.16",
|
||||
"@logicflow/extension": "^2.0.21",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-search": "^0.15.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
@@ -24,13 +24,12 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.10.4",
|
||||
"element-plus": "^2.10.5",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-sql-languages": "^0.15.1",
|
||||
"monaco-themes": "^0.4.5",
|
||||
"monaco-themes": "^0.4.6",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.3",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
@@ -38,7 +37,7 @@
|
||||
"sortablejs": "^1.15.6",
|
||||
"sql-formatter": "^15.6.5",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^v3.6.0-alpha.2",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-router": "^4.5.1",
|
||||
@@ -52,20 +51,20 @@
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
||||
"@typescript-eslint/parser": "^8.35.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vue/compiler-sfc": "^3.5.17",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/compiler-sfc": "^3.5.18",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"code-inspector-plugin": "^0.20.12",
|
||||
"code-inspector-plugin": "^1.0.4",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-vue": "^10.2.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.1",
|
||||
"sass": "^1.89.2",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vue-eslint-parser": "^10.1.4"
|
||||
"vue-eslint-parser": "^10.2.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
@@ -2,38 +2,34 @@
|
||||
<el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
|
||||
<div class="h-full">
|
||||
<el-watermark
|
||||
:zIndex="10000000"
|
||||
:zIndex="100000"
|
||||
:width="210"
|
||||
v-if="themeConfig.isWatermark"
|
||||
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
|
||||
:content="themeConfig.watermarkText"
|
||||
class="!h-full"
|
||||
>
|
||||
<router-view v-show="themeConfig.lockScreenTime !== 0" />
|
||||
<router-view />
|
||||
</el-watermark>
|
||||
<router-view v-if="!themeConfig.isWatermark" v-show="themeConfig.lockScreenTime !== 0" />
|
||||
<router-view v-if="!themeConfig.isWatermark" />
|
||||
|
||||
<LockScreen v-if="themeConfig.isLockScreen" />
|
||||
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
|
||||
<Setings />
|
||||
</div>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="app">
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue';
|
||||
import { onMounted, nextTick, watch, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import LockScreen from '@/layout/lockScreen/index.vue';
|
||||
import Setings from '@/layout/navBars/breadcrumb/setings.vue';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
import { useIntervalFn } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import EnumValue from './common/Enum';
|
||||
import { I18nEnum } from './common/commonEnum';
|
||||
import { saveThemeConfig } from './common/utils/storage';
|
||||
|
||||
const setingsRef = ref();
|
||||
const route = useRoute();
|
||||
|
||||
const themeConfigStores = useThemeConfig();
|
||||
@@ -42,19 +38,9 @@ const { themeConfig } = storeToRefs(themeConfigStores);
|
||||
// 定义变量内容
|
||||
const { locale, t } = useI18n();
|
||||
|
||||
// 布局配置弹窗打开
|
||||
const openSetingsDrawer = () => {
|
||||
setingsRef.value.openDrawer();
|
||||
};
|
||||
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 监听布局配置弹窗点击打开
|
||||
mittBus.on('openSetingsDrawer', () => {
|
||||
openSetingsDrawer();
|
||||
});
|
||||
|
||||
// 初始化系统主题
|
||||
themeConfigStores.initThemeConfig();
|
||||
});
|
||||
@@ -120,11 +106,6 @@ const refreshWatermarkTime = () => {
|
||||
themeConfigStores.setWatermarkNowTime();
|
||||
};
|
||||
|
||||
// 页面销毁时,关闭监听布局配置
|
||||
onUnmounted(() => {
|
||||
mittBus.off('openSetingsDrawer', () => {});
|
||||
});
|
||||
|
||||
// 监听路由的变化,设置网站标题
|
||||
watch(
|
||||
() => route.path,
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.4 KiB |
@@ -9,6 +9,11 @@ export const I18nEnum = {
|
||||
En: EnumValue.of('en', 'English').setExtra({ icon: 'icon layout/en', el: enLocale }),
|
||||
};
|
||||
|
||||
export const LinkTypeEnum = {
|
||||
Iframes: EnumValue.of(1, 'ifrmaes'),
|
||||
Link: EnumValue.of(2, 'link'),
|
||||
};
|
||||
|
||||
// 资源类型
|
||||
export const ResourceTypeEnum = {
|
||||
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
|
||||
@@ -53,16 +58,20 @@ export const MsgSubtypeEnum = {
|
||||
notifyType: 'success',
|
||||
}),
|
||||
MachineFileUploadFail: EnumValue.of('machine.file.upload.fail', 'machine.fileUploadFail').setExtra({
|
||||
notifyType: 'error',
|
||||
notifyType: 'danger',
|
||||
}),
|
||||
|
||||
DbDumpFail: EnumValue.of('db.dump.fail', 'db.dbDumpFail').setExtra({
|
||||
notifyType: 'error',
|
||||
notifyType: 'danger',
|
||||
}),
|
||||
SqlScriptRunSuccess: EnumValue.of('db.sqlscript.run.success', 'db.sqlScriptRunSuccess').setExtra({
|
||||
notifyType: 'success',
|
||||
}),
|
||||
SqlScriptRunFail: EnumValue.of('db.sqlscript.run.fail', 'db.sqlScriptRunFail').setExtra({
|
||||
notifyType: 'error',
|
||||
notifyType: 'danger',
|
||||
}),
|
||||
|
||||
FlowUserTaskTodo: EnumValue.of('flow.usertask.todo', 'flow.todoTask').setExtra({
|
||||
notifyType: 'primary',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createWebSocket } from './request';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import { MsgSubtypeEnum } from './commonEnum';
|
||||
import EnumValue from './Enum';
|
||||
import { h } from 'vue';
|
||||
import { MessageRenderer } from '@/components/message/message';
|
||||
|
||||
class SysSocket {
|
||||
/**
|
||||
@@ -66,7 +68,7 @@ class SysSocket {
|
||||
ElNotification({
|
||||
duration: 0,
|
||||
title,
|
||||
message: message.msg,
|
||||
message: h(MessageRenderer, { content: message.msg }),
|
||||
type: msgSubtype?.extra.notifyType || 'info',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { nextTick } from 'vue';
|
||||
import '@/theme/loading.scss';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
/**
|
||||
* 页面全局 Loading
|
||||
@@ -9,33 +11,57 @@ import '@/theme/loading.scss';
|
||||
export const NextLoading = {
|
||||
// 创建 loading
|
||||
start: () => {
|
||||
// 如果已经存在loading元素,则不重复创建
|
||||
if (document.querySelector('.loading-next')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bodys: Element = document.body;
|
||||
const div = <HTMLElement>document.createElement('div');
|
||||
div.setAttribute('class', 'loading-next');
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
if (themeConfig.value.isDark) {
|
||||
div.classList.add('dark');
|
||||
}
|
||||
|
||||
const htmls = `
|
||||
<div class="loading-next-box">
|
||||
<div class="loading-next-box-warp">
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div class="loading-next-box">
|
||||
<div class="loading-next-box-warp">
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
div.innerHTML = htmls;
|
||||
bodys.insertBefore(div, bodys.childNodes[0]);
|
||||
|
||||
// 插入到body的第一个子元素之前,避免影响布局
|
||||
if (bodys.firstChild) {
|
||||
bodys.insertBefore(div, bodys.firstChild);
|
||||
} else {
|
||||
bodys.appendChild(div);
|
||||
}
|
||||
},
|
||||
// 移除 loading
|
||||
done: (time: number = 1000) => {
|
||||
done: (time: number = 500) => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
const el = <HTMLElement>document.querySelector('.loading-next');
|
||||
el?.parentNode?.removeChild(el);
|
||||
if (el) {
|
||||
// 添加淡出效果
|
||||
el.style.transition = 'opacity 0.3s ease-out';
|
||||
el.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
el?.parentNode?.removeChild(el);
|
||||
}, 300);
|
||||
}
|
||||
}, time);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// https://www.npmjs.com/package/mitt
|
||||
import mitt, { Emitter } from 'mitt';
|
||||
|
||||
// 类型
|
||||
const emitter: Emitter<any> = mitt<any>();
|
||||
|
||||
// 导出
|
||||
export default emitter;
|
||||
129
frontend/src/components/message/message.ts
Normal file
129
frontend/src/components/message/message.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { ElLink, ElText } from 'element-plus';
|
||||
import { defineAsyncComponent, defineComponent, h } from 'vue';
|
||||
|
||||
type Size = 'large' | 'default' | 'small';
|
||||
|
||||
interface ComponentConfig {
|
||||
component: any;
|
||||
getDefaultProps?: (size: Size) => Record<string, any>;
|
||||
}
|
||||
|
||||
const linkConf = {
|
||||
component: ElLink,
|
||||
getDefaultProps: (size: Size) => {
|
||||
return {
|
||||
type: 'primary',
|
||||
verticalAlign: 'baseline',
|
||||
style: {
|
||||
fontSize: size === 'small' ? '12px' : size === 'large' ? '16px' : '14px',
|
||||
verticalAlign: 'baseline',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const components = {
|
||||
'el-link': linkConf,
|
||||
a: linkConf,
|
||||
|
||||
'error-text': {
|
||||
component: ElText,
|
||||
getDefaultProps: (size: Size) => {
|
||||
return {
|
||||
type: 'danger',
|
||||
size,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
'machine-info': {
|
||||
component: defineAsyncComponent(() => import('@/views/ops/machine/component/MachineDetail.vue')),
|
||||
getDefaultProps: (size: Size) => {
|
||||
return {
|
||||
size,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
'db-info': {
|
||||
component: defineAsyncComponent(() => import('@/views/ops/db/component/DbDetail.vue')),
|
||||
getDefaultProps: (size: Size) => {
|
||||
return {
|
||||
size,
|
||||
};
|
||||
},
|
||||
},
|
||||
} as Record<string, ComponentConfig>;
|
||||
|
||||
export const MessageRenderer = defineComponent({
|
||||
props: {
|
||||
content: String,
|
||||
size: {
|
||||
type: String as () => Size,
|
||||
default: 'default',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const parseContent = (content: string) => {
|
||||
if (!content) {
|
||||
return [h('span', '')];
|
||||
}
|
||||
|
||||
// 创建一个包装容器来处理HTML内容
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = content;
|
||||
|
||||
const parseNode = (node: Node): any => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return node.textContent;
|
||||
}
|
||||
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as HTMLElement;
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
let attrs: Record<string, any> = {};
|
||||
|
||||
// 提取属性
|
||||
for (let i = 0; i < element.attributes.length; i++) {
|
||||
const attr = element.attributes[i];
|
||||
attrs[attr.name] = attr.value;
|
||||
}
|
||||
|
||||
const componentConf = components[tagName];
|
||||
if (!componentConf) {
|
||||
return h(tagName, attrs, Array.from(element.childNodes).map(parseNode));
|
||||
}
|
||||
|
||||
// 存在默认组件配置,则合并
|
||||
if (componentConf.getDefaultProps) {
|
||||
const defaultProps = componentConf.getDefaultProps(props.size);
|
||||
attrs = {
|
||||
...defaultProps,
|
||||
...attrs,
|
||||
};
|
||||
}
|
||||
|
||||
return h(componentConf.component, attrs, {
|
||||
default: () => Array.from(element.childNodes).map(parseNode),
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
return Array.from(container.childNodes).map(parseNode);
|
||||
};
|
||||
|
||||
return () => {
|
||||
// 根据 size 属性确定根元素的 class
|
||||
const rootClass = props.size === 'small' ? 'text-sm' : props.size === 'large' ? 'text-lg' : 'text-base';
|
||||
try {
|
||||
const elements = parseContent(props.content || '');
|
||||
return h('div', { class: rootClass }, elements);
|
||||
} catch (e) {
|
||||
console.error('消息渲染失败:', e);
|
||||
return h('div', { class: rootClass }, props.content || '');
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col flex-1 overflow-hidden">
|
||||
<transition name="el-zoom-in-top">
|
||||
<transition name="page-table-search-form">
|
||||
<!-- 查询表单 -->
|
||||
<SearchForm v-if="isShowSearch" :items="tableSearchItems" v-model="queryForm" :search="search" :reset="reset" :search-col="searchCol">
|
||||
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
||||
@@ -171,9 +171,9 @@ import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import Api from '@/common/Api';
|
||||
import SearchForm from '@/components/SearchForm/index.vue';
|
||||
import { SearchItem } from '../SearchForm/index';
|
||||
import SearchFormItem from '../SearchForm/components/SearchFormItem.vue';
|
||||
import SearchForm from '@/components/pagetable/SearchForm/index.vue';
|
||||
import { SearchItem } from './SearchForm/index';
|
||||
import SearchFormItem from './SearchForm/components/SearchFormItem.vue';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { usePageTable } from '@/hooks/usePageTable';
|
||||
import { ElInput, ElTable } from 'element-plus';
|
||||
@@ -365,4 +365,22 @@ defineExpose({
|
||||
total,
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.page-table-search-form-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.page-table-search-form-leave-active {
|
||||
transition: all 0.3s ease-in;
|
||||
}
|
||||
|
||||
.page-table-search-form-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.95);
|
||||
}
|
||||
|
||||
.page-table-search-form-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
</template>
|
||||
<script setup lang="ts" name="SearchForm">
|
||||
import { computed, ref } from 'vue';
|
||||
import { BreakPoint } from '@/components/Grid/interface/index';
|
||||
import { BreakPoint } from '@/components/pagetable/Grid/interface/index';
|
||||
import { Delete, Search, ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import SearchFormItem from './components/SearchFormItem.vue';
|
||||
import Grid from '@/components/Grid/index.vue';
|
||||
import GridItem from '@/components/Grid/components/GridItem.vue';
|
||||
import Grid from '@/components/pagetable/Grid/index.vue';
|
||||
import GridItem from '@/components/pagetable/Grid/components/GridItem.vue';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { SearchItem } from './index';
|
||||
|
||||
@@ -69,6 +69,7 @@ export default {
|
||||
fieldNotEmpty: '{field} cannot be empty',
|
||||
selectAll: 'Select all',
|
||||
MultiPlaceholder: 'Multiple are separated by commas',
|
||||
appSlogan: 'Simple, efficient and secure',
|
||||
},
|
||||
layout: {
|
||||
user: {
|
||||
@@ -150,8 +151,6 @@ export default {
|
||||
isUniqueOpened: 'Menu accordion',
|
||||
isFixedHeader: 'Fixed header',
|
||||
isClassicSplitMenu: 'Classic layout split menu',
|
||||
isLockScreen: 'Open the lock screen',
|
||||
lockScreenTime: 'screen locking(s/s)',
|
||||
interfaceDisplay: 'Interface display',
|
||||
isShowLogo: 'Sidebar logo',
|
||||
isBreadcrumb: 'Open breadcrumb',
|
||||
|
||||
@@ -69,6 +69,7 @@ export default {
|
||||
fieldNotEmpty: '{field}不能为空',
|
||||
selectAll: '全选',
|
||||
MultiPlaceholder: '多个用逗号隔开',
|
||||
appSlogan: '简洁 · 高效 · 安全',
|
||||
},
|
||||
layout: {
|
||||
user: {
|
||||
@@ -152,8 +153,6 @@ export default {
|
||||
isUniqueOpened: '菜单手风琴',
|
||||
isFixedHeader: '固定 Header',
|
||||
isClassicSplitMenu: '经典布局分割菜单',
|
||||
isLockScreen: '开启锁屏',
|
||||
lockScreenTime: '自动锁屏(s/秒)',
|
||||
interfaceDisplay: '界面显示',
|
||||
isShowLogo: '侧边栏 Logo',
|
||||
isBreadcrumb: '开启 Breadcrumb',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-aside class="layout-aside" :class="setCollapseWidth" v-if="state.clientWidth > 1000">
|
||||
<el-aside class="layout-aside" :class="setCollapseWidth" v-if="clientWidth > 1000">
|
||||
<Logo v-if="setShowLogo" />
|
||||
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef">
|
||||
<Vertical :menuList="state.menuList" :class="setCollapseWidth" />
|
||||
@@ -16,25 +16,30 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="layoutAside">
|
||||
import { reactive, computed, watch, getCurrentInstance, onBeforeMount, onUnmounted } from 'vue';
|
||||
import { reactive, computed, watch, getCurrentInstance, onBeforeMount, onUnmounted, inject } from 'vue';
|
||||
import pinia from '@/store/index';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useRoutesList } from '@/store/routesList';
|
||||
import Logo from '@/layout/logo/index.vue';
|
||||
import Vertical from '@/layout/navMenu/vertical.vue';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
const { routesList } = storeToRefs(useRoutesList());
|
||||
|
||||
const state: any = reactive({
|
||||
menuList: [],
|
||||
clientWidth: '',
|
||||
const { width: clientWidth } = useWindowSize();
|
||||
|
||||
const state = reactive({
|
||||
menuList: [] as any[],
|
||||
});
|
||||
|
||||
// 注入 菜单数据
|
||||
const columnsMenuData: any = inject('columnsMenuData', null);
|
||||
const classicMenuData: any = inject('classicMenuData', null);
|
||||
|
||||
// 设置菜单展开/收起时的宽度
|
||||
const setCollapseWidth = computed(() => {
|
||||
let { layout, isCollapse, menuBar } = themeConfig.value;
|
||||
@@ -64,7 +69,9 @@ const setShowLogo = computed(() => {
|
||||
|
||||
// 设置/过滤路由(非静态路由/是否显示在菜单中)
|
||||
const setFilterRoutes = () => {
|
||||
if (themeConfig.value.layout === 'columns') return false;
|
||||
if (themeConfig.value.layout === 'columns') {
|
||||
return false;
|
||||
}
|
||||
state.menuList = filterRoutesFun(routesList.value);
|
||||
};
|
||||
|
||||
@@ -78,53 +85,58 @@ const filterRoutesFun = (arr: Array<object>) => {
|
||||
return item;
|
||||
});
|
||||
};
|
||||
// 设置菜单导航是否固定(移动端)
|
||||
const initMenuFixed = (clientWidth: number) => {
|
||||
state.clientWidth = clientWidth;
|
||||
};
|
||||
|
||||
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
|
||||
watch(themeConfig.value, (val) => {
|
||||
if (val.isShowLogoChange !== val.isShowLogo) {
|
||||
if (!proxy.$refs.layoutAsideScrollbarRef) return false;
|
||||
if (!proxy.$refs.layoutAsideScrollbarRef) {
|
||||
return false;
|
||||
}
|
||||
proxy.$refs.layoutAsideScrollbarRef.update();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听路由的变化,动态赋值给菜单中
|
||||
watch(pinia.state, (val) => {
|
||||
if (val.routesList.routesList.length === state.menuList.length) return false;
|
||||
if (val.routesList.routesList.length === state.menuList.length) {
|
||||
return false;
|
||||
}
|
||||
let { layout, isClassicSplitMenu } = val.themeConfig.themeConfig;
|
||||
if (layout === 'classic' && isClassicSplitMenu) return false;
|
||||
if (layout === 'classic' && isClassicSplitMenu) {
|
||||
return;
|
||||
}
|
||||
setFilterRoutes();
|
||||
});
|
||||
|
||||
// 监听经典布局分割菜单的变化
|
||||
watch(
|
||||
() => themeConfig.value.isClassicSplitMenu,
|
||||
() => {
|
||||
// 当经典布局分割菜单选项变化时,重新设置过滤路由
|
||||
setFilterRoutes();
|
||||
}
|
||||
);
|
||||
|
||||
// 页面加载前
|
||||
onBeforeMount(() => {
|
||||
initMenuFixed(document.body.clientWidth);
|
||||
setFilterRoutes();
|
||||
mittBus.on('setSendColumnsChildren', (res: any) => {
|
||||
state.menuList = res.children;
|
||||
});
|
||||
mittBus.on('setSendClassicChildren', (res: any) => {
|
||||
let { layout, isClassicSplitMenu } = themeConfig.value;
|
||||
if (layout === 'classic' && isClassicSplitMenu) {
|
||||
state.menuList = [];
|
||||
state.menuList = res.children;
|
||||
}
|
||||
});
|
||||
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
|
||||
setFilterRoutes();
|
||||
});
|
||||
mittBus.on('layoutMobileResize', (res: any) => {
|
||||
initMenuFixed(res.clientWidth);
|
||||
});
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
mittBus.off('setSendColumnsChildren');
|
||||
mittBus.off('setSendClassicChildren');
|
||||
mittBus.off('getBreadcrumbIndexSetFilterRoutes');
|
||||
mittBus.off('layoutMobileResize');
|
||||
|
||||
if (columnsMenuData) {
|
||||
watch(columnsMenuData, (newVal) => {
|
||||
if (newVal) {
|
||||
state.menuList = newVal.children;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (classicMenuData) {
|
||||
watch(classicMenuData, (newVal) => {
|
||||
let { layout, isClassicSplitMenu } = themeConfig.value;
|
||||
if (newVal && layout === 'classic' && isClassicSplitMenu) {
|
||||
state.menuList = [];
|
||||
state.menuList = newVal.children;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="layout-columns-aside">
|
||||
<div class="w-[64px] h-full bg-[var(--bg-columnsMenuBar)]">
|
||||
<el-scrollbar>
|
||||
<ul>
|
||||
<ul class="relative">
|
||||
<li
|
||||
v-for="(v, k) in state.columnsAsideList"
|
||||
:key="k"
|
||||
@@ -11,63 +11,86 @@
|
||||
if (el) columnsAsideOffsetTopRefs[k] = el;
|
||||
}
|
||||
"
|
||||
:class="{ 'layout-columns-active': state.liIndex === k }"
|
||||
:class="[
|
||||
{ 'text-white': state.liIndex === k },
|
||||
'color-[var(--bg-columnsMenuBarColor)] w-full h-[50px] text-center flex cursor-pointer relative z-[1] transition-[color] duration-300 ease-in-out',
|
||||
]"
|
||||
:title="$t(v.meta.title)"
|
||||
>
|
||||
<div class="layout-columns-aside-li-box" v-if="!v.meta.link || (v.meta.link && v.meta.linkType == 1)">
|
||||
<div class="mx-auto my-auto" v-if="!v.meta.link || (v.meta.link && v.meta.linkType == 1)">
|
||||
<i :class="v.meta.icon"></i>
|
||||
<div class="layout-columns-aside-li-box-title !text-[12px]">
|
||||
{{ $t(v.meta.title) && $t(v.meta.title).length >= 4 ? $t(v.meta.title).substr(0, 4) : $t(v.meta.title) }}
|
||||
<div class="pt-[1px] !text-[12px]">
|
||||
{{ $t(v.meta.title) && $t(v.meta.title).length >= 4 ? $t(v.meta.title).substring(0, 4) : $t(v.meta.title) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-columns-aside-li-box" v-else>
|
||||
<a :href="v.meta.link" target="_blank">
|
||||
<div class="mx-auto my-auto" v-else>
|
||||
<a :href="v.meta.link" target="_blank" class="no-underline color-[var(--bg-columnsMenuBarColor)]">
|
||||
<i :class="v.meta.icon"></i>
|
||||
<div class="layout-columns-aside-li-box-title !text-[12px]">
|
||||
{{ $t(v.meta.title) && $t(v.meta.title).length >= 4 ? $t(v.meta.title).substr(0, 4) : $t(v.meta.title) }}
|
||||
<div class="pt-[1px] !text-[12px]">
|
||||
{{ $t(v.meta.title) && $t(v.meta.title).length >= 4 ? $t(v.meta.title).substring(0, 4) : $t(v.meta.title) }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<div ref="columnsAsideActiveRef" :class="setColumnsAsideStyle"></div>
|
||||
<div
|
||||
ref="columnsAsideActiveRef"
|
||||
:class="[
|
||||
'absolute z-[0] bg-[var(--el-color-primary)] text-white transition-all duration-300 ease-in-out',
|
||||
setColumnsAsideStyle === 'columnsRound'
|
||||
? 'left-1/2 top-[2px] h-[44px] w-[58px] -translate-x-1/2 rounded-[5px]'
|
||||
: 'left-0 top-0 h-[50px] w-full rounded-[0]',
|
||||
]"
|
||||
></div>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="layoutColumnsAside">
|
||||
import { reactive, ref, computed, onMounted, nextTick, getCurrentInstance, watch } from 'vue';
|
||||
import { reactive, ref, computed, onMounted, nextTick, watch, inject } from 'vue';
|
||||
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
|
||||
import pinia from '@/store/index';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useRoutesList } from '@/store/routesList';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
|
||||
const columnsAsideOffsetTopRefs: any = ref([]);
|
||||
const columnsAsideActiveRef = ref();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const state: any = reactive({
|
||||
columnsAsideList: [],
|
||||
const state = reactive({
|
||||
columnsAsideList: [] as any[],
|
||||
liIndex: 0,
|
||||
difference: 0,
|
||||
routeSplit: [],
|
||||
routeSplit: [] as any[],
|
||||
});
|
||||
|
||||
// 注入 columnsMenuData
|
||||
const columnsMenuData: any = inject('columnsMenuData');
|
||||
|
||||
// 设置高亮样式
|
||||
const setColumnsAsideStyle = computed(() => {
|
||||
return useThemeConfig().themeConfig.columnsAsideStyle;
|
||||
});
|
||||
|
||||
// 设置菜单高亮位置移动
|
||||
const setColumnsAsideMove = (k: number) => {
|
||||
state.liIndex = k;
|
||||
columnsAsideActiveRef.value.style.top = `${columnsAsideOffsetTopRefs.value[k].offsetTop + state.difference}px`;
|
||||
};
|
||||
|
||||
// 菜单高亮点击事件
|
||||
const onColumnsAsideMenuClick = (v: Object, k: number) => {
|
||||
const onColumnsAsideMenuClick = (v: any, k: number) => {
|
||||
setColumnsAsideMove(k);
|
||||
let { path, redirect } = v as any;
|
||||
if (redirect) router.push(redirect);
|
||||
else router.push(path);
|
||||
if (v.children && v.children.length > 0) {
|
||||
router.push(v.children[0].path);
|
||||
} else {
|
||||
router.push(v.path);
|
||||
}
|
||||
// if (redirect) {
|
||||
// router.push(redirect);
|
||||
// } else {
|
||||
// router.push(path);
|
||||
// }
|
||||
};
|
||||
// 设置高亮动态位置
|
||||
const onColumnsAsideDown = (k: number) => {
|
||||
@@ -80,119 +103,92 @@ const setFilterRoutes = () => {
|
||||
state.columnsAsideList = filterRoutesFun(useRoutesList().routesList);
|
||||
const resData: any = setSendChildren(route.path);
|
||||
onColumnsAsideDown(resData.item[0].k);
|
||||
mittBus.emit('setSendColumnsChildren', resData);
|
||||
if (columnsMenuData) {
|
||||
columnsMenuData.value = resData;
|
||||
}
|
||||
};
|
||||
// 传送当前子级数据到菜单中
|
||||
const setSendChildren = (path: string) => {
|
||||
const currentPathSplit = path.split('/');
|
||||
let currentData: any = {};
|
||||
state.columnsAsideList.map((v: any, k: number) => {
|
||||
if (v.path === `/${currentPathSplit[1]}`) {
|
||||
v['k'] = k;
|
||||
currentData['item'] = [{ ...v }];
|
||||
currentData['children'] = [{ ...v }];
|
||||
if (v.children) currentData['children'] = v.children;
|
||||
const result = findRootRoute(state.columnsAsideList, path);
|
||||
|
||||
if (result) {
|
||||
const k = state.columnsAsideList.findIndex((v: any) => v === result);
|
||||
if (k !== -1) {
|
||||
result['k'] = k;
|
||||
currentData['item'] = [{ ...result }];
|
||||
currentData['children'] = [{ ...result }];
|
||||
if (result.children) currentData['children'] = result.children;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return currentData;
|
||||
};
|
||||
|
||||
// 路由过滤递归函数
|
||||
const filterRoutesFun = (arr: Array<object>) => {
|
||||
return arr
|
||||
.filter((item: any) => !item.meta.isHide)
|
||||
.map((item: any) => {
|
||||
item = Object.assign({}, item);
|
||||
if (item.children) item.children = filterRoutesFun(item.children);
|
||||
if (item.children) {
|
||||
item.children = filterRoutesFun(item.children);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
// tagsView 点击时,根据路由查找下标 columnsAsideList,实现左侧菜单高亮
|
||||
const setColumnsMenuHighlight = (path: string) => {
|
||||
state.routeSplit = path.split('/');
|
||||
state.routeSplit.shift();
|
||||
const routeFirst = `/${state.routeSplit[0]}`;
|
||||
const currentSplitRoute = state.columnsAsideList.find((v: any) => v.path === routeFirst);
|
||||
// 延迟拿值,防止取不到
|
||||
setTimeout(() => {
|
||||
onColumnsAsideDown(currentSplitRoute.k);
|
||||
}, 0);
|
||||
const rootRoute = findRootRoute(state.columnsAsideList, path);
|
||||
if (rootRoute) {
|
||||
// 延迟拿值,防止取不到
|
||||
setTimeout(() => {
|
||||
onColumnsAsideDown(rootRoute.k);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 递归查找路由并返回根节点
|
||||
const findRootRoute = (routes: any[], currentPath: string): any => {
|
||||
for (const route of routes) {
|
||||
// 直接匹配
|
||||
if (route.path === currentPath) {
|
||||
return route;
|
||||
}
|
||||
|
||||
// 在子路由中查找
|
||||
if (route.children && route.children.length > 0) {
|
||||
const found = findRootRoute(route.children, currentPath);
|
||||
if (found) {
|
||||
// 如果在子路由中找到了,返回根节点
|
||||
return route;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 监听路由的变化,动态赋值给菜单中
|
||||
watch(pinia.state, (val) => {
|
||||
val.themeConfig.themeConfig.columnsAsideStyle === 'columnsRound' ? (state.difference = 3) : (state.difference = 0);
|
||||
if (val.routesList.routesList.length === state.columnsAsideList.length) return false;
|
||||
if (val.routesList.routesList.length === state.columnsAsideList.length) {
|
||||
return;
|
||||
}
|
||||
setFilterRoutes();
|
||||
});
|
||||
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
setFilterRoutes();
|
||||
});
|
||||
|
||||
// 路由更新时
|
||||
onBeforeRouteUpdate((to) => {
|
||||
setColumnsMenuHighlight(to.path);
|
||||
mittBus.emit('setSendColumnsChildren', setSendChildren(to.path));
|
||||
|
||||
if (columnsMenuData) {
|
||||
columnsMenuData.value = setSendChildren(to.path);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-columns-aside {
|
||||
width: 64px;
|
||||
height: 100%;
|
||||
background: var(--bg-columnsMenuBar);
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
|
||||
li {
|
||||
color: var(--bg-columnsMenuBarColor);
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.layout-columns-aside-li-box {
|
||||
margin: auto;
|
||||
|
||||
.layout-columns-aside-li-box-title {
|
||||
padding-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--bg-columnsMenuBarColor);
|
||||
}
|
||||
}
|
||||
|
||||
.layout-columns-active {
|
||||
color: #ffffff;
|
||||
transition: 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.columns-round {
|
||||
background: var(--el-color-primary);
|
||||
color: #ffffff;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 2px;
|
||||
height: 44px;
|
||||
width: 58px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 0;
|
||||
transition: 0.3s ease-in-out;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.columns-card {
|
||||
@extend .columns-round;
|
||||
top: 0;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
<template>
|
||||
<el-main class="layout-main !h-full">
|
||||
<el-scrollbar ref="layoutScrollbarRef" view-class="!h-full" v-show="!state.currentRouteMeta.link && state.currentRouteMeta.linkType != 1">
|
||||
<el-scrollbar ref="layoutScrollbarRef" view-class="!h-full">
|
||||
<LayoutParentView />
|
||||
</el-scrollbar>
|
||||
|
||||
<Link class="!h-full" :meta="state.currentRouteMeta" v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2" />
|
||||
|
||||
<Iframes
|
||||
class="!h-full"
|
||||
:meta="state.currentRouteMeta"
|
||||
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 1 && state.isShowLink"
|
||||
@getCurrentRouteMeta="onGetCurrentRouteMeta"
|
||||
/>
|
||||
</el-main>
|
||||
|
||||
<el-footer v-if="themeConfig.isFooter">
|
||||
@@ -20,39 +11,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layoutMain">
|
||||
import { reactive, getCurrentInstance, watch, onBeforeMount } from 'vue';
|
||||
import { getCurrentInstance, watch, defineAsyncComponent } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import LayoutParentView from '@/layout/routerView/parent.vue';
|
||||
import Footer from '@/layout/footer/index.vue';
|
||||
import Link from '@/layout/routerView/link.vue';
|
||||
import Iframes from '@/layout/routerView/iframes.vue';
|
||||
|
||||
const LayoutParentView = defineAsyncComponent(() => import('@/layout/routerView/parent.vue'));
|
||||
const Footer = defineAsyncComponent(() => import('@/layout/footer/index.vue'));
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
const route = useRoute();
|
||||
const state = reactive({
|
||||
currentRouteMeta: {} as any,
|
||||
isShowLink: false,
|
||||
});
|
||||
|
||||
// 子组件触发更新
|
||||
const onGetCurrentRouteMeta = () => {
|
||||
initCurrentRouteMeta(route.meta);
|
||||
};
|
||||
// 初始化当前路由 meta 信息
|
||||
const initCurrentRouteMeta = (meta: object) => {
|
||||
state.isShowLink = false;
|
||||
state.currentRouteMeta = meta;
|
||||
setTimeout(() => {
|
||||
state.isShowLink = true;
|
||||
}, 100);
|
||||
};
|
||||
// 页面加载前
|
||||
onBeforeMount(() => {
|
||||
initCurrentRouteMeta(route.meta);
|
||||
});
|
||||
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
|
||||
watch(themeConfig.value, (val) => {
|
||||
if (val.isFixedHeaderChange !== val.isFixedHeader) {
|
||||
@@ -64,7 +34,6 @@ watch(themeConfig.value, (val) => {
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
initCurrentRouteMeta(route.meta);
|
||||
proxy.$refs.layoutScrollbarRef.wrapRef.scrollTop = 0;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,42 +6,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layout">
|
||||
import { onBeforeMount, onUnmounted } from 'vue';
|
||||
import { getLocal, setLocal } from '@/common/utils/storage';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import Defaults from '@/layout/main/defaults.vue';
|
||||
import Classic from '@/layout/main/classic.vue';
|
||||
import Transverse from '@/layout/main/transverse.vue';
|
||||
import Columns from '@/layout/main/columns.vue';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
// 窗口大小改变时(适配移动端)
|
||||
const onLayoutResize = () => {
|
||||
if (!getLocal('oldLayout')) setLocal('oldLayout', themeConfig.value.layout);
|
||||
const clientWidth = document.body.clientWidth;
|
||||
if (clientWidth < 1000) {
|
||||
themeConfig.value.isCollapse = false;
|
||||
mittBus.emit('layoutMobileResize', {
|
||||
layout: 'defaults',
|
||||
clientWidth,
|
||||
});
|
||||
} else {
|
||||
mittBus.emit('layoutMobileResize', {
|
||||
layout: getLocal('oldLayout') ? getLocal('oldLayout') : 'defaults',
|
||||
clientWidth,
|
||||
});
|
||||
}
|
||||
};
|
||||
// 页面加载前
|
||||
onBeforeMount(() => {
|
||||
onLayoutResize();
|
||||
window.addEventListener('resize', onLayoutResize);
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', onLayoutResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
<template>
|
||||
<div v-show="state.isShowLockScreen">
|
||||
<div class="layout-lock-screen-mask"></div>
|
||||
<div class="layout-lock-screen-img" :class="{ 'layout-lock-screen-filter': state.isShowLoockLogin }"></div>
|
||||
<div class="layout-lock-screen">
|
||||
<div
|
||||
class="layout-lock-screen-date"
|
||||
ref="layoutLockScreenDateRef"
|
||||
@mousedown="onDownPc"
|
||||
@mousemove="onMovePc"
|
||||
@mouseup="onEnd"
|
||||
@touchstart.stop="onDownApp"
|
||||
@touchmove.stop="onMoveApp"
|
||||
@touchend.stop="onEnd"
|
||||
>
|
||||
<div class="layout-lock-screen-date-box">
|
||||
<div class="layout-lock-screen-date-box-time">
|
||||
{{ state.time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ state.time.s }}</span>
|
||||
</div>
|
||||
<div class="layout-lock-screen-date-box-info">{{ state.time.mdq }}</div>
|
||||
</div>
|
||||
<div class="layout-lock-screen-date-top">
|
||||
<SvgIcon name="ele-Top" />
|
||||
<div class="layout-lock-screen-date-top-text">上滑解锁</div>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="el-zoom-in-center">
|
||||
<div v-show="state.isShowLoockLogin" class="layout-lock-screen-login">
|
||||
<div class="layout-lock-screen-login-box">
|
||||
<div class="layout-lock-screen-login-box-img">
|
||||
<img src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" />
|
||||
</div>
|
||||
<div class="layout-lock-screen-login-box-name">Administrator</div>
|
||||
<div class="layout-lock-screen-login-box-value">
|
||||
<el-input
|
||||
placeholder="请输入密码"
|
||||
ref="layoutLockScreenInputRef"
|
||||
v-model="state.lockScreenPassword"
|
||||
@keyup.enter.native.stop="onLockScreenSubmit()"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="onLockScreenSubmit">
|
||||
<el-icon class="el-input__icon">
|
||||
<ele-Right />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-lock-screen-login-icon">
|
||||
<SvgIcon name="ele-Microphone" :size="20" />
|
||||
<SvgIcon name="ele-AlarmClock" :size="20" />
|
||||
<SvgIcon name="ele-SwitchButton" :size="20" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="layoutLockScreen">
|
||||
import { nextTick, onMounted, reactive, ref, onUnmounted } from 'vue';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { setLocal } from '@/common/utils/storage';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
|
||||
// 定义变量内容
|
||||
const layoutLockScreenDateRef = ref<any>();
|
||||
const layoutLockScreenInputRef = ref();
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||
const state = reactive({
|
||||
transparency: 1,
|
||||
downClientY: 0,
|
||||
moveDifference: 0,
|
||||
isShowLoockLogin: false,
|
||||
isFlags: false,
|
||||
querySelectorEl: '' as any,
|
||||
time: {
|
||||
hm: '',
|
||||
s: '',
|
||||
mdq: '',
|
||||
},
|
||||
setIntervalTime: 0,
|
||||
isShowLockScreen: false,
|
||||
isShowLockScreenIntervalTime: 0,
|
||||
lockScreenPassword: '',
|
||||
});
|
||||
|
||||
// 鼠标按下 pc
|
||||
const onDownPc = (down: MouseEvent) => {
|
||||
state.isFlags = true;
|
||||
state.downClientY = down.clientY;
|
||||
};
|
||||
// 鼠标按下 app
|
||||
const onDownApp = (down: TouchEvent) => {
|
||||
state.isFlags = true;
|
||||
state.downClientY = down.touches[0].clientY;
|
||||
};
|
||||
// 鼠标移动 pc
|
||||
const onMovePc = (move: MouseEvent) => {
|
||||
state.moveDifference = move.clientY - state.downClientY;
|
||||
onMove();
|
||||
};
|
||||
// 鼠标移动 app
|
||||
const onMoveApp = (move: TouchEvent) => {
|
||||
state.moveDifference = move.touches[0].clientY - state.downClientY;
|
||||
onMove();
|
||||
};
|
||||
// 鼠标移动事件
|
||||
const onMove = () => {
|
||||
if (state.isFlags) {
|
||||
const el = <HTMLElement>state.querySelectorEl;
|
||||
const opacitys = (state.transparency -= 1 / 200);
|
||||
if (state.moveDifference >= 0) return false;
|
||||
el.setAttribute('style', `top:${state.moveDifference}px;cursor:pointer;opacity:${opacitys};`);
|
||||
if (state.moveDifference < -400) {
|
||||
el.setAttribute('style', `top:${-el.clientHeight}px;cursor:pointer;transition:all 0.3s ease;`);
|
||||
state.moveDifference = -el.clientHeight;
|
||||
setTimeout(() => {
|
||||
el && el.parentNode?.removeChild(el);
|
||||
}, 300);
|
||||
}
|
||||
if (state.moveDifference === -el.clientHeight) {
|
||||
state.isShowLoockLogin = true;
|
||||
layoutLockScreenInputRef.value.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
// 鼠标松开
|
||||
const onEnd = () => {
|
||||
state.isFlags = false;
|
||||
state.transparency = 1;
|
||||
if (state.moveDifference >= -400) {
|
||||
(<HTMLElement>state.querySelectorEl).setAttribute('style', `top:0px;opacity:1;transition:all 0.3s ease;`);
|
||||
}
|
||||
};
|
||||
// 获取要拖拽的初始元素
|
||||
const initGetElement = () => {
|
||||
nextTick(() => {
|
||||
state.querySelectorEl = layoutLockScreenDateRef.value;
|
||||
});
|
||||
};
|
||||
// 时间初始化
|
||||
const initTime = () => {
|
||||
state.time.hm = formatDate(new Date(), 'HH:MM');
|
||||
state.time.s = formatDate(new Date(), 'SS');
|
||||
state.time.mdq = formatDate(new Date(), 'mm月dd日,WWW');
|
||||
};
|
||||
// 时间初始化定时器
|
||||
const initSetTime = () => {
|
||||
initTime();
|
||||
state.setIntervalTime = window.setInterval(() => {
|
||||
initTime();
|
||||
}, 1000);
|
||||
};
|
||||
// 锁屏时间定时器
|
||||
const initLockScreen = () => {
|
||||
if (themeConfig.value.isLockScreen) {
|
||||
state.isShowLockScreenIntervalTime = window.setInterval(() => {
|
||||
if (themeConfig.value.lockScreenTime <= 1) {
|
||||
state.isShowLockScreen = true;
|
||||
setLocalThemeConfig();
|
||||
return false;
|
||||
}
|
||||
themeConfig.value.lockScreenTime--;
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(state.isShowLockScreenIntervalTime);
|
||||
}
|
||||
};
|
||||
// 存储布局配置
|
||||
const setLocalThemeConfig = () => {
|
||||
themeConfig.value.isDrawer = false;
|
||||
setLocal('themeConfig', themeConfig.value);
|
||||
};
|
||||
// 密码输入点击事件
|
||||
const onLockScreenSubmit = () => {
|
||||
themeConfig.value.isLockScreen = false;
|
||||
themeConfig.value.lockScreenTime = 30;
|
||||
setLocalThemeConfig();
|
||||
};
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
initGetElement();
|
||||
initSetTime();
|
||||
initLockScreen();
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
window.clearInterval(state.setIntervalTime);
|
||||
window.clearInterval(state.isShowLockScreenIntervalTime);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-lock-screen-fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.layout-lock-screen-filter {
|
||||
filter: blur(1px);
|
||||
}
|
||||
.layout-lock-screen-mask {
|
||||
background: var(--el-color-white);
|
||||
@extend .layout-lock-screen-fixed;
|
||||
z-index: 9999990;
|
||||
}
|
||||
.layout-lock-screen-img {
|
||||
@extend .layout-lock-screen-fixed;
|
||||
background: url('@/assets/image/login-bg-main.svg') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
z-index: 9999991;
|
||||
}
|
||||
.layout-lock-screen {
|
||||
@extend .layout-lock-screen-fixed;
|
||||
z-index: 9999992;
|
||||
&-date {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-color-white);
|
||||
z-index: 9999993;
|
||||
user-select: none;
|
||||
&-box {
|
||||
position: absolute;
|
||||
left: 30px;
|
||||
bottom: 50px;
|
||||
&-time {
|
||||
font-size: 100px;
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
&-info {
|
||||
font-size: 40px;
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
&-minutes {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
&-top {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
border-radius: 100%;
|
||||
border: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--el-color-white);
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 50px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
i {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
&-text {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 150%;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-white);
|
||||
left: 50%;
|
||||
line-height: 1.2;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: all 0.3s ease;
|
||||
width: 35px;
|
||||
}
|
||||
&:hover {
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 0 12px 0 rgba(255, 255, 255, 0.5);
|
||||
color: var(--el-color-white);
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease;
|
||||
i {
|
||||
transform: translateY(-40px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.layout-lock-screen-date-top-text {
|
||||
opacity: 1;
|
||||
top: 50%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&-login {
|
||||
position: relative;
|
||||
z-index: 9999994;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: var(--el-color-white);
|
||||
&-box {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
&-img {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
margin: auto;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
&-name {
|
||||
font-size: 26px;
|
||||
margin: 15px 0 30px;
|
||||
}
|
||||
}
|
||||
&-icon {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 30px;
|
||||
i {
|
||||
font-size: 20px;
|
||||
margin-left: 15px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.el-input-group__append) {
|
||||
background: var(--el-color-white);
|
||||
padding: 0px 15px;
|
||||
}
|
||||
:deep(.el-input__inner) {
|
||||
border-right-color: var(--el-border-color-extra-light);
|
||||
&:hover {
|
||||
border-color: var(--el-border-color-extra-light);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,6 @@ import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import config from '@/common/config';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
@@ -30,7 +29,6 @@ const setShowLogo = computed(() => {
|
||||
// logo 点击实现菜单展开/收起
|
||||
const onThemeConfigChange = () => {
|
||||
if (themeConfig.value.layout === 'transverse') return false;
|
||||
mittBus.emit('onMenuClick');
|
||||
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -19,6 +19,11 @@ import Aside from '@/layout/component/aside.vue';
|
||||
import Header from '@/layout/component/header.vue';
|
||||
import Main from '@/layout/component/main.vue';
|
||||
import TagsView from '@/layout/navBars/tagsView/tagsView.vue';
|
||||
import { provide, ref } from 'vue';
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
// 提供 classic 布局的菜单数据
|
||||
const classicMenuData = ref<any>(null);
|
||||
provide('classicMenuData', classicMenuData);
|
||||
</script>
|
||||
|
||||
@@ -14,13 +14,17 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="layoutColumns">
|
||||
import { computed } from 'vue';
|
||||
import { computed, provide, ref } from 'vue';
|
||||
import Aside from '@/layout/component/aside.vue';
|
||||
import Header from '@/layout/component/header.vue';
|
||||
import Main from '@/layout/component/main.vue';
|
||||
import ColumnsAside from '@/layout/component/columnsAside.vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
|
||||
// 提供响应式数据给子组件
|
||||
const columnsMenuData = ref<any>(null);
|
||||
provide('columnsMenuData', columnsMenuData);
|
||||
|
||||
const isFixedHeader = computed(() => {
|
||||
return useThemeConfig().themeConfig.isFixedHeader;
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useRoutesList } from '@/store/routesList';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
const { routesList } = storeToRefs(useRoutesList());
|
||||
@@ -45,7 +44,6 @@ const onBreadcrumbClick = (v: any) => {
|
||||
};
|
||||
// 展开/收起左侧菜单点击
|
||||
const onThemeConfigChange = () => {
|
||||
mittBus.emit('onMenuClick');
|
||||
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
|
||||
};
|
||||
// 处理面包屑数据
|
||||
|
||||
@@ -18,7 +18,6 @@ import Breadcrumb from '@/layout/navBars/breadcrumb/breadcrumb.vue';
|
||||
import User from '@/layout/navBars/breadcrumb/user.vue';
|
||||
import Logo from '@/layout/logo/index.vue';
|
||||
import Horizontal from '@/layout/navMenu/horizontal.vue';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
const { routesList } = storeToRefs(useRoutesList());
|
||||
@@ -42,8 +41,6 @@ const setFilterRoutes = () => {
|
||||
let { layout, isClassicSplitMenu } = themeConfig.value;
|
||||
if (layout === 'classic' && isClassicSplitMenu) {
|
||||
state.menuList = delClassicChildren(filterRoutesFun(routesList.value));
|
||||
const resData = setSendClassicChildren(route.path);
|
||||
mittBus.emit('setSendClassicChildren', resData);
|
||||
} else {
|
||||
state.menuList = filterRoutesFun(routesList.value);
|
||||
}
|
||||
@@ -87,13 +84,6 @@ watch(pinia.state, (val) => {
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
setFilterRoutes();
|
||||
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
|
||||
setFilterRoutes();
|
||||
});
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
mittBus.off('getBreadcrumbIndexSetFilterRoutes');
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -134,15 +134,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt14">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.menuBarActiveColor') }}
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-switch v-model="themeConfig.isMenuBarColorHighlight" @change="onMenuBarHighlightChange"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt14">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.isMenuBarColorGradual') }}
|
||||
@@ -236,23 +227,6 @@
|
||||
</el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.isLockScreen') }}
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-switch v-model="themeConfig.isLockScreen"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt11">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.lockScreenTime') }}
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-input-number v-model="themeConfig.lockScreenTime" controls-position="right" :min="0" :max="9999" size="small" style="width: 90px">
|
||||
</el-input-number>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 界面显示 -->
|
||||
<el-divider content-position="left">{{ $t('layout.config.interfaceDisplay') }}</el-divider>
|
||||
@@ -309,7 +283,7 @@
|
||||
{{ $t('layout.config.isSortableTagsView') }}
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-switch v-model="themeConfig.isSortableTagsView" @change="onSortableTagsViewChange"></el-switch>
|
||||
<el-switch v-model="themeConfig.isSortableTagsView"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
@@ -451,19 +425,60 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="layoutBreadcrumbSeting">
|
||||
import { nextTick, onUnmounted, onMounted, ref } from 'vue';
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import ClipboardJS from 'clipboard';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { getLightColor } from '@/common/utils/theme';
|
||||
import { setLocal, getLocal } from '@/common/utils/storage';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
|
||||
import themes from '@/components/terminal/themes';
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
|
||||
const copyConfigBtnRef = ref();
|
||||
const { themeConfig } = storeToRefs(useThemeConfig()) as any;
|
||||
|
||||
// 获取窗口大小
|
||||
const { width } = useWindowSize();
|
||||
|
||||
watch(width, () => {
|
||||
checkClientWidth();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
checkClientWidth();
|
||||
window.addEventListener('load', () => {
|
||||
// 刷新页面时,设置了值,直接取缓存中的值进行初始化
|
||||
setTimeout(() => {
|
||||
// 顶栏背景渐变
|
||||
if (getLocal('navbarsBgStyle') && themeConfig.value.isTopBarColorGradual) {
|
||||
const breadcrumbIndexEl: any = document.querySelector('.layout-navbars-breadcrumb-index');
|
||||
breadcrumbIndexEl.style.cssText = getLocal('navbarsBgStyle');
|
||||
}
|
||||
// 菜单背景渐变
|
||||
if (getLocal('asideBgStyle') && themeConfig.value.isMenuBarColorGradual) {
|
||||
const asideEl: any = document.querySelector('.layout-container .el-aside');
|
||||
asideEl.style.cssText = getLocal('asideBgStyle');
|
||||
}
|
||||
// 分栏菜单背景渐变
|
||||
if (getLocal('columnsBgStyle') && themeConfig.value.isColumnsMenuBarColorGradual) {
|
||||
const asideEl: any = document.querySelector('.layout-container .layout-columns-aside');
|
||||
asideEl.style.cssText = getLocal('columnsBgStyle');
|
||||
}
|
||||
// 灰色模式/色弱模式
|
||||
if (getLocal('appFilterStyle')) {
|
||||
const appEl: any = document.querySelector('#app');
|
||||
appEl.style.cssText = getLocal('appFilterStyle');
|
||||
}
|
||||
// // 语言国际化
|
||||
// if (getLocal('themeConfig')) proxy.$i18n.locale = getLocal('themeConfig').globalI18n;
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 1、全局主题
|
||||
const onColorPickerChange = (color: string) => {
|
||||
setPropertyFun(`--color-${color}`, themeConfig.value[color]);
|
||||
@@ -512,26 +527,9 @@ const setGraduaFun = (el: string, bool: boolean, color: string) => {
|
||||
if (elColumns) setLocal('columnsBgStyle', elColumns.style.cssText);
|
||||
});
|
||||
};
|
||||
// 2、菜单 / 顶栏 --> 菜单字体背景高亮
|
||||
const onMenuBarHighlightChange = () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
let elsItems = document.querySelectorAll('.el-menu-item');
|
||||
let elActive = document.querySelector('.el-menu-item.is-active');
|
||||
if (!elActive) return false;
|
||||
if (themeConfig.value.isMenuBarColorHighlight) {
|
||||
elsItems.forEach((el: any) => el.setAttribute('id', ``));
|
||||
elActive.setAttribute('id', `add-is-active`);
|
||||
setLocal('menuBarHighlightId', elActive.getAttribute('id'));
|
||||
} else {
|
||||
elActive.setAttribute('id', ``);
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
// 3、界面设置 --> 菜单水平折叠
|
||||
const onThemeConfigChange = () => {
|
||||
onMenuBarHighlightChange();
|
||||
setDispatchThemeConfig();
|
||||
};
|
||||
// 3、界面设置 --> 固定 Header
|
||||
@@ -541,8 +539,6 @@ const onIsFixedHeaderChange = () => {
|
||||
// 3、界面设置 --> 经典布局分割菜单
|
||||
const onClassicSplitMenuChange = () => {
|
||||
themeConfig.value.isBreadcrumb = false;
|
||||
|
||||
mittBus.emit('getBreadcrumbIndexSetFilterRoutes');
|
||||
};
|
||||
// 4、界面显示 --> 侧边栏 Logo
|
||||
const onIsShowLogoChange = () => {
|
||||
@@ -554,10 +550,7 @@ const onIsBreadcrumbChange = () => {
|
||||
themeConfig.value.isClassicSplitMenu = false;
|
||||
}
|
||||
};
|
||||
// 4、界面显示 --> 开启 TagsView 拖拽
|
||||
const onSortableTagsViewChange = () => {
|
||||
mittBus.emit('openOrCloseSortable');
|
||||
};
|
||||
|
||||
// 4、界面显示 --> 暗模式/灰色模式/色弱模式
|
||||
const onAddFilterChange = (attr: string) => {
|
||||
if (attr === 'grayscale') {
|
||||
@@ -571,14 +564,16 @@ const onAddFilterChange = (attr: string) => {
|
||||
|
||||
setLocal('appFilterStyle', appEle.style.cssText);
|
||||
};
|
||||
|
||||
// 5、布局切换
|
||||
const onSetLayout = (layout: string) => {
|
||||
setLocal('oldLayout', layout);
|
||||
if (themeConfig.value.layout === layout) return false;
|
||||
if (themeConfig.value.layout === layout) {
|
||||
return;
|
||||
}
|
||||
themeConfig.value.layout = layout;
|
||||
themeConfig.value.isDrawer = false;
|
||||
initSetLayoutChange();
|
||||
onMenuBarHighlightChange();
|
||||
};
|
||||
// 设置布局切换,重置主题样式
|
||||
const initSetLayoutChange = () => {
|
||||
@@ -627,14 +622,6 @@ const onDrawerClose = () => {
|
||||
themeConfig.value.isShowLogoChange = false;
|
||||
themeConfig.value.isDrawer = false;
|
||||
};
|
||||
// 布局配置弹窗打开
|
||||
const openDrawer = () => {
|
||||
themeConfig.value.isDrawer = true;
|
||||
nextTick(() => {
|
||||
// 初始化复制功能,防止点击两次才可以复制
|
||||
onCopyConfigClick(copyConfigBtnRef.value?.$el);
|
||||
});
|
||||
};
|
||||
|
||||
// 触发 store 布局配置更新
|
||||
const setDispatchThemeConfig = () => {
|
||||
@@ -665,63 +652,23 @@ const onCopyConfigClick = (target: any) => {
|
||||
clipboard.destroy();
|
||||
});
|
||||
};
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 监听菜单点击,菜单字体背景高亮
|
||||
mittBus.on('onMenuClick', () => {
|
||||
onMenuBarHighlightChange();
|
||||
});
|
||||
// 监听窗口大小改变,非默认布局,设置成默认布局(适配移动端)
|
||||
mittBus.on('layoutMobileResize', (res: any) => {
|
||||
themeConfig.value.layout = res.layout;
|
||||
themeConfig.value.isDrawer = false;
|
||||
initSetLayoutChange();
|
||||
onMenuBarHighlightChange();
|
||||
themeConfig.value.isCollapse = false;
|
||||
});
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
// 刷新页面时,设置了值,直接取缓存中的值进行初始化
|
||||
setTimeout(() => {
|
||||
// 顶栏背景渐变
|
||||
if (getLocal('navbarsBgStyle') && themeConfig.value.isTopBarColorGradual) {
|
||||
const breadcrumbIndexEl: any = document.querySelector('.layout-navbars-breadcrumb-index');
|
||||
breadcrumbIndexEl.style.cssText = getLocal('navbarsBgStyle');
|
||||
}
|
||||
// 菜单背景渐变
|
||||
if (getLocal('asideBgStyle') && themeConfig.value.isMenuBarColorGradual) {
|
||||
const asideEl: any = document.querySelector('.layout-container .el-aside');
|
||||
asideEl.style.cssText = getLocal('asideBgStyle');
|
||||
}
|
||||
// 分栏菜单背景渐变
|
||||
if (getLocal('columnsBgStyle') && themeConfig.value.isColumnsMenuBarColorGradual) {
|
||||
const asideEl: any = document.querySelector('.layout-container .layout-columns-aside');
|
||||
asideEl.style.cssText = getLocal('columnsBgStyle');
|
||||
}
|
||||
// 菜单字体背景高亮
|
||||
if (getLocal('menuBarHighlightId') && themeConfig.value.isMenuBarColorHighlight) {
|
||||
let els = document.querySelector('.el-menu-item.is-active');
|
||||
if (!els) return false;
|
||||
els.setAttribute('id', getLocal('menuBarHighlightId'));
|
||||
}
|
||||
// 灰色模式/色弱模式
|
||||
if (getLocal('appFilterStyle')) {
|
||||
const appEl: any = document.querySelector('#app');
|
||||
appEl.style.cssText = getLocal('appFilterStyle');
|
||||
}
|
||||
// // 语言国际化
|
||||
// if (getLocal('themeConfig')) proxy.$i18n.locale = getLocal('themeConfig').globalI18n;
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
onUnmounted(() => {
|
||||
// 取消监听菜单点击,菜单字体背景高亮
|
||||
mittBus.off('onMenuClick');
|
||||
mittBus.off('layoutMobileResize');
|
||||
});
|
||||
const checkClientWidth = () => {
|
||||
const oldLayout = getLocal('oldLayout');
|
||||
if (!oldLayout) {
|
||||
setLocal('oldLayout', themeConfig.value.layout);
|
||||
}
|
||||
if (width.value < 1000) {
|
||||
themeConfig.value.isCollapse = false;
|
||||
themeConfig.value.layout = 'defaults';
|
||||
} else {
|
||||
themeConfig.value.layout = oldLayout ? oldLayout : 'defaults';
|
||||
}
|
||||
|
||||
defineExpose({ openDrawer });
|
||||
themeConfig.value.isDrawer = false;
|
||||
initSetLayoutChange();
|
||||
themeConfig.value.isCollapse = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<SvgIcon name="setting" :title="$t('layout.user.layoutConf')" />
|
||||
</div>
|
||||
|
||||
<el-popover @show="onShowMsgs" placement="bottom" trigger="click" :width="450">
|
||||
<el-popover @show="onShowMsgs" @hide="userNewsRef?.clearMsg()" placement="bottom" trigger="click" :width="500">
|
||||
<template #reference>
|
||||
<div class="layout-navbars-breadcrumb-user-icon">
|
||||
<el-badge :show-zero="false" :value="state.unreadMsgCount">
|
||||
@@ -92,7 +92,6 @@ import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { clearSession } from '@/common/utils/storage';
|
||||
import UserNews from '@/layout/navBars/breadcrumb/userNews.vue';
|
||||
import SearchMenu from '@/layout/navBars/breadcrumb/search.vue';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
import openApi from '@/common/openApi';
|
||||
import { getThemeConfig } from '@/common/utils/storage';
|
||||
import { useDark, usePreferredDark } from '@vueuse/core';
|
||||
@@ -150,7 +149,7 @@ const onScreenfullClick = () => {
|
||||
};
|
||||
// 布局配置 icon 点击时
|
||||
const onLayoutSetingClick = () => {
|
||||
mittBus.emit('openSetingsDrawer');
|
||||
themeConfig.value.isDrawer = true;
|
||||
};
|
||||
// 下拉菜单点击时
|
||||
const onHandleCommandClick = (path: string) => {
|
||||
|
||||
@@ -1,47 +1,62 @@
|
||||
<template>
|
||||
<div class="flex flex-col w-full rounded-md shadow-sm">
|
||||
<div class="rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden w-full">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-gray-200 px-4 py-2 text-sm text-gray-700">
|
||||
<div class="font-medium">{{ $t('layout.user.newTitle') }}</div>
|
||||
<div v-if="unreadCount > 0" class="color-primary cursor-pointer opacity-80 transition-opacity hover:opacity-100" @click="onRead()">
|
||||
{{ $t('layout.user.newBtn') }}
|
||||
</div>
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-100 dark:border-gray-700">
|
||||
<h3 class="font-semibold text-lg text-gray-800 dark:text-gray-100 flex items-center">
|
||||
<SvgIcon class="mr-2" name="Bell" :size="16" />
|
||||
{{ $t('layout.user.newTitle') }}
|
||||
</h3>
|
||||
<el-badge :value="unreadCount" :max="99" :hidden="unreadCount === 0" type="primary">
|
||||
<el-button v-if="unreadCount > 0" size="small" type="primary" link @click="onRead()" class="text-sm">
|
||||
{{ $t('layout.user.newBtn') }}
|
||||
</el-button>
|
||||
</el-badge>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<el-scrollbar height="350px" v-loading="loadingMsgs" class="px-4 py-2 text-sm">
|
||||
<el-scrollbar height="360px" v-loading="loadingMsgs" class="px-3 py-2" :class="{ 'py-8': msgs.length === 0 }">
|
||||
<template v-if="msgs.length > 0">
|
||||
<div
|
||||
v-for="(v, k) in msgs"
|
||||
:key="k"
|
||||
class="pt-1 mt-0.5"
|
||||
:style="{ backgroundColor: v.status == 1 ? 'var(--el-color-info-light-9)' : 'transparent' }"
|
||||
class="px-3 py-3 my-1 rounded-lg transition-all duration-200 cursor-pointer hover:shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
:class="{
|
||||
' hover:bg-gray-100 dark:hover:bg-gray-200 border border-blue-100 dark:border-blue-800/50': v.status == -1,
|
||||
'bg-gray-50 hover:bg-gray-100 dark:bg-gray-600/20 dark:hover:bg-gray-200 border border-transparent': v.status == 1,
|
||||
}"
|
||||
@click="onRead(v)"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<el-text size="small" tag="b" :type="EnumValue.getEnumByValue(MsgSubtypeEnum, v.subtype)?.extra?.notifyType">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="EnumValue.getEnumByValue(MsgSubtypeEnum, v.subtype)?.extra?.notifyType || 'info'"
|
||||
effect="light"
|
||||
class="rounded-full"
|
||||
>
|
||||
{{ $t(EnumValue.getEnumByValue(MsgSubtypeEnum, v.subtype)?.label || '') }}
|
||||
</el-tag>
|
||||
<el-text size="small" type="info" class="text-xs whitespace-nowrap ml-2">
|
||||
{{ formatDate(v.createTime) }}
|
||||
</el-text>
|
||||
</div>
|
||||
<div class="text-gray-500 mt-1 mb-1">{{ v.msg }}</div>
|
||||
<div class="text-gray-500">{{ formatDate(v.createTime) }}</div>
|
||||
<div class="mt-2 border-t border-gray-200"></div>
|
||||
<div class="mt-2 text-gray-700 dark:text-gray-300 text-sm leading-relaxed">
|
||||
<MessageRenderer :content="v.msg" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button class="w-full mt-1" size="small" @click="loadMsgs()" v-if="!loadMoreDisable"> {{ $t('redis.loadMore') }} </el-button>
|
||||
<div class="text-center py-3" v-if="!loadMoreDisable">
|
||||
<el-button link type="primary" size="small" @click="loadMsgs()">
|
||||
{{ $t('redis.loadMore') }}
|
||||
<SvgIcon name="ArrowDown" />
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-empty v-if="msgs.length == 0 && !loadingMsgs" :image-size="100" :description="$t('layout.user.newDesc')" />
|
||||
<div v-else-if="!loadingMsgs" class="text-center py-6">
|
||||
<SvgIcon name="ChatLineRound" :size="36" class="mb-3 text-gray-300 dark:text-gray-600" />
|
||||
<p class="text-gray-500 dark:text-gray-400 text-2xl">{{ $t('layout.user.newDesc') }}</p>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- Footer -->
|
||||
<!-- <div
|
||||
v-if="msgs.length > 0"
|
||||
class="color-primary flex h-9 items-center justify-center border-t border-gray-200 text-sm cursor-pointer opacity-80 transition-opacity hover:opacity-100"
|
||||
@click="toMsgCenter"
|
||||
>
|
||||
{{ $t('layout.user.newGo') }}
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,13 +64,14 @@
|
||||
import { MsgSubtypeEnum } from '@/common/commonEnum';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { MessageRenderer } from '@/components/message/message';
|
||||
import { personApi } from '@/views/personal/api';
|
||||
import { useIntervalFn } from '@vueuse/core';
|
||||
import { onMounted, ref, watchEffect } from 'vue';
|
||||
|
||||
const emit = defineEmits(['update:count']);
|
||||
|
||||
const msgQuery = ref<any>({
|
||||
const msgQuery = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
@@ -87,6 +103,7 @@ const loadMsgs = async (research: boolean = false) => {
|
||||
msgQuery.value.pageNum = 1;
|
||||
msgs.value = [];
|
||||
}
|
||||
|
||||
const msgList = await getMsgs();
|
||||
msgs.value.push(...msgList.list);
|
||||
msgQuery.value.pageNum += 1;
|
||||
@@ -111,12 +128,13 @@ const onRead = async (msg: any = null) => {
|
||||
}
|
||||
|
||||
await personApi.readMsg.request({ id: msg?.id || 0 });
|
||||
loadMsgs(true);
|
||||
|
||||
if (!msg) {
|
||||
loadMsgs(true);
|
||||
// 如果是全部已读,重置未读消息数
|
||||
unreadCount.value = 0;
|
||||
} else {
|
||||
msg.status = 1;
|
||||
// 如果是单条已读,减少未读消息数
|
||||
unreadCount.value = Math.max(unreadCount.value - 1, 0);
|
||||
}
|
||||
@@ -124,9 +142,23 @@ const onRead = async (msg: any = null) => {
|
||||
|
||||
defineExpose({
|
||||
loadMsgs,
|
||||
clearMsg: function () {
|
||||
msgQuery.value.pageNum = 1;
|
||||
msgs.value = [];
|
||||
loadingMsgs.value = true;
|
||||
},
|
||||
});
|
||||
|
||||
const toMsgCenter = () => {};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-scrollbar__view) {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.el-tag) {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -46,12 +46,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="layoutTagsView">
|
||||
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, getCurrentInstance } from 'vue';
|
||||
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, getCurrentInstance, watch } from 'vue';
|
||||
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
|
||||
import screenfull from 'screenfull';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
import Sortable from 'sortablejs';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import { getTagViews, setTagViews, removeTagViews } from '@/common/utils/storage';
|
||||
@@ -185,7 +184,7 @@ const refreshCurrentTagsView = async (path: string) => {
|
||||
const item = getTagsView(path);
|
||||
await keepAliveNamesStores.delCachedView(item);
|
||||
keepAliveNamesStores.addCachedView(item);
|
||||
mittBus.emit('onTagsViewRefreshRouterView', path);
|
||||
useTagsViews().setCurrentRefreshPath(path);
|
||||
};
|
||||
|
||||
const getTagsView = (path: string) => {
|
||||
@@ -375,18 +374,15 @@ const initSortable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载前
|
||||
onBeforeMount(() => {
|
||||
// 监听布局配置界面开启/关闭拖拽
|
||||
mittBus.on('openOrCloseSortable', () => {
|
||||
initSortable();
|
||||
});
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
// 取消监听布局配置界面开启/关闭拖拽
|
||||
mittBus.off('openOrCloseSortable');
|
||||
});
|
||||
watch(
|
||||
() => themeConfig.value.isSortableTagsView,
|
||||
(isSortableTagsView: boolean) => {
|
||||
if (isSortableTagsView) {
|
||||
initSortable();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 页面更新时
|
||||
onBeforeUpdate(() => {
|
||||
tagsRefs.value = [];
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
<template>
|
||||
<div class="el-menu-horizontal-warp">
|
||||
<el-scrollbar @wheel.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">
|
||||
<el-menu router :default-active="state.defaultActive" background-color="transparent" mode="horizontal" @select="onHorizontalSelect">
|
||||
<template v-for="val in menuLists">
|
||||
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
|
||||
<template #title>
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
<span>{{ $t(val.meta.title) }}</span>
|
||||
</template>
|
||||
<SubItem :chil="val.children" />
|
||||
</el-sub-menu>
|
||||
<el-menu-item :index="val.path" :key="val?.path" v-else>
|
||||
<template #title v-if="!val.meta.link || (val.meta.link && val.meta.linkType == 1)">
|
||||
<el-menu
|
||||
router
|
||||
:default-active="state.defaultActive"
|
||||
background-color="transparent"
|
||||
mode="horizontal"
|
||||
@select="onHorizontalSelect"
|
||||
class="horizontal-menu"
|
||||
>
|
||||
<template v-for="val in menuLists">
|
||||
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
|
||||
<template #title>
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
<span>{{ $t(val.meta.title) }}</span>
|
||||
</template>
|
||||
<SubItem :chil="val.children" />
|
||||
</el-sub-menu>
|
||||
<el-menu-item :index="val.path" :key="val?.path" v-else>
|
||||
<template #title v-if="!val.meta.link || (val.meta.link && val.meta.linkType == 1)">
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
{{ $t(val.meta.title) }}
|
||||
</template>
|
||||
<template #title v-else>
|
||||
<a class="w-full" :href="val.meta.link" target="_blank">
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
{{ $t(val.meta.title) }}
|
||||
</template>
|
||||
<template #title v-else>
|
||||
<a :href="val.meta.link" target="_blank">
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
{{ $t(val.meta.title) }}
|
||||
</a>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</a>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="navMenuHorizontal">
|
||||
import { reactive, computed, getCurrentInstance, onMounted, nextTick } from 'vue';
|
||||
import { reactive, computed, onMounted, inject } from 'vue';
|
||||
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
|
||||
import SubItem from '@/layout/navMenu/subItem.vue';
|
||||
import { useRoutesList } from '@/store/routesList';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
@@ -45,28 +49,18 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
const route = useRoute();
|
||||
const state: any = reactive({
|
||||
defaultActive: null,
|
||||
});
|
||||
// 注入 classicMenuData
|
||||
const classicMenuData: any = inject('classicMenuData', null);
|
||||
|
||||
// 获取父级菜单数据
|
||||
const menuLists = computed(() => {
|
||||
return props.menuList;
|
||||
});
|
||||
// 设置横向滚动条可以鼠标滚轮滚动
|
||||
const onElMenuHorizontalScroll = (e: any) => {
|
||||
const eventDelta = e.wheelDelta || -e.deltaY * 40;
|
||||
proxy.$refs.elMenuHorizontalScrollRef.$refs.wrapRef.scrollLeft = proxy.$refs.elMenuHorizontalScrollRef.$refs.wrapRef.scrollLeft + eventDelta / 4;
|
||||
};
|
||||
// 初始化数据,页面刷新时,滚动条滚动到对应位置
|
||||
const initElMenuOffsetLeft = () => {
|
||||
nextTick(() => {
|
||||
let els: any = document.querySelector('.el-menu.el-menu--horizontal li.is-active');
|
||||
if (!els) return false;
|
||||
proxy.$refs.elMenuHorizontalScrollRef.$refs.wrapRef.scrollLeft = els.offsetLeft;
|
||||
});
|
||||
};
|
||||
|
||||
// 设置页面当前路由高亮
|
||||
const setCurrentRouterHighlight = (path: string) => {
|
||||
const currentPathSplit = path.split('/');
|
||||
@@ -102,17 +96,17 @@ const setSendClassicChildren = (path: string) => {
|
||||
};
|
||||
// 菜单激活回调
|
||||
const onHorizontalSelect = (path: string) => {
|
||||
mittBus.emit('setSendClassicChildren', setSendClassicChildren(path));
|
||||
if (classicMenuData) {
|
||||
classicMenuData.value = setSendClassicChildren(path);
|
||||
}
|
||||
};
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
initElMenuOffsetLeft();
|
||||
setCurrentRouterHighlight(route.path);
|
||||
});
|
||||
// 路由更新时
|
||||
onBeforeRouteUpdate((to) => {
|
||||
setCurrentRouterHighlight(to.path);
|
||||
mittBus.emit('onMenuClick');
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -122,19 +116,42 @@ onBeforeRouteUpdate((to) => {
|
||||
overflow: hidden;
|
||||
margin-right: 30px;
|
||||
|
||||
::v-deep(.el-scrollbar__bar.is-vertical) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::v-deep(a) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-menu.el-menu--horizontal {
|
||||
display: flex;
|
||||
.horizontal-menu {
|
||||
border: none !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
::v-deep(.el-menu-item) {
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
padding: 0 15px !important;
|
||||
margin: 0 5px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
::v-deep(.el-sub-menu__title) {
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
padding: 0 25px 0 15px !important; /* 右边留出更多空间给箭头图标 */
|
||||
margin: 0 5px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
::v-deep(.el-sub-menu__icon-arrow) {
|
||||
right: 5px !important;
|
||||
margin-top: -5px !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-menu-item.is-active),
|
||||
::v-deep(.el-sub-menu.is-active .el-sub-menu__title) {
|
||||
color: #409eff;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<span>{{ $t(val.meta.title) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a :href="val.meta.link" target="_blank">
|
||||
<a class="w-full" :href="val.meta.link" target="_blank">
|
||||
<SvgIcon :name="val.meta.icon" />
|
||||
{{ $t(val.meta.title) }}
|
||||
</a>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<span>{{ $t(val.meta.title) }}</span>
|
||||
</template>
|
||||
<template #title v-else>
|
||||
<a :href="val.meta.link" target="_blank">{{ $t(val.meta.title) }}</a></template
|
||||
<a class="w-full" :href="val.meta.link" target="_blank">{{ $t(val.meta.title) }}</a></template
|
||||
>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
@@ -34,7 +34,6 @@ import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
|
||||
import SubItem from '@/layout/navMenu/subItem.vue';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
@@ -46,23 +45,29 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const state = reactive({
|
||||
defaultActive: route.path,
|
||||
});
|
||||
|
||||
// 获取父级菜单数据
|
||||
const menuLists = computed(() => {
|
||||
return props.menuList;
|
||||
});
|
||||
|
||||
// 设置菜单的收起/展开
|
||||
const setIsCollapse = computed(() => {
|
||||
return document.body.clientWidth < 1000 ? false : themeConfig.value.isCollapse;
|
||||
});
|
||||
|
||||
// 路由更新时
|
||||
onBeforeRouteUpdate((to) => {
|
||||
state.defaultActive = to.path;
|
||||
mittBus.emit('onMenuClick');
|
||||
const clientWidth = document.body.clientWidth;
|
||||
if (clientWidth < 1000) themeConfig.value.isCollapse = false;
|
||||
if (clientWidth < 1000) {
|
||||
themeConfig.value.isCollapse = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,59 +1,114 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="layout-view-bg-white flex !h-full" v-loading="iframeLoading">
|
||||
<iframe :src="iframeUrl" frameborder="0" height="100%" width="100%" id="iframe" v-show="!iframeLoading"></iframe>
|
||||
<div class="h-full">
|
||||
<div class="w-full h-full relative" v-for="v in setIframeList" :key="v.path">
|
||||
<transition-group :name="name">
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full flex justify-center items-center bg-white z-[100]"
|
||||
v-if="v.meta.loading"
|
||||
:key="`${v.path}-loading`"
|
||||
>
|
||||
<div class="flex flex-col items-center text-gray-500">
|
||||
<i class="el-icon-loading"></i>
|
||||
<div class="mt-2.5 text-sm">loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
:src="v.meta.link"
|
||||
:key="v.path"
|
||||
frameborder="0"
|
||||
height="100%"
|
||||
width="100%"
|
||||
style="position: absolute"
|
||||
:data-url="v.path"
|
||||
v-show="getRoutePath === v.path"
|
||||
ref="iframeRef"
|
||||
/>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, onMounted, onBeforeMount, onUnmounted, nextTick } from 'vue';
|
||||
<script setup lang="ts" name="layoutIframeView">
|
||||
import { computed, watch, ref, nextTick } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
export default defineComponent({
|
||||
name: 'layoutIfameView',
|
||||
props: {
|
||||
meta: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
// 定义父组件传过来的值
|
||||
const props = defineProps({
|
||||
// 刷新 iframe
|
||||
refreshKey: {
|
||||
type: String,
|
||||
default: () => '',
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const route = useRoute();
|
||||
const state = reactive({
|
||||
iframeLoading: true,
|
||||
iframeUrl: '',
|
||||
});
|
||||
// 初始化页面加载 loading
|
||||
const initIframeLoad = () => {
|
||||
nextTick(() => {
|
||||
state.iframeLoading = true;
|
||||
const iframe = document.getElementById('iframe');
|
||||
if (!iframe) return false;
|
||||
iframe.onload = () => {
|
||||
state.iframeLoading = false;
|
||||
};
|
||||
});
|
||||
};
|
||||
// 页面加载前
|
||||
onBeforeMount(() => {
|
||||
state.iframeUrl = props.meta.link;
|
||||
mittBus.on('onTagsViewRefreshRouterView', (path: string) => {
|
||||
if (route.path !== path) return false;
|
||||
emit('getCurrentRouteMeta');
|
||||
});
|
||||
});
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
initIframeLoad();
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
mittBus.off('onTagsViewRefreshRouterView', () => {});
|
||||
});
|
||||
return {
|
||||
...toRefs(state),
|
||||
};
|
||||
// 过渡动画 name
|
||||
name: {
|
||||
type: String,
|
||||
default: () => 'slide-right',
|
||||
},
|
||||
// iframe 列表
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const iframeRef = ref();
|
||||
const route = useRoute();
|
||||
|
||||
// 处理 list 列表,当打开时,才进行加载
|
||||
const setIframeList = computed(() => {
|
||||
return props.list.filter((v: any) => v.meta?.isIframeOpen) as any[];
|
||||
});
|
||||
|
||||
// 获取 iframe 当前路由 path
|
||||
const getRoutePath = computed(() => {
|
||||
return route.path;
|
||||
});
|
||||
|
||||
// 关闭 iframe loading
|
||||
const closeIframeLoading = (val: string, item: any) => {
|
||||
nextTick(() => {
|
||||
if (!iframeRef.value) return false;
|
||||
iframeRef.value.forEach((v: HTMLElement) => {
|
||||
if (v.dataset.url === val) {
|
||||
v.onload = () => {
|
||||
if (item.meta?.isIframeOpen && item.meta.loading) item.meta.loading = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 监听路由变化,初始化 iframe 数据,防止多个 iframe 时,切换不生效
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(val) => {
|
||||
const item: any = props.list.find((v: any) => v.path === val);
|
||||
if (!item) return false;
|
||||
if (!item.meta.isIframeOpen) item.meta.isIframeOpen = true;
|
||||
closeIframeLoading(val, item);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 iframe refreshKey 变化,用于 tagsview 右键菜单刷新
|
||||
watch(
|
||||
() => props.refreshKey,
|
||||
() => {
|
||||
const item: any = props.list.find((v: any) => v.path === route.path);
|
||||
if (!item) return false;
|
||||
if (item.meta.isIframeOpen) item.meta.isIframeOpen = false;
|
||||
setTimeout(() => {
|
||||
item.meta.isIframeOpen = true;
|
||||
item.meta.loading = true;
|
||||
closeIframeLoading(route.fullPath, item);
|
||||
});
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="layout-view-bg-white flex layout-view-link">
|
||||
<a :href="currentRouteMeta.link" target="_blank" class="flex-margin"> {{ $t(currentRouteMeta.title) }}:{{ currentRouteMeta.link }} </a>
|
||||
<div class="card flex flex-col h-full p-4 layout-link-container">
|
||||
<div class="flex-1 overflow-auto layout-padding-view">
|
||||
<div class="flex flex-col items-center justify-center h-full layout-link-warp">
|
||||
<i class="relative text-8xl text-primary layout-link-icon iconfont icon-xingqiu">
|
||||
<span
|
||||
class="absolute top-0 left-[50px] w-4 h-24 bg-gradient-to-b from-white/5 via-white/20 to-white/5 transform -rotate-12 animate-pulse"
|
||||
></span>
|
||||
</i>
|
||||
<div class="mt-4 text-sm text-gray-500 opacity-70 layout-link-msg">页面 "{{ $t(state.title) }}" 已在新窗口中打开</div>
|
||||
<el-button class="mt-8 rounded-full" round size="default" @click="onGotoFullPage">
|
||||
<i class="iconfont icon-lianjie"></i>
|
||||
<span>立即前往体验</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
export default defineComponent({
|
||||
name: 'layoutLinkView',
|
||||
props: {
|
||||
meta: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// 获取父级菜单数据
|
||||
const currentRouteMeta = computed(() => {
|
||||
return props.meta;
|
||||
});
|
||||
return {
|
||||
currentRouteMeta,
|
||||
};
|
||||
},
|
||||
<script setup lang="ts" name="layoutLinkView">
|
||||
import { reactive, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
// 定义变量内容
|
||||
const route = useRoute();
|
||||
const state = reactive({
|
||||
title: '',
|
||||
link: '',
|
||||
});
|
||||
|
||||
// 立即前往
|
||||
const onGotoFullPage = () => {
|
||||
window.open(state.link);
|
||||
// const { origin, pathname } = window.location;
|
||||
// if (verifyUrl(<string>state.isLink)) window.open(state.isLink);
|
||||
// else window.open(`${origin}${pathname}#${state.isLink}`);
|
||||
};
|
||||
|
||||
// 监听路由的变化,设置内容
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
state.title = <string>route.meta.title;
|
||||
state.link = <string>route.meta.link;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-link-container {
|
||||
.layout-link-warp {
|
||||
margin: auto;
|
||||
.layout-link-msg {
|
||||
font-size: 12px;
|
||||
color: var(--next-bg-topBarColor);
|
||||
opacity: 0.7;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,51 +1,76 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition appear :name="setTransitionName" mode="out-in">
|
||||
<transition appear :name="themeConfig.animation" mode="out-in">
|
||||
<keep-alive :include="getKeepAliveNames">
|
||||
<component :is="Component" :key="state.refreshRouterViewKey" />
|
||||
<component :is="Component" :key="state.refreshRouterViewKey" v-show="!isIframePage" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
|
||||
<transition :name="themeConfig.animation" mode="out-in">
|
||||
<Iframes class="w-full" v-show="isIframePage" :refreshKey="state.iframeRefreshKey" :name="themeConfig.animation" :list="state.iframes" />
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="layoutParentView">
|
||||
import { computed, watch, reactive, onBeforeMount, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { computed, watch, reactive, onBeforeMount, onMounted, nextTick, defineAsyncComponent } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useKeepALiveNames } from '@/store/keepAliveNames';
|
||||
import mittBus from '@/common/utils/mitt';
|
||||
import { getTagViews } from '@/common/utils/storage';
|
||||
import { useTagsViews } from '@/store/tagsViews';
|
||||
import { LinkTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const Iframes = defineAsyncComponent(() => import('@/layout/routerView/iframes.vue'));
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
const { keepAliveNames, cachedViews } = storeToRefs(useKeepALiveNames());
|
||||
|
||||
const state: any = reactive({
|
||||
refreshRouterViewKey: null,
|
||||
keepAliveNameList: [],
|
||||
const state = reactive({
|
||||
refreshRouterViewKey: '',
|
||||
keepAliveNameList: [] as any[],
|
||||
iframeRefreshKey: '', // iframe tagsview 右键菜单刷新时
|
||||
iframes: [] as any[],
|
||||
});
|
||||
|
||||
const { currentRefreshPath } = storeToRefs(useTagsViews());
|
||||
|
||||
// 获取组件缓存列表(name值)
|
||||
const getKeepAliveNames = computed(() => {
|
||||
return themeConfig.value.isTagsview ? cachedViews.value : state.keepAliveNameList;
|
||||
});
|
||||
|
||||
// 设置 iframe 显示/隐藏
|
||||
const isIframePage = computed(() => {
|
||||
return route.meta.linkType == LinkTypeEnum.Iframes.value;
|
||||
});
|
||||
|
||||
watch(currentRefreshPath, (path) => {
|
||||
if (decodeURI(route.fullPath) !== path) {
|
||||
return;
|
||||
}
|
||||
state.keepAliveNameList = keepAliveNames.value.filter((name: string) => route.name !== name);
|
||||
state.refreshRouterViewKey = '';
|
||||
state.iframeRefreshKey = '';
|
||||
nextTick(() => {
|
||||
state.refreshRouterViewKey = path;
|
||||
state.iframeRefreshKey = path;
|
||||
state.keepAliveNameList = keepAliveNames.value;
|
||||
});
|
||||
useTagsViews().setCurrentRefreshPath('');
|
||||
});
|
||||
|
||||
// 页面加载前,处理缓存,页面刷新时路由缓存处理
|
||||
onBeforeMount(() => {
|
||||
state.keepAliveNameList = keepAliveNames.value;
|
||||
mittBus.on('onTagsViewRefreshRouterView', (path: string) => {
|
||||
if (decodeURI(route.fullPath) !== path) return false;
|
||||
state.keepAliveNameList = keepAliveNames.value.filter((name: string) => route.name !== name);
|
||||
state.refreshRouterViewKey = '';
|
||||
nextTick(() => {
|
||||
state.refreshRouterViewKey = path;
|
||||
state.keepAliveNameList = keepAliveNames.value;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
getIframesRoutes();
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if (themeConfig.value.isCacheTagsView) {
|
||||
@@ -55,6 +80,7 @@ onMounted(() => {
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// 监听路由变化,防止 tagsView 多标签时,切换动画消失
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
@@ -65,12 +91,15 @@ watch(
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
// 设置主界面切换动画
|
||||
const setTransitionName = computed(() => {
|
||||
return themeConfig.value.animation;
|
||||
});
|
||||
// 页面卸载时
|
||||
onUnmounted(() => {
|
||||
mittBus.off('onTagsViewRefreshRouterView');
|
||||
});
|
||||
|
||||
// 获取 iframe 组件列表(未进行渲染)
|
||||
const getIframesRoutes = async () => {
|
||||
router.getRoutes().forEach((v) => {
|
||||
if (v.meta.linkType === LinkTypeEnum.Iframes.value) {
|
||||
v.meta.isIframeOpen = false;
|
||||
v.meta.loading = true;
|
||||
state.iframes.push({ ...v });
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useKeepALiveNames } from '@/store/keepAliveNames';
|
||||
import router from '.';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { LAYOUT_ROUTE_NAME } from './staticRouter';
|
||||
import { LinkTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
/**
|
||||
* 获取目录下的 route.ts 全部文件
|
||||
@@ -119,9 +120,19 @@ export function backEndRouterConverter(allModuleRoutes: any, routes: any, callba
|
||||
delete item['name'];
|
||||
|
||||
// route.name == resource.meta.routeName
|
||||
item.name = item.meta.routeName;
|
||||
// routerName == 模块下route.ts 字段key == 组件名
|
||||
item.component = allModuleRoutes[item.meta.routeName];
|
||||
const routerName = item.meta.routeName;
|
||||
item.name = routerName;
|
||||
// 如果是外链类型,name的路由名都是Link 或者 Iframes会导致路由名重复,无法添加多个外链
|
||||
if (item.meta.link) {
|
||||
if (item.meta.linkType == LinkTypeEnum.Link.value) {
|
||||
item.component = () => import('@/layout/routerView/link.vue');
|
||||
} else {
|
||||
item.component = () => import('@/layout/routerView/iframes.vue');
|
||||
}
|
||||
} else {
|
||||
// routerName == 模块下route.ts 字段key == 组件名
|
||||
item.component = allModuleRoutes[routerName];
|
||||
}
|
||||
delete item.meta['routeName'];
|
||||
|
||||
// route.redirect == resource.meta.redirect
|
||||
|
||||
@@ -6,10 +6,14 @@ import { defineStore } from 'pinia';
|
||||
export const useTagsViews = defineStore('tagsViews', {
|
||||
state: (): TagsViewsState => ({
|
||||
tagsViews: [],
|
||||
currentRefreshPath: '',
|
||||
}),
|
||||
actions: {
|
||||
setTagsViews(data: Array<TagsView>) {
|
||||
this.tagsViews = data;
|
||||
},
|
||||
setCurrentRefreshPath(path: string) {
|
||||
this.currentRefreshPath = path;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -63,10 +63,6 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
isFixedHeaderChange: false,
|
||||
// 是否开启经典布局分割菜单(仅经典布局生效)
|
||||
isClassicSplitMenu: false,
|
||||
// 是否开启自动锁屏
|
||||
isLockScreen: false,
|
||||
// 开启自动锁屏倒计时(s/秒)
|
||||
lockScreenTime: 30,
|
||||
|
||||
/* 界面显示
|
||||
------------------------------- */
|
||||
@@ -138,6 +134,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
globalTitle: 'mayfly',
|
||||
// 网站副标题(登录页顶部文字)
|
||||
globalViceTitle: 'mayfly-go',
|
||||
appSlogan: 'common.appSlogan',
|
||||
// 网站logo icon, base64编码内容
|
||||
logoIcon: logoIcon,
|
||||
// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
|
||||
|
||||
@@ -32,11 +32,12 @@
|
||||
/* NavMenu 导航菜单
|
||||
------------------------------- */
|
||||
$radius: 6px;
|
||||
$menuHeight: 46px !important;
|
||||
$menuHeight: 42px !important;
|
||||
$spacing: 8px;
|
||||
|
||||
// 鼠标 hover 时颜色
|
||||
.el-menu-hover-bg-color {
|
||||
background-color: var(--bg-menuBarActiveColor) !important;
|
||||
background-color: #f0f8ff !important; // 更舒适的悬停背景色
|
||||
}
|
||||
|
||||
// 默认样式修改
|
||||
@@ -45,21 +46,45 @@ $menuHeight: 46px !important;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
height: $menuHeight;
|
||||
line-height: $menuHeight;
|
||||
border-radius: $radius;
|
||||
color: #5a5a5a; // 统一调整菜单字体颜色为更舒适的深灰色
|
||||
transition: all 0.2s ease;
|
||||
|
||||
// 第三方图标字体间距/大小设置
|
||||
.icon,
|
||||
.fa {
|
||||
@include mixins.generalIcon;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
color: var(--bg-menuBarColor);
|
||||
height: $menuHeight;
|
||||
.el-menu-item {
|
||||
margin: 2px $spacing;
|
||||
width: calc(100% - #{2 * $spacing});
|
||||
}
|
||||
|
||||
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
|
||||
.el-menu--collapse {
|
||||
width: 64px !important;
|
||||
|
||||
// 菜单收起时,图标不居中问题
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
margin: 4px 0;
|
||||
width: auto;
|
||||
|
||||
.iconfont,
|
||||
.fa {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-sub-menu__title {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 外部链接时
|
||||
@@ -71,29 +96,48 @@ $menuHeight: 46px !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// 第三方图标字体间距/大小设置
|
||||
.el-menu-item .icon,
|
||||
.el-sub-menu .icon,
|
||||
.el-menu-item .fa,
|
||||
.el-sub-menu .fa {
|
||||
@include mixins.generalIcon;
|
||||
}
|
||||
|
||||
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
|
||||
.el-menu-item.is-active,
|
||||
.el-sub-menu.is-active .el-sub-menu__title,
|
||||
.el-sub-menu.is-active>.el-sub-menu__title,
|
||||
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
|
||||
@extend .el-menu-hover-bg-color;
|
||||
border-radius: $radius;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.el-menu-item:hover {
|
||||
@extend .el-menu-hover-bg-color;
|
||||
border-radius: $radius;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
|
||||
// 确保展开的子菜单项在hover时也使用统一的样式
|
||||
.el-sub-menu.is-opened .el-sub-menu__title:hover {
|
||||
@extend .el-menu-hover-bg-color;
|
||||
}
|
||||
|
||||
// 只有直接激活的菜单项才应用高亮样式
|
||||
.el-menu-item.is-active {
|
||||
color: #409eff !important;
|
||||
}
|
||||
|
||||
// 只有当前路由匹配的菜单项才应用高亮样式
|
||||
.el-menu-item.is-active:not(.is-disabled) {
|
||||
color: #409eff !important;
|
||||
}
|
||||
|
||||
// 重置所有子菜单标题的默认样式,确保与普通菜单项一致
|
||||
.el-sub-menu .el-sub-menu__title {
|
||||
color: #5a5a5a !important;
|
||||
}
|
||||
|
||||
// 只有真正激活且未展开的子菜单才应用高亮样式
|
||||
.el-sub-menu.is-active:not(.is-opened)>.el-sub-menu__title {
|
||||
color: #409eff !important;
|
||||
}
|
||||
|
||||
// 展开的子菜单保持默认样式
|
||||
.el-sub-menu.is-active.is-opened>.el-sub-menu__title {
|
||||
background-color: unset !important;
|
||||
color: #5a5a5a !important;
|
||||
}
|
||||
|
||||
// 水平菜单、横向菜单折叠 a 标签
|
||||
@@ -107,24 +151,11 @@ $menuHeight: 46px !important;
|
||||
|
||||
// 水平菜单
|
||||
.el-menu--vertical {
|
||||
background: var(--bg-menuBar);
|
||||
background: #fafafa; // 更舒适的背景色
|
||||
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
|
||||
.el-popper.is-pure.is-light {
|
||||
.el-menu--vertical {
|
||||
.el-sub-menu .el-sub-menu__title {
|
||||
background-color: unset !important;
|
||||
color: var(--bg-menuBarColor);
|
||||
}
|
||||
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 横向菜单
|
||||
@@ -133,33 +164,31 @@ $menuHeight: 46px !important;
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu {
|
||||
height: 48px !important;
|
||||
line-height: 48px !important;
|
||||
height: $menuHeight;
|
||||
line-height: $menuHeight;
|
||||
color: var(--bg-topBarColor);
|
||||
border-radius: $radius;
|
||||
padding: 0 10px !important; // 减小内边距
|
||||
|
||||
.el-sub-menu__title {
|
||||
height: 48px !important;
|
||||
line-height: 48px !important;
|
||||
height: $menuHeight;
|
||||
line-height: $menuHeight;
|
||||
color: var(--bg-topBarColor);
|
||||
}
|
||||
|
||||
.el-popper.is-pure.is-light {
|
||||
.el-menu--horizontal {
|
||||
.el-sub-menu .el-sub-menu__title {
|
||||
background-color: unset !important;
|
||||
color: var(--bg-topBarColor);
|
||||
}
|
||||
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
border-radius: $radius;
|
||||
padding: 0 10px !important; // 减小内边距
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active,
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
color: var(--el-menu-active-color);
|
||||
color: #409eff;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.el-menu-item:hover,
|
||||
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
|
||||
background-color: rgba(64, 158, 255, 0.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,28 +200,107 @@ $menuHeight: 46px !important;
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
height: 48px !important;
|
||||
height: $menuHeight;
|
||||
line-height: $menuHeight;
|
||||
color: var(--bg-topBarColor);
|
||||
border-radius: $radius;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0 10px !important; // 减小内边距
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.el-menu-item:not(.is-active):hover,
|
||||
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
|
||||
color: var(--bg-topBarColor);
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.el-menu-item.is-active,
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
// 为水平菜单的子菜单项正确处理箭头图标位置
|
||||
.el-sub-menu {
|
||||
.el-sub-menu__title {
|
||||
padding-right: 20px !important; // 调整箭头图标空间
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
// 确保水平菜单的箭头图标正确显示在右侧
|
||||
.el-sub-menu__icon-arrow {
|
||||
right: 8px !important;
|
||||
margin-top: -6px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除可能的伪元素下划线
|
||||
.el-menu-item::after,
|
||||
.el-sub-menu__title::after {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单收起时,图标不居中问题
|
||||
.el-menu--collapse {
|
||||
|
||||
.el-menu-item .iconfont,
|
||||
.el-sub-menu .iconfont,
|
||||
.el-menu-item .fa,
|
||||
.el-sub-menu .fa {
|
||||
margin-right: 0 !important;
|
||||
// 暗黑模式下的菜单样式
|
||||
html.dark {
|
||||
.el-menu-hover-bg-color {
|
||||
background-color: #2c2c2c !important; // 暗黑模式下的悬停背景色
|
||||
}
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
padding-right: 0 !important;
|
||||
color: #b2b2b2; // 暗黑模式下的菜单字体颜色
|
||||
}
|
||||
|
||||
.el-menu-item.is-active,
|
||||
.el-menu-item.is-active:not(.is-disabled) {
|
||||
color: #409eff !important;
|
||||
}
|
||||
|
||||
// 重置所有子菜单标题的默认样式,确保与普通菜单项一致 - 暗黑模式
|
||||
.el-sub-menu .el-sub-menu__title {
|
||||
color: #b2b2b2 !important;
|
||||
}
|
||||
|
||||
// 只有真正激活且未展开的子菜单才应用高亮样式 - 暗黑模式
|
||||
.el-sub-menu.is-active:not(.is-opened)>.el-sub-menu__title {
|
||||
color: #409eff !important;
|
||||
}
|
||||
|
||||
// 展开的子菜单保持默认样式 - 暗黑模式
|
||||
.el-sub-menu.is-active.is-opened>.el-sub-menu__title {
|
||||
background-color: unset !important;
|
||||
color: #b2b2b2 !important;
|
||||
}
|
||||
|
||||
// 水平菜单、横向菜单折叠背景色 - 暗黑模式
|
||||
.el-popper.is-pure.is-light {
|
||||
|
||||
// 水平菜单
|
||||
.el-menu--vertical {
|
||||
background: #1f1f1f; // 暗黑模式下的背景色
|
||||
}
|
||||
}
|
||||
|
||||
// 横向菜单(经典、横向)布局 - 暗黑模式
|
||||
.el-menu.el-menu--horizontal {
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
color: #b2b2b2; // 暗黑模式下的字体颜色
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
background-color: #1f1f1f; // 暗黑模式下的菜单背景色
|
||||
}
|
||||
|
||||
// 确保暗黑模式下展开的子菜单项在hover时也使用统一的样式
|
||||
.el-sub-menu.is-opened .el-sub-menu__title:hover {
|
||||
@extend .el-menu-hover-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,129 @@
|
||||
.loading-next {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
|
||||
z-index: 999999;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&.dark {
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #111827 100%);
|
||||
}
|
||||
}
|
||||
.loading-next .loading-next-box {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.loading-next-box {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.loading-next .loading-next-box-warp {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
.loading-next-box-warp {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
transform: rotate(45deg);
|
||||
animation: loading-next-box-warp 2s infinite linear;
|
||||
}
|
||||
.loading-next .loading-next-box-warp .loading-next-box-item {
|
||||
width: 33.333333%;
|
||||
height: 33.333333%;
|
||||
background: var(--el-color-primary);
|
||||
float: left;
|
||||
animation: loading-next-animation 1.2s infinite ease;
|
||||
border-radius: 1px;
|
||||
|
||||
.loading-next-box-item {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
background: #0ea5e9; // cyan-500
|
||||
border-radius: 2px;
|
||||
animation: loading-next-box-item 2s infinite linear;
|
||||
|
||||
&:nth-child(1) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
top: 0;
|
||||
left: 15px;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
&:nth-child(6) {
|
||||
top: 15px;
|
||||
right: 0;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
&:nth-child(7) {
|
||||
bottom: 15px;
|
||||
right: 0;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
&:nth-child(8) {
|
||||
bottom: 0;
|
||||
left: 15px;
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
|
||||
&:nth-child(9) {
|
||||
top: 15px;
|
||||
left: 0;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
}
|
||||
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(7) {
|
||||
animation-delay: 0s;
|
||||
|
||||
@keyframes loading-next-box-warp {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(4),
|
||||
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(8) {
|
||||
animation-delay: 0.1s;
|
||||
|
||||
@keyframes loading-next-box-item {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(1),
|
||||
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(5),
|
||||
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(9) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(2),
|
||||
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(6) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
@keyframes loading-next-animation {
|
||||
0%,
|
||||
70%,
|
||||
100% {
|
||||
transform: scale3D(1, 1, 1);
|
||||
}
|
||||
35% {
|
||||
transform: scale3D(0, 0, 1);
|
||||
}
|
||||
|
||||
// 暗黑模式样式
|
||||
.dark {
|
||||
.loading-next-box-item {
|
||||
background: #06b6d4; // cyan-400 (暗色模式下更亮一些)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
4
frontend/src/types/pinia.d.ts
vendored
4
frontend/src/types/pinia.d.ts
vendored
@@ -25,8 +25,6 @@ declare interface ThemeConfigState {
|
||||
isFixedHeader: boolean;
|
||||
isFixedHeaderChange: boolean;
|
||||
isClassicSplitMenu: boolean;
|
||||
isLockScreen: boolean;
|
||||
lockScreenTime: number;
|
||||
isShowLogo: boolean;
|
||||
isShowLogoChange: boolean;
|
||||
isBreadcrumb: boolean;
|
||||
@@ -49,6 +47,7 @@ declare interface ThemeConfigState {
|
||||
isRequestRoutes: boolean;
|
||||
globalTitle: string;
|
||||
globalViceTitle: string;
|
||||
appSlogan: string;
|
||||
logoIcon: string;
|
||||
globalI18n: string;
|
||||
globalComponentSize: string;
|
||||
@@ -98,6 +97,7 @@ declare interface TagsView {
|
||||
// TagsView 路由列表
|
||||
declare interface TagsViewsState<> {
|
||||
tagsViews: TagsView[];
|
||||
currentRefreshPath: string; // 当前刷新的路由 path
|
||||
}
|
||||
|
||||
// 路由列表
|
||||
|
||||
@@ -43,7 +43,7 @@ import { procdefApi, procinstApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import ProcdefEdit from './ProcdefEdit.vue';
|
||||
import { ProcdefStatus } from './enums';
|
||||
import TagCodePath from '../ops/component/TagCodePath.vue';
|
||||
|
||||
@@ -46,7 +46,7 @@ import { ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { procinstApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import ProcinstDetail from './ProcinstDetail.vue';
|
||||
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
||||
import { formatTime } from '@/common/utils/format';
|
||||
|
||||
@@ -63,7 +63,7 @@ import { ref, toRefs, reactive, Ref, useTemplateRef } from 'vue';
|
||||
import { procinstTaskApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import ProcinstDetail from './ProcinstDetail.vue';
|
||||
import { FlowBizType, ProcinstStatus, ProcinstTaskStatus } from './enums';
|
||||
import { formatTime } from '@/common/utils/format';
|
||||
|
||||
@@ -504,4 +504,11 @@ defineExpose({
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
// 修复表单label与输入框不对齐的问题
|
||||
:deep(.el-form-item .el-form-item__label) {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,79 +1,101 @@
|
||||
<template>
|
||||
<div class="login-container flex">
|
||||
<div class="login-left">
|
||||
<div class="login-left-logo">
|
||||
<img :src="themeConfig.logoIcon" />
|
||||
<div class="login-left-logo-text">
|
||||
<span>{{ themeConfig.globalViceTitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-left-img">
|
||||
<img :src="loginBgImg" />
|
||||
</div>
|
||||
<img :src="loginBgSplitImg" class="login-left-waves" />
|
||||
</div>
|
||||
<div class="flex min-h-screen bg-gradient-to-br from-blue-50 to-cyan-100 dark:from-gray-900 dark:to-gray-950">
|
||||
<div class="w-full flex items-center justify-center p-4">
|
||||
<div
|
||||
class="bg-white/90 backdrop-blur-lg border border-white rounded-3xl shadow-2xl w-full max-w-md overflow-hidden dark:bg-gray-800/90 dark:border-gray-700/50 transition-all duration-300 hover:shadow-2xl flex flex-col my-8"
|
||||
>
|
||||
<div class="bg-gradient-to-br from-cyan-500/5 to-blue-600/5 dark:from-cyan-400/5 dark:to-blue-500/5 flex-grow"></div>
|
||||
|
||||
<div class="login-right flex">
|
||||
<div class="login-right-warp flex-margin">
|
||||
<span class="login-right-warp-one"> </span>
|
||||
<span class="login-right-warp-two"></span>
|
||||
|
||||
<div class="login-right-warp-mian">
|
||||
<div class="login-right-warp-main-title">
|
||||
{{ themeConfig.globalViceTitle }}
|
||||
|
||||
<el-dropdown
|
||||
:show-timeout="70"
|
||||
:hide-timeout="50"
|
||||
trigger="click"
|
||||
@command="
|
||||
(lang: string) => {
|
||||
themeConfig.globalI18n = lang;
|
||||
}
|
||||
"
|
||||
>
|
||||
<!-- Logo and Title Section -->
|
||||
<div class="text-center pt-10 pb-6 px-4">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="flex items-center justify-center mb-4 transform transition-transform duration-300 hover:scale-105">
|
||||
<img :src="themeConfig.logoIcon" class="w-16 h-16 drop-shadow-lg mr-3" />
|
||||
<div>
|
||||
<SvgIcon
|
||||
:size="16"
|
||||
:name="EnumValue.getEnumByValue(I18nEnum, themeConfig.globalI18n)?.extra.icon"
|
||||
:title="$t('layout.user.langSwitch')"
|
||||
style="margin-top: 50px; margin-left: 20px"
|
||||
/>
|
||||
<h1
|
||||
class="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-cyan-600 to-blue-600 dark:from-cyan-400 dark:to-blue-400"
|
||||
>
|
||||
{{ themeConfig.globalViceTitle }}
|
||||
</h1>
|
||||
<p v-if="themeConfig.appSlogan" class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ $t(themeConfig.appSlogan) }}</p>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item in I18nEnum"
|
||||
:key="item.value"
|
||||
:command="item.value"
|
||||
:disabled="themeConfig.globalI18n === item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Language Switch -->
|
||||
<div class="absolute top-4 right-4 z-20">
|
||||
<el-dropdown
|
||||
:show-timeout="70"
|
||||
:hide-timeout="50"
|
||||
trigger="click"
|
||||
@command="
|
||||
(lang: string) => {
|
||||
themeConfig.globalI18n = lang;
|
||||
}
|
||||
"
|
||||
>
|
||||
<div class="cursor-pointer p-2 rounded-full hover:bg-white/30 dark:hover:bg-gray-700/50 transition-colors">
|
||||
<SvgIcon
|
||||
:size="18"
|
||||
:name="EnumValue.getEnumByValue(I18nEnum, themeConfig.globalI18n)?.extra.icon"
|
||||
:title="$t('layout.user.langSwitch')"
|
||||
class="text-gray-500 hover:text-cyan-600 transition-colors dark:text-gray-400 dark:hover:text-cyan-400"
|
||||
/>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item in I18nEnum"
|
||||
:key="item.value"
|
||||
:command="item.value"
|
||||
:disabled="themeConfig.globalI18n === item.value"
|
||||
class="flex items-center"
|
||||
>
|
||||
<span class="mr-2">{{ item.extra.flag }}</span>
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- Login Form Section -->
|
||||
<div class="px-8 pb-8 flex-grow">
|
||||
<div v-if="!state.isScan">
|
||||
<el-tabs v-model="state.tabsActiveName" class="custom-tabs">
|
||||
<el-tab-pane :label="$t('login.accountPasswordLogin')" name="account">
|
||||
<Account ref="loginForm" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="login-right-warp-main-form">
|
||||
<div v-if="!state.isScan">
|
||||
<el-tabs v-model="state.tabsActiveName">
|
||||
<el-tab-pane :label="$t('login.accountPasswordLogin')" name="account">
|
||||
<Account ref="loginForm" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="!mt-4" v-if="state.oauth2LoginConfig.enable">
|
||||
<el-text size="small">{{ $t('login.thirdPartyLogin') }}: </el-text>
|
||||
<el-tooltip :content="state.oauth2LoginConfig.name" placement="bottom-start">
|
||||
<el-button link size="small" type="primary" @click="oauth2Login">
|
||||
<el-icon :size="18">
|
||||
<Link />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<!-- Third Party Login Divider -->
|
||||
<div class="mt-8 flex items-center" v-if="state.oauth2LoginConfig.enable">
|
||||
<div class="flex-1 border-t border-gray-200 dark:border-gray-600"></div>
|
||||
<span class="px-4 text-sm text-gray-500 bg-white dark:bg-gray-800 dark:text-gray-400">{{ $t('login.thirdPartyLogin') }}</span>
|
||||
<div class="flex-1 border-t border-gray-200 dark:border-gray-600"></div>
|
||||
</div>
|
||||
|
||||
<!-- OAuth2 Login Button -->
|
||||
<div class="mt-6 text-center" v-if="state.oauth2LoginConfig.enable">
|
||||
<el-tooltip :content="state.oauth2LoginConfig.name" placement="bottom">
|
||||
<el-button
|
||||
circle
|
||||
type="primary"
|
||||
@click="oauth2Login"
|
||||
class="shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105 border-0"
|
||||
size="large"
|
||||
>
|
||||
<SvgIcon name="link" :size="20" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-center pb-6 text-xs text-gray-500 dark:text-gray-400">
|
||||
© {{ new Date().getFullYear() }} {{ themeConfig.globalViceTitle }}. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,14 +105,13 @@
|
||||
<script setup lang="ts" name="loginIndex">
|
||||
import { ref, defineAsyncComponent, onMounted, reactive } from 'vue';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import loginBgImg from '@/assets/image/login-bg-main.svg';
|
||||
import loginBgSplitImg from '@/assets/image/login-bg-split.svg';
|
||||
import openApi from '@/common/openApi';
|
||||
import config from '@/common/config';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import EnumValue from '../../common/Enum';
|
||||
import { I18nEnum } from '../../common/commonEnum';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { I18nEnum } from '@/common/commonEnum';
|
||||
import { NextLoading } from '@/common/utils/loading';
|
||||
|
||||
// 引入组件
|
||||
const Account = defineAsyncComponent(() => import('./component/AccountLogin.vue'));
|
||||
@@ -112,9 +133,19 @@ const state = reactive({
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
storesThemeConfig.setWatermarkUser(true);
|
||||
locale.value = themeConfig.value.globalI18n;
|
||||
state.oauth2LoginConfig = await openApi.oauth2LoginConfig();
|
||||
try {
|
||||
if (themeConfig.value.isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
NextLoading.start();
|
||||
storesThemeConfig.setWatermarkUser(true);
|
||||
locale.value = themeConfig.value.globalI18n;
|
||||
state.oauth2LoginConfig = await openApi.oauth2LoginConfig();
|
||||
} finally {
|
||||
NextLoading.done();
|
||||
}
|
||||
});
|
||||
|
||||
const oauth2Login = () => {
|
||||
@@ -126,208 +157,107 @@ const oauth2Login = () => {
|
||||
let oauthWindow = window.open(config.baseApiUrl + '/auth/oauth2/login', 'oauth2', `height=${height},width=${width},top=${iTop},left=${iLeft},location=no`);
|
||||
if (oauthWindow) {
|
||||
const handler = (e: any) => {
|
||||
window.removeEventListener('message', handler);
|
||||
if (e.data.action === 'oauthLogin') {
|
||||
window.removeEventListener('message', handler);
|
||||
loginForm.value!.loginResDeal(e.data);
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', handler);
|
||||
setInterval(() => {
|
||||
setTimeout(() => {
|
||||
if (oauthWindow!.closed) {
|
||||
window.removeEventListener('message', handler);
|
||||
}
|
||||
}, 1000);
|
||||
}, 10000);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-container {
|
||||
height: 100%;
|
||||
background: var(--bg-main-color);
|
||||
<style scoped>
|
||||
.custom-tabs :deep(.el-tabs__nav-wrap)::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-left {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background-color: rgba(211, 239, 255, 1);
|
||||
margin-right: 100px;
|
||||
.custom-tabs :deep(.el-tabs__header) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-left-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 80px;
|
||||
z-index: 1;
|
||||
animation: logoAnimation 0.3s ease;
|
||||
.custom-tabs :deep(.el-tabs__item) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
padding: 0 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
.custom-tabs :deep(.el-tabs__item.is-active) {
|
||||
color: #0ea5e9;
|
||||
}
|
||||
|
||||
.login-left-logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.custom-tabs :deep(.el-tabs__active-bar) {
|
||||
background-color: #0ea5e9;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 10px;
|
||||
font-size: 28px;
|
||||
color: #26a59a;
|
||||
}
|
||||
.dark .custom-tabs :deep(.el-tabs__item) {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.login-left-logo-text-msg {
|
||||
font-size: 12px;
|
||||
color: #32a99e;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dark .custom-tabs :deep(.el-tabs__item.is-active) {
|
||||
color: #0ea5e9;
|
||||
}
|
||||
|
||||
.login-left-img {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
height: 52%;
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: error-num 0.6s ease;
|
||||
}
|
||||
}
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 0 1px #e5e7eb inset !important;
|
||||
transition: all 0.3s ease;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.login-left-waves {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -100px;
|
||||
}
|
||||
}
|
||||
:deep(.el-input__wrapper.is-focus) {
|
||||
box-shadow: 0 0 0 1px #0ea5e9 inset !important;
|
||||
}
|
||||
|
||||
.login-right {
|
||||
width: 700px;
|
||||
.dark :deep(.el-input__wrapper) {
|
||||
box-shadow: 0 0 0 1px #374151 inset !important;
|
||||
}
|
||||
|
||||
.login-right-warp {
|
||||
border: 1px solid var(--el-color-primary-light-3);
|
||||
border-radius: 3px;
|
||||
width: 500px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-main-color);
|
||||
.dark :deep(.el-input__wrapper.is-focus) {
|
||||
box-shadow: 0 0 0 1px #0ea5e9 inset !important;
|
||||
}
|
||||
|
||||
.login-right-warp-one,
|
||||
.login-right-warp-two {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
/* 默认蓝色渐变按钮 */
|
||||
:deep(.el-button--primary) {
|
||||
border-radius: 12px;
|
||||
height: 42px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
background: linear-gradient(135deg, #0ea5e9, #0284c7);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
:deep(.el-button--primary:hover) {
|
||||
background: linear-gradient(135deg, #0284c7, #0ea5e9);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(2, 132, 199, 0.3);
|
||||
}
|
||||
|
||||
.login-right-warp-one {
|
||||
&::before {
|
||||
filter: hue-rotate(0deg);
|
||||
top: 0px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
}
|
||||
/* 高级阴影效果 */
|
||||
.shadow-2xl {
|
||||
box-shadow:
|
||||
0 4px 20px rgba(0, 0, 0, 0.05),
|
||||
0 8px 32px rgba(0, 0, 0, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
&::after {
|
||||
filter: hue-rotate(60deg);
|
||||
top: -100%;
|
||||
right: 2px;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.login-right-warp-two {
|
||||
&::before {
|
||||
filter: hue-rotate(120deg);
|
||||
bottom: 2px;
|
||||
right: -100%;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
filter: hue-rotate(300deg);
|
||||
bottom: -100%;
|
||||
left: 0px;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.login-right-warp-mian {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.login-right-warp-main-title {
|
||||
height: 110px;
|
||||
line-height: 110px;
|
||||
font-size: 27px;
|
||||
text-align: center;
|
||||
letter-spacing: 3px;
|
||||
animation: logoAnimation 0.3s ease;
|
||||
animation-delay: 0.3s;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.login-right-warp-main-form {
|
||||
flex: 1;
|
||||
padding: 0 50px 50px;
|
||||
|
||||
.login-content-main-sacn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all ease 0.3s;
|
||||
color: var(--el-color-primary);
|
||||
|
||||
&-delta {
|
||||
position: absolute;
|
||||
width: 35px;
|
||||
height: 70px;
|
||||
z-index: 2;
|
||||
top: 2px;
|
||||
right: 21px;
|
||||
background: var(--el-color-white);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transition: all ease 0.3s;
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
i {
|
||||
width: 47px;
|
||||
height: 50px;
|
||||
display: inline-block;
|
||||
font-size: 48px;
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.dark .shadow-2xl {
|
||||
box-shadow:
|
||||
0 4px 20px rgba(0, 0, 0, 0.2),
|
||||
0 8px 32px rgba(0, 0, 0, 0.25),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,7 +30,7 @@ import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
import { channelApi } from '../api';
|
||||
import { ChannelStatusEnum, ChannelTypeEnum } from '../enums';
|
||||
|
||||
@@ -61,7 +61,7 @@ import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
import { tmplApi } from '../api';
|
||||
import { TmplStatusEnum, TmplTypeEnum, ChannelTypeEnum } from '../enums';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { onMounted, toRaw, unref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import openApi from '@/common/openApi';
|
||||
@@ -30,12 +30,13 @@ onMounted(async () => {
|
||||
|
||||
const res: any = await openApi.oauth2Callback(queryParam);
|
||||
ElMessage.success(t('system.oauth.authSuccess'));
|
||||
top?.opener.postMessage(res);
|
||||
top?.opener.postMessage(toRaw(res), '*');
|
||||
window.close();
|
||||
} catch (e: any) {
|
||||
console.error('oauth2 callback handle error: ', e);
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 1500);
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OptionsApi, SearchItem } from '@/components/SearchForm';
|
||||
import { OptionsApi, SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { tagApi } from '../tag/api';
|
||||
|
||||
@@ -37,7 +37,7 @@ import { toRefs, reactive, Ref, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
@@ -50,7 +50,7 @@ import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
||||
|
||||
@@ -64,7 +64,7 @@ import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
||||
|
||||
@@ -45,7 +45,7 @@ import { dbApi } from './api';
|
||||
import { DbSqlExecTypeEnum, DbSqlExecStatusEnum } from './enums';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -88,7 +88,7 @@ import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import { DbTransferRunningStateEnum } from './enums';
|
||||
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||
|
||||
@@ -83,7 +83,7 @@ import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { getDbDialect } from './dialect';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
|
||||
import ResourceTags from '../component/ResourceTags.vue';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
|
||||
@@ -54,7 +54,7 @@ import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from './enums';
|
||||
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
|
||||
|
||||
74
frontend/src/views/ops/db/component/DbDetail.vue
Normal file
74
frontend/src/views/ops/db/component/DbDetail.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<el-popover @before-enter="getDetail" width="500" :title="$t('common.detail')" trigger="click">
|
||||
<template #reference>
|
||||
<el-link type="primary" :style="{ fontSize: props.size == 'small' ? '12px' : '14px', verticalAlign: 'baseline' }">
|
||||
<slot></slot>
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
<el-descriptions v-loading="state.loading" :size="props.size" :column="3" border>
|
||||
<el-descriptions-item :span="1" label="ID">{{ state.detail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.code')">{{ state.detail.code }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.name')">{{ state.detail.name }}</el-descriptions-item>
|
||||
|
||||
<!-- <el-descriptions-item :span="3" :label="$t('tag.relateTag')"><ResourceTags :tags="state.detail.tags" /></el-descriptions-item> -->
|
||||
|
||||
<el-descriptions-item :span="3" label="Host">
|
||||
<SvgIcon :name="getDbDialect(state.detail.type).getInfo().icon" :size="20" />
|
||||
{{ state.detail.host }}:{{ state.detail.port }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('db.acName')">{{ state.detail.authCertName }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ state.detail.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" :label="$t('common.createTime')">{{ formatDate(state.detail.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.creator')">{{ state.detail.creator }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" :label="$t('common.updateTime')">{{ formatDate(state.detail.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.modifier')">{{ state.detail.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
import { dbApi } from '../api';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { getDbDialect } from '../dialect/index';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
requierd: true,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
detail: {} as any,
|
||||
});
|
||||
|
||||
const getDetail = async () => {
|
||||
try {
|
||||
state.detail = {};
|
||||
state.loading = true;
|
||||
const res = await dbApi.dbs.request({
|
||||
id: props.id,
|
||||
});
|
||||
if (res.total == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.detail = res.list?.[0];
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -301,13 +301,13 @@ const getKey = () => {
|
||||
return props.dbId + ':' + props.dbName;
|
||||
};
|
||||
|
||||
/**
|
||||
/*
|
||||
* 执行sql
|
||||
*/
|
||||
const onRunSql = async (newTab = false) => {
|
||||
// 没有选中的文本,则为全部文本
|
||||
let sql = getSql() as string;
|
||||
notBlank(sql && sql.trim(), t('db.noSelctRunSqlMsg'));
|
||||
notBlank(sql && sql.trim(), t('db.noSelctRunSqlTips'));
|
||||
// 去除字符串前的空格、换行等
|
||||
sql = sql.replace(/(^\s*)/g, '');
|
||||
|
||||
@@ -315,17 +315,8 @@ const onRunSql = async (newTab = false) => {
|
||||
|
||||
if (sqls.length == 1) {
|
||||
const oneSql = sqls[0];
|
||||
// 简单截取前十个字符
|
||||
const sqlPrefix = oneSql.slice(0, 10).toLowerCase();
|
||||
const nonQuery =
|
||||
sqlPrefix.startsWith('update') ||
|
||||
sqlPrefix.startsWith('insert') ||
|
||||
sqlPrefix.startsWith('delete') ||
|
||||
sqlPrefix.startsWith('alter') ||
|
||||
sqlPrefix.startsWith('drop') ||
|
||||
sqlPrefix.startsWith('create');
|
||||
let execRemark;
|
||||
if (nonQuery) {
|
||||
if (!getNowDbInst().isQuerySql(oneSql)) {
|
||||
const res: any = await ElMessageBox.prompt(t('db.enterExecRemarkTips'), 'Tip', {
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
@@ -334,17 +325,115 @@ const onRunSql = async (newTab = false) => {
|
||||
execRemark = res.value;
|
||||
}
|
||||
runSql(oneSql, execRemark, newTab);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理多条SQL - 合并相同类型的结果
|
||||
await runMultipleSqls(sqls, newTab);
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行多条SQL并合并结果
|
||||
*/
|
||||
const runMultipleSqls = async (sqls: string[], newTab: boolean) => {
|
||||
// 分类SQL语句
|
||||
const nonQuerySqls: string[] = []; // 影响行数类SQL (UPDATE, INSERT, DELETE等)
|
||||
const querySqls: string[] = []; // 查询类SQL (SELECT等)
|
||||
|
||||
const dbInst = getNowDbInst();
|
||||
// 分类SQL
|
||||
sqls.forEach((sql) => {
|
||||
if (!dbInst.isQuerySql(sql)) {
|
||||
nonQuerySqls.push(sql);
|
||||
} else {
|
||||
querySqls.push(sql);
|
||||
}
|
||||
});
|
||||
|
||||
// 先执行非查询类SQL(可以合并结果)
|
||||
if (nonQuerySqls.length > 0) {
|
||||
await runNonQuerySqls(nonQuerySqls, newTab);
|
||||
newTab = true; // 后续查询需要新标签页
|
||||
}
|
||||
|
||||
// 再执行查询类SQL(每条需要独立标签页)
|
||||
for (let i = 0; i < querySqls.length; i++) {
|
||||
const sql = querySqls[i];
|
||||
await runSql(sql, '', newTab || i > 0);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行非查询类SQL并合并结果
|
||||
*/
|
||||
const runNonQuerySqls = async (sqls: string[], newTab: boolean) => {
|
||||
let execRes: ExecResTab;
|
||||
let i = 0;
|
||||
let id;
|
||||
|
||||
// 获取或创建结果标签页
|
||||
if (newTab || state.execResTabs.length == 0) {
|
||||
id = state.execResTabs.length == 0 ? 1 : state.execResTabs[state.execResTabs.length - 1].id + 1;
|
||||
execRes = new ExecResTab(id);
|
||||
state.execResTabs.push(execRes);
|
||||
i = state.execResTabs.length - 1;
|
||||
} else {
|
||||
let isFirst = true;
|
||||
for (let s of sqls) {
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
runSql(s, '', newTab);
|
||||
} else {
|
||||
runSql(s, '', true);
|
||||
i = state.execResTabs.findIndex((x) => x.id == state.activeTab);
|
||||
execRes = state.execResTabs[i];
|
||||
if (unref(execRes.loading)) {
|
||||
ElMessage.error(t('db.currentSqlTabIsRunning'));
|
||||
return;
|
||||
}
|
||||
id = execRes.id;
|
||||
}
|
||||
|
||||
state.activeTab = id;
|
||||
const startTime = new Date().getTime();
|
||||
|
||||
try {
|
||||
execRes.errorMsg = '';
|
||||
execRes.sql = sqls.join('\n\n---\n\n'); // 显示所有SQL
|
||||
|
||||
// 执行所有非查询SQL
|
||||
const results: any[] = [];
|
||||
for (const sql of sqls) {
|
||||
try {
|
||||
const { data, execute } = getNowDbInst().execSql(props.dbName, sql, '');
|
||||
await execute();
|
||||
const result: any = (data.value as any)[0];
|
||||
results.push({
|
||||
sql: result.sql,
|
||||
rowsAffected: result.res?.[0]?.rowsAffected,
|
||||
error: result.errorMsg || '-',
|
||||
});
|
||||
} catch (error: any) {
|
||||
results.push({
|
||||
sql: sql,
|
||||
error: error.message || error.msg,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表格列
|
||||
state.execResTabs[i].tableColumn = [
|
||||
{ columnName: 'sql', columnType: 'string', show: true },
|
||||
{ columnName: 'rowsAffected', columnType: 'number', show: true },
|
||||
{ columnName: 'error', columnType: 'string', show: true },
|
||||
];
|
||||
|
||||
state.execResTabs[i].data = results;
|
||||
cancelUpdateFields(execRes);
|
||||
} catch (e: any) {
|
||||
execRes.data = [];
|
||||
execRes.tableColumn = [];
|
||||
execRes.table = '';
|
||||
state.execResTabs[i].errorMsg = e.message || e.msg || 'Execution failed';
|
||||
return;
|
||||
} finally {
|
||||
execRes.execTime = new Date().getTime() - startTime;
|
||||
}
|
||||
|
||||
execRes.table = '';
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -381,6 +381,24 @@ export class DbInst {
|
||||
return this.getDialect().quoteIdentifier(name);
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断sql是否为查询类sql
|
||||
* @param sql sql
|
||||
* @returns
|
||||
*/
|
||||
isQuerySql(sql: string) {
|
||||
// 简单截取前十个字符
|
||||
const sqlPrefix = sql.slice(0, 10).toLowerCase();
|
||||
const nonQuery =
|
||||
sqlPrefix.startsWith('update') ||
|
||||
sqlPrefix.startsWith('insert') ||
|
||||
sqlPrefix.startsWith('delete') ||
|
||||
sqlPrefix.startsWith('alter') ||
|
||||
sqlPrefix.startsWith('drop') ||
|
||||
sqlPrefix.startsWith('create');
|
||||
return !nonQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或新建dbInst,如果缓存中不存在则新建,否则直接返回
|
||||
* @param inst 数据库实例,后端返回的列表接口中的信息
|
||||
|
||||
@@ -69,7 +69,7 @@ import { formatDate } from '@/common/utils/format';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
|
||||
import ResourceTags from '../component/ResourceTags.vue';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
|
||||
@@ -270,7 +270,7 @@ import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { formatByteSize, formatDate } from '@/common/utils/format';
|
||||
import { TagResourceTypePath } from '@/common/commonEnum';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
|
||||
|
||||
@@ -96,10 +96,10 @@ import ScriptEdit from './ScriptEdit.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { DynamicFormDialog } from '@/components/dynamic-form';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
import { OptionsApi } from '@/components/SearchForm/index';
|
||||
import { OptionsApi } from '@/components/pagetable/SearchForm/index';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-popover placement="right" width="auto" :title="$t('common.detail')" trigger="click">
|
||||
<template #reference>
|
||||
<el-link @click="getMachineDetail" type="primary">{{ props.code }}</el-link>
|
||||
</template>
|
||||
<el-popover @before-enter="getMachineDetail" width="500" :title="$t('common.detail')" trigger="click">
|
||||
<template #reference>
|
||||
<el-link type="primary" :style="{ fontSize: props.size == 'small' ? '12px' : '14px', verticalAlign: 'baseline' }">
|
||||
<slot>{{ props.code }}</slot>
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
<el-descriptions v-loading="state.loading" :column="3" border>
|
||||
<el-descriptions-item :span="1" label="ID">{{ state.machineDetail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.code')">{{ state.machineDetail.code }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.name')">{{ state.machineDetail.name }}</el-descriptions-item>
|
||||
<el-descriptions v-loading="state.loading" :size="props.size" :column="3" border>
|
||||
<el-descriptions-item :span="1" label="ID">{{ state.machineDetail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.code')">{{ state.machineDetail.code }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.name')">{{ state.machineDetail.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('tag.relateTag')"><ResourceTags :tags="state.machineDetail.tags" /></el-descriptions-item>
|
||||
<el-descriptions-item :span="3" :label="$t('tag.relateTag')"><ResourceTags :tags="state.machineDetail.tags" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="IP">{{ state.machineDetail.ip }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('machine.port')">{{ state.machineDetail.port }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="2" label="IP">{{ state.machineDetail.ip }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('machine.port')">{{ state.machineDetail.port }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ state.machineDetail.remark }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ state.machineDetail.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1.5" :label="$t('machine.sshTunnel')">
|
||||
{{ state.machineDetail.sshTunnelMachineId > 0 ? $t('common.yes') : $t('common.no') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" :label="$t('machine.terminalPlayback')">
|
||||
{{ state.machineDetail.enableRecorder == 1 ? $t('common.yes') : $t('common.no') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" :label="$t('machine.sshTunnel')">
|
||||
{{ state.machineDetail.sshTunnelMachineId > 0 ? $t('common.yes') : $t('common.no') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" :label="$t('machine.terminalPlayback')">
|
||||
{{ state.machineDetail.enableRecorder == 1 ? $t('common.yes') : $t('common.no') }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" :label="$t('common.createTime')">{{ formatDate(state.machineDetail.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.creator')">{{ state.machineDetail.creator }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="2" :label="$t('common.createTime')">{{ formatDate(state.machineDetail.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.creator')">{{ state.machineDetail.creator }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" :label="$t('common.updateTime')">{{ formatDate(state.machineDetail.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.modifier')">{{ state.machineDetail.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-popover>
|
||||
</div>
|
||||
<el-descriptions-item :span="2" :label="$t('common.updateTime')">{{ formatDate(state.machineDetail.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.modifier')">{{ state.machineDetail.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -45,6 +45,10 @@ const props = defineProps({
|
||||
type: [String],
|
||||
requierd: true,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
|
||||
@@ -34,7 +34,7 @@ import { cronJobApi } from '../api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { CronJobExecStatusEnum } from '../enums';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import MachineDetail from '../component/MachineDetail.vue';
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -46,7 +46,7 @@ import { cronJobApi } from '../api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { CronJobStatusEnum, CronJobSaveExecResTypeEnum } from '../enums';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import TagCodePath from '../../component/TagCodePath.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
|
||||
@@ -51,7 +51,7 @@ import { TableColumn } from '@/components/pagetable';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
|
||||
const MongoEdit = defineAsyncComponent(() => import('./MongoEdit.vue'));
|
||||
|
||||
@@ -148,7 +148,7 @@ import { TableColumn } from '@/components/pagetable';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -45,7 +45,7 @@ import { resourceAuthCertApi } from './api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { AuthCertCiphertextTypeEnum, AuthCertTypeEnum } from './enums';
|
||||
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import ResourceAuthCertEdit from '../component/ResourceAuthCertEdit.vue';
|
||||
|
||||
@@ -114,7 +114,7 @@ import { tagApi } from './api';
|
||||
import { notBlank } from '@/common/assert';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import TagTreeCheck from '../component/TagTreeCheck.vue';
|
||||
|
||||
@@ -70,7 +70,7 @@ import { formatDate } from '@/common/utils/format';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
|
||||
const perms = {
|
||||
|
||||
@@ -85,7 +85,7 @@ import { roleApi, accountApi } from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { ResourceTypeEnum, RoleStatusEnum } from '../enums';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { getMenuIcon } from '../resource/index';
|
||||
|
||||
@@ -57,7 +57,7 @@ import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { DynamicForm } from '@/components/dynamic-form';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
>
|
||||
<el-select class="!w-full" @change="onChangeLinkType" v-model="form.meta.linkType">
|
||||
<el-option :key="0" :label="$t('system.menu.no')" :value="0"> </el-option>
|
||||
<el-option :key="1" :label="$t('system.menu.inline')" :value="1"> </el-option>
|
||||
<el-option :key="2" :label="$t('system.menu.externalLink')" :value="2"> </el-option>
|
||||
<el-option :key="1" :label="$t('system.menu.inline')" :value="LinkTypeEnum.Iframes.value"> </el-option>
|
||||
<el-option :key="2" :label="$t('system.menu.externalLink')" :value="LinkTypeEnum.Link.value"> </el-option>
|
||||
</el-select>
|
||||
</FormItemTooltip>
|
||||
</el-col>
|
||||
@@ -85,7 +85,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watchEffect, useTemplateRef } from 'vue';
|
||||
import { toRefs, reactive, watchEffect, useTemplateRef, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { resourceApi } from '../api';
|
||||
import { ResourceTypeEnum } from '../enums';
|
||||
@@ -96,6 +96,7 @@ import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
||||
import { Rules } from '@/common/rule';
|
||||
import { useI18nFormValidate } from '@/hooks/useI18n';
|
||||
import { LinkTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -159,7 +160,6 @@ const state = reactive({
|
||||
routeName: '',
|
||||
icon: '',
|
||||
redirect: '',
|
||||
component: '',
|
||||
isKeepAlive: true,
|
||||
isHide: false,
|
||||
isAffix: false,
|
||||
@@ -174,7 +174,7 @@ const { form, submitForm } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveResouceExec } = resourceApi.save.useApi(submitForm);
|
||||
|
||||
watchEffect(() => {
|
||||
watch(visible, () => {
|
||||
if (!visible.value) {
|
||||
return;
|
||||
}
|
||||
@@ -197,9 +197,7 @@ watchEffect(() => {
|
||||
});
|
||||
|
||||
// 改变外链类型
|
||||
const onChangeLinkType = () => {
|
||||
state.form.meta.component = '';
|
||||
};
|
||||
const onChangeLinkType = (linkType: number) => {};
|
||||
|
||||
const onConfirm = async () => {
|
||||
await useI18nFormValidate(menuFormRef);
|
||||
|
||||
@@ -47,7 +47,7 @@ import { accountApi, roleApi } from '../api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import AccountSelectFormItem from '../account/components/AccountSelectFormItem.vue';
|
||||
import { useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { RoleStatusEnum } from '../enums';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import AccountAllocation from './AccountAllocation.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
|
||||
@@ -14,7 +14,7 @@ import { logApi, accountApi } from '../api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { LogTypeEnum } from '../enums';
|
||||
import { OptionsApi, SearchItem } from '@/components/SearchForm';
|
||||
import { OptionsApi, SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import AccountInfo from '../account/components/AccountInfo.vue';
|
||||
|
||||
const searchItems = [
|
||||
|
||||
@@ -234,7 +234,7 @@ func (d *Db) DumpSql(rc *req.Ctx) {
|
||||
rc.GetWriter().Write([]byte(msg))
|
||||
global.EventBus.Publish(rc.MetaCtx, event.EventTopicMsgTmplSend, &msgdto.MsgTmplSendEvent{
|
||||
TmplChannel: msgdto.MsgTmplDbDumpFail,
|
||||
Params: collx.M{"error": msg},
|
||||
Params: collx.M{"dbId": dbConn.Info.Id, "dbName": dbConn.Info.Name, "error": msg},
|
||||
ReceiverIds: []uint64{la.Id},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ func (d *dbSqlExecAppImpl) ExecReader(ctx context.Context, execReader *dto.SqlRe
|
||||
|
||||
msgEvent := &msgdto.MsgTmplSendEvent{
|
||||
TmplChannel: msgdto.MsgTmplSqlScriptRunSuccess,
|
||||
Params: collx.M{"filename": filename, "db": dbConn.Info.GetLogDesc()},
|
||||
Params: collx.M{"filename": filename, "dbId": dbConn.Info.Id, "dbName": dbConn.Info.Name},
|
||||
}
|
||||
|
||||
progressMsgEvent := &msgdto.MsgTmplSendEvent{
|
||||
|
||||
@@ -2,7 +2,7 @@ package dbi
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"strings"
|
||||
@@ -98,7 +98,10 @@ func GetLocalSql(file, key string) string {
|
||||
}
|
||||
|
||||
bytes, err := metasql.ReadFile(file)
|
||||
biz.ErrIsNilAppendErr(err, "failed to get the contents of the sql meta file: %s")
|
||||
if err != nil {
|
||||
logx.Error("failed to read sql metadata file: %s, err: %v", file, err)
|
||||
return ""
|
||||
}
|
||||
allSql := string(bytes)
|
||||
|
||||
sqls := strings.Split(allSql, "---------------------------------------")
|
||||
|
||||
@@ -103,17 +103,24 @@ func (u *UserTaskNodeBehavior) Execute(ctx *ExecutionCtx) error {
|
||||
|
||||
// 用户账号类型
|
||||
if !strings.Contains(candidate, ":") {
|
||||
params := map[string]any{
|
||||
"creator": procinst.Creator,
|
||||
"procdefName": procinst.ProcdefName,
|
||||
"bizKey": procinst.BizKey,
|
||||
"taskName": flowNode.Name,
|
||||
"procinstRemark": procinst.Remark,
|
||||
}
|
||||
// 发送通知消息
|
||||
global.EventBus.Publish(ctx, event.EventTopicBizMsgTmplSend, msgdto.BizMsgTmplSend{
|
||||
BizType: FlowTaskNotifyBizKey,
|
||||
BizId: procinst.ProcdefId,
|
||||
Params: map[string]any{
|
||||
"creator": procinst.Creator,
|
||||
"procdefName": procinst.ProcdefName,
|
||||
"bizKey": procinst.BizKey,
|
||||
"taskName": flowNode.Name,
|
||||
"procinstRemark": procinst.Remark,
|
||||
},
|
||||
global.EventBus.Publish(context.Background(), event.EventTopicBizMsgTmplSend, &msgdto.BizMsgTmplSend{
|
||||
BizType: FlowTaskNotifyBizKey,
|
||||
BizId: procinst.ProcdefId,
|
||||
Params: params,
|
||||
ReceiverIds: []uint64{cast.ToUint64(candidate)},
|
||||
})
|
||||
|
||||
global.EventBus.Publish(context.Background(), event.EventTopicMsgTmplSend, &msgdto.MsgTmplSendEvent{
|
||||
TmplChannel: msgdto.MsgTmplFlowUserTaskTodo,
|
||||
Params: params,
|
||||
ReceiverIds: []uint64{cast.ToUint64(candidate)},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func (p *procdefAppImpl) SaveProcdef(ctx context.Context, defParam *dto.SaveProc
|
||||
return p.Save(ctx, def)
|
||||
}, func(ctx context.Context) error {
|
||||
// 保存通知消息模板
|
||||
if err := p.msgTmplBizApp.SaveBizTmpl(ctx, msgdto.MsgTmplBizSave{
|
||||
if err := p.msgTmplBizApp.SaveBizTmpl(ctx, &msgdto.MsgTmplBizSave{
|
||||
TmplId: defParam.MsgTmplId,
|
||||
BizType: FlowTaskNotifyBizKey,
|
||||
BizId: def.Id,
|
||||
|
||||
@@ -287,7 +287,7 @@ func (m *MachineFile) UploadFile(rc *req.Ctx) {
|
||||
}
|
||||
if mi != nil {
|
||||
msgEvent.Params["machineName"] = mi.Name
|
||||
msgEvent.Params["machineIp"] = mi.Ip
|
||||
msgEvent.Params["machineCode"] = mi.Code
|
||||
}
|
||||
global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, msgEvent)
|
||||
|
||||
@@ -364,7 +364,7 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
|
||||
"filename": folderName,
|
||||
"path": basePath,
|
||||
"machineName": mi.Name,
|
||||
"machineIp": mi.Ip,
|
||||
"machineCode": mi.Code,
|
||||
},
|
||||
ReceiverIds: []uint64{rc.GetLoginAccount().Id},
|
||||
}
|
||||
|
||||
@@ -79,6 +79,12 @@ var (
|
||||
},
|
||||
Channels: []*entity.MsgChannel{MsgChannelWs},
|
||||
}
|
||||
|
||||
MsgTmplFlowUserTaskTodo = newMsgTmpl(entity.MsgTypeNotify,
|
||||
entity.MsgSubtypeFlowUserTaskTodo,
|
||||
entity.MsgStatusUnRead,
|
||||
imsg.FlowUserTaskTodoMsg,
|
||||
MsgChannelSite)
|
||||
)
|
||||
|
||||
func newMsgTmpl(mtype entity.MsgType, subtype entity.MsgSubtype, status entity.MsgStatus, msgId i18n.MsgId, channels ...*entity.MsgChannel) *MsgTmplChannel {
|
||||
|
||||
@@ -13,7 +13,7 @@ type MsgTmplBiz interface {
|
||||
base.App[*entity.MsgTmplBiz]
|
||||
|
||||
// SaveBizTmpl 保存消息模板关联业务信息
|
||||
SaveBizTmpl(ctx context.Context, bizTmpl dto.MsgTmplBizSave) error
|
||||
SaveBizTmpl(ctx context.Context, bizTmpl *dto.MsgTmplBizSave) error
|
||||
|
||||
// DeleteByBiz 根据业务删除消息模板业务关联
|
||||
DeleteByBiz(ctx context.Context, bizType string, bizId uint64) error
|
||||
@@ -22,7 +22,7 @@ type MsgTmplBiz interface {
|
||||
DeleteByTmplId(ctx context.Context, tmplId uint64) error
|
||||
|
||||
// Send 发送消息
|
||||
Send(ctx context.Context, sendParam dto.BizMsgTmplSend) error
|
||||
Send(ctx context.Context, sendParam *dto.BizMsgTmplSend) error
|
||||
}
|
||||
|
||||
type msgTmplBizAppImpl struct {
|
||||
@@ -33,7 +33,7 @@ type msgTmplBizAppImpl struct {
|
||||
|
||||
var _ (MsgTmplBiz) = (*msgTmplBizAppImpl)(nil)
|
||||
|
||||
func (m *msgTmplBizAppImpl) SaveBizTmpl(ctx context.Context, bizTmpl dto.MsgTmplBizSave) error {
|
||||
func (m *msgTmplBizAppImpl) SaveBizTmpl(ctx context.Context, bizTmpl *dto.MsgTmplBizSave) error {
|
||||
msgTmplId := bizTmpl.TmplId
|
||||
bizId := bizTmpl.BizId
|
||||
bizType := bizTmpl.BizType
|
||||
@@ -83,7 +83,7 @@ func (m *msgTmplBizAppImpl) DeleteByTmplId(ctx context.Context, tmplId uint64) e
|
||||
return m.DeleteByCond(ctx, &entity.MsgTmplBiz{TmplId: tmplId})
|
||||
}
|
||||
|
||||
func (m *msgTmplBizAppImpl) Send(ctx context.Context, sendParam dto.BizMsgTmplSend) error {
|
||||
func (m *msgTmplBizAppImpl) Send(ctx context.Context, sendParam *dto.BizMsgTmplSend) error {
|
||||
// 获取业务关联的消息模板
|
||||
msgTmplBiz := &entity.MsgTmplBiz{
|
||||
BizId: sendParam.BizId,
|
||||
|
||||
@@ -40,6 +40,9 @@ const (
|
||||
MsgSubtypeDbDumpFail MsgSubtype = "db.dump.fail"
|
||||
MsgSubtypeSqlScriptRunFail MsgSubtype = "db.sqlscript.run.fail"
|
||||
MsgSubtypeSqlScriptRunSuccess MsgSubtype = "db.sqlscript.run.success"
|
||||
|
||||
// flow
|
||||
MsgSubtypeFlowUserTaskTodo MsgSubtype = "flow.usertask.todo" // 用户任务待办
|
||||
)
|
||||
|
||||
type MsgStatus int8
|
||||
|
||||
@@ -12,10 +12,12 @@ var En = map[i18n.MsgId]string{
|
||||
|
||||
LoginMsg: "Log in to [{{.ip}}]-[{{.time}}]",
|
||||
|
||||
MachineFileUploadSuccessMsg: "[{{.filename}}] -> {{.machineName}}[{{.machineIp}}:{{.path}}]",
|
||||
MachineFileUploadFailMsg: "[{{.filename}}] -> {{.machineName}}[{{.machineIp}}:{{.path}}]. error: {{.error}}",
|
||||
MachineFileUploadSuccessMsg: "[{{.filename}}] -> <machine-info code={{.machineCode}}>{{.machineName}}</machine-info> [{{.path}}]",
|
||||
MachineFileUploadFailMsg: "[{{.filename}}] -> <machine-info code={{.machineCode}}>{{.machineName}}</machine-info> [{{.path}}]. error: {{.error}}",
|
||||
|
||||
DbDumpFailMsg: "Database dump failed, error: {{.error}}",
|
||||
SqlScriptRunFailMsg: "Script {{.filename}} execution failed on database {{.db}}, error: {{.error}}",
|
||||
SqlScriptRunSuccessMsg: "Script {{.filename}} executed successfully on database {{.db}}, cost {{.cost}}",
|
||||
DbDumpFailMsg: "Database [<db-info id={{.dbId}}>{{.dbName}}</db-info>] dump failed, error: <error-text>{{.error}}</error-text>",
|
||||
SqlScriptRunFailMsg: "Script {{.filename}} execution failed on database [<db-info id={{.dbId}}>{{.dbName}}</db-info>], error: <error-text>{{.error}}</error-text>",
|
||||
SqlScriptRunSuccessMsg: "Script {{.filename}} executed successfully on database [<db-info id={{.dbId}}>{{.dbName}}</db-info>], cost {{.cost}}",
|
||||
|
||||
FlowUserTaskTodoMsg: "Work order [{{.procdefName}}] submitted by [{{.creator}}] is now at [{{.taskName}}] node. Please process it promptly. <a href='#/flow/procinst-tasks'>Handle it >>></a>",
|
||||
}
|
||||
|
||||
@@ -26,4 +26,6 @@ const (
|
||||
DbDumpFailMsg
|
||||
SqlScriptRunFailMsg
|
||||
SqlScriptRunSuccessMsg
|
||||
|
||||
FlowUserTaskTodoMsg
|
||||
)
|
||||
|
||||
@@ -12,10 +12,12 @@ var Zh_CN = map[i18n.MsgId]string{
|
||||
|
||||
LoginMsg: "于[{{.ip}}]-[{{.time}}]登录",
|
||||
|
||||
MachineFileUploadSuccessMsg: "[{{.filename}}] -> {{.machineName}}[{{.machineIp}}:{{.path}}]",
|
||||
MachineFileUploadFailMsg: "[{{.filename}}] -> {{.machineName}}[{{.machineIp}}:{{.path}}]。错误信息:{{.error}}",
|
||||
MachineFileUploadSuccessMsg: "[{{.filename}}] -> <machine-info code={{.machineCode}}>{{.machineName}}</machine-info>【{{.path}}】",
|
||||
MachineFileUploadFailMsg: "[{{.filename}}] -> <machine-info code={{.machineCode}}>{{.machineName}}</machine-info>【{{.path}}】。错误信息:<error-text>{{.error}}</error-text>",
|
||||
|
||||
DbDumpFailMsg: "数据库dump失败,错误信息:{{.error}}",
|
||||
SqlScriptRunFailMsg: "数据库 {{.db}} 的脚本 {{.filename}} 执行失败,错误:{{.error}}",
|
||||
SqlScriptRunSuccessMsg: "数据库 {{.db}} 的脚本 {{.filename}} 执行成功,耗时 {{.cost}}",
|
||||
DbDumpFailMsg: "数据库【<db-info id={{.dbId}}>{{.dbName}}</db-info>】导出失败,错误信息:<error-text>{{.error}}</error-text>",
|
||||
SqlScriptRunFailMsg: "数据库【<db-info id={{.dbId}}>{{.dbName}}</db-info>】的脚本 {{.filename}} 执行失败,错误:<error-text>{{.error}}</error-text>",
|
||||
SqlScriptRunSuccessMsg: "数据库【<db-info id={{.dbId}}>{{.dbName}}</db-info>】的脚本 {{.filename}} 执行成功,耗时 {{.cost}}",
|
||||
|
||||
FlowUserTaskTodoMsg: "【{{.creator}}】提交的流程【{{.procdefName}}】已进入【{{.taskName}}】节点,请及时处理,去处理 <a href='#/flow/procinst-tasks'> >>></a>",
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"mayfly-go/pkg/eventbus"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/ioc"
|
||||
"mayfly-go/pkg/logx"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -29,15 +30,17 @@ func Init() {
|
||||
msgx.RegisterMsgSender(msgx.ChannelTypeSiteMsg, application.GetMsgApp())
|
||||
|
||||
msgTmplBizApp := ioc.Get[application.MsgTmplBiz]("MsgTmplBizApp")
|
||||
|
||||
global.EventBus.SubscribeAsync(event.EventTopicBizMsgTmplSend, "BizMsgTmplSend", func(ctx context.Context, event *eventbus.Event[any]) error {
|
||||
return msgTmplBizApp.Send(ctx, event.Val.(dto.BizMsgTmplSend))
|
||||
return msgTmplBizApp.Send(ctx, event.Val.(*dto.BizMsgTmplSend))
|
||||
}, false)
|
||||
|
||||
msgTmplApp := ioc.Get[application.MsgTmpl]("MsgTmplApp")
|
||||
|
||||
global.EventBus.SubscribeAsync(event.EventTopicMsgTmplSend, "MsgTmplSend", func(ctx context.Context, event *eventbus.Event[any]) error {
|
||||
eventVal := event.Val.(*dto.MsgTmplSendEvent)
|
||||
eventVal, ok := event.Val.(*dto.MsgTmplSendEvent)
|
||||
if !ok {
|
||||
logx.Error("the event value is not of type *dto.MsgTmplSendEvent")
|
||||
return nil
|
||||
}
|
||||
return msgTmplApp.SendMsg(ctx, &dto.MsgTmplSend{
|
||||
Tmpl: eventVal.TmplChannel.Tmpl,
|
||||
Channels: eventVal.TmplChannel.Channels,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type MsgType int8
|
||||
@@ -70,18 +71,18 @@ type MsgSender interface {
|
||||
Send(ctx context.Context, channel *Channel, msg *Msg) error
|
||||
}
|
||||
|
||||
var messageSenders = make(map[ChannelType]MsgSender)
|
||||
var messageSenders sync.Map
|
||||
|
||||
// RegisterMsgSender 注册消息发送器
|
||||
func RegisterMsgSender(channel ChannelType, sender MsgSender) {
|
||||
messageSenders[channel] = sender
|
||||
messageSenders.Store(channel, sender)
|
||||
}
|
||||
|
||||
// GetMsgSender 获取消息发送器
|
||||
func GetMsgSender(channel ChannelType) (MsgSender, error) {
|
||||
sender, ok := messageSenders[channel]
|
||||
sender, ok := messageSenders.Load(channel)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported message channel %s", channel)
|
||||
}
|
||||
return sender, nil
|
||||
return sender.(MsgSender), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user