refactor: 消息模块调整 & 样式优化

This commit is contained in:
meilin.huang
2025-08-02 22:08:56 +08:00
parent 6ad6c69660
commit 7d344c71e1
95 changed files with 1664 additions and 1476 deletions

View File

@@ -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%",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
// https://www.npmjs.com/package/mitt
import mitt, { Emitter } from 'mitt';
// 类型
const emitter: Emitter<any> = mitt<any>();
// 导出
export default emitter;

View 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 || '');
}
};
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
};
// 处理面包屑数据

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (暗色模式下更亮一些)
}
}

View File

@@ -1,2 +1,3 @@
@import "tailwindcss";
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

View File

@@ -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
}
// 路由列表

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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 = '';
};
/**

View File

@@ -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 数据库实例,后端返回的列表接口中的信息

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,4 +26,6 @@ const (
DbDumpFailMsg
SqlScriptRunFailMsg
SqlScriptRunSuccessMsg
FlowUserTaskTodoMsg
)

View File

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

View File

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

View File

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