17 Commits

Author SHA1 Message Date
meilin.huang
a4d3a4627a refactor: 系统水印优化等 2023-10-14 16:00:16 +08:00
meilin.huang
77ae6e3bab refactor: 系统水印重构 2023-10-14 00:38:51 +08:00
meilin.huang
e0f1f40ba0 fix: 缓存使用redis无法set问题修复&admin账号默认有所有菜单 2023-10-12 21:50:55 +08:00
meilin.huang
d300f604f1 review 2023-10-12 12:14:56 +08:00
may-fly
2c2c0ff40b Merge pull request #69 from kanzihuang/feat-progress-notify-pullrequest
feat: 显示 SQL 文件执行进度
2023-10-10 20:52:24 -05:00
kanzihuang
b4ddbbd38f fix: 使用最新版 vitess sqlparser 解析 SQL 语句
解决 xwb1989/sqlparser 不支持 current_timestamp() 的问题
2023-10-10 23:56:01 +08:00
wanli
7544288451 feat: 前端显示 SQL 文件执行进度 2023-10-10 23:28:25 +08:00
meilin.huang
41443dccc0 feat: 支持sqlite存储数据 2023-10-10 23:21:29 +08:00
meilin.huang
22e218fc5f refactor: 系统消息调整 2023-10-10 17:39:46 +08:00
meilin.huang
4da0d1abaa refactor: form表单label统一去除':' 2023-10-09 17:29:52 +08:00
meilin.huang
6563b53436 refactor: 包依赖升级等 2023-10-08 12:14:19 +08:00
meilin.huang
fac71a4794 fix: 前端代理默认端口调整&水印开关不生效 2023-09-27 17:19:58 +08:00
meilin.huang
92dff6fdc3 refactor: review 2023-09-26 17:38:52 +08:00
meilin.huang
a1eca3d691 refactor: 登录页调整 2023-09-23 22:52:05 +08:00
meilin.huang
6681dc1057 refactor: 数据库sql提示优化&机器终端支持全屏 2023-09-20 20:42:23 +08:00
meilin.huang
829a68feaa fix: 数据库多库切换关键字提示错误修复&sql编辑器组件统一 2023-09-19 23:00:32 +08:00
meilin.huang
72677e270d feat: 前端用户信息迁移至localstorage 2023-09-16 17:07:48 +08:00
149 changed files with 2744 additions and 2253 deletions

View File

@@ -13,7 +13,7 @@
<img src="https://img.shields.io/docker/pulls/mayflygo/mayfly-go.svg?label=docker%20pulls&color=fac858" alt="docker pulls"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.20%2B-yellow.svg" alt="golang"/>
<img src="https://img.shields.io/badge/Golang-1.21%2B-yellow.svg" alt="golang"/>
</a>
<a href="https://cn.vuejs.org" target="_blank">
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
@@ -22,7 +22,7 @@
### 介绍
web 版 **linux(终端[终端回放] 文件 脚本 进程)、数据库mysql postgres、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库mysql postgres、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
### 开发语言与主要框架

View File

@@ -74,12 +74,13 @@ function build() {
# fi
if [ "${copyDocScript}" == "1" ] ; then
echo_green "拷贝脚本等资源文件[config.yml.example、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
echo_green "拷贝脚本等资源文件[config.yml.example、mayfly-go.sql、mayfly-go.sqlite、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml.example ${toFolder}
cp ${server_folder}/mayfly-go.sql ${toFolder}
cp ${server_folder}/readme.txt ${toFolder}
cp ${server_folder}/startup.sh ${toFolder}
cp ${server_folder}/shutdown.sh ${toFolder}
cp ${server_folder}/resources/script/startup.sh ${toFolder}
cp ${server_folder}/resources/script/shutdown.sh ${toFolder}
cp ${server_folder}/resources/script/sql/mayfly-go.sql ${toFolder}
cp ${server_folder}/resources/data/mayfly-go.sqlite ${toFolder}
fi
echo_yellow ">>>>>>>>>>>>>>>>>>>${os}-${arch}打包构建完成<<<<<<<<<<<<<<<<<<<<\n"

View File

@@ -2,4 +2,8 @@
ENV = 'development'
# 本地环境接口地址
VITE_API_URL = '/api'
VITE_API_URL = '/api'
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -2,4 +2,8 @@
ENV = 'production'
# 线上环境接口地址
VITE_API_URL = '/api'
VITE_API_URL = '/api'
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -1,76 +1,76 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
plugins: ['vue', '@typescript-eslint'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.vue'],
rules: {
'no-undef': 'off',
},
},
],
rules: {
// http://eslint.cn/docs/rules/
// https://eslint.vuejs.org/rules/
// https://typescript-eslint.io/rules/no-unused-vars/
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-unused-vars': [2],
'vue/custom-event-name-casing': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/html-self-closing': 'off',
'vue/no-multiple-template-root': 'off',
'vue/require-default-prop': 'off',
'vue/no-v-model-argument': 'off',
'vue/no-arrow-functions-in-watch': 'off',
'vue/no-template-key': 'off',
'vue/no-v-html': 'off',
'vue/comment-directive': 'off',
'vue/no-parsing-error': 'off',
'vue/no-deprecated-v-on-native-modifier': 'off',
'vue/multi-word-component-names': 'off',
'no-useless-escape': 'off',
'no-sparse-arrays': 'off',
'no-prototype-builtins': 'off',
'no-constant-condition': 'off',
'no-use-before-define': 'off',
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
'generator-star-spacing': 'off',
'no-unreachable': 'off',
'no-multiple-template-root': 'off',
'no-unused-vars': 'error',
'no-v-model-argument': 'off',
'no-case-declarations': 'off',
'no-console': 'error',
'no-redeclare': 'off',
},
};
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
plugins: ['vue', '@typescript-eslint'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.vue'],
rules: {
'no-undef': 'off',
},
},
],
rules: {
// http://eslint.cn/docs/rules/
// https://eslint.vuejs.org/rules/
// https://typescript-eslint.io/rules/no-unused-vars/
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-unused-vars': [2],
'vue/custom-event-name-casing': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/html-self-closing': 'off',
'vue/no-multiple-template-root': 'off',
'vue/require-default-prop': 'off',
'vue/no-v-model-argument': 'off',
'vue/no-arrow-functions-in-watch': 'off',
'vue/no-template-key': 'off',
'vue/no-v-html': 'off',
'vue/comment-directive': 'off',
'vue/no-parsing-error': 'off',
'vue/no-deprecated-v-on-native-modifier': 'off',
'vue/multi-word-component-names': 'off',
'no-useless-escape': 'off',
'no-sparse-arrays': 'off',
'no-prototype-builtins': 'off',
'no-constant-condition': 'off',
'no-use-before-define': 'off',
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
'generator-star-spacing': 'off',
'no-unreachable': 'off',
'no-multiple-template-root': 'off',
'no-unused-vars': 'error',
'no-v-model-argument': 'off',
'no-case-declarations': 'off',
// 'no-console': 'error',
'no-redeclare': 'off',
},
};

View File

@@ -10,27 +10,27 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"asciinema-player": "^3.5.0",
"asciinema-player": "^3.6.2",
"axios": "^1.5.0",
"countup.js": "^2.7.0",
"cropperjs": "^1.5.11",
"echarts": "^5.4.0",
"element-plus": "^2.3.12",
"element-plus": "^2.4.0",
"jsencrypt": "^3.3.1",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.43.0",
"monaco-editor": "^0.44.0",
"monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.6",
"pinia": "^2.1.7",
"qrcode.vue": "^3.4.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.13.0",
"sortablejs": "^1.15.0",
"sql-formatter": "^12.1.2",
"vue": "^3.3.4",
"vue-clipboard3": "^1.0.1",
"vue-router": "^4.2.4",
"vue-router": "^4.2.5",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0",
@@ -40,20 +40,19 @@
"@types/lodash": "^4.14.178",
"@types/node": "^15.6.0",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.10.6",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/compiler-sfc": "^3.0.11",
"dotenv": "^10.0.0",
"@types/sortablejs": "^1.15.3",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/compiler-sfc": "^3.3.4",
"dotenv": "^16.3.1",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^8.2.0",
"prettier": "^2.3.0",
"sass": "^1.62.0",
"sass-loader": "^13.2.0",
"sass": "^1.69.0",
"typescript": "^5.0.2",
"vite": "^4.4.9",
"vue-eslint-parser": "^9.1.1"
"vite": "^4.4.11",
"vue-eslint-parser": "^9.3.1"
},
"browserslist": [
"> 1%",

View File

@@ -1,20 +1,33 @@
<template>
<router-view v-show="themeConfig.lockScreenTime !== 0" />
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
<div class="h100">
<el-watermark
:zIndex="10000000"
:width="210"
v-if="themeConfig.isWatermark"
:font="{ color: 'rgba(180, 180, 180, 0.5)' }"
:content="themeConfig.watermarkText"
class="h100"
>
<router-view v-show="themeConfig.lockScreenTime !== 0" />
</el-watermark>
<router-view v-if="!themeConfig.isWatermark" v-show="themeConfig.lockScreenTime !== 0" />
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime !== 0" />
</div>
</template>
<script setup lang="ts" name="app">
import { ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { useRoute } from 'vue-router';
// import { useTagsViewRoutes } from '@/store/tagsViewRoutes';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { getLocal } from '@/common/utils/storage';
import LockScreen from '@/views/layout/lockScreen/index.vue';
import Setings from '@/views/layout/navBars/breadcrumb/setings.vue';
import Watermark from '@/common/utils/wartermark';
import LockScreen from '@/layout/lockScreen/index.vue';
import Setings from '@/layout/navBars/breadcrumb/setings.vue';
import mittBus from '@/common/utils/mitt';
import { getThemeConfig } from './common/utils/storage';
import { useWatermark } from '@/common/sysconfig';
const setingsRef = ref();
const route = useRoute();
@@ -27,14 +40,6 @@ const openSetingsDrawer = () => {
setingsRef.value.openDrawer();
};
// 设置初始化,防止刷新时恢复默认
onBeforeMount(() => {
// 设置批量第三方 icon 图标
// setIntroduction.cssCdn();
// // 设置批量第三方 js
// setIntroduction.jsCdn();
});
// 页面加载时
onMounted(() => {
nextTick(() => {
@@ -42,16 +47,61 @@ onMounted(() => {
mittBus.on('openSetingsDrawer', () => {
openSetingsDrawer();
});
// 获取缓存中的布局配置
if (getLocal('themeConfig')) {
themeConfigStores.setThemeConfig({ themeConfig: getLocal('themeConfig') });
const tc = getThemeConfig();
if (tc) {
themeConfigStores.setThemeConfig({ themeConfig: tc });
document.documentElement.style.cssText = getLocal('themeConfigStyle');
themeConfigStores.switchDark(tc.isDark);
}
// 是否开启水印
useWatermark().then((res) => {
themeConfigStores.setWatermarkConfig(res);
});
});
});
// 监听 themeConfig isWartermark配置文件的变化
watch(
() => themeConfig.value.isWatermark,
(val) => {
if (val) {
setTimeout(() => {
setWatermarkContent();
refreshWatermarkTime();
}, 500);
}
}
);
const setWatermarkContent = () => {
themeConfigStores.setWatermarkUser();
themeConfigStores.setWatermarkNowTime();
};
let refreshWatermarkTimeInterval: any = null;
/**
* 刷新水印时间
*/
const refreshWatermarkTime = () => {
if (refreshWatermarkTimeInterval) {
clearInterval(refreshWatermarkTimeInterval);
}
refreshWatermarkTimeInterval = setInterval(() => {
if (themeConfig.value.isWatermark) {
themeConfigStores.setWatermarkNowTime();
} else {
clearInterval(refreshWatermarkTimeInterval);
}
}, 10000);
};
// 页面销毁时,关闭监听布局配置
onUnmounted(() => {
clearInterval(refreshWatermarkTimeInterval);
mittBus.off('openSetingsDrawer', () => {});
});
@@ -60,8 +110,6 @@ watch(
() => route.path,
() => {
nextTick(() => {
// 路由变化更新水印
Watermark.use();
document.title = `${route.meta.title} - ${themeConfig.value.globalTitle}` || themeConfig.value.globalTitle;
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -11,7 +11,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.5.2',
version: 'v1.5.3',
};
export default config;

View File

@@ -1,7 +1,7 @@
import router from '../router';
import Axios from 'axios';
import config from './config';
import { getSession } from './utils/storage';
import { getToken } from './utils/storage';
import { templateResolve } from './utils/string';
import { ElMessage } from 'element-plus';
@@ -29,7 +29,7 @@ enum ResultEnum {
}
const baseUrl: string = config.baseApiUrl;
const baseWsUrl: string = config.baseWsUrl;
// const baseWsUrl: string = config.baseWsUrl;
/**
* 通知错误消息
@@ -43,14 +43,14 @@ function notifyErrorMsg(msg: string) {
// create an axios instance
const service = Axios.create({
baseURL: baseUrl, // url = base url + request url
timeout: 20000, // request timeout
timeout: 60000, // request timeout
});
// request interceptor
service.interceptors.request.use(
(config: any) => {
// do something before request is sent
const token = getSession('token');
const token = getToken();
if (token) {
// 设置token
config.headers['Authorization'] = token;
@@ -143,8 +143,8 @@ function request(method: string, url: string, params: any = null, headers: any =
.request(query)
.then((res) => res)
.catch((e) => {
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可
if (e.msg) {
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可。忽略登录超时或没有权限的提示直接跳转至401页面
if (e.msg && e?.code != ResultEnum.NO_PERMISSION) {
notifyErrorMsg(e.msg);
}
return Promise.reject(e);
@@ -176,7 +176,7 @@ function del(url: string, params: any = null, headers: any = null, options: any
function getApiUrl(url: string) {
// 只是返回api地址而不做请求用在上传组件之类的
return baseUrl + url + '?token=' + getSession('token');
return baseUrl + url + '?token=' + getToken();
}
export default {

View File

@@ -1,43 +1,77 @@
import Config from './config';
import { ElNotification } from 'element-plus';
import { ElNotification, NotificationHandle } from 'element-plus';
import SocketBuilder from './SocketBuilder';
import { getSession } from '@/common/utils/storage';
import { getToken } from '@/common/utils/storage';
import { createVNode, reactive } from "vue";
import { buildProgressProps } from "@/components/progress-notify/progress-notify";
import ProgressNotify from '/src/components/progress-notify/progress-notify.vue';
export default {
/**
* 全局系统消息websocket
*/
sysMsgSocket() {
const token = getSession('token');
const token = getToken();
if (!token) {
return null;
}
const messageTypes = {
0: "error",
1: "success",
2: "info",
}
const notifyMap: Map<Number, any> = new Map()
return SocketBuilder.builder(`${Config.baseWsUrl}/sysmsg?token=${token}`)
.message((event: { data: string }) => {
const message = JSON.parse(event.data);
let mtype: string;
switch (message.type) {
case 0:
mtype = 'error';
break;
case 2:
mtype = 'info';
break;
case 1:
mtype = 'success';
const type = messageTypes[message.type]
switch (message.category) {
case "execSqlFileProgress":
const content = JSON.parse(message.msg)
const id = content.id
let progress = notifyMap.get(id)
if (content.terminated) {
if (progress != undefined) {
progress.notification?.close()
notifyMap.delete(id)
progress = undefined
}
return
}
if (progress == undefined) {
progress = {
props: reactive(buildProgressProps()),
notification: undefined,
}
}
progress.props.progress.sqlFileName = content.sqlFileName
progress.props.progress.executedStatements = content.executedStatements
if (!notifyMap.has(id)) {
const vNodeMessage = createVNode(
ProgressNotify,
progress.props,
null,
)
progress.notification = ElNotification({
duration: 0,
title: message.title,
message: vNodeMessage,
type: type,
showClose: false,
});
notifyMap.set(id, progress)
}
break;
default:
mtype = 'info';
ElNotification({
duration: 0,
title: message.title,
message: message.msg,
type: type,
});
break;
}
if (mtype == undefined) {
return;
}
ElNotification({
duration: 0,
title: message.title,
message: message.msg,
type: mtype as any,
});
})
.open((event: any) => console.log(event))
.build();

View File

@@ -3,7 +3,7 @@ import openApi from './openApi';
// 登录是否使用验证码配置key
const AccountLoginSecurity = 'AccountLoginSecurity';
const UseLoginCaptchaConfigKey = 'UseLoginCaptcha';
const UseWartermarkConfigKey = 'UseWartermark';
const UseWatermarkConfigKey = 'UseWatermark';
/**
* 获取系统配置值
@@ -53,12 +53,21 @@ export async function useLoginCaptcha(): Promise<boolean> {
}
/**
* 是否启用水印
* 是否启用水印信息配置
*
* @returns
*/
export async function useWartermark(): Promise<boolean> {
return await getBoolConfigValue(UseWartermarkConfigKey, true);
export async function useWatermark(): Promise<any> {
const value = await getConfigValue(UseWatermarkConfigKey);
if (!value) {
return {
isUse: true,
};
}
const jsonValue = JSON.parse(value);
// 将字符串转为bool
jsonValue.isUse = convertBool(jsonValue.isUse, true);
return jsonValue;
}
function convertBool(value: string, defaultValue: boolean) {
@@ -77,4 +86,3 @@ export async function getLdapEnabled(): Promise<any> {
const value = await openApi.getLdapEnabled();
return convertBool(value, false);
}

View File

@@ -1,46 +1,42 @@
import { nextTick } from 'vue';
import loadingCss from '@/theme/loading.scss?inline';
import '@/theme/loading.scss';
// 定义方法
/**
* 页面全局 Loading
* @method start 创建 loading
* @method done 移除 loading
*/
export const NextLoading = {
// 载入 css
setCss: () => {
let link = document.createElement('link');
link.rel = 'stylesheet';
link.href = loadingCss;
link.crossOrigin = 'anonymous';
document.getElementsByTagName('head')[0].appendChild(link);
},
// 创建 loading
start: () => {
const bodys: any = document.body;
const div = document.createElement('div');
const bodys: Element = document.body;
const div = <HTMLElement>document.createElement('div');
div.setAttribute('class', 'loading-next');
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 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>
`;
div.innerHTML = htmls;
bodys.insertBefore(div, bodys.childNodes[0]);
},
// 移除 loading
done: () => {
done: (time: number = 1000) => {
nextTick(() => {
setTimeout(() => {
const el = document.querySelector('.loading-next');
el && el.parentNode?.removeChild(el);
}, 1000);
const el = <HTMLElement>document.querySelector('.loading-next');
el?.parentNode?.removeChild(el);
}, time);
});
},
};

View File

@@ -1,17 +1,70 @@
const TokenKey = 'token';
const UserKey = 'user';
const TagViewsKey = 'tagViews';
// 获取请求token
export function getToken(): string {
return getLocal(TokenKey);
}
// 保存用户访问token
export function saveToken(token: string) {
setLocal(TokenKey, token);
}
// 获取登录用户基础信息
export function getUser() {
return getLocal(UserKey);
}
// 保存用户信息
export function saveUser(userinfo: any) {
setLocal(UserKey, userinfo);
}
export function saveThemeConfig(themeConfig: any) {
setLocal('themeConfig', themeConfig);
}
export function getThemeConfig() {
return getLocal('themeConfig');
}
// 清除用户相关的用户信息
export function clearUser() {
removeLocal(TokenKey);
removeLocal(UserKey);
}
export function getTagViews() {
return getSession(TagViewsKey);
}
export function setTagViews(tagViews: Array<object>) {
setSession(TagViewsKey, tagViews);
}
export function removeTagViews() {
removeSession(TagViewsKey);
}
// 1. localStorage
// 设置永久缓存
export function setLocal(key: string, val: any) {
window.localStorage.setItem(key, JSON.stringify(val));
}
// 获取永久缓存
export function getLocal(key: string) {
let json: any = window.localStorage.getItem(key);
return JSON.parse(json);
}
// 移除永久缓存
export function removeLocal(key: string) {
window.localStorage.removeItem(key);
}
// 移除全部永久缓存
export function clearLocal() {
window.localStorage.clear();
@@ -22,33 +75,20 @@ export function clearLocal() {
export function setSession(key: string, val: any) {
window.sessionStorage.setItem(key, JSON.stringify(val));
}
// 获取临时缓存
export function getSession(key: string) {
let json: any = window.sessionStorage.getItem(key);
return JSON.parse(json);
}
// 移除临时缓存
export function removeSession(key: string) {
window.sessionStorage.removeItem(key);
}
// 移除全部临时缓存
export function clearSession() {
clearUser();
window.sessionStorage.clear();
}
export function getUserInfo4Session() {
return getSession('userInfo');
}
export function setUserInfo2Session(userinfo: any) {
setSession('userInfo', userinfo);
}
// 获取是否开启水印
export function getUseWatermark4Session() {
return getSession('useWatermark');
}
export function setUseWatermark2Session(useWatermark: boolean) {
setSession('useWatermark', useWatermark);
}

View File

@@ -0,0 +1,13 @@
const mode = import.meta.env.VITE_ROUTER_MODE;
/**
* @description 获取不同路由模式所对应的 url
* @returns {String}
*/
export function getNowUrl() {
const url = {
hash: location.hash.substring(1),
history: location.pathname + location.search,
};
return url[mode];
}

View File

@@ -1,65 +0,0 @@
import { getUseWatermark4Session, getUserInfo4Session } from '@/common/utils/storage';
import { dateFormat2 } from '@/common/utils/date';
// 页面添加水印效果
const setWatermark = (str: any) => {
const id = '1.23452384164.123412416';
if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id) as any);
const can = document.createElement('canvas');
can.width = 400;
can.height = 250;
const cans: any = can.getContext('2d');
cans.rotate((-20 * Math.PI) / 180);
cans.font = '14px Vedana';
cans.fillStyle = 'rgba(200, 200, 200, 0.35)';
cans.textAlign = 'left';
cans.textBaseline = 'Middle';
// cans.fillText('mayfly go', can.width / 4, can.height )
cans.fillText(str, can.width / 8, can.height / 2);
const div = document.createElement('div');
div.id = id;
div.style.pointerEvents = 'none';
div.style.top = '30px';
div.style.left = '0px';
div.style.position = 'fixed';
div.style.zIndex = '10000000';
div.style.width = document.documentElement.clientWidth + 'px';
div.style.height = document.documentElement.clientHeight + 'px';
div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`;
document.body.appendChild(div);
return id;
};
function set(str: any) {
let id = setWatermark(str);
if (document.getElementById(id) === null) id = setWatermark(str);
}
function del() {
let id = '1.23452384164.123412416';
if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id) as any);
}
const watermark = {
use: () => {
setTimeout(() => {
const userinfo = getUserInfo4Session();
if (userinfo && getUseWatermark4Session()) {
set(`${userinfo.username} ${dateFormat2('yyyy-MM-dd HH:mm:ss', new Date())}`);
} else {
del();
}
}, 1500);
},
// 设置水印
set: (str: any) => {
set(str);
},
// 删除水印
del: () => {
del();
},
};
export default watermark;

View File

@@ -9,7 +9,6 @@
<script lang="ts" setup>
import { ref, watch, toRefs, reactive, onMounted, onBeforeUnmount } from 'vue';
// import * as monaco from 'monaco-editor';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
// 相关语言
import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution.js';
@@ -155,7 +154,22 @@ const options = {
},
};
const monacoTextarea: any = ref();
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
let completionItemProvider: any = null;
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === 'json') {
return new JsonWorker();
}
return new EditorWorker();
},
};
const state = reactive({
editorHeight: '500px',
languageMode: 'shell',
});
@@ -173,6 +187,7 @@ onBeforeUnmount(() => {
monacoEditorIns.dispose();
}
if (completionItemProvider) {
console.log('unmount=> dispose completion item provider');
completionItemProvider.dispose();
}
});
@@ -203,20 +218,6 @@ watch(
}
);
const monacoTextarea: any = ref(null);
let monacoEditorIns: editor.IStandaloneCodeEditor = null as any;
let completionItemProvider: any = null;
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === 'json') {
return new JsonWorker();
}
return new EditorWorker();
},
};
const initMonacoEditorIns = () => {
console.log('初始化monaco编辑器');
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
@@ -260,6 +261,7 @@ const setEditorValue = (value: any) => {
*/
const registerCompletionItemProvider = () => {
if (completionItemProvider) {
console.log('exist competion item provider, dispose now');
completionItemProvider.dispose();
}
if (state.languageMode == 'shell') {
@@ -299,7 +301,11 @@ const format = () => {
monacoEditorIns.trigger('', 'editor.action.formatDocument', '');
};
defineExpose({ format });
const getEditor = () => {
return monacoEditorIns;
};
defineExpose({ getEditor, format });
</script>
<style lang="scss">

View File

@@ -0,0 +1,25 @@
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
/**
* key: language, value: CompletionItemProvider
*/
const completionItemProviders: Map<string, any> = new Map();
export function registerCompletionItemProvider(language: string, completionItemProvider: any, replace: boolean = true) {
const exist = completionItemProviders.get(language);
if (exist) {
if (!replace) {
return;
}
exist.dispose();
}
completionItemProviders.set(language, monaco.languages.registerCompletionItemProvider(language, completionItemProvider));
}
export function dispposeCompletionItemProvider(language: string) {
const exist = completionItemProviders.get(language);
if (exist) {
exist.dispose();
completionItemProviders.delete(language);
}
}

View File

@@ -0,0 +1,12 @@
export const buildProgressProps = (): any => {
return {
progress: {
sqlFileName: {
type: String,
},
executedStatements: {
type: Number,
},
},
};
};

View File

@@ -0,0 +1,34 @@
<template>
<el-descriptions border size="small" :title="`${progress.sqlFileName}`">
<el-descriptions-item label="时间">{{ state.elapsedTime }}</el-descriptions-item>
<el-descriptions-item label="已处理">{{ progress.executedStatements }}</el-descriptions-item>
</el-descriptions>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive } from 'vue';
import { formatTime } from 'element-plus/es/components/countdown/src/utils';
import { buildProgressProps } from './progress-notify';
const props = defineProps(buildProgressProps());
const state = reactive({
elapsedTime: '00:00:00',
});
let timer: any = undefined;
const startTime = Date.now();
onMounted(async () => {
timer = setInterval(() => {
const elapsed = Date.now() - startTime;
state.elapsedTime = formatTime(elapsed, 'HH:mm:ss');
}, 1000);
});
onUnmounted(async () => {
if (timer != undefined) {
clearInterval(timer); // 在Vue实例销毁前清除我们的定时器
timer = undefined;
}
});
</script>

View File

@@ -52,13 +52,13 @@
title="最小化"
/>
<!-- <SvgIcon name="FullScreen" @click="handlerFullScreen(openTerminal)" :size="20" class="pointer-icon mr10" title="全屏|退出全屏" /> -->
<SvgIcon name="FullScreen" @click="handlerFullScreen(openTerminal)" :size="20" class="pointer-icon mr10" title="全屏|退出全屏" />
<SvgIcon name="Close" class="pointer-icon" @click="close(openTerminal.terminalId)" title="关闭" :size="20" />
</div>
</div>
</template>
<div class="terminal-wrapper" style="height: calc(100vh - 215px)">
<div class="terminal-wrapper" :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '47px' : '200px'})` }">
<TerminalBody
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
@@ -230,6 +230,16 @@ function maximize(terminalId: any) {
}, 250);
}
const handlerFullScreen = (terminal: any) => {
terminal.fullscreen = !terminal.fullscreen;
const terminalRef = openTerminalRefs[terminal.terminalId];
// fit
setTimeout(() => {
terminalRef?.fitTerminal();
terminalRef?.focus();
}, 250);
};
const closeMinimizeTerminal = (terminalId: any) => {
delete state.minimizeTerminals[terminalId];
close(terminalId);
@@ -249,9 +259,11 @@ defineExpose({
padding: 10px;
}
// .terminal-dialog {
// height: calc(100vh - 200px) !important;
// }
// 取消body最大高度否则全屏有问题
.el-dialog__body {
max-height: 100% !important;
overflow: hidden !important;
}
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
padding: 0px !important;

View File

@@ -21,8 +21,8 @@ import pinia from '@/store/index';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoutesList } from '@/store/routesList';
import Logo from '@/views/layout/logo/index.vue';
import Vertical from '@/views/layout/navMenu/vertical.vue';
import Logo from '@/layout/logo/index.vue';
import Vertical from '@/layout/navMenu/vertical.vue';
import mittBus from '@/common/utils/mitt';
const { proxy } = getCurrentInstance() as any;
@@ -38,8 +38,7 @@ const state: any = reactive({
// /
const setCollapseWidth = computed(() => {
let { layout, isCollapse, menuBar } = themeConfig.value;
let asideBrColor =
menuBar === '#FFFFFF' || menuBar === '#FFF' || menuBar === '#fff' || menuBar === '#ffffff' ? 'layout-el-aside-br-color' : '';
let asideBrColor = menuBar === '#FFFFFF' || menuBar === '#FFF' || menuBar === '#fff' || menuBar === '#ffffff' ? 'layout-el-aside-br-color' : '';
if (layout === 'columns') {
// 1px
if (isCollapse) {

View File

@@ -6,7 +6,7 @@
<script lang="ts">
import { computed } from 'vue';
import NavBarsIndex from '@/views/layout/navBars/index.vue';
import NavBarsIndex from '@/layout/navBars/index.vue';
import { useThemeConfig } from '@/store/themeConfig';
export default {
name: 'layoutHeader',

View File

@@ -1,16 +1,25 @@
<template>
<el-main class="layout-main">
<el-scrollbar class="layout-scrollbar" ref="layoutScrollbarRef"
<el-scrollbar
class="layout-scrollbar"
ref="layoutScrollbarRef"
v-show="!state.currentRouteMeta.link && state.currentRouteMeta.linkType != 1"
:style="{ minHeight: `calc(100vh - ${state.headerHeight}` }">
:style="{ minHeight: `calc(100vh - ${state.headerHeight}` }"
>
<LayoutParentView />
<Footer v-if="themeConfig.isFooter" />
</el-scrollbar>
<Link :style="{ height: `calc(100vh - ${state.headerHeight}` }" :meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2" />
<Iframes :style="{ height: `calc(100vh - ${state.headerHeight}` }" :meta="state.currentRouteMeta"
<Link
:style="{ height: `calc(100vh - ${state.headerHeight}` }"
:meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 2"
/>
<Iframes
:style="{ height: `calc(100vh - ${state.headerHeight}` }"
:meta="state.currentRouteMeta"
v-if="state.currentRouteMeta.link && state.currentRouteMeta.linkType == 1 && state.isShowLink"
@getCurrentRouteMeta="onGetCurrentRouteMeta" />
@getCurrentRouteMeta="onGetCurrentRouteMeta"
/>
</el-main>
</template>
@@ -19,10 +28,10 @@ import { reactive, getCurrentInstance, watch, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import LayoutParentView from '@/views/layout/routerView/parent.vue';
import Footer from '@/views/layout/footer/index.vue';
import Link from '@/views/layout/routerView/link.vue';
import Iframes from '@/views/layout/routerView/iframes.vue';
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 { proxy } = getCurrentInstance() as any;
const { themeConfig } = storeToRefs(useThemeConfig());

View File

@@ -10,10 +10,10 @@ import { onBeforeMount, onUnmounted } from 'vue';
import { getLocal, setLocal } from '@/common/utils/storage';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import Defaults from '@/views/layout/main/defaults.vue';
import Classic from '@/views/layout/main/classic.vue';
import Transverse from '@/views/layout/main/transverse.vue';
import Columns from '@/views/layout/main/columns.vue';
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());

View File

@@ -213,7 +213,7 @@ onUnmounted(() => {
}
.layout-lock-screen-img {
@extend .layout-lock-screen-fixed;
background: url('@/assets/image/bg-login.png') no-repeat;
background: url('@/assets/image/login-bg-main.svg') no-repeat;
background-size: 100% 100%;
z-index: 9999991;
}

View File

@@ -15,10 +15,10 @@
<script lang="ts" setup name="layoutClassic">
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import Aside from '@/views/layout/component/aside.vue';
import Header from '@/views/layout/component/header.vue';
import Main from '@/views/layout/component/main.vue';
import TagsView from '@/views/layout/navBars/tagsView/tagsView.vue';
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';
const { themeConfig } = storeToRefs(useThemeConfig());
</script>

View File

@@ -17,10 +17,10 @@
<script lang="ts">
import { computed } from 'vue';
import Aside from '@/views/layout/component/aside.vue';
import Header from '@/views/layout/component/header.vue';
import Main from '@/views/layout/component/main.vue';
import ColumnsAside from '@/views/layout/component/columnsAside.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';
export default {
name: 'layoutColumns',

View File

@@ -15,9 +15,9 @@
<script lang="ts">
import { computed, getCurrentInstance, watch } from 'vue';
import { useRoute } from 'vue-router';
import Aside from '@/views/layout/component/aside.vue';
import Header from '@/views/layout/component/header.vue';
import Main from '@/views/layout/component/main.vue';
import Aside from '@/layout/component/aside.vue';
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
import { useThemeConfig } from '@/store/themeConfig';
export default {
name: 'layoutDefaults',

View File

@@ -7,8 +7,8 @@
</template>
<script lang="ts">
import Header from '@/views/layout/component/header.vue';
import Main from '@/views/layout/component/main.vue';
import Header from '@/layout/component/header.vue';
import Main from '@/layout/component/main.vue';
export default {
name: 'layoutTransverse',
components: { Header, Main },

View File

@@ -14,10 +14,10 @@ import pinia from '@/store/index';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoutesList } from '@/store/routesList';
import Breadcrumb from '@/views/layout/navBars/breadcrumb/breadcrumb.vue';
import User from '@/views/layout/navBars/breadcrumb/user.vue';
import Logo from '@/views/layout/logo/index.vue';
import Horizontal from '@/views/layout/navMenu/horizontal.vue';
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());

View File

@@ -374,7 +374,7 @@
</div>
</div>
</div>
<div class="copy-config">
<!-- <div class="copy-config">
<el-alert title="点击下方按钮,复制布局配置去 /src/store/modules/themeConfig.ts中修改" type="warning" :closable="false"> </el-alert>
<el-button
size="small"
@@ -385,7 +385,7 @@
@click="onCopyConfigClick($event.target)"
>一键复制配置
</el-button>
</div>
</div> -->
</el-scrollbar>
</el-drawer>
</div>

View File

@@ -56,7 +56,7 @@
<crop />
</el-icon>
</div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<el-dropdown trigger="click" :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link" style="cursor: pointer">
<img :src="userInfo.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
{{ userInfo.name || userInfo.username }}
@@ -75,7 +75,7 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUser">
import { ref, computed, reactive, onMounted, nextTick } from 'vue';
import { ref, computed, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull';
@@ -83,11 +83,12 @@ import { resetRoute } from '@/router/index';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
import { useThemeConfig } from '@/store/themeConfig';
import { clearSession, setLocal, getLocal, removeLocal } from '@/common/utils/storage';
import UserNews from '@/views/layout/navBars/breadcrumb/userNews.vue';
import SearchMenu from '@/views/layout/navBars/breadcrumb/search.vue';
import { clearSession, removeLocal } 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 { saveThemeConfig, getThemeConfig } from '@/common/utils/storage';
const router = useRouter();
const searchRef = ref();
@@ -99,7 +100,8 @@ const state = reactive({
disabledSize: '',
});
const { userInfo } = storeToRefs(useUserInfo());
const { themeConfig } = storeToRefs(useThemeConfig());
const themeConfigStore = useThemeConfig();
const { themeConfig } = storeToRefs(themeConfigStore);
//
const layoutUserFlexNum = computed(() => {
@@ -164,16 +166,8 @@ const onHandleCommandClick = (path: string) => {
};
const switchDark = (isDark: boolean) => {
themeConfig.value.isDark = isDark;
setLocal('themeConfig', themeConfig.value);
const body = document.documentElement as HTMLElement;
if (isDark) {
body.setAttribute('class', 'dark');
themeConfig.value.editorTheme = 'vs-dark';
} else {
body.setAttribute('class', '');
themeConfig.value.editorTheme = 'SolarizedLight';
}
themeConfigStore.switchDark(isDark);
saveThemeConfig(themeConfig.value);
};
// //
@@ -185,7 +179,7 @@ const onSearchClick = () => {
const onComponentSizeChange = (size: string) => {
removeLocal('themeConfig');
themeConfig.value.globalComponentSize = size;
setLocal('themeConfig', themeConfig.value);
saveThemeConfig(themeConfig.value);
// proxy.$ELEMENT.size = size;
initComponentSize();
window.location.reload();
@@ -193,7 +187,7 @@ const onComponentSizeChange = (size: string) => {
//
const initComponentSize = () => {
switch (getLocal('themeConfig').globalComponentSize) {
switch (getThemeConfig().globalComponentSize) {
case '':
state.disabledSize = '';
break;
@@ -211,12 +205,10 @@ const initComponentSize = () => {
//
onMounted(() => {
if (getLocal('themeConfig')) {
const isDark = themeConfig.value.isDark;
state.isDark = isDark;
switchDark(isDark);
const themeConfig = getThemeConfig();
if (themeConfig) {
initComponentSize();
state.isDark = themeConfig.isDark;
}
});
</script>

View File

@@ -8,8 +8,8 @@
<script lang="ts">
import { computed } from 'vue';
import { useThemeConfig } from '@/store/themeConfig';
import BreadcrumbIndex from '@/views/layout/navBars/breadcrumb/index.vue';
import TagsView from '@/views/layout/navBars/tagsView/tagsView.vue';
import BreadcrumbIndex from '@/layout/navBars/breadcrumb/index.vue';
import TagsView from '@/layout/navBars/tagsView/tagsView.vue';
export default {
name: 'layoutNavBars',
components: { BreadcrumbIndex, TagsView },

View File

@@ -0,0 +1,138 @@
<template>
<transition name="el-zoom-in-center">
<div
aria-hidden="true"
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
role="tooltip"
data-popper-placement="bottom"
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
:key="Math.random()"
v-show="state.isShow"
>
<ul class="el-dropdown-menu">
<template v-for="(v, k) in state.dropdownList">
<li
class="el-dropdown-menu__item"
aria-disabled="false"
tabindex="-1"
:key="k"
v-if="!v.affix"
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
>
<SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span>
</li>
</template>
</ul>
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
</div>
</transition>
</template>
<script setup lang="ts" name="layoutTagsViewContextmenu">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
// 定义父组件传过来的值
const props = defineProps({
dropdown: {
type: Object,
default: () => {
return {
x: 0,
y: 0,
};
},
},
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(['currentContextmenuClick']);
// 定义变量内容
const state = reactive({
isShow: false,
dropdownList: [
{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' },
{ contextMenuClickId: 1, txt: '关闭', affix: false, icon: 'Close' },
{ contextMenuClickId: 2, txt: '关闭其他', affix: false, icon: 'CircleClose' },
{ contextMenuClickId: 3, txt: '关闭所有', affix: false, icon: 'FolderDelete' },
{
contextMenuClickId: 4,
txt: '当前页全屏',
affix: false,
icon: 'full-screen',
},
],
item: {} as any,
arrowLeft: 10,
});
// 父级传过来的坐标 x,y 值
const dropdowns = computed(() => {
// 117 为 `Dropdown 下拉菜单` 的宽度
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
return {
x: document.documentElement.clientWidth - 117 - 5,
y: props.dropdown.y,
};
} else {
return props.dropdown;
}
});
// 当前项菜单点击
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
emit('currentContextmenuClick', { id: contextMenuClickId, path: state.item.path });
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
state.item = item;
item.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
closeContextmenu();
setTimeout(() => {
state.isShow = true;
}, 10);
};
// 关闭右键菜单
const closeContextmenu = () => {
state.isShow = false;
};
// 监听页面监听进行右键菜单的关闭
onMounted(() => {
document.body.addEventListener('click', closeContextmenu);
});
// 页面卸载时,移除右键菜单监听事件
onUnmounted(() => {
document.body.removeEventListener('click', closeContextmenu);
});
// 监听下拉菜单位置
watch(
() => props.dropdown,
({ x }) => {
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
else state.arrowLeft = 10;
},
{
deep: true,
}
);
// 暴露变量
defineExpose({
openContextmenu,
});
</script>
<style scoped lang="scss">
.custom-contextmenu {
transform-origin: center top;
z-index: 2190;
position: fixed;
.el-dropdown-menu__item {
font-size: 12px !important;
white-space: nowrap;
i {
font-size: 12px !important;
}
}
}
</style>

View File

@@ -3,7 +3,7 @@
<el-scrollbar ref="scrollbarRef" @wheel.prevent="onHandleScroll">
<ul class="layout-navbars-tagsview-ul" :class="setTagsStyle" ref="tagsUlRef">
<li
v-for="(v, k) in state.tagsViewList"
v-for="(v, k) in tagsViews"
:key="k"
class="layout-navbars-tagsview-ul-li"
:data-name="v.name"
@@ -17,8 +17,8 @@
"
>
<SvgIcon name="iconfont icon-tag-view-active" class="layout-navbars-tagsview-ul-li-iconfont font14" v-if="isActive(v)" />
<SvgIcon :name="v.meta.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
<span>{{ v.meta.title }}</span>
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && themeConfig.isTagsviewIcon" />
<span>{{ v.title }}</span>
<template v-if="isActive(v)">
<SvgIcon
name="RefreshRight"
@@ -28,7 +28,7 @@
<SvgIcon
name="Close"
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-active"
v-if="!v.meta.isAffix"
v-if="!v.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</template>
@@ -36,7 +36,7 @@
<SvgIcon
name="Close"
class="font14 layout-navbars-tagsview-ul-li-icon layout-icon-three"
v-if="!v.meta.isAffix"
v-if="!v.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</li>
@@ -52,17 +52,24 @@ import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import screenfull from 'screenfull';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { getSession, setSession, removeSession } from '@/common/utils/storage';
import mittBus from '@/common/utils/mitt';
import Sortable from 'sortablejs';
import Contextmenu from '@/views/layout/navBars/tagsView/contextmenu.vue';
import Contextmenu from '@/layout/navBars/tagsView/contextmenu.vue';
import { getTagViews, setTagViews, removeTagViews } from '@/common/utils/storage';
import { useTagsViews } from '@/store/tagsViews';
import { useKeepALiveNames } from '@/store/keepAliveNames';
const { proxy } = getCurrentInstance() as any;
const tagsRefs = ref([]) as any;
const scrollbarRef = ref();
const contextmenuRef = ref();
const tagsUlRef = ref();
const { themeConfig } = storeToRefs(useThemeConfig());
const { tagsViews } = storeToRefs(useTagsViews());
const keepAliveNamesStores = useKeepALiveNames();
const route = useRoute();
const router = useRouter();
@@ -70,7 +77,6 @@ const state = reactive({
routePath: route.fullPath,
dropdown: { x: '', y: '' },
tagsRefsIndex: 0,
tagsViewList: [] as any,
sortable: '' as any,
});
@@ -81,119 +87,153 @@ const setTagsStyle = computed(() => {
// tagsViewList
const addBrowserSetSession = (tagsViewList: Array<object>) => {
setSession('tagsViewList', tagsViewList);
setTagViews(tagsViewList);
};
// vuex tagsViewRoutes
// tagsViewRoutes
const getTagsViewRoutes = () => {
state.routePath = route.fullPath;
state.tagsViewList = [];
if (!themeConfig.value.isCacheTagsView) removeSession('tagsViewList');
tagsViews.value = [];
if (!themeConfig.value.isCacheTagsView) {
removeTagViews();
}
initTagsView();
};
// vuex isAffix
// isAffix
const initTagsView = () => {
if (getSession('tagsViewList') && themeConfig.value.isCacheTagsView) {
state.tagsViewList = getSession('tagsViewList');
const tagViews = getTagViews();
if (tagViews && themeConfig.value.isCacheTagsView) {
tagsViews.value = tagViews;
} else {
state.tagsViewList?.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) state.tagsViewList.push({ ...v });
tagsViews.value?.map((v: any) => {
if (v.isAffix && !v.isHide) {
tagsViews.value.push({ ...v });
keepAliveNamesStores.setCacheKeepAlive(v);
}
});
addTagsView(route.fullPath);
}
// (li)
getTagsRefsIndex(route.fullPath);
setTagsRefsIndex(route.fullPath);
//
tagsViewmoveToCurrentTag();
};
// 1 tagsViewisHide tagsView
// pathfullPath
const addTagsView = (path: string, to: any = null) => {
if (!to) {
to = route;
}
path = decodeURI(path);
for (let tv of state.tagsViewList) {
if (tv.fullPath === path) {
return false;
const addTagsView = (path: string, to: any = null, tagViewIndex: number = -1) => {
nextTick(async () => {
if (!to) {
to = route;
}
}
const tagView = { ...to };
// Converting circular structure to JSON
tagView.matched = null;
tagView.redirectedFrom = null;
state.tagsViewList.push(tagView);
addBrowserSetSession(state.tagsViewList);
for (let tv of tagsViews.value) {
if (tv.path === path) {
return false;
}
}
const tagView = {
path: path,
name: to.name,
query: to.query,
title: to.meta.title,
icon: to.meta.icon,
isAffix: to.meta.isAffix,
isKeepAlive: to.meta.isKeepAlive,
};
if (tagViewIndex != -1) {
tagsViews.value.splice(tagViewIndex + 1, 0, tagView);
} else {
tagsViews.value.push(tagView);
}
await keepAliveNamesStores.addCachedView(tagView);
addBrowserSetSession(tagsViews.value);
});
};
// 2 tagsView
// pathfullPath
const refreshCurrentTagsView = (path: string) => {
const refreshCurrentTagsView = async (path: string) => {
const item = getTagsView(path);
await keepAliveNamesStores.delCachedView(item);
keepAliveNamesStores.addCachedView(item);
mittBus.emit('onTagsViewRefreshRouterView', path);
};
const getTagsView = (path: string) => {
return tagsViews.value.find((v: any) => v.path === path);
};
// 3 tagsViewisAffix
// pathfullPath
const closeCurrentTagsView = (path: string) => {
state.tagsViewList.map((v: any, k: number, arr: any) => {
if (!v.meta.isAffix) {
if (v.fullPath === path) {
state.tagsViewList.splice(k, 1);
tagsViews.value.map((v: TagsView, k: number, arr: any) => {
if (!v.isAffix) {
if (v.path === path) {
keepAliveNamesStores.delCachedView(v);
tagsViews.value.splice(k, 1);
setTimeout(() => {
if (state.routePath !== path) {
return;
}
let next;
let next: TagsView;
//
if (state.tagsViewList.length === k) {
if (tagsViews.value.length === k) {
next = k !== arr.length ? arr[k] : arr[arr.length - 1];
} else {
next = arr[k];
}
if (next.meta.isDynamic) {
router.push({ name: next.name, params: next.params });
} else {
if (next) {
router.push({ path: next.path, query: next.query });
} else {
router.push({ path: '/' });
}
}, 0);
}
}
});
addBrowserSetSession(state.tagsViewList);
addBrowserSetSession(tagsViews.value);
};
// 4 tagsViewisAffix
const closeOtherTagsView = (path: string) => {
const oldTagViews = state.tagsViewList;
state.tagsViewList = [];
oldTagViews.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) state.tagsViewList.push({ ...v });
const oldTagViews = tagsViews.value;
tagsViews.value = [];
oldTagViews.map((v: TagsView) => {
if (v.isAffix && !v.isHide) {
keepAliveNamesStores.delOthersCachedViews(v);
tagsViews.value.push({ ...v });
}
});
addTagsView(path);
};
// 5 tagsViewisAffix
const closeAllTagsView = (path: string) => {
const oldTagViews = state.tagsViewList;
state.tagsViewList = [];
keepAliveNamesStores.delAllCachedViews();
const oldTagViews = tagsViews.value;
tagsViews.value = [];
oldTagViews.map((v: any) => {
if (v.meta.isAffix && !v.meta.isHide) {
state.tagsViewList.push({ ...v });
if (state.tagsViewList.some((v: any) => v.path === path)) router.push({ path, query: route.query });
else router.push({ path: v.path, query: route.query });
if (v.isAffix && !v.isHide) {
tagsViews.value.push({ ...v });
if (tagsViews.value.some((v: any) => v.path === path)) {
router.push({ path, query: route.query });
}
}
});
addBrowserSetSession(state.tagsViewList);
if (tagsViews.value) {
router.push({ path: '/' });
}
addBrowserSetSession(tagsViews.value);
};
// 6
const openCurrenFullscreen = (path: string) => {
const item = state.tagsViewList.find((v: any) => v.fullPath === path);
const item = tagsViews.value.find((v: any) => v.path === path);
nextTick(() => {
router.push({ path, query: item.query });
router.push({ path, query: item?.query });
const element = document.querySelector('.layout-main');
const screenfulls: any = screenfull;
screenfulls.request(element);
@@ -203,17 +243,17 @@ const openCurrenFullscreen = (path: string) => {
const onCurrentContextmenuClick = (data: any) => {
// pathfullPath
let { id, path } = data;
let currentTag = state.tagsViewList.find((v: any) => v.fullPath === path);
let currentTag = tagsViews.value.find((v: any) => v.path === path);
switch (id) {
case 0:
refreshCurrentTagsView(path);
router.push({ path, query: currentTag.query });
router.push({ path, query: currentTag?.query });
break;
case 1:
closeCurrentTagsView(path);
break;
case 2:
router.push({ path, query: currentTag.query });
router.push({ path, query: currentTag?.query });
closeOtherTagsView(path);
break;
case 3:
@@ -225,8 +265,8 @@ const onCurrentContextmenuClick = (data: any) => {
}
};
//
const isActive = (route: any) => {
return route.fullPath === state.routePath;
const isActive = (tagView: TagsView) => {
return tagView.path === state.routePath;
};
// x,y props
const onContextmenu = (v: any, e: any) => {
@@ -237,7 +277,7 @@ const onContextmenu = (v: any, e: any) => {
};
// tagsView
const onTagsClick = (v: any, k: number) => {
state.routePath = decodeURI(v.fullPath);
state.routePath = decodeURI(v.path);
state.tagsRefsIndex = k;
router.push(v);
};
@@ -302,9 +342,9 @@ const tagsViewmoveToCurrentTag = () => {
});
};
// tagsView tagsView
const getTagsRefsIndex = (path: string) => {
if (state.tagsViewList.length > 0) {
state.tagsRefsIndex = state.tagsViewList.findIndex((item: any) => item.fullPath === path);
const setTagsRefsIndex = (path: string) => {
if (tagsViews.value.length > 0) {
state.tagsRefsIndex = tagsViews.value.findIndex((item: any) => item.path === path);
}
};
// tagsView
@@ -319,7 +359,7 @@ const initSortable = () => {
onEnd: () => {
const sortEndList: any = [];
state.sortable.toArray().map((val: any) => {
state.tagsViewList.map((v: any) => {
tagsViews.value.map((v: any) => {
if (v.name === val) sortEndList.push({ ...v });
});
});
@@ -329,18 +369,6 @@ const initSortable = () => {
}
};
// tagsView
// watch(
// pinia.state,
// (val) => {
// if (val.tagsViewRoutes.tagsViewRoutes.length === state.tagsViewRoutesList.length) return false;
// getTagsViewRoutes();
// },
// {
// deep: true,
// }
// );
//
onBeforeMount(() => {
// 0 1 2 3 4
@@ -371,9 +399,10 @@ onMounted(() => {
});
//
onBeforeRouteUpdate((to) => {
state.routePath = decodeURI(to.fullPath);
addTagsView(to.fullPath, to);
getTagsRefsIndex(to.fullPath);
const path = decodeURI(to.fullPath);
state.routePath = path;
addTagsView(path, to, state.tagsRefsIndex);
setTagsRefsIndex(path);
tagsViewmoveToCurrentTag();
});
</script>

View File

@@ -1,24 +1,23 @@
<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">
<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"/>
<SvgIcon :name="val.meta.icon" />
<span>{{ 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"/>
<SvgIcon :name="val.meta.icon" />
{{ val.meta.title }}
</template>
<template #title v-else>
<a :href="val.meta.link" target="_blank">
<SvgIcon :name="val.meta.icon"/>
<SvgIcon :name="val.meta.icon" />
{{ val.meta.title }}
</a>
</template>
@@ -32,7 +31,7 @@
<script lang="ts" setup name="navMenuHorizontal">
import { reactive, computed, getCurrentInstance, onMounted, nextTick } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import SubItem from '@/views/layout/navMenu/subItem.vue';
import SubItem from '@/layout/navMenu/subItem.vue';
import { useRoutesList } from '@/store/routesList';
import { useThemeConfig } from '@/store/themeConfig';
import mittBus from '@/common/utils/mitt';

View File

@@ -1,21 +1,28 @@
<template>
<el-menu router :default-active="state.defaultActive" background-color="transparent" :collapse="setIsCollapse"
:unique-opened="themeConfig.isUniqueOpened" :collapse-transition="false">
<el-menu
router
:default-active="state.defaultActive"
background-color="transparent"
:collapse="setIsCollapse"
:unique-opened="themeConfig.isUniqueOpened"
:collapse-transition="false"
>
<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"/>
<SvgIcon :name="val.meta.icon" />
<span>{{ val.meta.title }}</span>
</template>
<SubItem :chil="val.children" />
</el-sub-menu>
<el-menu-item :index="val.path" :key="val?.path" v-else>
<SvgIcon :name="val.meta.icon"/>
<SvgIcon :name="val.meta.icon" />
<template #title v-if="!val.meta.link || (val.meta.link && val.meta.linkType == 1)">
<span>{{ val.meta.title }}</span>
</template>
<template #title v-else>
<a :href="val.meta.link" target="_blank">{{ val.meta.title }}</a></template>
<a :href="val.meta.link" target="_blank">{{ val.meta.title }}</a></template
>
</el-menu-item>
</template>
</el-menu>
@@ -26,7 +33,7 @@ import { reactive, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import SubItem from '@/views/layout/navMenu/subItem.vue';
import SubItem from '@/layout/navMenu/subItem.vue';
import mittBus from '@/common/utils/mitt';
//

View File

@@ -2,7 +2,7 @@
<div class="h100">
<router-view v-slot="{ Component }">
<transition :name="setTransitionName" mode="out-in">
<keep-alive :include="state.keepAliveNameList">
<keep-alive :include="getKeepAliveNames">
<component :is="Component" :key="state.refreshRouterViewKey" class="w100" />
</keep-alive>
</transition>
@@ -11,38 +11,62 @@
</template>
<script lang="ts" setup name="layoutParentView">
import { computed, reactive, onBeforeMount, onUnmounted, nextTick } from 'vue';
import { computed, watch, reactive, onBeforeMount, onMounted, onUnmounted, nextTick } from 'vue';
import { useRoute } 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';
const route = useRoute();
const { themeConfig } = storeToRefs(useThemeConfig());
const { keepAliveNames } = storeToRefs(useKeepALiveNames());
const { keepAliveNames, cachedViews } = storeToRefs(useKeepALiveNames());
const state: any = reactive({
refreshRouterViewKey: null,
keepAliveNameList: [],
keepAliveNameNewList: [],
});
// refreshRouterViewKey
// onBeforeRouteUpdate((to: any) => {
// state.refreshRouterViewKey = decodeURI(to.fullPath);
// });
// (name)
const getKeepAliveNames = computed(() => {
return themeConfig.value.isTagsview ? cachedViews.value : state.keepAliveNameList;
});
//
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 = route.path;
state.refreshRouterViewKey = '';
nextTick(() => {
state.refreshRouterViewKey = null;
state.refreshRouterViewKey = path;
state.keepAliveNameList = keepAliveNames.value;
});
});
});
//
onMounted(() => {
nextTick(() => {
setTimeout(() => {
if (themeConfig.value.isCacheTagsView) {
let tagsViewArr: any = getTagViews() || [];
cachedViews.value = tagsViewArr.filter((item: any) => item?.isKeepAlive).map((item: any) => item.name as string);
}
}, 0);
});
});
// tagsView
watch(
() => route.fullPath,
() => {
state.refreshRouterViewKey = decodeURI(route.fullPath);
},
{
immediate: true,
}
);
//
const setTransitionName = computed(() => {
return themeConfig.value.animation;

View File

@@ -1,7 +1,7 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { getSession, clearSession } from '@/common/utils/storage';
import { clearSession, getToken } from '@/common/utils/storage';
import { templateResolve } from '@/common/utils/string';
import { NextLoading } from '@/common/utils/loading';
import { dynamicRoutes, staticRoutes, pathMatch } from './route';
@@ -18,7 +18,7 @@ import { useKeepALiveNames } from '@/store/keepAliveNames';
* @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json
*/
const viewsModules: any = import.meta.glob(['../views/**/*.{vue,tsx}', '!../views/layout/**/*.{vue,tsx}']);
const viewsModules: any = import.meta.glob(['../views/**/*.{vue,tsx}']);
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...viewsModules });
// 添加静态路由
@@ -29,8 +29,7 @@ const router = createRouter({
// 前端控制路由:初始化方法,防止刷新时丢失
export function initAllFun() {
NextLoading.start(); // 界面 loading 动画开始执行
const token = getSession('token'); // 获取浏览器缓存 token 值
const token = getToken(); // 获取浏览器缓存 token 值
if (!token) {
// 无 token 停止执行下一步
return false;
@@ -38,18 +37,15 @@ export function initAllFun() {
useUserInfo().setUserInfo({});
router.addRoute(pathMatch); // 添加404界面
resetRoute(); // 删除/重置路由
// 添加动态路由
setFilterRouteEnd().forEach((route: any) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
router.addRoute(dynamicRoutes[0]);
// 过滤权限菜单
useRoutesList().setRoutesList(setFilterMenuFun(dynamicRoutes[0].children, useUserInfo().userInfo.menus));
useRoutesList().setRoutesList(dynamicRoutes[0].children);
}
// 后端控制路由:执行路由数据初始化
export async function initBackEndControlRoutesFun() {
NextLoading.start(); // 界面 loading 动画开始执行
const token = getSession('token'); // 获取浏览器缓存 token 值
const token = getToken(); // 获取浏览器缓存 token 值
if (!token) {
// 无 token 停止执行下一步
return false;
@@ -57,14 +53,25 @@ export async function initBackEndControlRoutesFun() {
useUserInfo().setUserInfo({});
// 获取路由
let menuRoute = await getBackEndControlRoutes();
dynamicRoutes[0].children = backEndRouterConverter(menuRoute); // 处理路由component
const cacheList: Array<string> = [];
// 处理路由component
dynamicRoutes[0].children = backEndRouterConverter(menuRoute, (router: any) => {
// 可能为false时不存在isKeepAlive属性
if (!router.meta.isKeepAlive) {
router.meta.isKeepAlive = false;
}
if (router.meta.isKeepAlive) {
cacheList.push(router.name);
}
});
useKeepALiveNames().setCacheKeepAlive(cacheList);
// 添加404界面
router.addRoute(pathMatch);
resetRoute(); // 删除/重置路由
// 添加动态路由
formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes)).forEach((route: any) => {
router.addRoute(route as unknown as RouteRecordRaw);
});
router.addRoute(dynamicRoutes[0] as unknown as RouteRecordRaw);
useRoutesList().setRoutesList(dynamicRoutes[0].children);
}
@@ -81,8 +88,10 @@ export async function getBackEndControlRoutes() {
}
}
type RouterConvCallbackFunc = (router: any) => void;
// 后端控制路由,后端返回路由 转换为vue route
export function backEndRouterConverter(routes: any, parentPath: string = '/') {
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
if (!routes) return;
return routes.map((item: any) => {
if (!item.meta) {
@@ -117,7 +126,9 @@ export function backEndRouterConverter(routes: any, parentPath: string = '/') {
item.redirect = item.meta.redirect;
delete item.meta['redirect'];
}
item.children && backEndRouterConverter(item.children, item.path);
// 存在回调,则执行回调
callbackFunc && callbackFunc(item);
item.children && backEndRouterConverter(item.children, callbackFunc, item.path);
return item;
});
}
@@ -143,86 +154,6 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
}
}
// 多级嵌套数组处理成一维数组
export function formatFlatteningRoutes(arr: any) {
if (arr.length <= 0) return false;
for (let i = 0; i < arr.length; i++) {
if (arr[i].children) {
arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
}
}
return arr;
}
// 多级嵌套数组处理后的一维数组,再处理成 `定义动态路由` 的格式
// 只保留二级也就是二级以上全部处理成只有二级keep-alive 支持二级缓存
// isKeepAlive 处理 `name` 值,进行缓存。顶级关闭,全部不缓存
export function formatTwoStageRoutes(arr: any) {
if (arr.length <= 0) return false;
const newArr: any = [];
const cacheList: Array<string> = [];
arr.forEach((v: any) => {
if (v.path === '/') {
newArr.push({ component: v.component, name: v.name, path: v.path, redirect: v.redirect, meta: v.meta, children: [] });
} else {
newArr[0].children.push({ ...v });
if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive) {
cacheList.push(v.name);
}
}
});
useKeepALiveNames().setCacheKeepAlive(cacheList);
return newArr;
}
// 判断路由code 是否包含当前登录用户menus字段中menus为字符串code数组
export function hasAnth(menus: any, route: any) {
if (route.meta && route.meta.code) {
return menus.includes(route.meta.code);
}
return true;
}
// 递归过滤有权限的路由
export function setFilterMenuFun(routes: any, menus: any) {
const menu: any = [];
routes.forEach((route: any) => {
const item = { ...route };
if (hasAnth(menus, item)) {
if (item.children) {
item.children = setFilterMenuFun(item.children, menus);
}
menu.push(item);
}
});
return menu;
}
// 获取当前用户的权限去比对路由表,用于动态路由的添加
export function setFilterRoute(chil: any) {
let filterRoute: any = [];
chil.forEach((route: any) => {
// 如果路由需要拥有指定code才可访问则校验该用户菜单是否存在该code
if (route.meta.code) {
useUserInfo().userInfo.menus.forEach((m: any) => {
if (route.meta.code == m) {
filterRoute.push({ ...route });
}
});
} else {
filterRoute.push({ ...route });
}
});
return filterRoute;
}
// 比对后的路由表,进行重新赋值
export function setFilterRouteEnd() {
let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes));
filterRouteEnd[0].children = setFilterRoute(filterRouteEnd[0].children);
return filterRouteEnd;
}
// 删除/重置路由
export function resetRoute() {
useRoutesList().routesList.forEach((route: any) => {
@@ -232,14 +163,19 @@ export function resetRoute() {
}
export async function initRouter() {
// 初始化方法执行
const { isRequestRoutes } = useThemeConfig(pinia).themeConfig;
if (!isRequestRoutes) {
// 未开启后端控制路由
initAllFun();
} else if (isRequestRoutes) {
// 后端控制路由isRequestRoutes 为 true则开启后端控制路由
await initBackEndControlRoutesFun();
NextLoading.start(); // 界面 loading 动画开始执行
try {
// 初始化方法执行
const { isRequestRoutes } = useThemeConfig(pinia).themeConfig;
if (!isRequestRoutes) {
// 未开启后端控制路由
initAllFun();
} else if (isRequestRoutes) {
// 后端控制路由isRequestRoutes 为 true则开启后端控制路由
await initBackEndControlRoutesFun();
}
} finally {
NextLoading.done();
}
}
@@ -252,11 +188,11 @@ router.beforeEach(async (to, from, next) => {
if (to.meta.title) NProgress.start();
// 如果有标题参数,则再原标题后加上参数来区别
if (to.meta.titleRename) {
if (to.meta.titleRename && to.meta.title) {
to.meta.title = templateResolve(to.meta.title as string, to.query);
}
const token = getSession('token');
const token = getToken();
if ((to.path === '/login' || to.path == '/oauth2/callback') && !token) {
next();
NProgress.done();
@@ -270,7 +206,7 @@ router.beforeEach(async (to, from, next) => {
if (SysWs) {
SysWs.close();
SysWs = null;
SysWs = undefined;
}
return;
}
@@ -297,7 +233,6 @@ router.beforeEach(async (to, from, next) => {
// 路由加载后
router.afterEach(() => {
NProgress.done();
NextLoading.done();
});
// 导出路由

View File

@@ -1,5 +1,5 @@
import { RouteRecordRaw } from 'vue-router';
import Layout from '@/views/layout/index.vue';
import Layout from '@/layout/index.vue';
// 定义动态路由
export const dynamicRoutes = [

View File

@@ -18,14 +18,16 @@ export const useKeepALiveNames = defineStore('keepALiveNames', {
this.keepAliveNames = data;
},
async addCachedView(view: any) {
if (view.meta.isKeepAlive) this.cachedViews?.push(view.name);
if (view.isKeepAlive) {
this.cachedViews?.push(view.name);
}
},
async delCachedView(view: any) {
const index = this.cachedViews.indexOf(view.name);
index > -1 && this.cachedViews.splice(index, 1);
},
async delOthersCachedViews(view: any) {
if (view.meta.isKeepAlive) this.cachedViews = [view.name];
if (view.isKeepAlive) this.cachedViews = [view.name];
else this.cachedViews = [];
},
async delAllCachedViews() {

View File

@@ -0,0 +1,25 @@
import { getNowUrl } from '@/common/utils/url';
import { defineStore } from 'pinia';
/**
* tags view
*/
export const useTagsViews = defineStore('tagsViews', {
state: (): TagsViewsState => ({
tagsViews: [],
}),
actions: {
setTagsViews(data: Array<TagsView>) {
this.tagsViews = data;
},
// 设置当前页面的tags view title
setNowTitle(title: string) {
this.tagsViews.forEach((item) => {
// console.log(getNowUrl(), item.path);
if (item.path == getNowUrl()) {
item.title = title;
}
});
},
},
});

View File

@@ -1,4 +1,6 @@
import { defineStore } from 'pinia';
import { dateFormat2 } from '@/common/utils/date';
import { useUserInfo } from '@/store/userInfo';
export const useThemeConfig = defineStore('themeConfig', {
state: (): ThemeConfigState => ({
@@ -88,9 +90,9 @@ export const useThemeConfig = defineStore('themeConfig', {
// 是否开启色弱模式
isInvert: false,
// 是否开启水印
isWartermark: false,
// 水印文案
wartermarkText: 'mayfly',
isWatermark: false,
// 水印文案数组0->用户信息 1->当前时间 2->额外信息
watermarkText: ['', '', ''],
/* 其它设置
------------------------------- */
@@ -140,5 +142,40 @@ export const useThemeConfig = defineStore('themeConfig', {
setThemeConfig(data: ThemeConfigState) {
this.themeConfig = data.themeConfig;
},
// 切换暗模式
switchDark(isDark: boolean) {
this.themeConfig.isDark = isDark;
const body = document.documentElement as HTMLElement;
if (isDark) {
body.setAttribute('class', 'dark');
this.themeConfig.editorTheme = 'vs-dark';
} else {
body.setAttribute('class', '');
this.themeConfig.editorTheme = 'SolarizedLight';
}
},
// 设置水印配置信息
setWatermarkConfig(useWatermarkConfig: any) {
this.themeConfig.watermarkText = [];
this.themeConfig.isWatermark = useWatermarkConfig.isUse;
if (!useWatermarkConfig.isUse) {
return;
}
// 索引2为用户自定义水印信息
this.themeConfig.watermarkText[2] = useWatermarkConfig.content;
},
// 设置水印用户信息
setWatermarkUser(del: boolean = false) {
const userinfo = useUserInfo().userInfo;
let desc = '';
if (!del && userinfo && userinfo.username) {
desc = `${userinfo.username}(${userinfo.name})`;
}
this.themeConfig.watermarkText[0] = desc;
},
// 设置水印时间为当前时间
setWatermarkNowTime() {
this.themeConfig.watermarkText[1] = dateFormat2('yyyy-MM-dd HH:mm:ss', new Date());
},
},
});

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { getSession } from '@/common/utils/storage';
import { getUser } from '@/common/utils/storage';
export const useUserInfo = defineStore('userInfo', {
state: (): UserInfoState => ({
@@ -8,7 +8,7 @@ export const useUserInfo = defineStore('userInfo', {
actions: {
// 设置用户信息
async setUserInfo(data: object) {
const ui = getSession('userInfo');
const ui = getUser();
if (ui) {
this.userInfo = ui;
} else {

View File

@@ -83,3 +83,42 @@
opacity: 1;
}
}
/* 登录页动画
------------------------------- */
@keyframes loginLeft {
0% {
left: -100%;
}
50%,
100% {
left: 100%;
}
}
@keyframes loginTop {
0% {
top: -100%;
}
50%,
100% {
top: 100%;
}
}
@keyframes loginRight {
0% {
right: -100%;
}
50%,
100% {
right: 100%;
}
}
@keyframes loginBottom {
0% {
bottom: -100%;
}
50%,
100% {
bottom: 100%;
}
}

View File

@@ -335,8 +335,14 @@
/* Set padding to ensure the height is 32px */
// padding: 6px 12px;
background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
}
.el-popper.is-customized .el-popper__arrow::before {
}
.el-popper.is-customized .el-popper__arrow::before {
background: linear-gradient(45deg, #b2e68d, #bce689);
right: 0;
}
}
.el-dialog {
border-radius: 6px; /* 设置圆角 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */
}

View File

@@ -48,4 +48,4 @@
35% {
transform: scale3D(0, 0, 1);
}
}
}

View File

@@ -40,8 +40,8 @@ declare interface ThemeConfigState {
isDark: boolean;
isGrayscale: boolean;
isInvert: boolean;
isWartermark: boolean;
wartermarkText: string;
isWatermark: boolean;
watermarkText: Array<string>;
tagsStyle: string;
animation: string;
columnsAsideStyle: string;
@@ -60,10 +60,40 @@ declare interface ThemeConfigState {
};
}
declare interface TagsView {
/**
* 路径
*/
path: string;
/**
* 标题
*/
title: string;
/**
* router name
*/
name: string;
/**
* router query
*/
query: any;
/**
* 图标
*/
icon: string;
isAffix: boolean;
isKeepAlive: boolean;
isHide?: boolean;
}
// TagsView 路由列表
declare interface TagsViewRoutesState<T = any> {
tagsViewRoutes: T[];
isTagsViewCurrenFull: Boolean;
declare interface TagsViewsState<T = any> {
tagsViews: TagsView[];
}
// 路由列表

View File

@@ -1,14 +1,5 @@
/* eslint-disable */
import {IDisposable} from 'monaco-editor';
declare global {
interface Window {
completionItemProvider?: IDisposable | undefined;
}
}
// 申明外部 npm 插件模块
declare module 'sql-formatter';
declare module 'jsoneditor';
declare module 'asciinema-player';
declare module 'monaco-editor';
declare module 'vue-grid-layout';
declare module 'vue-grid-layout';

View File

@@ -12,7 +12,7 @@
</div>
</div>
<div class="right">
<img src="@/assets/image/401.png" />
<img src="@/assets/image/401.svg" />
</div>
</div>
</div>
@@ -20,7 +20,7 @@
<script lang="ts">
import { useRouter } from 'vue-router';
import { clearSession } from '@/common/utils/storage.ts';
import { clearSession } from '@/common/utils/storage';
export default {
name: '401',
setup() {
@@ -39,7 +39,7 @@ export default {
<style scoped lang="scss">
.error {
height: 100%;
background-color: white;
background-color: var(--bg-main-color);
display: flex;
.error-flex {
margin: auto;
@@ -64,7 +64,7 @@ export default {
}
.left-item-title {
font-size: 20px;
color: #333333;
// color: #333333;
margin: 15px 0 5px 0;
animation-delay: 0.1s;
}

View File

@@ -12,7 +12,7 @@
</div>
</div>
<div class="right">
<img src="@/assets/image/404.png" />
<img src="@/assets/image/404.svg" />
</div>
</div>
</div>
@@ -37,7 +37,7 @@ export default {
<style scoped lang="scss">
.error {
height: 100%;
background-color: white;
background-color: var(--bg-main-color);
display: flex;
.error-flex {
margin: auto;
@@ -62,7 +62,7 @@ export default {
}
.left-item-title {
font-size: 20px;
color: #333333;
// color: #333333;
margin: 15px 0 5px 0;
animation-delay: 0.1s;
}

View File

@@ -1,138 +0,0 @@
<template>
<transition name="el-zoom-in-center">
<div
aria-hidden="true"
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
role="tooltip"
data-popper-placement="bottom"
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
:key="Math.random()"
v-show="state.isShow"
>
<ul class="el-dropdown-menu">
<template v-for="(v, k) in state.dropdownList">
<li
class="el-dropdown-menu__item"
aria-disabled="false"
tabindex="-1"
:key="k"
v-if="!v.affix"
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
>
<SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span>
</li>
</template>
</ul>
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
</div>
</transition>
</template>
<script setup lang="ts" name="layoutTagsViewContextmenu">
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
// 定义父组件传过来的值
const props = defineProps({
dropdown: {
type: Object,
default: () => {
return {
x: 0,
y: 0,
};
},
},
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(['currentContextmenuClick']);
// 定义变量内容
const state = reactive({
isShow: false,
dropdownList: [
{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' },
{ contextMenuClickId: 1, txt: '关闭', affix: false, icon: 'Close' },
{ contextMenuClickId: 2, txt: '关闭其他', affix: false, icon: 'CircleClose' },
{ contextMenuClickId: 3, txt: '关闭所有', affix: false, icon: 'FolderDelete' },
{
contextMenuClickId: 4,
txt: '当前页全屏',
affix: false,
icon: 'full-screen',
},
],
item: {} as any,
arrowLeft: 10,
});
// 父级传过来的坐标 x,y 值
const dropdowns = computed(() => {
// 117 为 `Dropdown 下拉菜单` 的宽度
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
return {
x: document.documentElement.clientWidth - 117 - 5,
y: props.dropdown.y,
};
} else {
return props.dropdown;
}
});
// 当前项菜单点击
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
emit('currentContextmenuClick', { id: contextMenuClickId, path: state.item.fullPath });
};
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
const openContextmenu = (item: any) => {
state.item = item;
item.meta?.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
closeContextmenu();
setTimeout(() => {
state.isShow = true;
}, 10);
};
// 关闭右键菜单
const closeContextmenu = () => {
state.isShow = false;
};
// 监听页面监听进行右键菜单的关闭
onMounted(() => {
document.body.addEventListener('click', closeContextmenu);
});
// 页面卸载时,移除右键菜单监听事件
onUnmounted(() => {
document.body.removeEventListener('click', closeContextmenu);
});
// 监听下拉菜单位置
watch(
() => props.dropdown,
({ x }) => {
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
else state.arrowLeft = 10;
},
{
deep: true,
}
);
// 暴露变量
defineExpose({
openContextmenu,
});
</script>
<style scoped lang="scss">
.custom-contextmenu {
transform-origin: center top;
z-index: 2190;
position: fixed;
.el-dropdown-menu__item {
font-size: 12px !important;
white-space: nowrap;
i {
font-size: 12px !important;
}
}
}
</style>

View File

@@ -132,16 +132,18 @@ import { nextTick, onMounted, ref, toRefs, reactive, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { initRouter } from '@/router/index';
import { getSession, setSession, setUserInfo2Session, setUseWatermark2Session } from '@/common/utils/storage';
import { saveToken, saveUser } from '@/common/utils/storage';
import { formatAxis } from '@/common/utils/format';
import openApi from '@/common/openApi';
import { RsaEncrypt } from '@/common/rsa';
import { getAccountLoginSecurity, getLdapEnabled, useWartermark } from '@/common/sysconfig';
import { getAccountLoginSecurity, getLdapEnabled } from '@/common/sysconfig';
import { letterAvatar } from '@/common/utils/string';
import { useUserInfo } from '@/store/userInfo';
import QrcodeVue from 'qrcode.vue';
import { personApi } from '@/views/personal/api';
import { AccountUsernamePattern } from '@/common/pattern';
import { getToken } from '@/common/utils/storage';
import { useThemeConfig } from '@/store/themeConfig';
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
@@ -149,6 +151,9 @@ const rules = {
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
};
// 定义变量内容
const storesThemeConfig = useThemeConfig();
const route = useRoute();
const router = useRouter();
const loginFormRef: any = ref(null);
@@ -159,7 +164,7 @@ const baseInfoFormRef: any = ref(null);
const state = reactive({
accountLoginSecurity: {
useCaptcha: true,
useCaptcha: false,
useOtp: false,
loginFailCount: 5,
loginFailMin: 10,
@@ -354,7 +359,7 @@ const loginResDeal = (loginRes: any) => {
};
// 存储用户信息到浏览器缓存
setUserInfo2Session(userInfos);
saveUser(userInfos);
// 1、请注意执行顺序(存储用户信息到vuex)
useUserInfo().setUserInfo(userInfos);
@@ -376,10 +381,10 @@ const loginResDeal = (loginRes: any) => {
// 登录成功后的跳转
const signInSuccess = async (accessToken: string = '') => {
if (!accessToken) {
accessToken = getSession('token');
accessToken = getToken();
}
// 存储 token 到浏览器缓存
setSession('token', accessToken);
saveToken(accessToken);
// 初始化路由
await initRouter();
@@ -404,9 +409,8 @@ const toIndex = async () => {
// 关闭 loading
state.loading.signIn = true;
ElMessage.success(`${currentTimeInfo},欢迎回来!`);
if (await useWartermark()) {
setUseWatermark2Session(true);
}
// 水印设置用户信息
storesThemeConfig.setWatermarkUser();
}, 300);
};

View File

@@ -1,70 +1,77 @@
<template>
<div class="login-container">
<div class="login-logo">
<span>{{ themeConfig.globalViceTitle }}</span>
<div class="login-container flex">
<div class="login-left">
<div class="login-left-logo">
<img :src="logoMini" />
<div class="login-left-logo-text">
<span>mayfly-go</span>
<!-- <span class="login-left-logo-text-msg">mayfly-go</span> -->
</div>
</div>
<div class="login-left-img">
<img :src="loginBgImg" />
</div>
<img :src="loginBgSplitImg" class="login-left-waves" />
</div>
<div class="login-content" :class="{ 'login-content-mobile': tabsActiveName === 'mobile' }">
<div class="login-content-main">
<h4 class="login-content-title">mayfly-go</h4>
<el-tabs v-model="tabsActiveName" @tab-click="onTabsClick">
<el-tab-pane label="账号密码登录" name="account" :disabled="tabsActiveName === 'account'">
<transition name="el-zoom-in-center">
<Account v-show="isTabPaneShow" ref="loginForm" />
</transition>
</el-tab-pane>
<!-- <el-tab-pane label="手机号登录" name="mobile" :disabled="tabsActiveName === 'mobile'">
<transition name="el-zoom-in-center">
<Mobile v-show="!isTabPaneShow" />
</transition>
</el-tab-pane> -->
</el-tabs>
<div class="mt20" v-show="oauth2LoginConfig.enable">
<el-button link size="small">第三方登录: </el-button>
<el-tooltip :content="oauth2LoginConfig.name" placement="top-start">
<el-button link size="small" type="primary" @click="oauth2Login">
<el-icon :size="18">
<Link />
</el-icon>
</el-button>
</el-tooltip>
<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">mayfly-go</div>
<div class="login-right-warp-main-form">
<div v-if="!state.isScan">
<el-tabs v-model="state.tabsActiveName">
<el-tab-pane label="账号密码登录" name="account">
<Account ref="loginForm" />
</el-tab-pane>
</el-tabs>
</div>
<div class="mt20" v-show="state.oauth2LoginConfig.enable">
<el-button link size="small">第三方登录: </el-button>
<el-tooltip :content="state.oauth2LoginConfig.name" placement="top-start">
<el-button link size="small" type="primary" @click="oauth2Login">
<el-icon :size="18">
<Link />
</el-icon>
</el-button>
</el-tooltip>
</div>
</div>
</div>
</div>
</div>
<!-- <div class="login-copyright">
<div class="mb5 login-copyright-company">mayfly</div>
<div class="login-copyright-msg">mayfly</div>
</div> -->
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted, h, ref } from 'vue';
import Account from '@/views/login/component/AccountLogin.vue';
import { storeToRefs } from 'pinia';
<script setup lang="ts" name="loginIndex">
import { ref, defineAsyncComponent, onMounted, reactive } from 'vue';
import { useThemeConfig } from '@/store/themeConfig';
import logoMini from '@/assets/image/logo.svg';
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';
const { themeConfig } = storeToRefs(useThemeConfig());
// 引入组件
const Account = defineAsyncComponent(() => import('./component/AccountLogin.vue'));
const loginForm = ref<{ loginResDeal: (data: any) => void } | null>(null);
// 定义变量内容
const storesThemeConfig = useThemeConfig();
const state = reactive({
tabsActiveName: 'account',
isTabPaneShow: true,
isScan: false,
oauth2LoginConfig: {
name: 'OAuth2登录',
enable: false,
},
});
const loginForm = ref<{ loginResDeal: (data: any) => void } | null>(null);
const { isTabPaneShow, tabsActiveName, oauth2LoginConfig: oauth2LoginConfig } = toRefs(state);
// 切换密码、手机登录
const onTabsClick = () => {
state.isTabPaneShow = !state.isTabPaneShow;
};
onMounted(async () => {
storesThemeConfig.setWatermarkUser(true);
state.oauth2LoginConfig = await openApi.oauth2LoginConfig();
});
@@ -94,76 +101,178 @@ const oauth2Login = () => {
<style scoped lang="scss">
.login-container {
width: 100%;
height: 100%;
background: url('@/assets/image/bg-login.png') no-repeat;
background-size: 100% 100%;
.login-logo {
position: absolute;
top: 30px;
left: 50%;
height: 50px;
display: flex;
align-items: center;
font-size: 20px;
color: var(--el-color-primary);
letter-spacing: 2px;
width: 90%;
transform: translateX(-50%);
}
.login-content {
width: 500px;
padding: 20px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) translate3d(0, 0, 0);
background-color: rgba(255, 255, 255, 0.99);
box-shadow: 0 2px 12px 0 var(--el-color-primary-light-5);
border-radius: 4px;
transition: height 0.2s linear;
height: 490px;
overflow: hidden;
z-index: 1;
.login-content-main {
margin: 0 auto;
width: 80%;
.login-content-title {
color: #333;
font-weight: 500;
font-size: 22px;
text-align: center;
letter-spacing: 4px;
margin: 15px 0 30px;
white-space: nowrap;
background: var(--bg-main-color);
.login-left {
flex: 1;
position: relative;
background-color: rgba(211, 239, 255, 1);
margin-right: 100px;
.login-left-logo {
display: flex;
align-items: center;
position: absolute;
top: 50px;
left: 80px;
z-index: 1;
animation: logoAnimation 0.3s ease;
img {
width: 52px;
height: 52px;
}
.login-left-logo-text {
display: flex;
flex-direction: column;
span {
margin-left: 10px;
font-size: 28px;
color: #26a59a;
}
.login-left-logo-text-msg {
font-size: 12px;
color: #32a99e;
}
}
}
}
.login-content-mobile {
height: 418px;
}
.login-copyright {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 30px;
text-align: center;
color: white;
font-size: 12px;
opacity: 0.8;
.login-copyright-company {
white-space: nowrap;
.login-left-img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 52%;
img {
width: 100%;
height: 100%;
animation: error-num 0.6s ease;
}
}
.login-copyright-msg {
@extend .login-copyright-company;
.login-left-waves {
position: absolute;
top: 0;
right: -100px;
}
}
.login-right {
width: 700px;
.login-right-warp {
border: 1px solid var(--el-color-primary-light-3);
border-radius: 3px;
width: 500px;
height: 500px;
position: relative;
overflow: hidden;
background-color: var(--bg-main-color);
.login-right-warp-one,
.login-right-warp-two {
position: absolute;
display: block;
width: inherit;
height: inherit;
&::before,
&::after {
content: '';
position: absolute;
z-index: 1;
}
}
.login-right-warp-one {
&::before {
filter: hue-rotate(0deg);
top: 0px;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, transparent, var(--el-color-primary));
animation: loginLeft 3s linear infinite;
}
&::after {
filter: hue-rotate(60deg);
top: -100%;
right: 2px;
width: 3px;
height: 100%;
background: linear-gradient(180deg, transparent, var(--el-color-primary));
animation: loginTop 3s linear infinite;
animation-delay: 0.7s;
}
}
.login-right-warp-two {
&::before {
filter: hue-rotate(120deg);
bottom: 2px;
right: -100%;
width: 100%;
height: 3px;
background: linear-gradient(270deg, transparent, var(--el-color-primary));
animation: loginRight 3s linear infinite;
animation-delay: 1.4s;
}
&::after {
filter: hue-rotate(300deg);
bottom: -100%;
left: 0px;
width: 3px;
height: 100%;
background: linear-gradient(360deg, transparent, var(--el-color-primary));
animation: loginBottom 3s linear infinite;
animation-delay: 2.1s;
}
}
.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;
}
}
}
}
}
}
}

View File

@@ -10,11 +10,11 @@
width="38%"
>
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-form-item prop="tagId" label="标签:" required>
<el-form-item prop="tagId" label="标签" required>
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="instanceId" label="数据库实例:" required>
<el-form-item prop="instanceId" label="数据库实例" required>
<el-select
:disabled="form.id !== undefined"
remote
@@ -37,11 +37,11 @@
</el-select>
</el-form-item>
<el-form-item prop="name" label="别名:" required>
<el-form-item prop="name" label="别名" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="database" label="数据库名:" required>
<el-form-item prop="database" label="数据库名" required>
<el-select
@change="changeDatabase"
v-model="databaseList"
@@ -58,7 +58,7 @@
</el-select>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
</el-form>

View File

@@ -172,7 +172,7 @@ import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dbApi } from './api';
import config from '@/common/config';
import { getSession } from '@/common/utils/storage';
import { getToken } from '@/common/utils/storage';
import { isTrue } from '@/common/assert';
import { Search as SearchIcon } from '@element-plus/icons-vue';
import { dateFormat } from '@/common/utils/date';
@@ -406,7 +406,7 @@ const dumpDbs = () => {
'href',
`${config.baseApiUrl}/dbs/${state.exportDialog.dbId}/dump?db=${state.exportDialog.value.join(',')}&type=${type}&extName=${
state.exportDialog.extName
}&token=${getSession('token')}`
}&token=${getToken()}`
);
a.click();
state.exportDialog.visible = false;

View File

@@ -4,16 +4,16 @@
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="name" label="别名:" required>
<el-form-item prop="name" label="别名" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="type" label="类型:" required>
<el-form-item prop="type" label="类型" required>
<el-select style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option key="item.id" label="mysql" value="mysql"> </el-option>
<el-option key="item.id" label="postgres" value="postgres"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-form-item prop="host" label="host" required>
<el-col :span="18">
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-col>
@@ -22,10 +22,10 @@
<el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input>
</el-col>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-form-item prop="username" label="用户名" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-form-item prop="password" label="密码">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
<template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
@@ -39,13 +39,13 @@
</el-input>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="params" label="连接参数:">
<el-form-item prop="params" label="连接参数">
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
<template #suffix>
<el-link
@@ -60,7 +60,7 @@
</el-input>
</el-form-item>
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>

View File

@@ -2,7 +2,12 @@
<div>
<el-row class="mb5">
<el-col :span="4">
<el-button type="primary" icon="plus" @click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases.split(' ') }, state.db)" size="small"
<el-button
:disabled="!state.db || !nowDbInst.id"
type="primary"
icon="plus"
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases?.split(' ') }, state.db)"
size="small"
>新建查询</el-button
>
</el-col>
@@ -95,13 +100,14 @@
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, toRefs } from 'vue';
import { defineAsyncComponent, onMounted, reactive, ref, toRefs, onBeforeUnmount } from 'vue';
import { ElMessage } from 'element-plus';
import { DbInst, TabInfo, TabType } from './db';
import { DbInst, TabInfo, TabType, registerDbCompletionItemProvider } from './db';
import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { dbApi } from './api';
import { dispposeCompletionItemProvider } from '../../../components/monaco/completionItemProvider';
const Query = defineAsyncComponent(() => import('./component/tab/Query.vue'));
const TableData = defineAsyncComponent(() => import('./component/tab/TableData.vue'));
@@ -144,12 +150,15 @@ const state = reactive({
const { nowDbInst } = toRefs(state);
onMounted(() => {
self.completionItemProvider?.dispose();
setHeight();
// 监听浏览器窗口大小变化,更新对应组件高度
window.onresize = () => setHeight();
});
onBeforeUnmount(() => {
dispposeCompletionItemProvider('sql');
});
/**
* 设置editor高度和数据表高度
*/
@@ -207,7 +216,6 @@ const loadNode = async (node: any) => {
// 点击数据库实例 -> 加载库列表
if (nodeType === NodeType.DbInst) {
const dbs = params.database.split(' ')?.sort();
console.log(dbs);
return dbs.map((x: any) => {
return new TagTreeNode(`${data.key}.${x}`, x, NodeType.Db).withParams({
tagPath: params.tagPath,
@@ -406,9 +414,15 @@ const onTabChange = () => {
state.db = '';
return;
}
const nowTab = state.tabs.get(state.activeName);
state.nowDbInst = DbInst.getInst(nowTab?.dbId);
state.db = nowTab?.db as string;
if (nowTab?.type == TabType.Query) {
// 注册sql提示
registerDbCompletionItemProvider('sql', nowTab.dbId, nowTab.db, nowTab.params.dbs);
}
};
const onGenerateInsertSql = async (sql: string) => {

View File

@@ -44,9 +44,7 @@
</div>
</div>
<div class="mt5 sqlEditor">
<div :id="'MonacoTextarea-' + ti.key" :style="{ height: editorHeight }"></div>
</div>
<MonacoEditor ref="monacoEditorRef" class="mt5" v-model="state.sql" language="sql" :height="editorHeight" :id="'MonacoTextarea-' + ti.key" />
<div class="editor-move-resize" @mousedown="onDragSetHeight">
<el-icon>
@@ -90,44 +88,22 @@
<script lang="ts" setup>
import { nextTick, watch, onMounted, reactive, toRefs, ref, Ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import { getSession } from '@/common/utils/storage';
import { getToken } from '@/common/utils/storage';
import { isTrue, notBlank } from '@/common/assert';
import { format as sqlFormatter } from 'sql-formatter';
import config from '@/common/config';
import { ElMessage, ElMessageBox } from 'element-plus';
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
import { language as addSqlLanguage } from '../../lang/mysql.js';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
// import * as monaco from 'monaco-editor';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { editor, languages, Position } from 'monaco-editor';
// 相关语言
import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js';
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js';
import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineCompletions.js';
// 右键菜单
import 'monaco-editor/esm/vs/editor/contrib/contextmenu/browser/contextmenu.js';
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/browser/caretOperations.js';
import 'monaco-editor/esm/vs/editor/contrib/clipboard//browser/clipboard.js';
import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController.js';
import 'monaco-editor/esm/vs/editor/contrib/format//browser/formatActions.js';
import { editor } from 'monaco-editor';
// 主题仓库 https://github.com/brijeshb42/monaco-themes
// 主题例子 https://editor.bitwiser.in/
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
import DbTable from '../DbTable.vue';
import { DbInst, TabInfo } from '../../db';
import { TabInfo } from '../../db';
import { exportCsv } from '@/common/utils/export';
import { dateStrFormat } from '@/common/utils/date';
import { dbApi } from '../../api';
const sqlCompletionKeywords = [...sqlLanguage.keywords, ...addSqlLanguage.keywords];
const sqlCompletionOperators = [...sqlLanguage.operators, ...addSqlLanguage.operators];
const sqlCompletionBuiltinFunctions = [...sqlLanguage.builtinFunctions, ...addSqlLanguage.builtinFunctions];
const sqlCompletionBuiltinVariables = [...sqlLanguage.builtinVariables, ...addSqlLanguage.builtinVariables];
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess']);
@@ -147,11 +123,12 @@ const props = defineProps({
},
});
const { themeConfig } = storeToRefs(useThemeConfig());
const token = getSession('token');
let monacoEditor = {} as editor.IStandaloneCodeEditor;
const token = getToken();
const monacoEditorRef: any = ref(null);
const dbTableRef = ref(null) as Ref;
let monacoEditor: editor.IStandaloneCodeEditor;
const state = reactive({
token,
ti: {} as TabInfo,
@@ -180,15 +157,6 @@ watch(
}
);
// 监听 themeConfig editorTheme配置文件的变化
watch(
() => themeConfig.value.editorTheme,
(val) => {
console.log('monaco editor theme change: ', val);
monaco?.editor?.setTheme(val);
}
);
onMounted(async () => {
console.log('in query mounted');
state.ti = props.data;
@@ -207,39 +175,8 @@ onMounted(async () => {
await state.ti.getNowDbInst().loadDbHints(state.ti.db);
});
self.MonacoEnvironment = {
getWorker() {
return new EditorWorker();
},
};
const initMonacoEditor = () => {
registerSqlCompletionItemProvider();
let monacoTextarea = document.getElementById('MonacoTextarea-' + state.ti.key) as HTMLElement;
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
// 初始化一些主题
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
monacoEditor = monaco.editor.create(monacoTextarea, {
language: 'sql',
theme: themeConfig.value.editorTheme,
automaticLayout: true, //自适应宽高布局
folding: false,
roundedSelection: false, // 禁用选择文本背景的圆角
matchBrackets: 'near',
linkedEditing: true,
cursorBlinking: 'smooth', // 光标闪烁样式
mouseWheelZoom: true, // 在按住Ctrl键的同时使用鼠标滚轮时在编辑器中缩放字体
overviewRulerBorder: false, // 不要滚动条的边框
tabSize: 2, // tab 缩进长度
// fontFamily: 'JetBrainsMono', // 字体 暂时不要设置,否则光标容易错位
fontWeight: 'bold',
// letterSpacing: 1, 字符间距
// quickSuggestions:false, // 禁用代码提示
minimap: {
enabled: false, // 不要小地图
},
});
monacoEditor = monacoEditorRef.value.getEditor();
// 注册快捷键ctrl + R 运行选中的sql
monacoEditor.addAction({
@@ -294,11 +231,6 @@ const initMonacoEditor = () => {
}
},
});
// 如果sql有值则默认赋值
if (state.sql) {
monacoEditor.getModel()?.setValue(state.sql);
}
};
/**
@@ -308,7 +240,7 @@ const onDragSetHeight = () => {
document.onmousemove = (e) => {
e.preventDefault();
//得到鼠标拖动的宽高距离:取绝对值
state.editorHeight = `${document.getElementById('MonacoTextarea-' + state.ti.key)!.offsetHeight + e.movementY}px`;
state.editorHeight = `${document.getElementById('MonacoTextarea-' + state.ti.key)!.clientHeight + e.movementY}px`;
state.tableDataHeight -= e.movementY;
};
document.onmouseup = () => {
@@ -590,308 +522,6 @@ const submitUpdateFields = () => {
const cancelUpdateFields = () => {
dbTableRef.value.cancelUpdateFields();
};
const registerSqlCompletionItemProvider = () => {
// 参考 https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example
self.completionItemProvider =
self.completionItemProvider ||
monaco.languages.registerCompletionItemProvider('sql', {
triggerCharacters: ['.', ' '],
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
let word = model.getWordUntilPosition(position);
const nowTab = props.data;
if (!nowTab) {
return;
}
const { db, dbId } = nowTab;
const dbInst = DbInst.getInst(dbId);
const { lineNumber, column } = position;
const { startColumn, endColumn } = word;
// 当前行文本
let lineContent = model.getLineContent(lineNumber);
// 注释行不需要代码提示
if (lineContent.startsWith('--')) {
return { suggestions: [] };
}
let range = {
startLineNumber: lineNumber,
endLineNumber: lineNumber,
startColumn,
endColumn,
};
// 光标前文本
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
});
const textBeforePointerMulti = model.getValueInRange({
startLineNumber: 1,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
});
// 光标后文本
const textAfterPointerMulti = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: column,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()),
});
// // const nextTokens = textAfterPointer.trim().split(/\s+/)
// // const nextToken = nextTokens[0].toLowerCase()
const tokens = textBeforePointer.trim().split(/\s+/);
let lastToken = tokens[tokens.length - 1].toLowerCase();
const secondToken = (tokens.length > 2 && tokens[tokens.length - 2].toLowerCase()) || '';
const dbs = (nowTab.params && nowTab.params.dbs && nowTab.params.dbs) || [];
// console.log("光标前文本:=>" + textBeforePointerMulti)
// console.log("最后输入的:=>" + lastToken)
let suggestions: languages.CompletionItem[] = [];
const tables = await dbInst.loadTables(db);
async function hintTableColumns(tableName: any, db: any) {
let dbHits = await dbInst.loadDbHints(db);
let columns = dbHits[tableName];
let suggestions: languages.CompletionItem[] = [];
columns?.forEach((a: string, index: any) => {
// 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间]
const nameAndComment = a.split(' ');
const fieldName = nameAndComment[0];
suggestions.push({
label: {
label: a,
description: 'column',
},
kind: monaco.languages.CompletionItemKind.Property,
detail: '', // 不显示detail, 否则选中时备注等会被遮挡
insertText: fieldName, // create_time
range,
sortText: 100 + index + '', // 使用表字段声明顺序排序,排序需为字符串类型
});
});
return suggestions;
}
if (lastToken.indexOf('.') > -1 || secondToken.indexOf('.') > -1) {
// 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】
let str = lastToken.substring(0, lastToken.lastIndexOf('.'));
if (lastToken.trim().startsWith('.')) {
str = secondToken;
}
// 如果字符串粘连起了如:'a.creator,a.',需要重新取出别名
let aliasArr = lastToken.split(',');
if (aliasArr.length > 1) {
lastToken = aliasArr[aliasArr.length - 1];
str = lastToken.substring(0, lastToken.lastIndexOf('.'));
if (lastToken.trim().startsWith('.')) {
str = secondToken;
}
}
// 库.表名联想
if (dbs && dbs.filter((a: any) => a === str)?.length > 0) {
let tables = await dbInst.loadTables(str);
let suggestions: languages.CompletionItem[] = [];
for (let item of tables) {
const { tableName, tableComment } = item;
suggestions.push({
label: {
label: tableName + (tableComment ? ' - ' + tableComment : ''),
description: 'table',
},
kind: monaco.languages.CompletionItemKind.File,
insertText: tableName,
range,
});
}
return { suggestions };
}
let sql = textBeforePointerMulti.split(';')[textBeforePointerMulti.split(';').length - 1] + textAfterPointerMulti.split(';')[0];
// 表别名.表字段联想
let tableInfo = getTableByAlias(sql, db, str);
if (tableInfo.tableName) {
let tableName = tableInfo.tableName;
let db = tableInfo.dbName;
// 取出表名并提示
let suggestions = await hintTableColumns(tableName, db);
if (suggestions.length > 0) {
return { suggestions };
}
}
return { suggestions: [] };
} else {
// 如果sql里含有表名则提示表字段
let mat = textBeforePointerMulti.match(/[from|update]\n*\s+\n*(\w+)\n*\s+\n*/i);
if (mat && mat.length > 1) {
let tableName = mat[1];
// 取出表名并提示
let addSuggestions = await hintTableColumns(tableName, db);
if (addSuggestions.length > 0) {
suggestions = suggestions.concat(addSuggestions);
}
}
}
// 表名联想
tables.forEach((tableMeta: any) => {
const { tableName, tableComment } = tableMeta;
suggestions.push({
label: {
label: tableName + ' - ' + tableComment,
description: 'table',
},
kind: monaco.languages.CompletionItemKind.File,
detail: tableComment,
insertText: tableName + ' ',
range,
});
});
// mysql关键字
sqlCompletionKeywords.forEach((item: any) => {
suggestions.push({
label: {
label: item,
description: 'keyword',
},
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item,
range,
});
});
// 操作符
sqlCompletionOperators.forEach((item: any) => {
suggestions.push({
label: {
label: item,
description: 'opt',
},
kind: monaco.languages.CompletionItemKind.Operator,
insertText: item,
range,
});
});
let replacedFunctions = [] as string[];
// 添加的函数
addSqlLanguage.replaceFunctions.forEach((item: any) => {
replacedFunctions.push(item.label);
suggestions.push({
label: {
label: item.label,
description: item.description,
},
kind: monaco.languages.CompletionItemKind.Function,
insertText: item.insertText,
range,
});
});
// 内置函数
sqlCompletionBuiltinFunctions.forEach((item: any) => {
replacedFunctions.indexOf(item) < 0 &&
suggestions.push({
label: {
label: item,
description: 'func',
},
kind: monaco.languages.CompletionItemKind.Function,
insertText: item,
range,
});
});
// 内置变量
sqlCompletionBuiltinVariables.forEach((item: string) => {
suggestions.push({
label: {
label: item,
description: 'var',
},
kind: monaco.languages.CompletionItemKind.Variable,
insertText: item,
range,
});
});
// 库名提示
if (dbs && dbs.length > 0) {
dbs.forEach((a: any) => {
suggestions.push({
label: {
label: a,
description: 'schema',
},
kind: monaco.languages.CompletionItemKind.Folder,
insertText: a,
range,
});
});
}
// 默认提示
return {
suggestions: suggestions,
};
},
});
};
/**
* 根据别名获取sql里的表名
* @param sql sql
* @param db 默认数据库
* @param alias 别名
*/
const getTableByAlias = (sql: string, db: string, alias: string): { dbName: string; tableName: string } => {
// 表别名:表名
let result = {};
let defName = '';
let defResult = {};
// 正则匹配取出表名和表别名
// 测试sql
/*
`select * from database.Outvisit l
left join patient p on l.patid=p.patientid
join patstatic c on l.patid=c.patid inner join patphone ph on l.patid=ph.patid
where l.name='kevin' and exsits(select 1 from pharmacywestpas pw where p.outvisitid=l.outvisitid)
unit all
select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)/gi)
*/
let match = sql.match(/(join|from)\n*\s+\n*(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)\n*/gi);
if (match && match.length > 0) {
match.forEach((a) => {
// 去掉前缀,取出
let t = a
.substring(5, a.length)
.replaceAll(/\s+/g, ' ')
.replaceAll(/\s+as\s+/gi, ' ')
.replaceAll(/\r\n/g, ' ')
.trim()
.split(/\s+/);
let withDb = t[0].split('.');
// 表名是 db名.表名
let tName = withDb.length > 1 ? withDb[1] : withDb[0];
let dbName = withDb.length > 1 ? withDb[0] : db || '';
if (t.length == 2) {
// 表别名:表名
result[t[1]] = { tableName: tName, dbName };
} else {
// 只有表名无别名 取第一个无别名的表为默认表
!defName && (defResult = { tableName: tName, dbName: db });
}
});
}
return result[alias] || defResult;
};
</script>
<style lang="scss">
@@ -905,12 +535,6 @@ select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(
text-decoration: none;
}
.sqlEditor {
font-size: 8pt;
font-weight: 600;
border: 1px solid var(--el-border-color-light, #ebeef5);
}
.update_field_active {
background-color: var(--el-color-success);
}

View File

@@ -3,6 +3,18 @@ import { dbApi } from './api';
import { getTextWidth } from '@/common/utils/string';
import SqlExecBox from './component/SqlExecBox';
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js';
import { language as addSqlLanguage } from './lang/mysql.js';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { editor, languages, Position } from 'monaco-editor';
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
const sqlCompletionKeywords = [...sqlLanguage.keywords, ...addSqlLanguage.keywords];
const sqlCompletionOperators = [...sqlLanguage.operators, ...addSqlLanguage.operators];
const sqlCompletionBuiltinFunctions = [...sqlLanguage.builtinFunctions, ...addSqlLanguage.builtinFunctions];
const sqlCompletionBuiltinVariables = [...sqlLanguage.builtinVariables, ...addSqlLanguage.builtinVariables];
const dbInstCache: Map<number, DbInst> = new Map();
export class DbInst {
@@ -463,3 +475,276 @@ export type FieldsMeta = {
// 新值
newValue: string;
};
/**
* 注册数据库表、字段等信息提示
*
* @param language 语言
* @param dbId 数据库id
* @param db 库名
* @param dbs 该库所有库名
*/
export function registerDbCompletionItemProvider(language: string, dbId: number, db: string, dbs: [] = []) {
registerCompletionItemProvider(language, {
triggerCharacters: ['.', ' '],
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
let word = model.getWordUntilPosition(position);
const dbInst = DbInst.getInst(dbId);
const { lineNumber, column } = position;
const { startColumn, endColumn } = word;
// 当前行文本
let lineContent = model.getLineContent(lineNumber);
// 注释行不需要代码提示
if (lineContent.startsWith('--')) {
return { suggestions: [] };
}
let range = {
startLineNumber: lineNumber,
endLineNumber: lineNumber,
startColumn,
endColumn,
};
// 光标前文本
const textBeforePointer = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
});
// // const nextTokens = textAfterPointer.trim().split(/\s+/)
// // const nextToken = nextTokens[0].toLowerCase()
const tokens = textBeforePointer.trim().split(/\s+/);
let lastToken = tokens[tokens.length - 1].toLowerCase();
const secondToken = (tokens.length > 2 && tokens[tokens.length - 2].toLowerCase()) || '';
// console.log("光标前文本:=>" + textBeforePointerMulti)
// console.log("最后输入的:=>" + lastToken)
let suggestions: languages.CompletionItem[] = [];
let alias = '';
if (lastToken.indexOf('.') > -1 || secondToken.indexOf('.') > -1) {
// 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】
alias = lastToken.substring(0, lastToken.lastIndexOf('.'));
if (lastToken.trim().startsWith('.')) {
alias = secondToken;
}
// 如果字符串粘连起了如:'a.creator,a.',需要重新取出别名
let aliasArr = lastToken.split(',');
if (aliasArr.length > 1) {
lastToken = aliasArr[aliasArr.length - 1];
alias = lastToken.substring(0, lastToken.lastIndexOf('.'));
if (lastToken.trim().startsWith('.')) {
alias = secondToken;
}
}
}
// 获取光标所在行之前的所有文本内容
const textBeforeCursor = model.getValueInRange({
startLineNumber: 1,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
});
// 获取光标所在行之后的所有文本内容
const textAfterCursor = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: column,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()),
});
// 检测光标前后文本中的分号位置,确定完整 SQL 语句的范围
const start = textBeforeCursor.lastIndexOf(';');
const end = textAfterCursor.indexOf(';');
let sqlStatement = '';
// 如果光标前后都有分号,则取二者之间的文本作为完整 SQL 语句
if (start !== -1 && end !== -1) {
sqlStatement = textBeforeCursor.substring(start + 1) + textAfterCursor.substring(0, end);
}
// 如果只有光标前面有分号,则取分号后的文本作为完整 SQL 语句
else if (start !== -1) {
sqlStatement = textBeforeCursor.substring(start + 1) + textAfterCursor;
}
// 如果只有光标后面有分号,则取分号前的文本作为完整 SQL 语句
else if (end !== -1) {
sqlStatement = textBeforeCursor + textAfterCursor.substring(0, end);
}
// 如果光标前后都没有分号,则取整个文本作为完整 SQL 语句
else {
sqlStatement = textBeforeCursor + textAfterCursor;
}
const tableName = getTableName4SqlCtx(sqlStatement, alias);
// 提出到表名,则将表对应的字段也添加进提示建议
if (tableName) {
let dbHits = await dbInst.loadDbHints(db);
let columns = dbHits[tableName];
columns?.forEach((a: string, index: any) => {
// 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间]
const nameAndComment = a.split(' ');
const fieldName = nameAndComment[0];
suggestions.push({
label: {
label: a,
description: 'column',
},
kind: monaco.languages.CompletionItemKind.Property,
detail: '', // 不显示detail, 否则选中时备注等会被遮挡
insertText: fieldName, // create_time
range,
sortText: 100 + index + '', // 使用表字段声明顺序排序,排序需为字符串类型
});
});
// 若存在字段提示,并且有别名,则提示字段即可,不完善后续的表名以及函数等
if (suggestions.length > 0 && alias) {
return {
suggestions: suggestions,
};
}
}
const tables = await dbInst.loadTables(db);
// 表名联想
tables.forEach((tableMeta: any, index: any) => {
const { tableName, tableComment } = tableMeta;
suggestions.push({
label: {
label: tableName + ' - ' + tableComment,
description: 'table',
},
kind: monaco.languages.CompletionItemKind.File,
detail: tableComment,
insertText: tableName + ' ',
range,
sortText: 300 + index + '',
});
});
// mysql关键字
sqlCompletionKeywords.forEach((item: any) => {
suggestions.push({
label: {
label: item,
description: 'keyword',
},
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item,
range,
});
});
// 操作符
sqlCompletionOperators.forEach((item: any) => {
suggestions.push({
label: {
label: item,
description: 'opt',
},
kind: monaco.languages.CompletionItemKind.Operator,
insertText: item,
range,
});
});
let replacedFunctions = [] as string[];
// 添加的函数
addSqlLanguage.replaceFunctions.forEach((item: any) => {
replacedFunctions.push(item.label);
suggestions.push({
label: {
label: item.label,
description: item.description,
},
kind: monaco.languages.CompletionItemKind.Function,
insertText: item.insertText,
range,
});
});
// 内置函数
sqlCompletionBuiltinFunctions.forEach((item: any) => {
replacedFunctions.indexOf(item) < 0 &&
suggestions.push({
label: {
label: item,
description: 'func',
},
kind: monaco.languages.CompletionItemKind.Function,
insertText: item,
range,
});
});
// 内置变量
sqlCompletionBuiltinVariables.forEach((item: string) => {
suggestions.push({
label: {
label: item,
description: 'var',
},
kind: monaco.languages.CompletionItemKind.Variable,
insertText: item,
range,
});
});
// 库名提示
if (dbs && dbs.length > 0) {
dbs.forEach((a: any) => {
suggestions.push({
label: {
label: a,
description: 'schema',
},
kind: monaco.languages.CompletionItemKind.Folder,
insertText: a,
range,
});
});
}
// 默认提示
return {
suggestions: suggestions,
};
},
});
}
function getTableName4SqlCtx(sql: string, alias: string = '') {
// 去除多余的换行、空格和制表符
sql = sql.replace(/[\r\n\s\t]+/g, ' ');
// 提取所有可能的表名和别名
const regex = /(?:(?:FROM|JOIN|UPDATE)\s+(\S+)\s+(?:AS\s+)?(\S+))/gi;
let matches;
const tables = [];
// 使用正则表达式匹配所有的表和别名
while ((matches = regex.exec(sql)) !== null) {
const tableName = matches[1].replace(/[`"]/g, '');
const tableAlias = matches[2] ? matches[2].replace(/[`"]/g, '') : tableName;
tables.push({ tableName, tableAlias });
}
// console.log('sql....', sql);
// console.log('alias....', alias);
// console.log('parset tables...', tables);
if (alias) {
// 如果指定了别名参数,则返回对应的表名
const table = tables.find((t) => t.tableAlias === alias);
return table ? table.tableName : '';
} else {
// 如果未指定别名参数,则返回第一个表名
return tables.length > 0 ? tables[0].tableName : '';
}
}

View File

@@ -124,7 +124,7 @@ import { formatByteSize } from '@/common/utils/format';
import { dbApi } from '../api';
import SqlExecBox from '../component/SqlExecBox';
import config from '@/common/config';
import { getSession } from '@/common/utils/storage';
import { getToken } from '@/common/utils/storage';
import { isTrue } from '@/common/assert';
const DbTableEdit = defineAsyncComponent(() => import('./DbTableEdit.vue'));
@@ -259,9 +259,7 @@ const dump = (db: string) => {
const a = document.createElement('a');
a.setAttribute(
'href',
`${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&token=${getSession(
'token'
)}`
`${config.baseApiUrl}/dbs/${props.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(',')}&token=${getToken()}`
);
a.click();
state.showDumpInfo = false;

View File

@@ -4,13 +4,13 @@
<el-form :model="form" ref="machineForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签:">
<el-form-item prop="tagId" label="标签">
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="name" label="名称:" required>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="ip" label="ip:" required>
<el-form-item prop="ip" label="ip" required>
<el-col :span="18">
<el-input :disabled="form.id" v-model.trim="form.ip" placeholder="主机ip" auto-complete="off"> </el-input>
</el-col>
@@ -20,36 +20,36 @@
</el-col>
</el-form-item>
<el-form-item prop="username" label="用户名:">
<el-form-item prop="username" label="用户名">
<el-input v-model.trim="form.username" placeholder="请输授权用户名" autocomplete="new-password"> </el-input>
</el-form-item>
<el-form-item label="认证方式:">
<el-form-item label="认证方式">
<el-select @change="changeAuthMethod" style="width: 100%" v-model="state.authType" placeholder="请选认证方式">
<el-option key="1" label="密码" :value="1"> </el-option>
<el-option key="2" label="授权凭证" :value="2"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="state.authType == 1" prop="password" label="密码:">
<el-form-item v-if="state.authType == 1" prop="password" label="密码">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
</el-input>
</el-form-item>
<el-form-item v-if="state.authType == 2" prop="authCertId" label="授权凭证:" required>
<el-form-item v-if="state.authType == 2" prop="authCertId" label="授权凭证" required>
<auth-cert-select ref="authCertSelectRef" v-model="form.authCertId" />
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-form-item prop="remark" label="备注">
<el-input type="textarea" v-model="form.remark"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="enableRecorder" label="终端回放:">
<el-form-item prop="enableRecorder" label="终端回放">
<el-checkbox v-model="form.enableRecorder" :true-label="1" :false-label="-1"></el-checkbox>
</el-form-item>
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>

View File

@@ -81,7 +81,6 @@ import { ref, toRefs, reactive, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { machineApi } from './api';
import { ScriptResultEnum } from './enums';
import { notEmpty } from '@/common/assert';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
const props = defineProps({

View File

@@ -1,6 +1,6 @@
import Api from '@/common/Api';
import config from '@/common/config';
import { getSession } from '@/common/utils/storage';
import { getToken } from '@/common/utils/storage';
export const machineApi = {
// 获取权限列表
@@ -63,5 +63,5 @@ export const cronJobApi = {
};
export function getMachineTerminalSocketUrl(machineId: any) {
return `${config.baseWsUrl}/machines/${machineId}/terminal?token=${getSession('token')}`;
return `${config.baseWsUrl}/machines/${machineId}/terminal?token=${getToken()}`;
}

View File

@@ -2,27 +2,27 @@
<div>
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" :destroy-on-close="true">
<el-form ref="acForm" :rules="rules" :model="form" label-width="auto">
<el-form-item prop="name" label="名称:" required>
<el-form-item prop="name" label="名称" required>
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item prop="authMethod" label="认证方式:" required>
<el-form-item prop="authMethod" label="认证方式" required>
<el-select style="width: 100%" v-model="form.authMethod" placeholder="请选择认证方式">
<el-option key="1" label="密码" :value="1"> </el-option>
<el-option key="2" label="密钥" :value="2"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="form.authMethod == 1" prop="password" label="密码:">
<el-form-item v-if="form.authMethod == 1" prop="password" label="密码">
<el-input type="password" show-password clearable v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
</el-input>
</el-form-item>
<el-form-item v-if="form.authMethod == 2" prop="password" label="秘钥:">
<el-form-item v-if="form.authMethod == 2" prop="password" label="秘钥">
<el-input type="textarea" :rows="5" v-model="form.password" placeholder="请将私钥文件内容拷贝至此"> </el-input>
</el-form-item>
<el-form-item v-if="form.authMethod == 2" prop="passphrase" label="秘钥密码:">
<el-form-item v-if="form.authMethod == 2" prop="passphrase" label="秘钥密码">
<el-input type="password" v-model="form.passphrase"> </el-input>
</el-form-item>
<el-form-item label="备注:">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
</el-form-item>
</el-form>

View File

@@ -5,7 +5,7 @@
<el-row class="mb10">
<el-breadcrumb separator-icon="ArrowRight">
<el-breadcrumb-item v-for="path in filePathNav">
<el-breadcrumb-item v-for="path in filePathNav" :key="path">
<el-link @click="setFiles(path.path)" style="font-weight: bold">{{ path.name }}</el-link>
</el-breadcrumb-item>
</el-breadcrumb>
@@ -131,7 +131,7 @@
<el-button-group v-if="state.copyOrMvFile.paths.length > 0" size="small" class="ml5">
<el-tooltip effect="customized" raw-content placement="top">
<template #content>
<div v-for="path in state.copyOrMvFile.paths">{{ path }}</div>
<div v-for="path in state.copyOrMvFile.paths" v-bind:key="path">{{ path }}</div>
</template>
<el-button @click="pasteFile" type="primary"
@@ -157,7 +157,7 @@
<SvgIcon :size="15" name="document" />
</span>
<span class="ml5" style="display: inline-block; width: 300px">
<span class="ml5" style="display: inline-block; width: 90%">
<div v-if="scope.row.nameEdit">
<el-input
@keyup.enter="fileRename(scope.row)"
@@ -195,15 +195,6 @@
操作
</template>
<template #default="scope">
<el-link
@click="downloadFile(scope.row)"
v-if="scope.row.type == '-'"
v-auth="'machine:file:write'"
type="primary"
icon="download"
:underline="false"
></el-link>
<el-link
@click="deleteFile([scope.row])"
v-if="!dontOperate(scope.row)"
@@ -211,7 +202,18 @@
type="danger"
icon="delete"
:underline="false"
title="删除"
></el-link>
<el-link
@click="downloadFile(scope.row)"
v-if="scope.row.type == '-'"
v-auth="'machine:file:write'"
type="primary"
icon="download"
:underline="false"
class="ml10"
title="下载"
></el-link>
<el-popover placement="top-start" :title="`${scope.row.path}-文件详情`" :width="520" trigger="click" @show="showFileStat(scope.row)">
@@ -244,10 +246,10 @@
width="400px"
>
<div>
<el-form-item prop="name" label="名称:">
<el-form-item prop="name" label="名称">
<el-input v-model.trim="createFileDialog.name" placeholder="请输入名称" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="type" label="类型:">
<el-form-item prop="type" label="类型">
<el-radio-group v-model="createFileDialog.type">
<el-radio label="d">文件夹</el-radio>
<el-radio label="-">文件</el-radio>
@@ -272,7 +274,7 @@ import { ref, toRefs, reactive, onMounted, computed } from 'vue';
import { ElMessage, ElMessageBox, ElInput } from 'element-plus';
import { machineApi } from '../api';
import { getSession } from '@/common/utils/storage';
import { getToken } from '@/common/utils/storage';
import config from '@/common/config';
import { isTrue } from '@/common/assert';
import MachineFileContent from './MachineFileContent.vue';
@@ -285,11 +287,11 @@ const props = defineProps({
isFolder: { type: Boolean, default: true },
});
const token = getSession('token');
const token = getToken();
const folderUploadRef: any = ref();
const folderType = 'd';
const fileType = '-';
// 路径分隔符
const pathSep = '/';
@@ -597,6 +599,7 @@ const deleteFile = async (files: any) => {
ElMessage.success('删除成功');
refresh();
} catch (e) {
//
} finally {
state.loading = false;
}

View File

@@ -4,7 +4,7 @@
<el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签:" required>
<el-form-item prop="tagId" label="标签" required>
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
@@ -23,7 +23,7 @@
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>

View File

@@ -4,20 +4,20 @@
<el-form :model="form" ref="redisForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签:" required>
<el-form-item prop="tagId" label="标签" required>
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="name" label="名称:" required>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入redis名称" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="mode" label="mode:" required>
<el-form-item prop="mode" label="mode" required>
<el-select style="width: 100%" v-model="form.mode" placeholder="请选择模式">
<el-option label="standalone" value="standalone"> </el-option>
<el-option label="cluster" value="cluster"> </el-option>
<el-option label="sentinel" value="sentinel"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-form-item prop="host" label="host" required>
<el-input
v-model.trim="form.host"
placeholder="请输入host:portsentinel模式为: mastername=sentinelhost:port若集群或哨兵需设多个节点可使用','分割"
@@ -25,10 +25,10 @@
type="textarea"
></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名:">
<el-form-item prop="username" label="用户名">
<el-input v-model.trim="form.username" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-form-item prop="password" label="密码">
<el-input
type="password"
show-password
@@ -44,7 +44,7 @@
</template></el-input
>
</el-form-item>
<el-form-item prop="db" label="库号:" required>
<el-form-item prop="db" label="库号" required>
<el-select
@change="changeDb"
:disabled="form.mode == 'cluster'"
@@ -58,13 +58,13 @@
<el-option v-for="db in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]" :key="db" :label="db" :value="db" />
</el-select>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<el-form-item prop="sshTunnelMachineId" label="SSH隧道">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>

View File

@@ -35,10 +35,10 @@
<el-dialog width="400px" title="团队编辑" :before-close="cancelSaveTeam" v-model="addTeamDialog.visible">
<el-form ref="teamForm" :model="addTeamDialog.form" label-width="auto">
<el-form-item prop="name" label="团队名:" required>
<el-form-item prop="name" label="团队名" required>
<el-input v-model="addTeamDialog.form.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="备注:">
<el-form-item label="备注">
<el-input v-model="addTeamDialog.form.remark" auto-complete="off"></el-input>
</el-form-item>
</el-form>
@@ -52,7 +52,7 @@
<el-dialog width="500px" :title="showTagDialog.title" :before-close="closeTagDialog" v-model="showTagDialog.visible">
<el-form label-width="auto">
<el-form-item prop="tag" label="标签:">
<el-form-item prop="tag" label="标签">
<el-tree-select
ref="tagTreeRef"
style="width: 100%"
@@ -111,7 +111,7 @@
<el-dialog width="400px" title="添加成员" :before-close="cancelAddMember" v-model="showMemDialog.addVisible">
<el-form :model="showMemDialog.memForm" label-width="auto">
<el-form-item label="账号:">
<el-form-item label="账号">
<el-select
style="width: 100%"
remote
@@ -344,7 +344,7 @@ const saveTags = async () => {
closeTagDialog();
};
const tagTreeNodeCheck = (data: any) => {
const tagTreeNodeCheck = () => {
// const node = tagTreeRef.value.getNode(data.id);
// console.log(node);
// // state.showTagDialog.tagTreeTeams = [16]

View File

@@ -179,7 +179,7 @@ import { dateFormat } from '@/common/utils/date';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '@/store/userInfo';
import config from '@/common/config';
import { getSession } from '@/common/utils/storage';
import { getToken } from '@/common/utils/storage';
const { userInfo } = storeToRefs(useUserInfo());
const state = reactive({
@@ -248,7 +248,7 @@ const bindOAuth2 = () => {
var iLeft = (window.screen.width - 10 - width) / 2; //获得窗口的水平位置;
// 小窗口打开oauth2鉴权
let oauthWindow = window.open(
config.baseApiUrl + '/auth/oauth2/bind?token=' + getSession('token'),
config.baseApiUrl + '/auth/oauth2/bind?token=' + getToken(),
'oauth2',
`height=${height},width=${width},top=${iTop},left=${iLeft},location=no`
);

View File

@@ -1,20 +1,20 @@
<template>
<div class="account-dialog">
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :show-close="false" width="35%" :destroy-on-close="true">
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :show-close="false" width="500px" :destroy-on-close="true">
<el-form :model="form" ref="accountForm" :rules="rules" label-width="auto">
<el-form-item prop="name" label="姓名:">
<el-form-item prop="name" label="姓名">
<el-input v-model.trim="form.name" placeholder="请输入姓名" auto-complete="off" clearable></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名:">
<el-form-item prop="username" label="用户名">
<el-input
:disabled="edit"
v-model.trim="form.username"
placeholder="请输入账号用户名,密码默认与账号名一致"
placeholder="请输入账号用户名,密码默认与用户名一致"
auto-complete="off"
clearable
></el-input>
</el-form-item>
<el-form-item v-if="edit" prop="password" label="密码:">
<el-form-item v-if="edit" prop="password" label="密码">
<el-input type="password" v-model.trim="form.password" placeholder="输入密码可修改用户密码" autocomplete="new-password"></el-input>
</el-form-item>
</el-form>

View File

@@ -2,13 +2,13 @@
<div>
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="750px" :destroy-on-close="true">
<el-form ref="configForm" :model="form" label-width="auto">
<el-form-item prop="name" label="配置项:" required>
<el-form-item prop="name" label="配置项" required>
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item prop="key" label="配置key:" required>
<el-form-item prop="key" label="配置key" required>
<el-input :disabled="form.id != null" v-model="form.key"></el-input>
</el-form-item>
<el-form-item prop="permission" label="权限:">
<el-form-item prop="permission" label="权限">
<el-select
style="width: 100%"
remote
@@ -59,7 +59,7 @@
<!-- <el-form-item prop="value" label="配置值:" required>
<el-input v-model="form.value"></el-input>
</el-form-item> -->
<el-form-item label="备注:">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
</el-form-item>
</el-form>

View File

@@ -2,13 +2,13 @@
<div class="role-dialog">
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" :destroy-on-close="true">
<el-form ref="roleForm" :model="form" label-width="auto">
<el-form-item prop="name" label="角色名称:" required>
<el-form-item prop="name" label="角色名称" required>
<el-input v-model="form.name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="code" label="角色code:" required>
<el-form-item prop="code" label="角色code" required>
<el-input :disabled="form.id != null" v-model="form.code" placeholder="COMMON开头则为所有账号共有角色" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="角色描述:">
<el-form-item label="角色描述">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入角色描述"></el-input>
</el-form-item>
</el-form>

View File

@@ -29,7 +29,7 @@ const viteConfig: UserConfig = {
open: VITE_OPEN,
proxy: {
'/api': {
target: 'http://localhost:8888',
target: 'http://localhost:18888',
ws: true,
changeOrigin: true,
},
@@ -49,6 +49,7 @@ const viteConfig: UserConfig = {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
echarts: ['echarts'],
monaco: ['monaco-editor'],
},
},
},

View File

@@ -2,11 +2,6 @@
# yarn lockfile v1
"@babel/parser@^7.16.4":
version "7.19.1"
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.19.1.tgz"
integrity sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A==
"@babel/parser@^7.20.15", "@babel/parser@^7.21.3":
version "7.21.8"
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8"
@@ -144,6 +139,18 @@
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.11.tgz#6526c7e1b40d5b9f0a222c6b767c22f6fb97aa57"
integrity sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==
"@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
resolved "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
dependencies:
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.5.1":
version "4.9.1"
resolved "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.9.1.tgz#449dfa81a57a1d755b09aa58d826c1262e4283b4"
integrity sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==
"@eslint/eslintrc@^2.0.0":
version "2.0.0"
resolved "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.0.0.tgz#943309d8697c52fc82c076e90c1c74fbbe69dbff"
@@ -231,10 +238,10 @@
resolved "https://registry.npmmirror.com/@types/antlr4/-/antlr4-4.7.0.tgz"
integrity sha512-WdyHH4PHxBQkeWoRTbuC/dvf0QErJpJE4UpESQSRmKoMER15DCLFHAHQjkwevMKQie0kqawS/eTY563GGMbz/g==
"@types/json-schema@^7.0.7":
version "7.0.9"
resolved "https://registry.npmmirror.com/@types/json-schema/download/@types/json-schema-7.0.9.tgz?cache=0&sync_timestamp=1637266073261&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40types%2Fjson-schema%2Fdownload%2F%40types%2Fjson-schema-7.0.9.tgz"
integrity sha1-l+3JA36gw4WFMgsolk3eOznkZg0=
"@types/json-schema@^7.0.12":
version "7.0.13"
resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85"
integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==
"@types/lodash-es@^4.17.6":
version "4.17.6"
@@ -263,100 +270,110 @@
resolved "https://registry.npmmirror.com/@types/nprogress/download/@types/nprogress-0.2.0.tgz"
integrity sha1-hsWTaC1BmSEqBQnMPE1WK7vW5F8=
"@types/sortablejs@^1.10.6":
version "1.10.7"
resolved "https://registry.npmmirror.com/@types/sortablejs/download/@types/sortablejs-1.10.7.tgz"
integrity sha1-q5A5yFQp8FFpVextvAuyATlBexU=
"@types/semver@^7.5.0":
version "7.5.3"
resolved "https://registry.npmmirror.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04"
integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==
"@types/sortablejs@^1.15.3":
version "1.15.3"
resolved "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.3.tgz#b9c0e2740100ae94919c9f138a38600c8f8124ea"
integrity sha512-v+zh6TZP/cLeMUK0MDx1onp8e7Jk2/4iTQ7sb/n80rTAvBm14yJkpOEfJdrTCkHiF7IZbPjxGX2NRJfogRoYIg==
"@types/web-bluetooth@^0.0.15":
version "0.0.15"
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz"
integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==
"@typescript-eslint/eslint-plugin@^4.23.0":
version "4.33.0"
resolved "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/download/@typescript-eslint/eslint-plugin-4.33.0.tgz"
integrity sha1-wk3HyAacdwa8QNmfb6h+3LIAUnY=
"@typescript-eslint/eslint-plugin@^6.7.4":
version "6.7.4"
resolved "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz#057338df21b6062c2f2fc5999fbea8af9973ac6d"
integrity sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==
dependencies:
"@typescript-eslint/experimental-utils" "4.33.0"
"@typescript-eslint/scope-manager" "4.33.0"
debug "^4.3.1"
functional-red-black-tree "^1.0.1"
ignore "^5.1.8"
regexpp "^3.1.0"
semver "^7.3.5"
tsutils "^3.21.0"
"@eslint-community/regexpp" "^4.5.1"
"@typescript-eslint/scope-manager" "6.7.4"
"@typescript-eslint/type-utils" "6.7.4"
"@typescript-eslint/utils" "6.7.4"
"@typescript-eslint/visitor-keys" "6.7.4"
debug "^4.3.4"
graphemer "^1.4.0"
ignore "^5.2.4"
natural-compare "^1.4.0"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/experimental-utils@4.33.0":
version "4.33.0"
resolved "https://registry.npmmirror.com/@typescript-eslint/experimental-utils/download/@typescript-eslint/experimental-utils-4.33.0.tgz"
integrity sha1-byp4akIJ+iIimJ6TgLUzGygQ9/0=
"@typescript-eslint/parser@^6.7.4":
version "6.7.4"
resolved "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-6.7.4.tgz#23d1dd4fe5d295c7fa2ab651f5406cd9ad0bd435"
integrity sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==
dependencies:
"@types/json-schema" "^7.0.7"
"@typescript-eslint/scope-manager" "4.33.0"
"@typescript-eslint/types" "4.33.0"
"@typescript-eslint/typescript-estree" "4.33.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/scope-manager" "6.7.4"
"@typescript-eslint/types" "6.7.4"
"@typescript-eslint/typescript-estree" "6.7.4"
"@typescript-eslint/visitor-keys" "6.7.4"
debug "^4.3.4"
"@typescript-eslint/parser@^4.23.0":
version "4.33.0"
resolved "https://registry.npmmirror.com/@typescript-eslint/parser/download/@typescript-eslint/parser-4.33.0.tgz"
integrity sha1-3+eXVw2WlOVgUo0Y7srYbIx0SJk=
"@typescript-eslint/scope-manager@6.7.4":
version "6.7.4"
resolved "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz#a484a17aa219e96044db40813429eb7214d7b386"
integrity sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==
dependencies:
"@typescript-eslint/scope-manager" "4.33.0"
"@typescript-eslint/types" "4.33.0"
"@typescript-eslint/typescript-estree" "4.33.0"
debug "^4.3.1"
"@typescript-eslint/types" "6.7.4"
"@typescript-eslint/visitor-keys" "6.7.4"
"@typescript-eslint/scope-manager@4.33.0":
version "4.33.0"
resolved "https://registry.npmmirror.com/@typescript-eslint/scope-manager/download/@typescript-eslint/scope-manager-4.33.0.tgz"
integrity sha1-045JKA2YPody4pEhz4xukiHygKM=
"@typescript-eslint/type-utils@6.7.4":
version "6.7.4"
resolved "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz#847cd3b59baf948984499be3e0a12ff07373e321"
integrity sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==
dependencies:
"@typescript-eslint/types" "4.33.0"
"@typescript-eslint/visitor-keys" "4.33.0"
"@typescript-eslint/typescript-estree" "6.7.4"
"@typescript-eslint/utils" "6.7.4"
debug "^4.3.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/types@4.33.0":
version "4.33.0"
resolved "https://registry.npmmirror.com/@typescript-eslint/types/download/@typescript-eslint/types-4.33.0.tgz"
integrity sha1-oeWQNqO1OuhDDO6/KpGdx/mvbXI=
"@typescript-eslint/types@6.7.4":
version "6.7.4"
resolved "https://registry.npmmirror.com/@typescript-eslint/types/-/types-6.7.4.tgz#5d358484d2be986980c039de68e9f1eb62ea7897"
integrity sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==
"@typescript-eslint/typescript-estree@4.33.0":
version "4.33.0"
resolved "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/download/@typescript-eslint/typescript-estree-4.33.0.tgz"
integrity sha1-DftRwpCPaMXAjYKu/q8WahfCRgk=
"@typescript-eslint/typescript-estree@6.7.4":
version "6.7.4"
resolved "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz#f2baece09f7bb1df9296e32638b2e1130014ef1a"
integrity sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==
dependencies:
"@typescript-eslint/types" "4.33.0"
"@typescript-eslint/visitor-keys" "4.33.0"
debug "^4.3.1"
globby "^11.0.3"
is-glob "^4.0.1"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/types" "6.7.4"
"@typescript-eslint/visitor-keys" "6.7.4"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/visitor-keys@4.33.0":
version "4.33.0"
resolved "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/download/@typescript-eslint/visitor-keys-4.33.0.tgz"
integrity sha1-KiL3ekFgQom3oYZYbp7EjKku8d0=
"@typescript-eslint/utils@6.7.4":
version "6.7.4"
resolved "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-6.7.4.tgz#2236f72b10e38277ee05ef06142522e1de470ff2"
integrity sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==
dependencies:
"@typescript-eslint/types" "4.33.0"
eslint-visitor-keys "^2.0.0"
"@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0"
"@typescript-eslint/scope-manager" "6.7.4"
"@typescript-eslint/types" "6.7.4"
"@typescript-eslint/typescript-estree" "6.7.4"
semver "^7.5.4"
"@vitejs/plugin-vue@^4.0.0":
version "4.0.0"
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz#93815beffd23db46288c787352a8ea31a0c03e5e"
integrity sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==
"@vue/compiler-core@3.2.39":
version "3.2.39"
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.39.tgz"
integrity sha512-mf/36OWXqWn0wsC40nwRRGheR/qoID+lZXbIuLnr4/AngM0ov8Xvv8GHunC0rKRIkh60bTqydlqTeBo49rlbqw==
"@typescript-eslint/visitor-keys@6.7.4":
version "6.7.4"
resolved "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz#80dfecf820fc67574012375859085f91a4dff043"
integrity sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/shared" "3.2.39"
estree-walker "^2.0.2"
source-map "^0.6.1"
"@typescript-eslint/types" "6.7.4"
eslint-visitor-keys "^3.4.1"
"@vitejs/plugin-vue@^4.4.0":
version "4.4.0"
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz#8ae96573236cdb12de6850a6d929b5537ec85390"
integrity sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==
"@vue/compiler-core@3.3.4":
version "3.3.4"
@@ -368,14 +385,6 @@
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-dom@3.2.39":
version "3.2.39"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.39.tgz"
integrity sha512-HMFI25Be1C8vLEEv1hgEO1dWwG9QQ8LTTPmCkblVJY/O3OvWx6r1+zsox5mKPMGvqYEZa6l8j+xgOfUspgo7hw==
dependencies:
"@vue/compiler-core" "3.2.39"
"@vue/shared" "3.2.39"
"@vue/compiler-dom@3.3.4":
version "3.3.4"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz#f56e09b5f4d7dc350f981784de9713d823341151"
@@ -384,7 +393,7 @@
"@vue/compiler-core" "3.3.4"
"@vue/shared" "3.3.4"
"@vue/compiler-sfc@3.3.4":
"@vue/compiler-sfc@3.3.4", "@vue/compiler-sfc@^3.3.4":
version "3.3.4"
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz#b19d942c71938893535b46226d602720593001df"
integrity sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==
@@ -400,30 +409,6 @@
postcss "^8.1.10"
source-map-js "^1.0.2"
"@vue/compiler-sfc@^3.0.11":
version "3.2.39"
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.39.tgz"
integrity sha512-fqAQgFs1/BxTUZkd0Vakn3teKUt//J3c420BgnYgEOoVdTwYpBTSXCMJ88GOBCylmUBbtquGPli9tVs7LzsWIA==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.39"
"@vue/compiler-dom" "3.2.39"
"@vue/compiler-ssr" "3.2.39"
"@vue/reactivity-transform" "3.2.39"
"@vue/shared" "3.2.39"
estree-walker "^2.0.2"
magic-string "^0.25.7"
postcss "^8.1.10"
source-map "^0.6.1"
"@vue/compiler-ssr@3.2.39":
version "3.2.39"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.39.tgz"
integrity sha512-EoGCJ6lincKOZGW+0Ky4WOKsSmqL7hp1ZYgen8M7u/mlvvEQUaO9tKKOy7K43M9U2aA3tPv0TuYYQFrEbK2eFQ==
dependencies:
"@vue/compiler-dom" "3.2.39"
"@vue/shared" "3.2.39"
"@vue/compiler-ssr@3.3.4":
version "3.3.4"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz#9d1379abffa4f2b0cd844174ceec4a9721138777"
@@ -437,17 +422,6 @@
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
"@vue/reactivity-transform@3.2.39":
version "3.2.39"
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.39.tgz"
integrity sha512-HGuWu864zStiWs9wBC6JYOP1E00UjMdDWIG5W+FpUx28hV3uz9ODOKVNm/vdOy/Pvzg8+OcANxAVC85WFBbl3A==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.39"
"@vue/shared" "3.2.39"
estree-walker "^2.0.2"
magic-string "^0.25.7"
"@vue/reactivity-transform@3.3.4":
version "3.3.4"
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929"
@@ -491,11 +465,6 @@
"@vue/compiler-ssr" "3.3.4"
"@vue/shared" "3.3.4"
"@vue/shared@3.2.39":
version "3.2.39"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.39.tgz"
integrity sha512-D3dl2ZB9qE6mTuWPk9RlhDeP1dgNRUKC3NJxji74A4yL8M2MwlhLKUC/49WHjrNzSPug58fWx/yFbaTzGAQSBw==
"@vue/shared@3.3.4":
version "3.3.4"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz#06e83c5027f464eef861c329be81454bc8b70780"
@@ -566,9 +535,9 @@ antlr4@4.7.2:
integrity sha512-vZA1xYufXLe3LX+ja9rIVxjRmILb1x3k7KYZHltRbfJtXjJ1DlFIqt+CbPYmghx0EuzY9DajiDw+MdyEt1qAsQ==
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.nlark.com/anymatch/download/anymatch-3.1.2.tgz"
integrity sha1-wFV8CWrzLxBhmPT04qODU343hxY=
version "3.1.3"
resolved "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
@@ -583,10 +552,10 @@ array-union@^2.1.0:
resolved "https://registry.npm.taobao.org/array-union/download/array-union-2.1.0.tgz?cache=0&sync_timestamp=1614624262896&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Farray-union%2Fdownload%2Farray-union-2.1.0.tgz"
integrity sha1-t5hCCtvrHego2ErNii4j0+/oXo0=
asciinema-player@^3.5.0:
version "3.5.0"
resolved "https://registry.npmmirror.com/asciinema-player/-/asciinema-player-3.5.0.tgz#a4d1c01b56b72dfb6834e9ff90fee5c9652c7dae"
integrity sha512-o4B2AscBuCZo4+JB9TBGrfZ7GQL99wsbm08WwmuNJTPd1lyLQJq8wgacnBsdvb2sC0K875ScYr8T5XmfeH/6dg==
asciinema-player@^3.6.2:
version "3.6.2"
resolved "https://registry.npmmirror.com/asciinema-player/-/asciinema-player-3.6.2.tgz#f62133f8d38875839881cd15ded713c6022021bd"
integrity sha512-698O3/Vm2+V6uFlc6oYma67IZByQsiNpduhEGhuqrxBmKpIYpgouLNNJ3R8DrRPTNNMISHfnLgvAp1x8ChgrTw==
dependencies:
"@babel/runtime" "^7.21.0"
solid-js "^1.3.0"
@@ -617,8 +586,8 @@ balanced-match@^1.0.0:
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.nlark.com/binary-extensions/download/binary-extensions-2.2.0.tgz"
integrity sha1-dfUC7q+f/eQvyYgpZFvk6na9ni0=
resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
brace-expansion@^1.1.7:
version "1.1.11"
@@ -649,9 +618,9 @@ chalk@^4.0.0:
supports-color "^7.1.0"
"chokidar@>=3.0.0 <4.0.0":
version "3.5.2"
resolved "https://registry.npmmirror.com/chokidar/download/chokidar-3.5.2.tgz"
integrity sha1-26OXb8rbAW9m/TZQIdkWANAcHnU=
version "3.5.3"
resolved "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
@@ -735,7 +704,7 @@ dayjs@^1.11.3:
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.3.tgz"
integrity sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==
debug@^4.1.1, debug@^4.3.1, debug@^4.3.2:
debug@^4.1.1, debug@^4.3.2:
version "4.3.3"
resolved "https://registry.npmmirror.com/debug/download/debug-4.3.3.tgz"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
@@ -783,10 +752,10 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dotenv@^10.0.0:
version "10.0.0"
resolved "https://registry.nlark.com/dotenv/download/dotenv-10.0.0.tgz"
integrity sha1-PUInuPuV+BCWzdK2ZlP7LHCFuoE=
dotenv@^16.3.1:
version "16.3.1"
resolved "https://registry.npmmirror.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
dt-sql-parser@^4.0.0-beta.3.2:
version "4.0.0-beta.3.2"
@@ -804,10 +773,10 @@ echarts@^5.4.0:
tslib "2.3.0"
zrender "5.4.0"
element-plus@^2.3.12:
version "2.3.12"
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.12.tgz#d3c91d0c701b2b3e67d06a351cb0c42dcc46460e"
integrity sha512-fAWpbKCyt+l1dsqSNPOs/F/dBN4Wp5CGAyxbiS5zqDwI4q3QPM+LxLU2h3GUHMIBtMGCvmsG98j5HPMkTKkvcA==
element-plus@^2.4.0:
version "2.4.0"
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.4.0.tgz#e79249ac4c0a606d377c2f31ad553aa992286fe3"
integrity sha512-yJEa8LXkGOOgkfkeqMMEdeX/Dc8EH9qPcRuX91dlhSXxgCKKbp9tH3QFTOG99ibZsrN/Em62nh7ddvbc7I1frw==
dependencies:
"@ctrl/tinycolor" "^3.4.1"
"@element-plus/icons-vue" "^2.0.6"
@@ -873,14 +842,6 @@ eslint-plugin-vue@^8.2.0:
semver "^7.3.5"
vue-eslint-parser "^8.0.1"
eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.npmmirror.com/eslint-scope/download/eslint-scope-5.1.1.tgz?cache=0&sync_timestamp=1637466913662&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-scope%2Fdownload%2Feslint-scope-5.1.1.tgz"
integrity sha1-54blmmbLkrP2wfsNUIqrF0hI9Iw=
dependencies:
esrecurse "^4.3.0"
estraverse "^4.1.1"
eslint-scope@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/eslint-scope/download/eslint-scope-6.0.0.tgz?cache=0&sync_timestamp=1637466831846&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Feslint-scope%2Fdownload%2Feslint-scope-6.0.0.tgz"
@@ -924,6 +885,11 @@ eslint-visitor-keys@^3.4.0:
resolved "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc"
integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==
eslint-visitor-keys@^3.4.1:
version "3.4.3"
resolved "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint@^8.35.0:
version "8.35.0"
resolved "https://registry.npmmirror.com/eslint/-/eslint-8.35.0.tgz#fffad7c7e326bae606f0e8f436a6158566d42323"
@@ -1018,11 +984,6 @@ esrecurse@^4.3.0:
dependencies:
estraverse "^5.2.0"
estraverse@^4.1.1:
version "4.3.0"
resolved "https://registry.npmmirror.com/estraverse/download/estraverse-4.3.0.tgz?cache=0&sync_timestamp=1635237716974&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Festraverse%2Fdownload%2Festraverse-4.3.0.tgz"
integrity sha1-OYrT88WiSUi+dyXoPRGn3ijNvR0=
estraverse@^5.1.0, estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.npmmirror.com/estraverse/download/estraverse-5.3.0.tgz?cache=0&sync_timestamp=1635237716974&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Festraverse%2Fdownload%2Festraverse-5.3.0.tgz"
@@ -1043,10 +1004,10 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.nlark.com/fast-deep-equal/download/fast-deep-equal-3.1.3.tgz"
integrity sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=
fast-glob@^3.1.1:
version "3.2.7"
resolved "https://registry.nlark.com/fast-glob/download/fast-glob-3.2.7.tgz"
integrity sha1-/Wy3otfpqnp4RhEehaGW1rL3ZqE=
fast-glob@^3.2.9:
version "3.3.1"
resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
@@ -1135,11 +1096,6 @@ fsevents@~2.3.2:
resolved "https://registry.npmmirror.com/fsevents/download/fsevents-2.3.2.tgz"
integrity sha1-ilJveLj99GI7cJ4Ll1xSwkwC/Ro=
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.npm.taobao.org/functional-red-black-tree/download/functional-red-black-tree-1.0.1.tgz?cache=0&sync_timestamp=1577806294691&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffunctional-red-black-tree%2Fdownload%2Ffunctional-red-black-tree-1.0.1.tgz"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.npmmirror.com/glob-parent/download/glob-parent-5.1.2.tgz"
@@ -1173,16 +1129,16 @@ globals@^13.19.0:
dependencies:
type-fest "^0.20.2"
globby@^11.0.3:
version "11.0.4"
resolved "https://registry.nlark.com/globby/download/globby-11.0.4.tgz"
integrity sha1-LLr/d8Lypi5x6bKBOme5ejowAaU=
globby@^11.1.0:
version "11.1.0"
resolved "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
dependencies:
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.1.1"
ignore "^5.1.4"
merge2 "^1.3.0"
fast-glob "^3.2.9"
ignore "^5.2.0"
merge2 "^1.4.1"
slash "^3.0.0"
good-listener@^1.2.2:
@@ -1197,25 +1153,25 @@ grapheme-splitter@^1.0.4:
resolved "https://registry.npmmirror.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
graphemer@^1.4.0:
version "1.4.0"
resolved "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.nlark.com/has-flag/download/has-flag-4.0.0.tgz?cache=0&sync_timestamp=1626715907927&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fhas-flag%2Fdownload%2Fhas-flag-4.0.0.tgz"
integrity sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=
ignore@^5.1.4, ignore@^5.1.8:
version "5.1.9"
resolved "https://registry.npmmirror.com/ignore/download/ignore-5.1.9.tgz?cache=0&sync_timestamp=1635926740448&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fignore%2Fdownload%2Fignore-5.1.9.tgz"
integrity sha1-nsGly+jhRG7GDUQgBg1Dqm5zgvs=
ignore@^5.2.0:
ignore@^5.2.0, ignore@^5.2.4:
version "5.2.4"
resolved "https://registry.npmmirror.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/immutable/download/immutable-4.0.0.tgz?cache=0&sync_timestamp=1633650644342&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fimmutable%2Fdownload%2Fimmutable-4.0.0.tgz"
integrity sha1-uG943mre82CDle+yaakUYnl+LCM=
version "4.3.4"
resolved "https://registry.npmmirror.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0"
@@ -1245,8 +1201,8 @@ inherits@2:
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.nlark.com/is-binary-path/download/is-binary-path-2.1.0.tgz"
integrity sha1-6h9/O4DwZCNug0cPhsCcJU+0Wwk=
resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
@@ -1304,11 +1260,6 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.nlark.com/json-stable-stringify-without-jsonify/download/json-stable-stringify-without-jsonify-1.0.1.tgz"
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
klona@^2.0.4:
version "2.0.5"
resolved "https://registry.npmmirror.com/klona/download/klona-2.0.5.tgz"
integrity sha1-0WZXTZAHY5XZljqnqSj6u412r7w=
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.nlark.com/levn/download/levn-0.4.1.tgz"
@@ -1351,13 +1302,6 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
magic-string@^0.25.7:
version "0.25.9"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz"
integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
dependencies:
sourcemap-codec "^1.4.8"
magic-string@^0.30.0:
version "0.30.0"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529"
@@ -1370,10 +1314,10 @@ memoize-one@^6.0.0:
resolved "https://registry.npmmirror.com/memoize-one/download/memoize-one-6.0.0.tgz?cache=0&sync_timestamp=1634697208428&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fmemoize-one%2Fdownload%2Fmemoize-one-6.0.0.tgz"
integrity sha1-slkbhx7YKUiu5HJ9xqvO7qyMEEU=
merge2@^1.3.0:
merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.npm.taobao.org/merge2/download/merge2-1.4.1.tgz"
integrity sha1-Q2iJL4hekHRVpv19xVwMnUBJkK4=
resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
version "4.0.4"
@@ -1414,10 +1358,10 @@ mitt@^3.0.1:
resolved "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
monaco-editor@^0.43.0:
version "0.43.0"
resolved "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.43.0.tgz#cb02a8d23d1249ad00b7cffe8bbecc2ac09d4baf"
integrity sha512-cnoqwQi/9fml2Szamv1XbSJieGJ1Dc8tENVMD26Kcfl7xGQWp7OBKMjlwKVGYFJ3/AXJjSOGvcqK7Ry/j9BM1Q==
monaco-editor@^0.44.0:
version "0.44.0"
resolved "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.44.0.tgz#3c0fe3655923bbf7dd647057302070b5095b6c59"
integrity sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==
monaco-sql-languages@^0.11.0:
version "0.11.0"
@@ -1468,15 +1412,10 @@ nearley@^2.20.1:
railroad-diagrams "^1.0.0"
randexp "0.4.6"
neo-async@^2.6.2:
version "2.6.2"
resolved "https://registry.npm.taobao.org/neo-async/download/neo-async-2.6.2.tgz"
integrity sha1-tKr7k+OustgXTKU88WOrfXMIMF8=
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.npm.taobao.org/normalize-path/download/normalize-path-3.0.0.tgz"
integrity sha1-Dc1p/yOhybEf0JeDFmRKA4ghamU=
resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
normalize-wheel-es@^1.2.0:
version "1.2.0"
@@ -1553,15 +1492,20 @@ picocolors@^1.0.0:
resolved "https://registry.npmmirror.com/picocolors/download/picocolors-1.0.0.tgz?cache=0&sync_timestamp=1634093378416&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fpicocolors%2Fdownload%2Fpicocolors-1.0.0.tgz"
integrity sha1-y1vcdP8/UYkiNur3nWi8RFZKuBw=
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
picomatch@^2.0.4, picomatch@^2.2.1:
version "2.3.1"
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^2.2.3:
version "2.3.0"
resolved "https://registry.nlark.com/picomatch/download/picomatch-2.3.0.tgz?cache=0&sync_timestamp=1621648246651&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fpicomatch%2Fdownload%2Fpicomatch-2.3.0.tgz"
integrity sha1-8fBh3o9qS/AiiS4tEoI0+5gwKXI=
pinia@^2.1.6:
version "2.1.6"
resolved "https://registry.npmmirror.com/pinia/-/pinia-2.1.6.tgz#e88959f14b61c4debd9c42d0c9944e2875cbe0fa"
integrity sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==
pinia@^2.1.7:
version "2.1.7"
resolved "https://registry.npmmirror.com/pinia/-/pinia-2.1.7.tgz#4cf5420d9324ca00b7b4984d3fbf693222115bbc"
integrity sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==
dependencies:
"@vue/devtools-api" "^6.5.0"
vue-demi ">=0.14.5"
@@ -1629,8 +1573,8 @@ randexp@0.4.6:
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.npm.taobao.org/readdirp/download/readdirp-3.6.0.tgz"
integrity sha1-dKNwvYVxFuJFspzJc0DNQxoCpsc=
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
@@ -1639,7 +1583,7 @@ regenerator-runtime@^0.13.11:
resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regexpp@^3.1.0, regexpp@^3.2.0:
regexpp@^3.2.0:
version "3.2.0"
resolved "https://registry.nlark.com/regexpp/download/regexpp-3.2.0.tgz?cache=0&sync_timestamp=1623668872577&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fregexpp%2Fdownload%2Fregexpp-3.2.0.tgz"
integrity sha1-BCWido2PI7rXDKS5BGH6LxIT4bI=
@@ -1680,18 +1624,10 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
sass-loader@^13.2.0:
version "13.2.0"
resolved "https://registry.npmmirror.com/sass-loader/-/sass-loader-13.2.0.tgz#80195050f58c9aac63b792fa52acb6f5e0f6bdc3"
integrity sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg==
dependencies:
klona "^2.0.4"
neo-async "^2.6.2"
sass@^1.62.0:
version "1.62.0"
resolved "https://registry.npmmirror.com/sass/-/sass-1.62.0.tgz#3686b2195b93295d20765135e562366b33ece37d"
integrity sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==
sass@^1.69.0:
version "1.69.0"
resolved "https://registry.npmmirror.com/sass/-/sass-1.69.0.tgz#5195075371c239ed556280cf2f5944d234f42679"
integrity sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
@@ -1721,6 +1657,13 @@ semver@^7.3.6:
dependencies:
lru-cache "^6.0.0"
semver@^7.5.4:
version "7.5.4"
resolved "https://registry.npmmirror.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.nlark.com/shebang-command/download/shebang-command-2.0.0.tgz"
@@ -1745,30 +1688,20 @@ solid-js@^1.3.0:
dependencies:
csstype "^3.1.0"
sortablejs@^1.13.0:
version "1.14.0"
resolved "https://registry.nlark.com/sortablejs/download/sortablejs-1.14.0.tgz?cache=0&sync_timestamp=1625423971526&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsortablejs%2Fdownload%2Fsortablejs-1.14.0.tgz"
integrity sha1-bS4XzL2yX0ZHNN9iHU811Ks1s9g=
sortablejs@^1.15.0:
version "1.15.0"
resolved "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.0.tgz#53230b8aa3502bb77a29e2005808ffdb4a5f7e2a"
integrity sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/source-map-js/download/source-map-js-1.0.1.tgz?cache=0&sync_timestamp=1636400753943&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fsource-map-js%2Fdownload%2Fsource-map-js-1.0.1.tgz"
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
source-map-js@^1.0.2:
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
source-map-js@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/source-map-js/download/source-map-js-1.0.1.tgz?cache=0&sync_timestamp=1636400753943&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fsource-map-js%2Fdownload%2Fsource-map-js-1.0.1.tgz"
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
sql-formatter@^12.1.2:
version "12.1.2"
@@ -1814,23 +1747,16 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
ts-api-utils@^1.0.1:
version "1.0.3"
resolved "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331"
integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.nlark.com/tslib/download/tslib-2.3.0.tgz"
integrity sha1-gDuM2rPhK6WBpMpByIObuw2ssJ4=
tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.nlark.com/tslib/download/tslib-1.14.1.tgz"
integrity sha1-zy04vcNKE0vK8QkcQfZhni9nLQA=
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.npm.taobao.org/tsutils/download/tsutils-3.21.0.tgz"
integrity sha1-tIcX05TOpsHglpg+7Vjp1hcVtiM=
dependencies:
tslib "^1.8.1"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.nlark.com/type-check/download/type-check-0.4.0.tgz"
@@ -1855,10 +1781,10 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
vite@^4.4.9:
version "4.4.9"
resolved "https://registry.npmmirror.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d"
integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==
vite@^4.4.11:
version "4.4.11"
resolved "https://registry.npmmirror.com/vite/-/vite-4.4.11.tgz#babdb055b08c69cfc4c468072a2e6c9ca62102b0"
integrity sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==
dependencies:
esbuild "^0.18.10"
postcss "^8.4.27"
@@ -1896,10 +1822,10 @@ vue-eslint-parser@^8.0.1:
lodash "^4.17.21"
semver "^7.3.5"
vue-eslint-parser@^9.1.1:
version "9.1.1"
resolved "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.1.1.tgz#3f4859be7e9bb7edaa1dc7edb05abffee72bf3dd"
integrity sha512-C2aI/r85Q6tYcz4dpgvrs4wH/MqVrRAVIdpYedrxnATDHHkb+TroeRcDpKWGZCx/OcECMWfz7tVwQ8e+Opy6rA==
vue-eslint-parser@^9.3.1:
version "9.3.1"
resolved "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz#429955e041ae5371df5f9e37ebc29ba046496182"
integrity sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==
dependencies:
debug "^4.3.4"
eslint-scope "^7.1.1"
@@ -1909,10 +1835,10 @@ vue-eslint-parser@^9.1.1:
lodash "^4.17.21"
semver "^7.3.6"
vue-router@^4.2.4:
version "4.2.4"
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.2.4.tgz#382467a7e2923e6a85f015d081e1508052c191b9"
integrity sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==
vue-router@^4.2.5:
version "4.2.5"
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a"
integrity sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==
dependencies:
"@vue/devtools-api" "^6.5.0"

View File

@@ -17,6 +17,7 @@ jwt:
# 资源密码aes加密key
aes:
key: 1111111111111111
# 若存在mysql配置优先使用mysql
mysql:
# 自动升级数据库
auto-migration: false
@@ -26,6 +27,9 @@ mysql:
db-name: mayfly-go
config: charset=utf8&loc=Local&parseTime=true
max-idle-conns: 5
sqlite:
path: ./mayfly-go.sqlite
max-idle-conns: 5
# 若同时部署多台机器则需要配置redis信息用于缓存权限码、验证码、公私钥等
# redis:
# host: localhost

View File

@@ -13,22 +13,24 @@ require (
github.com/go-sql-driver/mysql v1.7.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/gorilla/websocket v1.5.0
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231007020222-b91ee5ef3b31
github.com/lib/pq v1.10.9
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
github.com/mojocn/base64Captcha v1.3.5 //
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.6
github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.1.0
github.com/redis/go-redis/v9 v9.2.1
github.com/robfig/cron/v3 v3.0.1 //
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
github.com/stretchr/testify v1.8.4
go.mongodb.org/mongo-driver v1.12.1 // mongo
golang.org/x/crypto v0.13.0 // ssh
golang.org/x/oauth2 v0.12.0
golang.org/x/crypto v0.14.0 // ssh
golang.org/x/oauth2 v0.13.0
gopkg.in/yaml.v3 v3.0.1
// gorm
gorm.io/driver/mysql v1.5.1
gorm.io/gorm v1.25.4
gorm.io/driver/mysql v1.5.2
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5
)
require (
@@ -37,27 +39,31 @@ require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/montanaflynn/stats v0.7.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
@@ -65,12 +71,15 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/net v0.16.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect
google.golang.org/grpc v1.52.3 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
vitess.io/vitess v0.17.3 // indirect
)

View File

@@ -15,8 +15,8 @@ import (
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/otp"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/cryptox"
"mayfly-go/pkg/utils/jsonx"
"mayfly-go/pkg/ws"
"strconv"
"time"
@@ -43,7 +43,7 @@ func (a *AccountLogin) Login(rc *req.Ctx) {
username := loginForm.Username
clientIp := getIpAndRegion(rc)
rc.ReqParam = jsonx.Kvs("username", username, "ip", clientIp)
rc.ReqParam = collx.Kvs("username", username, "ip", clientIp)
originPwd, err := cryptox.DefaultRsaDecrypt(loginForm.Password, true)
biz.ErrIsNilAppendErr(err, "解密密码错误: %s")

View File

@@ -11,6 +11,7 @@ import (
"mayfly-go/pkg/cache"
"mayfly-go/pkg/otp"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/jsonx"
"mayfly-go/pkg/utils/netx"
"mayfly-go/pkg/utils/stringx"
@@ -29,7 +30,7 @@ func LastLoginCheck(account *sysentity.Account, accountLoginSecurity *config.Acc
biz.IsTrue(account.IsEnable(), "该账号不可用")
username := account.Username
res := map[string]any{
res := collx.M{
"name": account.Name,
"username": username,
"lastLoginTime": account.LastLoginTime,

View File

@@ -13,8 +13,8 @@ import (
"mayfly-go/pkg/captcha"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/cryptox"
"mayfly-go/pkg/utils/jsonx"
"strconv"
"strings"
"time"
@@ -49,7 +49,7 @@ func (a *LdapLogin) Login(rc *req.Ctx) {
username := loginForm.Username
clientIp := getIpAndRegion(rc)
rc.ReqParam = jsonx.Kvs("username", username, "ip", clientIp)
rc.ReqParam = collx.Kvs("username", username, "ip", clientIp)
originPwd, err := cryptox.DefaultRsaDecrypt(loginForm.Password, true)
biz.ErrIsNilAppendErr(err, "解密密码错误: %s")

View File

@@ -14,6 +14,7 @@ import (
"mayfly-go/pkg/cache"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/jsonx"
"mayfly-go/pkg/utils/stringx"
"net/http"
@@ -98,7 +99,7 @@ func (a *Oauth2Login) OAuth2Callback(rc *req.Ctx) {
account.Id = accountId
err = a.AccountApp.GetAccount(account, "username")
biz.ErrIsNilAppendErr(err, "该账号不存在")
rc.ReqParam = jsonx.Kvs("username", account.Username, "type", "bind")
rc.ReqParam = collx.Kvs("username", account.Username, "type", "bind")
err = a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
AccountId: accountId,
@@ -118,7 +119,7 @@ func (a *Oauth2Login) OAuth2Callback(rc *req.Ctx) {
UpdateTime: &now,
})
biz.ErrIsNilAppendErr(err, "绑定用户失败: %s")
res := map[string]any{
res := collx.M{
"action": "oauthBind",
"bind": true,
}
@@ -173,7 +174,7 @@ func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *config.Oa
biz.ErrIsNilAppendErr(err, "获取用户信息失败: %s")
clientIp := getIpAndRegion(rc)
rc.ReqParam = jsonx.Kvs("username", account.Username, "ip", clientIp, "type", "login")
rc.ReqParam = collx.Kvs("username", account.Username, "ip", clientIp, "type", "login")
res := LastLoginCheck(account, config.GetAccountLoginSecurity(), clientIp)
res["action"] = "oauthLogin"
@@ -220,7 +221,7 @@ func (a *Oauth2Login) Oauth2Unbind(rc *req.Ctx) {
// 获取oauth2登录配置信息因为有些字段是敏感字段故单独使用接口获取
func (c *Oauth2Login) Oauth2Config(rc *req.Ctx) {
oauth2LoginConfig := config.GetOauth2Login()
rc.ResData = map[string]any{
rc.ResData = collx.M{
"enable": oauth2LoginConfig.Enable,
"name": oauth2LoginConfig.Name,
}

View File

@@ -11,6 +11,7 @@ import (
redisentity "mayfly-go/internal/redis/domain/entity"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
)
type Index struct {
@@ -36,7 +37,7 @@ func (i *Index) Count(rc *req.Ctx) {
dbNum = i.DbApp.Count(&dbentity.DbQuery{TagIds: tagIds})
redisNum = i.RedisApp.Count(&redisentity.RedisQuery{TagIds: tagIds})
}
rc.ResData = map[string]any{
rc.ResData = collx.M{
"mongoNum": mongoNum,
"machineNum": machienNum,
"dbNum": dbNum,

View File

@@ -3,6 +3,8 @@ package consts
import "time"
const (
AdminId = 1
MachineConnExpireTime = 60 * time.Minute
DbConnExpireTime = 45 * time.Minute
RedisConnExpireTime = 30 * time.Minute

View File

@@ -3,25 +3,31 @@ package api
import (
"fmt"
"io"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/uniqueid"
"mayfly-go/pkg/ws"
"github.com/lib/pq"
"mayfly-go/internal/db/api/form"
"mayfly-go/internal/db/api/vo"
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/domain/entity"
msgapp "mayfly-go/internal/msg/application"
msgdto "mayfly-go/internal/msg/application/dto"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/gormx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/sqlparser"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/ws"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/xwb1989/sqlparser"
)
type Db struct {
@@ -77,9 +83,7 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
}
func (d *Db) getDbConnection(g *gin.Context) *application.DbConnection {
dbName := g.Query("db")
biz.NotEmpty(dbName, "db不能为空")
return d.DbApp.GetDbConnection(getDbId(g), dbName)
return d.DbApp.GetDbConnection(getDbId(g), getDbName(g))
}
func (d *Db) TableInfos(rc *req.Ctx) {
@@ -150,77 +154,117 @@ func (d *Db) ExecSql(rc *req.Ctx) {
rc.ResData = colAndRes
}
// progressCategory sql文件执行进度消息类型
const progressCategory = "execSqlFileProgress"
// progressMsg sql文件执行进度消息
type progressMsg struct {
Id uint64 `json:"id"`
SqlFileName string `json:"sqlFileName"`
ExecutedStatements int `json:"executedStatements"`
Terminated bool `json:"terminated"`
}
// 执行sql文件
func (d *Db) ExecSqlFile(rc *req.Ctx) {
g := rc.GinCtx
fileheader, err := g.FormFile("file")
multipart, err := g.Request.MultipartReader()
biz.ErrIsNilAppendErr(err, "读取sql文件失败: %s")
file, _ := fileheader.Open()
filename := fileheader.Filename
file, err := multipart.NextPart()
biz.ErrIsNilAppendErr(err, "读取sql文件失败: %s")
defer file.Close()
filename := file.FileName()
dbId := getDbId(g)
dbName := getDbName(g)
dbConn := d.getDbConnection(rc.GinCtx)
dbConn := d.DbApp.GetDbConnection(dbId, dbName)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.LoginAccount.Id, dbConn.Info.TagPath), "%s")
rc.ReqParam = fmt.Sprintf("%s -> filename: %s", dbConn.Info.GetLogDesc(), filename)
logExecRecord := true
// 如果执行sql文件大于该值则不记录sql执行记录
if fileheader.Size > 50*1024 {
logExecRecord = false
defer func() {
var errInfo string
switch t := recover().(type) {
case error:
errInfo = t.Error()
case string:
errInfo = t
}
if len(errInfo) > 0 {
d.MsgApp.CreateAndSend(rc.LoginAccount, msgdto.ErrSysMsg("sql脚本执行失败", fmt.Sprintf("[%s]%s执行失败: [%s]", filename, dbConn.Info.GetLogDesc(), errInfo)))
}
}()
execReq := &application.DbSqlExecReq{
DbId: dbId,
Db: dbName,
Remark: filename,
DbConn: dbConn,
LoginAccount: rc.LoginAccount,
}
go func() {
defer func() {
if err := recover(); err != nil {
var errInfo string
switch t := err.(type) {
case biz.BizError:
errInfo = t.Error()
case *biz.BizError:
errInfo = t.Error()
}
if len(errInfo) > 0 {
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrSysMsg("sql脚本执行失败", fmt.Sprintf("[%s]%s执行失败: [%s]", filename, dbConn.Info.GetLogDesc(), errInfo)))
}
}
}()
progressId := uniqueid.IncrementID()
executedStatements := 0
defer ws.SendJsonMsg(rc.LoginAccount.Id, msgdto.InfoSysMsg("sql脚本执行进度", &progressMsg{
Id: progressId,
SqlFileName: filename,
ExecutedStatements: executedStatements,
Terminated: true,
}).WithCategory(progressCategory))
execReq := &application.DbSqlExecReq{
DbId: dbId,
Db: dbName,
Remark: fileheader.Filename,
DbConn: dbConn,
LoginAccount: rc.LoginAccount,
var parser sqlparser.Parser
if dbConn.Info.Type == entity.DbTypeMysql {
parser = sqlparser.NewMysqlParser(file)
} else {
parser = sqlparser.NewPostgresParser(file)
}
ticker := time.NewTicker(time.Second * 1)
defer ticker.Stop()
for {
select {
case <-ticker.C:
ws.SendJsonMsg(rc.LoginAccount.Id, msgdto.InfoSysMsg("sql脚本执行进度", &progressMsg{
Id: progressId,
SqlFileName: filename,
ExecutedStatements: executedStatements,
Terminated: false,
}).WithCategory(progressCategory))
default:
}
tokens := sqlparser.NewTokenizer(file)
for {
stmt, err := sqlparser.ParseNext(tokens)
if err == io.EOF {
break
err = parser.Next()
if err == io.EOF {
break
}
if err != nil {
d.MsgApp.CreateAndSend(rc.LoginAccount, msgdto.ErrSysMsg("sql脚本执行失败", fmt.Sprintf("[%s][%s] 解析SQL失败: [%s]", filename, dbConn.Info.GetLogDesc(), err.Error())))
return
}
sql := parser.Current()
const prefixUse = "use "
if strings.HasPrefix(sql, prefixUse) {
dbNameExec := strings.Trim(sql[len(prefixUse):], " `;\n")
if len(dbNameExec) > 0 {
dbConn = d.DbApp.GetDbConnection(dbId, dbNameExec)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.LoginAccount.Id, dbConn.Info.TagPath), "%s")
execReq.DbConn = dbConn
}
if err != nil {
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrSysMsg("sql脚本执行失败", fmt.Sprintf("[%s][%s] 解析SQL失败: [%s]", filename, dbConn.Info.GetLogDesc(), err.Error())))
return
}
sql := sqlparser.String(stmt)
}
// 需要记录执行记录
const maxRecordStatements = 64
if executedStatements < maxRecordStatements {
execReq.Sql = sql
// 需要记录执行记录
if logExecRecord {
_, err = d.DbSqlExecApp.Exec(execReq)
} else {
_, err = dbConn.Exec(sql)
}
if err != nil {
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrSysMsg("sql脚本执行失败", fmt.Sprintf("[%s][%s] -> sql=[%s] 执行失败: [%s]", filename, dbConn.Info.GetLogDesc(), sql, err.Error())))
return
}
_, err = d.DbSqlExecApp.Exec(execReq)
} else {
_, err = dbConn.Exec(sql)
}
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.SuccessSysMsg("sql脚本执行成功", fmt.Sprintf("[%s]执行完成 -> %s", filename, dbConn.Info.GetLogDesc())))
}()
if err != nil {
d.MsgApp.CreateAndSend(rc.LoginAccount, msgdto.ErrSysMsg("sql脚本执行失败", fmt.Sprintf("[%s][%s] -> sql=[%s] 执行失败: [%s]", filename, dbConn.Info.GetLogDesc(), sql, err.Error())))
return
}
executedStatements++
}
d.MsgApp.CreateAndSend(rc.LoginAccount, msgdto.SuccessSysMsg("sql脚本执行成功", fmt.Sprintf("[%s]执行完成 -> %s", filename, dbConn.Info.GetLogDesc())))
}
// 数据库dump
@@ -266,16 +310,14 @@ func (d *Db) DumpSql(rc *req.Ctx) {
var msg string
if err := recover(); err != nil {
switch t := err.(type) {
case biz.BizError:
msg = t.Error()
case *biz.BizError:
case error:
msg = t.Error()
}
}
if len(msg) > 0 {
msg = "数据库导出失败: " + msg
writer.WriteString(msg)
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrSysMsg("数据库导出失败", msg))
d.MsgApp.CreateAndSend(rc.LoginAccount, msgdto.ErrSysMsg("数据库导出失败", msg))
}
writer.Close()
}()
@@ -283,26 +325,47 @@ func (d *Db) DumpSql(rc *req.Ctx) {
d.dumpDb(writer, dbId, dbName, tables, needStruct, needData, len(dbNames) > 1)
}
rc.ReqParam = fmt.Sprintf("DB[id=%d, tag=%s, name=%s, databases=%s, tables=%s, dumpType=%s]", db.Id, db.TagPath, db.Name, dbNamesStr, tablesStr, dumpType)
rc.ReqParam = collx.Kvs("db", db, "databases", dbNamesStr, "tables", tablesStr, "dumpType", dumpType)
}
func escapeSql(dbType string, sql string) string {
if dbType == entity.DbTypePostgres {
return pq.QuoteLiteral(sql)
} else {
sql = strings.ReplaceAll(sql, `\`, `\\`)
sql = strings.ReplaceAll(sql, `'`, `''`)
return "'" + sql + "'"
}
}
func quoteTable(dbType string, table string) string {
if dbType == entity.DbTypePostgres {
return "\"" + table + "\""
} else {
return "`" + table + "`"
}
}
func (d *Db) dumpDb(writer *gzipWriter, dbId uint64, dbName string, tables []string, needStruct bool, needData bool, switchDb bool) {
dbConn := d.DbApp.GetDbConnection(dbId, dbName)
writer.WriteString("-- ----------------------------")
writer.WriteString("\n-- ----------------------------")
writer.WriteString("\n-- 导出平台: mayfly-go")
writer.WriteString(fmt.Sprintf("\n-- 导出时间: %s ", time.Now().Format("2006-01-02 15:04:05")))
writer.WriteString(fmt.Sprintf("\n-- 导出数据库: %s ", dbName))
writer.WriteString("\n-- ----------------------------\n")
writer.TryFlush()
if switchDb {
switch dbConn.Info.Type {
case entity.DbTypeMysql:
writer.WriteString(fmt.Sprintf("use `%s`;\n", dbName))
writer.WriteString(fmt.Sprintf("USE `%s`;\n", dbName))
default:
biz.IsTrue(false, "数据库类型必须为 %s", entity.DbTypeMysql)
biz.IsTrue(false, "同时导出多个数据库,数据库类型必须为 %s", entity.DbTypeMysql)
}
}
if dbConn.Info.Type == entity.DbTypeMysql {
writer.WriteString("\nSET FOREIGN_KEY_CHECKS = 0;\n")
}
dbMeta := dbConn.GetMeta()
if len(tables) == 0 {
ti := dbMeta.GetTableInfos()
@@ -313,23 +376,22 @@ func (d *Db) dumpDb(writer *gzipWriter, dbId uint64, dbName string, tables []str
}
for _, table := range tables {
writer.TryFlush()
quotedTable := quoteTable(dbConn.Info.Type, table)
if needStruct {
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表结构: %s \n-- ----------------------------\n", table))
writer.WriteString(fmt.Sprintf("DROP TABLE IF EXISTS `%s`;\n", table))
writer.WriteString(fmt.Sprintf("DROP TABLE IF EXISTS %s;\n", quotedTable))
writer.WriteString(dbMeta.GetCreateTableDdl(table) + ";\n")
}
if !needData {
continue
}
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表记录: %s \n-- ----------------------------\n", table))
writer.WriteString("BEGIN;\n")
insertSql := "INSERT INTO `%s` VALUES (%s);\n"
insertSql := "INSERT INTO %s VALUES (%s);\n"
dbMeta.WalkTableRecord(table, func(record map[string]any, columns []string) {
var values []string
writer.TryFlush()
for _, column := range columns {
value := record[column]
if value == nil {
@@ -338,17 +400,18 @@ func (d *Db) dumpDb(writer *gzipWriter, dbId uint64, dbName string, tables []str
}
strValue, ok := value.(string)
if ok {
values = append(values, fmt.Sprintf("%#v", strValue))
strValue = escapeSql(dbConn.Info.Type, strValue)
values = append(values, strValue)
} else {
values = append(values, stringx.AnyToStr(value))
}
}
writer.WriteString(fmt.Sprintf(insertSql, table, strings.Join(values, ", ")))
writer.TryFlush()
writer.WriteString(fmt.Sprintf(insertSql, quotedTable, strings.Join(values, ", ")))
})
writer.WriteString("COMMIT;\n")
writer.TryFlush()
}
if dbConn.Info.Type == entity.DbTypeMysql {
writer.WriteString("\nSET FOREIGN_KEY_CHECKS = 1;\n")
}
}

Some files were not shown because too many files have changed in this diff Show More