22 Commits

Author SHA1 Message Date
meilin.huang
fb3f89c594 feat: 版本升级 2022-09-07 15:20:34 +08:00
meilin.huang
e7a66378ea feat: 终端勾选隧道保存报错问题修复&其他优化 2022-09-07 11:18:47 +08:00
meilin.huang
2f88b48973 feat: 新增终端回放记录&其他小优化 2022-08-29 21:43:24 +08:00
meilin.huang
7761fe0288 feat: 新增系统全局配置&修改账号密码 2022-08-26 20:15:36 +08:00
may-fly
09e6bdcf7e Merge pull request #9 from 1ch0/master
fix: store mongodb password incorrectly
2022-08-26 10:20:12 +08:00
1ch0
61a4d87f59 perf: hide mongodb passwords when printing logs 2022-08-26 10:01:08 +08:00
1ch0
c219ec33b0 fix: store mongodb password incorrectly 2022-08-26 09:58:01 +08:00
may-fly
fd86f36218 Merge pull request #8 from 1ch0/master
Perf: hide mongodb passwords when printing logs
2022-08-25 18:13:38 +08:00
Echo Cheng
efac41f392 Perf: hide mongodb passwords when printing logs 2022-08-25 17:58:07 +08:00
meilin.huang
52df61ae0d refactor: 构建发行版脚本优化 2022-08-24 21:36:16 +08:00
meilin.huang
cf2bc6785c feat: 使用embed将静态资源打包进二进制文件&其他小功能优化 2022-08-24 20:55:42 +08:00
meilin.huang
98a4c92576 feat: redis支持sentinel 2022-08-23 18:50:07 +08:00
meilin.huang
b1ee9b65ff fix: 小问题优化 2022-08-21 21:00:28 +08:00
meilin.huang
99cc4c5e5e fix: script type调整 2022-08-19 22:00:37 +08:00
meilin.huang
226bb8f089 fix: 终端断连提示 2022-08-19 21:42:26 +08:00
meilin.huang
37ed5134e8 feat: 机器脚本入参支持选择框 2022-08-15 20:14:02 +08:00
meilin.huang
0f54d4a472 refactor: code rewiew&功能小优化 2022-08-13 19:31:16 +08:00
Coder慌
64805360d6 update README.md. 2022-08-11 05:52:35 +00:00
Coder慌
7f69fe2ad9 update README.md. 2022-08-11 02:58:06 +00:00
meilin.huang
f913510d3c refactor: code review 2022-08-10 19:46:17 +08:00
meilin.huang
f2d9e7786d refactor: redis hash类型使用hscan获取数据 2022-08-05 21:41:21 +08:00
meilin.huang
e1afb1ed54 fix: sql脚本默认账号密码调整&终端默认配色调整 2022-08-04 20:47:13 +08:00
168 changed files with 4186 additions and 1255 deletions

View File

@@ -1,7 +1,25 @@
# 🌈mayfly-go # 🌈mayfly-go
<p align="center">
<a href="https://gitee.com/objs/mayfly-go" target="_blank">
<img src="https://gitee.com/objs/mayfly-go/badge/star.svg?theme=white" alt="star"/>
<img src="https://gitee.com/objs/mayfly-go/badge/fork.svg" alt="fork"/>
</a>
<a href="https://github.com/may-fly/mayfly-go" target="_blank">
<img src="https://img.shields.io/github/stars/may-fly/mayfly-go.svg?style=social" alt="github star"/>
<img src="https://img.shields.io/github/forks/may-fly/mayfly-go.svg?style=social" alt="github fork"/>
</a>
<a href="https://github.com/golang/go" target="_blank">
<img src="https://img.shields.io/badge/Golang-1.18%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">
</a>
</p>
### 介绍 ### 介绍
简单基于DDD(领域驱动设计)分层架构实现的web版 **linux、数据库mysql postgres、redis(单机 集群)、mongo统一管理操作平台** web版 **linux(终端[终端回放] 文件 脚本 进程)、数据库mysql postgres、redis(单机 哨兵 集群)、mongo统一管理操作平台**
### 开发语言与主要框架 ### 开发语言与主要框架
@@ -10,7 +28,7 @@
### 交流及问题反馈加 QQ 群 ### 交流及问题反馈加 QQ 群
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?jump_from=webapi">119699946</a> <a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=IdJSHW0jTMhmWFHBUS9a83wxtrxDDhFj&jump_from=webapi">119699946</a>
### 系统相关资料 ### 系统相关资料

View File

@@ -30,9 +30,9 @@ function buildWeb() {
echo_yellow "-------------------打包前端开始-------------------" echo_yellow "-------------------打包前端开始-------------------"
yarn run build yarn run build
if [ "${copy2Server}" == "1" ] ; then if [ "${copy2Server}" == "2" ] ; then
echo_green '将打包后的静态文件拷贝至server/static' echo_green '将打包后的静态文件拷贝至server/static/static'
rm -rf ${server_folder}/static && mkdir -p ${server_folder}/static && cp -r ${web_folder}/dist/* ${server_folder}/static rm -rf ${server_folder}/static/static && mkdir -p ${server_folder}/static/static && cp -r ${web_folder}/dist/* ${server_folder}/static/static
fi fi
echo_yellow ">>>>>>>>>>>>>>>>>>>打包前端结束<<<<<<<<<<<<<<<<<<<<\n" echo_yellow ">>>>>>>>>>>>>>>>>>>打包前端结束<<<<<<<<<<<<<<<<<<<<\n"
} }
@@ -44,6 +44,7 @@ function build() {
toFolder=$1 toFolder=$1
os=$2 os=$2
arch=$3 arch=$3
copyStatic=$4
echo_yellow "-------------------${os}-${arch}打包构建开始-------------------" echo_yellow "-------------------${os}-${arch}打包构建开始-------------------"
@@ -67,8 +68,10 @@ function build() {
echo_green "移动二进制文件至'${toFolder}'" echo_green "移动二进制文件至'${toFolder}'"
mv ${server_folder}/${execFileName} ${toFolder} mv ${server_folder}/${execFileName} ${toFolder}
echo_green "拷贝前端静态页面至'${toFolder}/static'" if [ "${copy2Server}" == "1" ] ; then
mkdir -p ${toFolder}/static && cp -r ${web_folder}/dist/* ${toFolder}/static echo_green "拷贝前端静态页面至'${toFolder}/static'"
mkdir -p ${toFolder}/static && cp -r ${web_folder}/dist/* ${toFolder}/static
fi
echo_green "拷贝脚本等资源文件[config.yml、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]" echo_green "拷贝脚本等资源文件[config.yml、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml ${toFolder} cp ${server_folder}/config.yml ${toFolder}
@@ -81,15 +84,19 @@ function build() {
} }
function buildLinuxAmd64() { function buildLinuxAmd64() {
build "$1/mayfly-go-linux-amd64" "linux" "amd64" build "$1/mayfly-go-linux-amd64" "linux" "amd64" $2
} }
function buildLinuxArm64() { function buildLinuxArm64() {
build "$1/mayfly-go-linux-arm64" "linux" "arm64" build "$1/mayfly-go-linux-arm64" "linux" "arm64" $2
} }
function buildWindows() { function buildWindows() {
build "$1/mayfly-go-windows" "windows" "amd64" build "$1/mayfly-go-windows" "windows" "amd64" $2
}
function buildMac() {
build "$1/mayfly-go-mac" "darwin" "amd64" $2
} }
function runBuild() { function runBuild() {
@@ -103,34 +110,34 @@ function runBuild() {
cd ${toPath} cd ${toPath}
toPath=`pwd` toPath=`pwd`
read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static]: " runBuildWeb read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
read -p "请选择构建版本[0|其他->全部 1->linux-amd64 2->linux-arm64 3->windows]: " buildType read -p "请选择构建版本[0|其他->全部 1->linux-amd64 2->linux-arm64 3->windows 4->mac]: " buildType
if [ "${runBuildWeb}" == "1" ];then
buildWeb if [ "${runBuildWeb}" == "1" ] || [ "${runBuildWeb}" == "2" ] ; then
fi buildWeb ${runBuildWeb}
if [ "${runBuildWeb}" == "2" ];then
buildWeb 1
fi fi
if [ "${buildType}" == "1" ];then case ${buildType} in
buildLinuxAmd64 ${toPath} "1")
exit; buildLinuxAmd64 ${toPath} ${runBuildWeb}
fi ;;
"2")
if [ "${buildType}" == "2" ];then buildLinuxArm64 ${toPath} ${runBuildWeb}
buildLinuxArm64 ${toPath} ;;
exit; "3")
fi buildWindows ${toPath} ${runBuildWeb}
;;
if [ "${buildType}" == "3" ];then "4")
buildWindows ${toPath} buildMac ${toPath} ${runBuildWeb}
exit; ;;
fi *)
buildLinuxAmd64 ${toPath} ${runBuildWeb}
buildLinuxAmd64 ${toPath} buildLinuxArm64 ${toPath} ${runBuildWeb}
buildLinuxArm64 ${toPath} buildWindows ${toPath} ${runBuildWeb}
buildWindows ${toPath} buildMac ${toPath} ${runBuildWeb}
;;
esac
} }
runBuild runBuild

View File

@@ -18,8 +18,7 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="text/javascript" src="./config.js"></script> <script type="application/javascript" src="./config.js"></script>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
<!-- <script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=wsijQt8sLXrCW71YesmispvYHitfG9gv&s=1"></script> -->
</body> </body>
</html> </html>

View File

@@ -7,21 +7,21 @@
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.0.6", "@element-plus/icons-vue": "^2.0.9",
"axios": "^0.27.2", "axios": "^0.27.2",
"codemirror": "^5.65.5", "codemirror": "^5.65.5",
"countup.js": "^2.0.7", "countup.js": "^2.0.7",
"cropperjs": "^1.5.11", "cropperjs": "^1.5.11",
"echarts": "^5.3.3", "echarts": "^5.3.3",
"element-plus": "^2.2.12", "element-plus": "^2.2.16",
"jsencrypt": "^3.2.1", "jsencrypt": "^3.2.1",
"jsoneditor": "^9.9.0", "jsoneditor": "^9.9.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"screenfull": "^5.1.0", "screenfull": "^6.0.2",
"sortablejs": "^1.13.0", "sortablejs": "^1.13.0",
"sql-formatter": "^8.2.0", "sql-formatter": "^9.2.0",
"vue": "^3.2.37", "vue": "^3.2.37",
"vue-clipboard3": "^1.0.1", "vue-clipboard3": "^1.0.1",
"vue-router": "^4.1.2", "vue-router": "^4.1.2",
@@ -38,6 +38,7 @@
"@typescript-eslint/parser": "^4.23.0", "@typescript-eslint/parser": "^4.23.0",
"@vitejs/plugin-vue": "^2.3.3", "@vitejs/plugin-vue": "^2.3.3",
"@vue/compiler-sfc": "^3.0.11", "@vue/compiler-sfc": "^3.0.11",
"asciinema-player": "^3.0.1",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"eslint": "^8.5.0", "eslint": "^8.5.0",
"eslint-plugin-vue": "^8.2.0", "eslint-plugin-vue": "^8.2.0",

View File

@@ -6,4 +6,5 @@ declare module '*.vue' {
} }
declare module 'codemirror'; declare module 'codemirror';
declare module 'sql-formatter'; declare module 'sql-formatter';
declare module 'jsoneditor'; declare module 'jsoneditor';
declare module 'asciinema-player';

View File

@@ -11,6 +11,8 @@ import { useStore } from '@/store/index.ts';
import { getLocal } from '@/common/utils/storage.ts'; import { getLocal } from '@/common/utils/storage.ts';
import LockScreen from '@/views/layout/lockScreen/index.vue'; import LockScreen from '@/views/layout/lockScreen/index.vue';
import Setings from '@/views/layout/navBars/breadcrumb/setings.vue'; import Setings from '@/views/layout/navBars/breadcrumb/setings.vue';
import Watermark from '@/common/utils/wartermark.ts';
export default defineComponent({ export default defineComponent({
name: 'app', name: 'app',
components: { LockScreen, Setings }, components: { LockScreen, Setings },
@@ -57,6 +59,8 @@ export default defineComponent({
() => route.path, () => route.path,
() => { () => {
nextTick(() => { nextTick(() => {
// 路由变化更新水印
Watermark.use();
document.title = `${route.meta.title} - ${getThemeConfig.value.globalTitle}` || getThemeConfig.value.globalTitle; document.title = `${route.meta.title} - ${getThemeConfig.value.globalTitle}` || getThemeConfig.value.globalTitle;
}); });
} }

View File

@@ -1,10 +1,11 @@
import request from './request' import request from './request'
export default { export default {
login: (param: any) => request.request('POST', '/sys/accounts/login', param, null), login: (param: any) => request.request('POST', '/sys/accounts/login', param),
changePwd: (param: any) => request.request('POST', '/sys/accounts/change-pwd', param, null), changePwd: (param: any) => request.request('POST', '/sys/accounts/change-pwd', param),
getPublicKey: () => request.request('GET', '/common/public-key', null, null), getPublicKey: () => request.request('GET', '/common/public-key'),
captcha: () => request.request('GET', '/sys/captcha', null, null), getConfigValue: (param: any) => request.request('GET', '/sys/configs/value', param),
logout: (param: any) => request.request('POST', '/sys/accounts/logout/{token}', param, null), captcha: () => request.request('GET', '/sys/captcha'),
getMenuRoute: (param: any) => request.request('Get', '/sys/resources/account', param, null) logout: (param: any) => request.request('POST', '/sys/accounts/logout/{token}', param),
getMenuRoute: (param: any) => request.request('Get', '/sys/resources/account', param)
} }

View File

@@ -28,7 +28,6 @@ export async function RsaEncrypt(value: any) {
if (encryptor != null) { if (encryptor != null) {
return encryptor.encrypt(value) return encryptor.encrypt(value)
} }
console.log(value)
encryptor = new JSEncrypt() encryptor = new JSEncrypt()
const publicKey = await getRsaPublicKey() as string; const publicKey = await getRsaPublicKey() as string;
notBlank(publicKey, "获取公钥失败") notBlank(publicKey, "获取公钥失败")

View File

@@ -0,0 +1,48 @@
import openApi from './openApi';
// 登录是否使用验证码配置key
const UseLoginCaptchaConfigKey = "UseLoginCaptcha"
const UseWartermarkConfigKey = "UseWartermark"
/**
* 获取系统配置值
*
* @param key 配置key
* @returns 配置值
*/
export async function getConfigValue(key: string) : Promise<string> {
return await openApi.getConfigValue({key}) as string
}
/**
* 获取bool类型系统配置值
*
* @param key 配置key
* @param defaultValue 默认值
* @returns 是否为ture1: true其他: false
*/
export async function getBoolConfigValue(key :string, defaultValue :boolean) : Promise<boolean> {
const value = await getConfigValue(key)
if (!value) {
return defaultValue;
}
return value == "1";
}
/**
* 是否使用登录验证码
*
* @returns
*/
export async function useLoginCaptcha() : Promise<boolean> {
return await getBoolConfigValue(UseLoginCaptchaConfigKey, true)
}
/**
* 是否启用水印
*
* @returns
*/
export async function useWartermark() : Promise<boolean> {
return await getBoolConfigValue(UseWartermarkConfigKey, true)
}

View File

@@ -35,3 +35,22 @@ export function removeSession(key: string) {
export function clearSession() { export function clearSession() {
window.sessionStorage.clear(); 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

@@ -1,21 +1,26 @@
import { getUseWatermark4Session, getUserInfo4Session } from '@/common/utils/storage.ts';
import { dateFormat } from '@/common/utils/date.ts'
// 页面添加水印效果 // 页面添加水印效果
const setWatermark = (str: any) => { const setWatermark = (str: any) => {
const id = '1.23452384164.123412416'; const id = '1.23452384164.123412416';
if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id) as any); if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id) as any);
const can = document.createElement('canvas'); const can = document.createElement('canvas');
can.width = 250; can.width = 400;
can.height = 180; can.height = 250;
const cans: any = can.getContext('2d'); const cans: any = can.getContext('2d');
cans.rotate((-20 * Math.PI) / 180); cans.rotate((-20 * Math.PI) / 180);
cans.font = '12px Vedana'; cans.font = '14px Vedana';
cans.fillStyle = 'rgba(200, 200, 200, 0.30)'; cans.fillStyle = 'rgba(200, 200, 200, 0.35)';
cans.textAlign = 'center'; cans.textAlign = 'left';
cans.textBaseline = 'Middle'; cans.textBaseline = 'Middle';
cans.fillText(str, can.width / 10, can.height / 2); // cans.fillText('mayfly go', can.width / 4, can.height )
cans.fillText(str, can.width / 8, can.height / 2);
const div = document.createElement('div'); const div = document.createElement('div');
div.id = id; div.id = id;
div.style.pointerEvents = 'none'; div.style.pointerEvents = 'none';
div.style.top = '35px'; div.style.top = '30px';
div.style.left = '0px'; div.style.left = '0px';
div.style.position = 'fixed'; div.style.position = 'fixed';
div.style.zIndex = '10000000'; div.style.zIndex = '10000000';
@@ -26,16 +31,34 @@ const setWatermark = (str: any) => {
return id; 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 = { const watermark = {
use: () => {
setTimeout(() => {
const userinfo = getUserInfo4Session()
if (userinfo && getUseWatermark4Session()) {
set(`${userinfo.username} ${dateFormat('yyyy-MM-dd HH:mm:ss', new Date())}`)
} else {
del();
}
}, 1500)
},
// 设置水印 // 设置水印
set: (str: any) => { set: (str: any) => {
let id = setWatermark(str); set(str)
if (document.getElementById(id) === null) id = setWatermark(str);
}, },
// 删除水印 // 删除水印
del: () => { del: () => {
let id = '1.23452384164.123412416'; del();
if (document.getElementById(id) !== null) document.body.removeChild(document.getElementById(id) as any);
}, },
}; };

View File

@@ -2,6 +2,7 @@ import RouterParent from '@/views/layout/routerView/parent.vue';
export const imports = { export const imports = {
'RouterParent': RouterParent, 'RouterParent': RouterParent,
"Home": () => import('@/views/home/index.vue'), "Home": () => import('@/views/home/index.vue'),
'Personal': () => import('@/views/personal/index.vue'), 'Personal': () => import('@/views/personal/index.vue'),
// machine // machine
@@ -11,6 +12,8 @@ export const imports = {
"RoleList": () => import('@/views/system/role'), "RoleList": () => import('@/views/system/role'),
"AccountList": () => import('@/views/system/account'), "AccountList": () => import('@/views/system/account'),
"SyslogList": () => import('@/views/system/syslog/SyslogList.vue'), "SyslogList": () => import('@/views/system/syslog/SyslogList.vue'),
"ConfigList": () => import('@/views/system/config/ConfigList.vue'),
// project // project
"ProjectList": () => import('@/views/ops/project/ProjectList.vue'), "ProjectList": () => import('@/views/ops/project/ProjectList.vue'),
// db // db

View File

@@ -163,7 +163,17 @@ export const staticRoutes: Array<RouteRecordRaw> = [
title: '终端 | {name}', title: '终端 | {name}',
// 是否根据query对标题名进行参数替换即最终显示为终端_机器名 // 是否根据query对标题名进行参数替换即最终显示为终端_机器名
titleRename: true, titleRename: true,
icon: 'iconfont icon-caidan', },
},
{
path: '/machine/terminal-rec',
name: 'machineTerminalRec',
component: () => import('@/views/ops/machine/MachineRec.vue'),
meta: {
// 将路径 'xxx?name=名字' 里的name字段值替换到title里
title: '终端回放 | {name}',
// 是否根据query对标题名进行参数替换即最终显示为终端_机器名
titleRename: true,
}, },
}, },
]; ];

View File

@@ -52,6 +52,7 @@ export interface ThemeConfigState {
terminalBackground: string; terminalBackground: string;
terminalCursor: string; terminalCursor: string;
terminalFontSize: number; terminalFontSize: number;
terminalFontWeight: string;
}; };
} }

View File

@@ -113,6 +113,7 @@ const themeConfigModule: Module<ThemeConfigState, RootStateTypes> = {
// ssh终端cursor色 // ssh终端cursor色
terminalCursor: '#268F81', terminalCursor: '#268F81',
terminalFontSize: 15, terminalFontSize: 15,
terminalFontWeight: 'normal',
/* 后端控制路由 /* 后端控制路由

View File

@@ -40,7 +40,7 @@
</el-input-number> </el-input-number>
</div> </div>
</div> </div>
<!-- <div class="layout-breadcrumb-seting-bar-flex mt15"> <div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div> <div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div>
<div class="layout-breadcrumb-seting-bar-flex-value"> <div class="layout-breadcrumb-seting-bar-flex-value">
<el-select @change="setLocalThemeConfig" v-model="getThemeConfig.terminalFontWeight" size="small" style="width: 90px"> <el-select @change="setLocalThemeConfig" v-model="getThemeConfig.terminalFontWeight" size="small" style="width: 90px">
@@ -48,7 +48,7 @@
<el-option label="bold" value="bold"> </el-option> <el-option label="bold" value="bold"> </el-option>
</el-select> </el-select>
</div> </div>
</div> --> </div>
<!-- 全局主题 --> <!-- 全局主题 -->
<el-divider content-position="left">全局主题</el-divider> <el-divider content-position="left">全局主题</el-divider>
@@ -273,23 +273,6 @@
<el-switch v-model="getThemeConfig.isInvert" @change="onAddFilterChange('invert')"></el-switch> <el-switch v-model="getThemeConfig.isInvert" @change="onAddFilterChange('invert')"></el-switch>
</div> </div>
</div> </div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">开启水印</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isWartermark" @change="onWartermarkChange"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt14">
<div class="layout-breadcrumb-seting-bar-flex-label">水印文案</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-input
v-model="getThemeConfig.wartermarkText"
size="small"
style="width: 90px"
@input="onWartermarkTextInput($event)"
></el-input>
</div>
</div>
<!-- 其它设置 --> <!-- 其它设置 -->
<el-divider content-position="left">其他设置</el-divider> <el-divider content-position="left">其他设置</el-divider>
@@ -440,8 +423,6 @@ import { ElMessage } from 'element-plus';
import ClipboardJS from 'clipboard'; import ClipboardJS from 'clipboard';
import { useStore } from '@/store/index.ts'; import { useStore } from '@/store/index.ts';
import { getLightColor } from '@/common/utils/theme.ts'; import { getLightColor } from '@/common/utils/theme.ts';
import Watermark from '@/common/utils/wartermark.ts';
import { verifyAndSpace } from '@/common/utils/toolsValidate.ts';
import { setLocal, getLocal, removeLocal } from '@/common/utils/storage.ts'; import { setLocal, getLocal, removeLocal } from '@/common/utils/storage.ts';
export default defineComponent({ export default defineComponent({
name: 'layoutBreadcrumbSeting', name: 'layoutBreadcrumbSeting',
@@ -572,18 +553,6 @@ export default defineComponent({
setLocalThemeConfig(); setLocalThemeConfig();
setLocal('appFilterStyle', appEle.style.cssText); setLocal('appFilterStyle', appEle.style.cssText);
}; };
// 4、界面显示 --> 开启水印
const onWartermarkChange = () => {
getThemeConfig.value.isWartermark ? Watermark.set(getThemeConfig.value.wartermarkText) : Watermark.del();
setLocalThemeConfig();
};
// 4、界面显示 --> 水印文案
const onWartermarkTextInput = (val: string) => {
getThemeConfig.value.wartermarkText = verifyAndSpace(val);
if (getThemeConfig.value.wartermarkText === '') return false;
if (getThemeConfig.value.isWartermark) Watermark.set(getThemeConfig.value.wartermarkText);
setLocalThemeConfig();
};
// 5、布局切换 // 5、布局切换
const onSetLayout = (layout: string) => { const onSetLayout = (layout: string) => {
setLocal('oldLayout', layout); setLocal('oldLayout', layout);
@@ -735,8 +704,6 @@ export default defineComponent({
const appEl: any = document.querySelector('#app'); const appEl: any = document.querySelector('#app');
appEl.style.cssText = getLocal('appFilterStyle'); appEl.style.cssText = getLocal('appFilterStyle');
} }
// 开启水印
onWartermarkChange();
// // 语言国际化 // // 语言国际化
// if (getLocal('themeConfig')) proxy.$i18n.locale = getLocal('themeConfig').globalI18n; // if (getLocal('themeConfig')) proxy.$i18n.locale = getLocal('themeConfig').globalI18n;
}, 1100); }, 1100);
@@ -762,8 +729,6 @@ export default defineComponent({
getThemeConfig, getThemeConfig,
onDrawerClose, onDrawerClose,
onAddFilterChange, onAddFilterChange,
onWartermarkChange,
onWartermarkTextInput,
onSetLayout, onSetLayout,
setLocalThemeConfig, setLocalThemeConfig,
onClassicSplitMenuChange, onClassicSplitMenuChange,

View File

@@ -14,11 +14,11 @@
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick"> <!-- <div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<el-icon title="菜单搜索"> <el-icon title="菜单搜索">
<search /> <search />
</el-icon> </el-icon>
</div> </div> -->
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick"> <div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
<el-icon title="布局设置"> <el-icon title="布局设置">
<setting /> <setting />
@@ -28,7 +28,7 @@
<el-popover <el-popover
placement="bottom" placement="bottom"
trigger="click" trigger="click"
v-model:visible="isShowUserNewsPopover" :visible="isShowUserNewsPopover"
:width="300" :width="300"
popper-class="el-popover-pupop-user-news" popper-class="el-popover-pupop-user-news"
> >

View File

@@ -9,7 +9,7 @@
<el-input type="password" placeholder="请输入密码" prefix-icon="lock" v-model="loginForm.password" autocomplete="off" show-password> <el-input type="password" placeholder="请输入密码" prefix-icon="lock" v-model="loginForm.password" autocomplete="off" show-password>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="captcha"> <el-form-item v-if="useLoginCaptcha" prop="captcha">
<el-row :gutter="15"> <el-row :gutter="15">
<el-col :span="16"> <el-col :span="16">
<el-input <el-input
@@ -78,10 +78,11 @@ import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { initBackEndControlRoutesFun } from '@/router/index.ts'; import { initBackEndControlRoutesFun } from '@/router/index.ts';
import { useStore } from '@/store/index.ts'; import { useStore } from '@/store/index.ts';
import { setSession } from '@/common/utils/storage.ts'; import { setSession, setUserInfo2Session, setUseWatermark2Session } from '@/common/utils/storage.ts';
import { formatAxis } from '@/common/utils/formatTime.ts'; import { formatAxis } from '@/common/utils/formatTime.ts';
import openApi from '@/common/openApi'; import openApi from '@/common/openApi';
import { RsaEncrypt } from '@/common/rsa'; import { RsaEncrypt } from '@/common/rsa';
import { useLoginCaptcha, useWartermark } from '@/common/sysconfig';
import { letterAvatar } from '@/common/utils/string'; import { letterAvatar } from '@/common/utils/string';
export default defineComponent({ export default defineComponent({
@@ -94,6 +95,7 @@ export default defineComponent({
const changePwdFormRef: any = ref(null); const changePwdFormRef: any = ref(null);
const state = reactive({ const state = reactive({
useLoginCaptcha: true,
captchaImage: '', captchaImage: '',
loginForm: { loginForm: {
username: '', username: '',
@@ -130,11 +132,17 @@ export default defineComponent({
}, },
}); });
onMounted(() => { onMounted(async () => {
// 移除公钥, 方便后续重新获取
sessionStorage.removeItem('RsaPublicKey');
state.useLoginCaptcha = await useLoginCaptcha();
getCaptcha(); getCaptcha();
}); });
const getCaptcha = async () => { const getCaptcha = async () => {
if (!state.useLoginCaptcha) {
return;
}
let res: any = await openApi.captcha(); let res: any = await openApi.captcha();
state.captchaImage = res.base64Captcha; state.captchaImage = res.base64Captcha;
state.loginForm.cid = res.cid; state.loginForm.cid = res.cid;
@@ -196,7 +204,7 @@ export default defineComponent({
}; };
// 存储用户信息到浏览器缓存 // 存储用户信息到浏览器缓存
setSession('userInfo', userInfos); setUserInfo2Session(userInfos);
// 1、请注意执行顺序(存储用户信息到vuex) // 1、请注意执行顺序(存储用户信息到vuex)
store.dispatch('userInfos/setUserInfos', userInfos); store.dispatch('userInfos/setUserInfos', userInfos);
if (!store.state.themeConfig.themeConfig.isRequestRoutes) { if (!store.state.themeConfig.themeConfig.isRequestRoutes) {
@@ -222,10 +230,13 @@ export default defineComponent({
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中 // 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
route.query?.redirect ? router.push(route.query.redirect as string) : router.push('/'); route.query?.redirect ? router.push(route.query.redirect as string) : router.push('/');
// 登录成功提示 // 登录成功提示
setTimeout(() => { setTimeout(async () => {
// 关闭 loading // 关闭 loading
state.loading.signIn = true; state.loading.signIn = true;
ElMessage.success(`${currentTimeInfo},欢迎回来!`); ElMessage.success(`${currentTimeInfo},欢迎回来!`);
if (await useWartermark()) {
setUseWatermark2Session(true);
}
}, 300); }, 300);
}; };

View File

@@ -24,7 +24,7 @@
</el-form-item> </el-form-item>
<el-form-item prop="host" label="host:" required> <el-form-item prop="host" label="host:" required>
<el-col :span="18"> <el-col :span="18">
<el-input v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input> <el-input :disabled="form.id" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-col> </el-col>
<el-col style="text-align: center" :span="1">:</el-col> <el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5"> <el-col :span="5">
@@ -55,20 +55,25 @@
<el-input v-model="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2"></el-input> <el-input v-model="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="database" label="数据库名:" required> <el-form-item prop="database" label="数据库名:" required>
<el-select <el-col :span="19">
@change="changeDatabase" <el-select
@focus="getAllDatabase" @change="changeDatabase"
v-model="databaseList" v-model="databaseList"
multiple multiple
collapse-tags collapse-tags
collapse-tags-tooltip collapse-tags-tooltip
filterable filterable
allow-create allow-create
placeholder="请确保数据库实例信息填写完整后选择数据库" placeholder="请确保数据库实例信息填写完整后获取库名"
style="width: 100%" style="width: 100%"
> >
<el-option v-for="db in allDatabases" :key="db" :label="db" :value="db" /> <el-option v-for="db in allDatabases" :key="db" :label="db" :value="db" />
</el-select> </el-select>
</el-col>
<el-col style="text-align: center" :span="1"><el-divider direction="vertical" border-style="dashed" /></el-col>
<el-col :span="4">
<el-link @click="getAllDatabase" :underline="false" type="success">获取库名</el-link>
</el-col>
</el-form-item> </el-form-item>
<el-form-item prop="enableSshTunnel" label="SSH隧道:"> <el-form-item prop="enableSshTunnel" label="SSH隧道:">
@@ -264,12 +269,10 @@ export default defineComponent({
}; };
const getAllDatabase = async () => { const getAllDatabase = async () => {
if (state.allDatabases.length != 0) {
return;
}
const reqForm = { ...state.form }; const reqForm = { ...state.form };
reqForm.password = await RsaEncrypt(reqForm.password); reqForm.password = await RsaEncrypt(reqForm.password);
state.allDatabases = await dbApi.getAllDatabase.request(reqForm); state.allDatabases = await dbApi.getAllDatabase.request(reqForm);
ElMessage.success('获取成功, 请选择需要管理操作的数据库')
}; };
const getDbPwd = async () => { const getDbPwd = async () => {

View File

@@ -7,7 +7,7 @@
>删除</el-button >删除</el-button
> >
<div style="float: right"> <div style="float: right">
<el-select v-model="query.projectId" placeholder="请选择项目" filterable clearable> <el-select @focus="getProjects" v-model="query.projectId" placeholder="请选择项目" filterable clearable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option> <el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select> </el-select>
<el-button v-waves type="primary" icon="search" @click="search()" class="ml5">查询</el-button> <el-button v-waves type="primary" icon="search" @click="search()" class="ml5">查询</el-button>
@@ -100,9 +100,17 @@
<el-button type="primary" size="small" @click="tableCreateDialog.visible = true">创建表</el-button> <el-button type="primary" size="small" @click="tableCreateDialog.visible = true">创建表</el-button>
</el-row> </el-row>
<el-table v-loading="tableInfoDialog.loading" border stripe :data="tableInfoDialog.infos" size="small"> <el-table v-loading="tableInfoDialog.loading" border stripe :data="filterTableInfos" size="small">
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip></el-table-column> <el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip></el-table-column> <template #header>
<el-input v-model="tableInfoDialog.tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable />
</template>
</el-table-column>
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip>
<template #header>
<el-input v-model="tableInfoDialog.tableCommentSearch" size="small" placeholder="备注: 输入可过滤" clearable />
</template>
</el-table-column>
<el-table-column <el-table-column
prop="tableRows" prop="tableRows"
label="Rows" label="Rows"
@@ -244,7 +252,7 @@
</template> </template>
<script lang='ts'> <script lang='ts'>
import { toRefs, reactive, onMounted, defineComponent } from 'vue'; import { toRefs, reactive, computed, onMounted, defineComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { formatByteSize } from '@/common/utils/format'; import { formatByteSize } from '@/common/utils/format';
import DbEdit from './DbEdit.vue'; import DbEdit from './DbEdit.vue';
@@ -317,6 +325,8 @@ export default defineComponent({
loading: false, loading: false,
visible: false, visible: false,
infos: [], infos: [],
tableNameSearch: '',
tableCommentSearch: '',
}, },
columnDialog: { columnDialog: {
visible: false, visible: false,
@@ -342,7 +352,26 @@ export default defineComponent({
onMounted(async () => { onMounted(async () => {
search(); search();
state.projects = await projectApi.accountProjects.request(null); });
const filterTableInfos = computed(() => {
const infos = state.tableInfoDialog.infos;
const tableNameSearch = state.tableInfoDialog.tableNameSearch;
const tableCommentSearch = state.tableInfoDialog.tableCommentSearch;
if (!tableNameSearch && !tableCommentSearch) {
return infos;
}
return infos.filter((data: any) => {
let tnMatch = true;
let tcMatch = true;
if (tableNameSearch) {
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase());
}
if (tableCommentSearch) {
tcMatch = data.tableComment.includes(tableCommentSearch);
}
return tnMatch && tcMatch;
});
}); });
const choose = (item: any) => { const choose = (item: any) => {
@@ -369,7 +398,12 @@ export default defineComponent({
search(); search();
}; };
const editDb = (isAdd = false) => { const getProjects = async () => {
state.projects = await projectApi.accountProjects.request(null);
};
const editDb = async (isAdd = false) => {
await getProjects();
if (isAdd) { if (isAdd) {
state.dbEditDialog.data = null; state.dbEditDialog.data = null;
state.dbEditDialog.title = '新增数据库资源'; state.dbEditDialog.title = '新增数据库资源';
@@ -572,6 +606,8 @@ export default defineComponent({
return { return {
...toRefs(state), ...toRefs(state),
getProjects,
filterTableInfos,
enums, enums,
search, search,
choose, choose,

View File

@@ -1,9 +1,5 @@
<template> <template>
<div> <div>
<el-button-group :style="btnStyle">
<el-button @click="onRunSql" type="success" icon="video-play" size="small" plain>执行</el-button>
<el-button @click="formatSql" type="primary" icon="magic-stick" size="small" plain>格式化</el-button>
</el-button-group>
<div class="toolbar"> <div class="toolbar">
<el-row type="flex" justify="space-between"> <el-row type="flex" justify="space-between">
<el-col :span="24"> <el-col :span="24">
@@ -58,6 +54,19 @@
<div> <div>
<div class="toolbar"> <div class="toolbar">
<div class="fl"> <div class="fl">
<el-link @click="onRunSql" :underline="false" class="ml15" icon="VideoPlay"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="format sql" placement="top">
<el-link @click="formatSql" type="primary" :underline="false" icon="MagicStick"></el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
<el-link @click="onCommit" type="success" :underline="false" icon="CircleCheck"></el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-upload <el-upload
style="display: inline-block" style="display: inline-block"
:before-upload="beforeUpload" :before-upload="beforeUpload"
@@ -72,11 +81,10 @@
multiple multiple
:limit="100" :limit="100"
> >
<el-button type="success" icon="video-play" plain size="small">sql脚本执行</el-button> <el-tooltip class="box-item" effect="dark" content="SQL脚本执行" placement="top">
<el-link type="success" :underline="false" icon="Document"></el-link>
</el-tooltip>
</el-upload> </el-upload>
<el-button @click="onCommit" class="ml5 mb5" type="success" icon="CircleCheck" plain size="small"
>commit</el-button
>
</div> </div>
<div style="float: right" class="fl"> <div style="float: right" class="fl">
@@ -101,7 +109,7 @@
</div> </div>
</div> </div>
<div @click="closeExecBtns" class="mt5 sqlEditor" @contextmenu="showExecBtns"> <div class="mt5 sqlEditor">
<textarea ref="codeTextarea"></textarea> <textarea ref="codeTextarea"></textarea>
</div> </div>
@@ -145,56 +153,64 @@
<el-tab-pane closable v-for="dt in dataTabs" :key="dt.name" :label="dt.label" :name="dt.name"> <el-tab-pane closable v-for="dt in dataTabs" :key="dt.name" :label="dt.label" :name="dt.name">
<el-row v-if="dbId"> <el-row v-if="dbId">
<el-link @click="onRefresh(dt.name)" icon="refresh" :underline="false" class="ml5"></el-link> <el-col :span="8">
<el-link @click="addRow" class="ml5" type="primary" icon="plus" :underline="false"></el-link> <el-link @click="onRefresh(dt.name)" icon="refresh" :underline="false" class="ml5"></el-link>
<el-link @click="onDeleteData" class="ml5" type="danger" icon="delete" :underline="false"></el-link> <el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="commit" placement="top"> <el-link @click="addRow" type="primary" icon="plus" :underline="false"></el-link>
<el-link @click="onCommit" class="ml5" type="success" icon="check" :underline="false"></el-link> <el-divider direction="vertical" border-style="dashed" />
</el-tooltip>
<el-tooltip class="box-item" effect="dark" content="生成insert sql" placement="top"> <el-link @click="onDeleteData" type="danger" icon="delete" :underline="false"></el-link>
<el-link @click="onGenerateInsertSql" type="success" class="ml20" :underline="false">gi</el-link> <el-divider direction="vertical" border-style="dashed" />
</el-tooltip>
</el-row> <el-tooltip class="box-item" effect="dark" content="commit" placement="top">
<el-row class="mt5"> <el-link @click="onCommit" type="success" icon="CircleCheck" :underline="false"></el-link>
<el-input </el-tooltip>
v-model="dt.condition" <el-divider direction="vertical" border-style="dashed" />
placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容点击查询按钮即可"
clearable <el-tooltip class="box-item" effect="dark" content="生成insert sql" placement="top">
size="small" <el-link @click="onGenerateInsertSql" type="success" :underline="false">gi</el-link>
> </el-tooltip>
<template #prepend> </el-col>
<el-popover v-model:visible="dt.selectColumnPopoverVisible" :width="320" placement="right"> <el-col :span="16">
<template #reference> <el-input
<el-link v-model="dt.condition"
@click="dt.selectColumnPopoverVisible = !dt.selectColumnPopoverVisible" placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容点击查询按钮即可"
type="success" clearable
:underline="false" size="small"
>选择列</el-link style="width: 100%"
>
<template #prepend>
<el-popover trigger="click" :width="320" placement="right">
<template #reference>
<el-link
type="success"
:underline="false"
>选择列</el-link
>
</template>
<el-table
:data="getColumns4Map(dt.name)"
max-height="500"
size="small"
@row-click="
(...event) => {
onConditionRowClick(event, dt);
}
"
style="cursor: pointer"
> >
</template> <el-table-column property="columnName" label="列名" show-overflow-tooltip> </el-table-column>
<el-table <el-table-column property="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
:data="getColumns4Map(dt.name)" </el-table>
max-height="500" </el-popover>
size="small" </template>
@row-click="
(...event) => {
onConditionRowClick(event, dt);
}
"
style="cursor: pointer"
>
<el-table-column property="columnName" label="列名" show-overflow-tooltip> </el-table-column>
<el-table-column property="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
</el-table>
</el-popover>
</template>
<template #append> <template #append>
<el-button @click="selectByCondition(dt.name, dt.condition)" icon="search" size="small"></el-button> <el-button @click="selectByCondition(dt.name, dt.condition)" icon="search" size="small"></el-button>
</template> </template>
</el-input> </el-input>
</el-col>
</el-row> </el-row>
<el-table <el-table
@cell-dblclick="cellClick" @cell-dblclick="cellClick"
@@ -344,14 +360,6 @@ export default defineComponent({
pageSize: 10, pageSize: 10,
envId: null, envId: null,
}, },
btnStyle: {
position: 'absolute',
zIndex: 1000,
display: 'none',
left: '',
top: '',
},
selectColumnPopoverVisible: false,
conditionDialog: { conditionDialog: {
title: '', title: '',
placeholder: '', placeholder: '',
@@ -417,7 +425,7 @@ export default defineComponent({
const setHeight = () => { const setHeight = () => {
// 默认300px // 默认300px
codemirror.setSize('auto', `${window.innerHeight - 538}px`); codemirror.setSize('auto', `${window.innerHeight - 538}px`);
state.dataTabsTableHeight = window.innerHeight - 258 - 33; state.dataTabsTableHeight = window.innerHeight - 274;
}; };
/** /**
@@ -486,7 +494,6 @@ export default defineComponent({
} catch (e: any) { } catch (e: any) {
state.queryTab.loading = false; state.queryTab.loading = false;
} }
closeExecBtns();
// 即只有以该字符串开头的sql才可修改表数据内容 // 即只有以该字符串开头的sql才可修改表数据内容
if (sql.startsWith('SELECT *') || sql.startsWith('select *') || sql.startsWith('SELECT\n *')) { if (sql.startsWith('SELECT *') || sql.startsWith('select *') || sql.startsWith('SELECT\n *')) {
@@ -729,7 +736,6 @@ export default defineComponent({
columnNames: [], columnNames: [],
pageNum: 1, pageNum: 1,
count: 0, count: 0,
selectColumnPopoverVisible: false,
}; };
tab.columnNames = await getColumnNames(tableName); tab.columnNames = await getColumnNames(tableName);
state.dataTabs[tableName] = tab; state.dataTabs[tableName] = tab;
@@ -769,7 +775,6 @@ export default defineComponent({
* 条件查询,点击列信息后显示输入对应的值 * 条件查询,点击列信息后显示输入对应的值
*/ */
const onConditionRowClick = (event: any, dataTab: any) => { const onConditionRowClick = (event: any, dataTab: any) => {
dataTab.selectColumnPopoverVisible = false;
const row = event[0]; const row = event[0];
state.conditionDialog.title = `请输入 [${row.columnName}] 的值`; state.conditionDialog.title = `请输入 [${row.columnName}] 的值`;
state.conditionDialog.placeholder = `${row.columnType} ${row.columnComment}`; state.conditionDialog.placeholder = `${row.columnType} ${row.columnComment}`;
@@ -993,7 +998,6 @@ export default defineComponent({
state.queryTab.execRes.tableColumn = []; state.queryTab.execRes.tableColumn = [];
state.cmOptions.hintOptions.tables = []; state.cmOptions.hintOptions.tables = [];
tableMap.clear(); tableMap.clear();
closeExecBtns();
}; };
const onDataSelectionChange = (datas: []) => { const onDataSelectionChange = (datas: []) => {
@@ -1169,7 +1173,6 @@ export default defineComponent({
let selectSql = codemirror.getSelection(); let selectSql = codemirror.getSelection();
isTrue(selectSql, '请选中需要格式化的sql'); isTrue(selectSql, '请选中需要格式化的sql');
codemirror.replaceSelection(sqlFormatter(selectSql)); codemirror.replaceSelection(sqlFormatter(selectSql));
closeExecBtns();
}; };
const search = async () => { const search = async () => {
@@ -1177,31 +1180,6 @@ export default defineComponent({
state.dbs = res.list; state.dbs = res.list;
}; };
/**
* 显示执行sql和格式化按钮
*/
const showExecBtns = (event: any) => {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
state.btnStyle.display = 'inline';
state.btnStyle.left = event.offsetX + 15 + 'px';
state.btnStyle.top = event.clientY - 80 + 'px';
};
/**
* 关闭执行sql和格式化按钮
*/
const closeExecBtns = () => {
if (state.btnStyle.left) {
state.btnStyle.display = 'none';
state.btnStyle.left = '';
state.btnStyle.top = '';
}
};
return { return {
...toRefs(state), ...toRefs(state),
codeTextarea, codeTextarea,
@@ -1237,8 +1215,6 @@ export default defineComponent({
onDeleteData, onDeleteData,
onTableSortChange, onTableSortChange,
onGenerateInsertSql, onGenerateInsertSql,
showExecBtns,
closeExecBtns,
}; };
}, },
}); });

View File

@@ -57,7 +57,7 @@
</el-row> </el-row>
</el-dialog> </el-dialog>
<el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="50%"> <el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="70%">
<el-progress <el-progress
v-if="uploadProgressShow" v-if="uploadProgressShow"
style="width: 90%; margin-left: 20px" style="width: 90%; margin-left: 20px"
@@ -66,7 +66,7 @@
:percentage="progressNum" :percentage="progressNum"
/> />
<div style="height: 45vh; overflow: auto"> <div style="height: 45vh; overflow: auto">
<el-tree v-if="tree.visible" ref="fileTree" :load="loadNode" :props="props" lazy node-key="id" :expand-on-click-node="true"> <el-tree v-if="tree.visible" ref="fileTree" :highlight-current="true" :load="loadNode" :props="props" lazy node-key="id" :expand-on-click-node="true">
<template #default="{ node, data }"> <template #default="{ node, data }">
<span class="custom-tree-node"> <span class="custom-tree-node">
<el-dropdown size="small" @visible-change="getFilePath(data, $event)" trigger="contextmenu"> <el-dropdown size="small" @visible-change="getFilePath(data, $event)" trigger="contextmenu">
@@ -85,6 +85,10 @@
{{ node.label }} {{ node.label }}
<span style="color: #67c23a" v-if="data.type == '-'">&nbsp;&nbsp;[{{ formatFileSize(data.size) }}]</span> <span style="color: #67c23a" v-if="data.type == '-'">&nbsp;&nbsp;[{{ formatFileSize(data.size) }}]</span>
</span> </span>
<span style="display: inline-block">
<span v-if="data.mode" style="color: #67c23a">&nbsp;&nbsp;[{{ data.mode }} {{ data.modTime }}]</span>
</span>
</span> </span>
<template #dropdown> <template #dropdown>

View File

@@ -12,7 +12,7 @@
</el-form-item> </el-form-item>
<el-form-item prop="ip" label="ip:" required> <el-form-item prop="ip" label="ip:" required>
<el-col :span="18"> <el-col :span="18">
<el-input v-model.trim="form.ip" placeholder="主机ip" auto-complete="off"></el-input> <el-input :disabled="form.id" v-model.trim="form.ip" placeholder="主机ip" auto-complete="off"></el-input>
</el-col> </el-col>
<el-col style="text-align: center" :span="1">:</el-col> <el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5"> <el-col :span="5">
@@ -52,6 +52,10 @@
<el-input type="textarea" v-model="form.remark"></el-input> <el-input type="textarea" v-model="form.remark"></el-input>
</el-form-item> </el-form-item>
<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="enableSshTunnel" label="SSH隧道:"> <el-form-item prop="enableSshTunnel" label="SSH隧道:">
<el-col :span="3"> <el-col :span="3">
<el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1" :false-label="-1"></el-checkbox> <el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1" :false-label="-1"></el-checkbox>
@@ -122,6 +126,7 @@ export default defineComponent({
remark: '', remark: '',
enableSshTunnel: null, enableSshTunnel: null,
sshTunnelMachineId: null, sshTunnelMachineId: null,
enableRecorder: -1,
}, },
pwd: '', pwd: '',
btnLoading: false, btnLoading: false,

View File

@@ -16,7 +16,7 @@
>删除</el-button >删除</el-button
> >
<div style="float: right"> <div style="float: right">
<el-select v-model="params.projectId" placeholder="请选择项目" @clear="search" filterable clearable> <el-select @focus="getProjects" v-model="params.projectId" placeholder="请选择项目" @clear="search" filterable clearable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option> <el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select> </el-select>
<el-input <el-input
@@ -68,41 +68,45 @@
<el-table-column prop="username" label="用户名" min-width="90"></el-table-column> <el-table-column prop="username" label="用户名" min-width="90"></el-table-column>
<el-table-column prop="projectName" label="项目" min-width="120"></el-table-column> <el-table-column prop="projectName" label="项目" min-width="120"></el-table-column>
<el-table-column prop="remark" label="备注" min-width="250" show-overflow-tooltip></el-table-column> <el-table-column prop="remark" label="备注" min-width="250" show-overflow-tooltip></el-table-column>
<el-table-column prop="ip" label="hasCli" width="70">
<template #default="scope">
{{ `${scope.row.hasCli ? '是' : '否'}` }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="165"> <el-table-column prop="createTime" label="创建时间" min-width="165">
<template #default="scope"> <template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }} {{ $filters.dateFormat(scope.row.createTime) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="creator" label="创建者" min-width="80"></el-table-column> <el-table-column prop="creator" label="创建者" min-width="80"></el-table-column>
<el-table-column label="操作" min-width="280" fixed="right"> <el-table-column label="操作" min-width="335" fixed="right">
<template #default="scope"> <template #default="scope">
<el-link <span v-auth="'machine:terminal'">
v-auth="'machine:terminal'" <el-link
:disabled="scope.row.status == -1" :disabled="scope.row.status == -1"
type="primary" type="primary"
@click="showTerminal(scope.row)" @click="showTerminal(scope.row)"
plain plain
size="small" size="small"
:underline="false" :underline="false"
>终端</el-link >终端</el-link
> >
<el-divider v-auth="'machine:terminal'" direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-link </span>
v-auth="'machine:file'"
type="success" <span v-auth="'machine:update'" v-if="scope.row.enableRecorder == 1">
:disabled="scope.row.status == -1" <el-link @click="showRec(scope.row)" plain :underline="false" size="small">终端回放</el-link>
@click="fileManage(scope.row)" <el-divider direction="vertical" border-style="dashed" />
plain </span>
size="small"
:underline="false" <span v-auth="'machine:file'">
>文件</el-link <el-link
> type="success"
<el-divider v-auth="'machine:file'" direction="vertical" border-style="dashed" /> :disabled="scope.row.status == -1"
@click="fileManage(scope.row)"
plain
size="small"
:underline="false"
>文件</el-link
>
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-link <el-link
:disabled="scope.row.status == -1" :disabled="scope.row.status == -1"
type="warning" type="warning"
@@ -113,10 +117,12 @@
>脚本</el-link >脚本</el-link
> >
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-link @click="showProcess(scope.row)" :disabled="scope.row.status == -1" plain :underline="false" size="small" <el-link @click="showProcess(scope.row)" :disabled="scope.row.status == -1" plain :underline="false" size="small"
>进程</el-link >进程</el-link
> >
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-link <el-link
:disabled="!scope.row.hasCli || scope.row.status == -1" :disabled="!scope.row.hasCli || scope.row.status == -1"
type="danger" type="danger"
@@ -228,11 +234,15 @@ export default defineComponent({
data: null, data: null,
title: '新增机器', title: '新增机器',
}, },
machineRecDialog: {
visible: false,
machineId: 0,
title: '',
},
}); });
onMounted(async () => { onMounted(async () => {
search(); search();
state.projects = await projectApi.accountProjects.request(null);
}); });
const choose = (item: any) => { const choose = (item: any) => {
@@ -255,12 +265,22 @@ export default defineComponent({
}; };
const closeCli = async (row: any) => { const closeCli = async (row: any) => {
await ElMessageBox.confirm(`确定关闭该机器客户端连接?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await machineApi.closeCli.request({ id: row.id }); await machineApi.closeCli.request({ id: row.id });
ElMessage.success('关闭成功'); ElMessage.success('关闭成功');
search(); search();
}; };
const openFormDialog = (machine: any) => { const getProjects = async () => {
state.projects = await projectApi.accountProjects.request(null);
};
const openFormDialog = async (machine: any) => {
await getProjects();
let dialogTitle; let dialogTitle;
if (machine) { if (machine) {
state.machineEditDialog.data = state.currentData as any; state.machineEditDialog.data = state.currentData as any;
@@ -338,9 +358,21 @@ export default defineComponent({
state.processDialog.visible = true; state.processDialog.visible = true;
}; };
const showRec = (row: any) => {
const { href } = router.resolve({
path: `/machine/terminal-rec`,
query: {
id: row.id,
name: `${row.name}[${row.ip}]-终端回放记录`,
},
});
window.open(href, '_blank');
};
return { return {
...toRefs(state), ...toRefs(state),
choose, choose,
getProjects,
showTerminal, showTerminal,
openFormDialog, openFormDialog,
deleteMachine, deleteMachine,
@@ -352,6 +384,7 @@ export default defineComponent({
submitSuccess, submitSuccess,
fileManage, fileManage,
search, search,
showRec,
handlePageChange, handlePageChange,
}; };
}, },

View File

@@ -0,0 +1,120 @@
<template>
<div>
<div class="toolbar">
<span style="dispaly: inline-block" class="ml10">{{ title }}</span>
<el-divider direction="vertical" border-style="dashed" />
<el-select @change="getUsers" v-model="operateDate" placeholder="操作日期" filterable>
<el-option v-for="item in operateDates" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-select class="ml10" @change="getRecs" filterable v-model="user" placeholder="请选择操作人">
<el-option v-for="item in users" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-select class="ml10" @change="playRec" filterable v-model="rec" placeholder="请选择操作记录">
<el-option v-for="item in recs" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-divider direction="vertical" border-style="dashed" />
快捷键-> space[空格键]: 暂停/播放 | f: 全屏/取消全屏
</div>
<div ref="playerRef" id="rc-player"></div>
</div>
</template>
<script lang="ts">
import { toRefs, onMounted, ref, reactive, defineComponent } from 'vue';
import { machineApi } from './api';
import * as AsciinemaPlayer from 'asciinema-player';
import 'asciinema-player/dist/bundle/asciinema-player.css';
import { useRoute } from 'vue-router';
export default defineComponent({
name: 'MachineRec',
components: {},
props: {
visible: { type: Boolean },
machineId: { type: Number },
title: { type: String },
},
setup(props: any, context) {
const route = useRoute();
const playerRef = ref(null);
const state = reactive({
dialogVisible: false,
title: '',
machineId: 0,
operateDates: [],
users: [],
recs: [],
operateDate: '',
user: '',
rec: '',
});
onMounted(() => {
state.machineId = Number.parseInt(route.query.id as string);
state.title = route.query.name as string;
getOperateDate();
});
const getOperateDate = async () => {
const res = await machineApi.recDirNames.request({ path: state.machineId });
state.operateDates = res as any;
};
const getUsers = async (operateDate: string) => {
state.users = [];
state.user = '';
state.recs = [];
state.rec = '';
const res = await machineApi.recDirNames.request({ path: `${state.machineId}/${operateDate}` });
state.users = res as any;
};
const getRecs = async (user: string) => {
state.recs = [];
state.rec = '';
const res = await machineApi.recDirNames.request({ path: `${state.machineId}/${state.operateDate}/${user}` });
state.recs = res as any;
};
let player: any = null;
const playRec = async (rec: string) => {
if (player) {
player.dispose();
}
const content = await machineApi.recDirNames.request({
isFile: '1',
path: `${state.machineId}/${state.operateDate}/${state.user}/${rec}`,
});
player = AsciinemaPlayer.create(`data:text/plain;base64,${content}`, playerRef.value, {
autoPlay: true,
speed: 1.0,
idleTimeLimit: 2,
});
};
/**
* 关闭取消按钮触发的事件
*/
const handleClose = () => {
context.emit('update:visible', false);
context.emit('update:machineId', null);
context.emit('cancel');
state.operateDates = [];
state.users = [];
state.recs = [];
state.operateDate = '';
state.user = '';
state.rec = '';
};
return {
...toRefs(state),
playerRef,
getUsers,
getRecs,
playRec,
handleClose,
};
},
});
</script>

View File

@@ -7,9 +7,9 @@
:before-close="cancel" :before-close="cancel"
:show-close="true" :show-close="true"
:destroy-on-close="true" :destroy-on-close="true"
width="800px" width="900px"
> >
<el-form :model="form" ref="scriptForm" label-width="70px"> <el-form :model="form" ref="scriptForm" label-width="70px" size="small">
<el-form-item prop="method" label="名称"> <el-form-item prop="method" label="名称">
<el-input v-model.trim="form.name" placeholder="请输入名称"></el-input> <el-input v-model.trim="form.name" placeholder="请输入名称"></el-input>
</el-form-item> </el-form-item>
@@ -29,13 +29,17 @@
</el-row> </el-row>
<el-form-item :key="param" v-for="(param, index) in params" prop="params" :label="`参数${index + 1}`"> <el-form-item :key="param" v-for="(param, index) in params" prop="params" :label="`参数${index + 1}`">
<el-row> <el-row>
<el-col :span="6"><el-input v-model="param.model" placeholder="内容中用{{.model}}替换"></el-input></el-col> <el-col :span="5"><el-input v-model="param.model" placeholder="内容中用{{.model}}替换"></el-input></el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" /> <el-divider :span="1" direction="vertical" border-style="dashed" />
<el-col :span="6"><el-input v-model="param.name" placeholder="字段名"></el-input></el-col> <el-col :span="4"><el-input v-model="param.name" placeholder="字段名"></el-input></el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" /> <el-divider :span="1" direction="vertical" border-style="dashed" />
<el-col :span="6"><el-input v-model="param.placeholder" placeholder="字段说明"></el-input></el-col> <el-col :span="4"><el-input v-model="param.placeholder" placeholder="字段说明"></el-input></el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" /> <el-divider :span="1" direction="vertical" border-style="dashed" />
<el-col :span="3"><el-button @click="onDeleteParam(index)" size="small" type="danger">删除</el-button></el-col> <el-col :span="4">
<el-input v-model="param.options" placeholder="可选值 ,分割"></el-input>
</el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" />
<el-col :span="2"><el-button @click="onDeleteParam(index)" size="small" type="danger">删除</el-button></el-col>
</el-row> </el-row>
</el-form-item> </el-form-item>
@@ -46,13 +50,12 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="cancel()" :disabled="submitDisabled" size="small"> </el-button> <el-button @click="cancel()" :disabled="submitDisabled"> </el-button>
<el-button <el-button
v-auth="'machine:script:save'" v-auth="'machine:script:save'"
type="primary" type="primary"
:loading="btnLoading" :loading="btnLoading"
@click="btnOk" @click="btnOk"
size="small"
:disabled="submitDisabled" :disabled="submitDisabled"
> </el-button > </el-button
> >

View File

@@ -76,7 +76,24 @@
<el-dialog title="脚本参数" v-model="scriptParamsDialog.visible" width="400px"> <el-dialog title="脚本参数" v-model="scriptParamsDialog.visible" width="400px">
<el-form ref="paramsForm" :model="scriptParamsDialog.params" label-width="70px" size="small"> <el-form ref="paramsForm" :model="scriptParamsDialog.params" label-width="70px" size="small">
<el-form-item v-for="item in scriptParamsDialog.paramsFormItem" :key="item.name" :prop="item.model" :label="item.name" required> <el-form-item v-for="item in scriptParamsDialog.paramsFormItem" :key="item.name" :prop="item.model" :label="item.name" required>
<el-input v-model="scriptParamsDialog.params[item.model]" :placeholder="item.placeholder" autocomplete="off"></el-input> <el-input
v-if="!item.options"
v-model="scriptParamsDialog.params[item.model]"
:placeholder="item.placeholder"
autocomplete="off"
clearable
></el-input>
<el-select
v-else
v-model="scriptParamsDialog.params[item.model]"
:placeholder="item.placeholder"
filterable
autocomplete="off"
clearable
style="width: 100%"
>
<el-option v-for="option in item.options.split(',')" :key="option" :label="option" :value="option" />
</el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -88,7 +105,6 @@
<el-dialog title="执行结果" v-model="resultDialog.visible" width="50%"> <el-dialog title="执行结果" v-model="resultDialog.visible" width="50%">
<div style="white-space: pre-line; padding: 10px; color: #000000"> <div style="white-space: pre-line; padding: 10px; color: #000000">
<!-- {{ resultDialog.result }} -->
<el-input v-model="resultDialog.result" :rows="20" type="textarea" /> <el-input v-model="resultDialog.result" :rows="20" type="textarea" />
</div> </div>
</el-dialog> </el-dialog>
@@ -97,12 +113,12 @@
v-if="terminalDialog.visible" v-if="terminalDialog.visible"
title="终端" title="终端"
v-model="terminalDialog.visible" v-model="terminalDialog.visible"
width="70%" width="80%"
:close-on-click-modal="false" :close-on-click-modal="false"
:modal="false" :modal="false"
@close="closeTermnial" @close="closeTermnial"
> >
<ssh-terminal ref="terminal" :cmd="terminalDialog.cmd" :machineId="terminalDialog.machineId" height="600px" /> <ssh-terminal ref="terminal" :cmd="terminalDialog.cmd" :machineId="terminalDialog.machineId" height="560px" />
</el-dialog> </el-dialog>
<script-edit <script-edit
@@ -196,7 +212,6 @@ export default defineComponent({
// 如果存在参数,则弹窗输入参数后执行 // 如果存在参数,则弹窗输入参数后执行
if (script.params) { if (script.params) {
state.scriptParamsDialog.paramsFormItem = JSON.parse(script.params); state.scriptParamsDialog.paramsFormItem = JSON.parse(script.params);
console.log(state.scriptParamsDialog.paramsFormItem);
if (state.scriptParamsDialog.paramsFormItem && state.scriptParamsDialog.paramsFormItem.length > 0) { if (state.scriptParamsDialog.paramsFormItem && state.scriptParamsDialog.paramsFormItem.length > 0) {
state.scriptParamsDialog.visible = true; state.scriptParamsDialog.visible = true;
return; return;
@@ -271,8 +286,6 @@ export default defineComponent({
const closeTermnial = () => { const closeTermnial = () => {
state.terminalDialog.visible = false; state.terminalDialog.visible = false;
state.terminalDialog.machineId = 0; state.terminalDialog.machineId = 0;
// const t: any = this.$refs['terminal']
// t.closeAll()
}; };
/** /**
@@ -298,8 +311,6 @@ export default defineComponent({
}; };
const submitSuccess = () => { const submitSuccess = () => {
// this.delChoose()
// this.search()
getScripts(); getScripts();
}; };
@@ -329,6 +340,7 @@ export default defineComponent({
context.emit('update:machineId', null); context.emit('update:machineId', null);
context.emit('cancel'); context.emit('cancel');
state.scriptTable = []; state.scriptTable = [];
state.scriptParamsDialog.paramsFormItem = [];
}; };
return { return {

View File

@@ -9,7 +9,7 @@ import { FitAddon } from 'xterm-addon-fit';
import { getSession } from '@/common/utils/storage.ts'; import { getSession } from '@/common/utils/storage.ts';
import config from '@/common/config'; import config from '@/common/config';
import { useStore } from '@/store/index.ts'; import { useStore } from '@/store/index.ts';
import { toRefs, watch, computed, reactive, defineComponent, onMounted, onBeforeUnmount } from 'vue'; import { nextTick, toRefs, watch, computed, reactive, defineComponent, onMounted, onBeforeUnmount } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'SshTerminal', name: 'SshTerminal',
@@ -27,22 +27,20 @@ export default defineComponent({
socket: null as any, socket: null as any,
}); });
const resize = 1;
const data = 2;
const ping = 3;
watch(props, (newValue) => { watch(props, (newValue) => {
state.machineId = newValue.machineId; state.machineId = newValue.machineId;
state.cmd = newValue.cmd; state.cmd = newValue.cmd;
state.height = newValue.height; state.height = newValue.height;
if (state.machineId) {
initSocket();
}
}); });
onMounted(() => { onMounted(() => {
state.machineId = props.machineId; state.machineId = props.machineId;
state.height = props.height; state.height = props.height;
state.cmd = props.cmd; state.cmd = props.cmd;
if (state.machineId) {
initSocket();
}
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -56,13 +54,17 @@ export default defineComponent({
return store.state.themeConfig.themeConfig; return store.state.themeConfig.themeConfig;
}); });
nextTick(() => {
initXterm();
initSocket();
});
function initXterm() { function initXterm() {
const term: any = new Terminal({ const term: any = new Terminal({
fontSize: getThemeConfig.value.terminalFontSize || 15, fontSize: getThemeConfig.value.terminalFontSize || 15,
// fontWeight: getThemeConfig.value.terminalFontWeight || 'normal', fontWeight: getThemeConfig.value.terminalFontWeight || 'normal',
fontFamily: 'JetBrainsMono, Consolas, Menlo, Monaco', fontFamily: 'JetBrainsMono, monaco, Consolas, Lucida Console, monospace',
cursorBlink: true, cursorBlink: true,
// cursorStyle: 'underline', //光标样式
disableStdin: false, disableStdin: false,
theme: { theme: {
foreground: getThemeConfig.value.terminalForeground || '#7e9192', //字体 foreground: getThemeConfig.value.terminalForeground || '#7e9192', //字体
@@ -82,6 +84,14 @@ export default defineComponent({
try { try {
// 窗口大小改变时触发xterm的resize方法使自适应 // 窗口大小改变时触发xterm的resize方法使自适应
fitAddon.fit(); fitAddon.fit();
if (state.term) {
state.term.focus();
send({
type: resize,
Cols: parseInt(state.term.cols),
Rows: parseInt(state.term.rows),
});
}
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
@@ -104,69 +114,52 @@ export default defineComponent({
term.onData((key: any) => { term.onData((key: any) => {
sendCmd(key); sendCmd(key);
}); });
// 为解决窗体resize方法才会向后端发送列数和行数所以页面加载时也要触发此方法
send({
type: 'resize',
Cols: parseInt(term.cols),
Rows: parseInt(term.rows),
});
// 如果有初始要执行的命令,则发送执行命令
if (state.cmd) {
sendCmd(state.cmd + ' \r');
}
} }
let pingInterval: any;
function initSocket() { function initSocket() {
state.socket = new WebSocket(`${config.baseWsUrl}/machines/${state.machineId}/terminal?token=${getSession('token')}`); state.socket = new WebSocket(
`${config.baseWsUrl}/machines/${state.machineId}/terminal?token=${getSession('token')}&cols=${state.term.cols}&rows=${
state.term.rows
}`
);
// 监听socket连接 // 监听socket连接
state.socket.onopen = open; state.socket.onopen = () => {
// 如果有初始要执行的命令,则发送执行命令
if (state.cmd) {
sendCmd(state.cmd + ' \r');
}
// 开启心跳
pingInterval = setInterval(() => {
send({ type: ping, msg: 'ping' });
}, 8000);
};
// 监听socket错误信息 // 监听socket错误信息
state.socket.onerror = error; state.socket.onerror = (e: any) => {
// 监听socket消息 console.log('连接错误', e);
state.socket.onmessage = getMessage; };
state.socket.onclose = () => {
if (state.term) {
state.term.writeln('\r\n\x1b[31m提示: 连接已关闭...');
}
if (pingInterval) {
clearInterval(pingInterval);
}
};
// 发送socket消息 // 发送socket消息
state.socket.onsend = send; state.socket.onsend = send;
// 监听socket消息
state.socket.onmessage = getMessage;
} }
function open() { function getMessage(msg: any) {
console.log('socket连接成功'); // msg.data是真正后端返回的数据
initXterm(); state.term.write(msg.data);
//开启心跳
// this.start();
}
function error() {
console.log('连接错误');
//重连
// reconnect();
}
function close() {
if (state.socket) {
state.socket.close();
console.log('socket关闭');
}
//重连
// this.reconnect()
}
function getMessage(msg: string) {
// console.log(msg)
state.term.write(msg['data']);
//msg是返回的数据
// msg = JSON.parse(msg.data);
// this.socket.send("ping");//有事没事ping一下看看ws还活着没
// //switch用于处理返回的数据根据返回数据的格式去判断
// switch (msg["operation"]) {
// case "stdout":
// this.term.write(msg["data"]);//这里write也许不是固定的失败后找后端看一下该怎么往term里面write
// break;
// default:
// console.error("Unexpected message type:", msg);//但是错误是固定的。。。。
// }
//收到服务器信息,心跳重置
// this.reset();
} }
function send(msg: any) { function send(msg: any) {
@@ -175,11 +168,18 @@ export default defineComponent({
function sendCmd(key: any) { function sendCmd(key: any) {
send({ send({
type: 'cmd', type: data,
msg: key, msg: key,
}); });
} }
function close() {
if (state.socket) {
state.socket.close();
console.log('socket关闭');
}
}
function closeAll() { function closeAll() {
close(); close();
if (state.term) { if (state.term) {

View File

@@ -33,5 +33,6 @@ export const machineApi = {
addConf: Api.create("/machines/{machineId}/files", 'post'), addConf: Api.create("/machines/{machineId}/files", 'post'),
// 删除配置的文件or目录 // 删除配置的文件or目录
delConf: Api.create("/machines/{machineId}/files/{id}", 'delete'), delConf: Api.create("/machines/{machineId}/files/{id}", 'delete'),
terminal: Api.create("/api/machines/{id}/terminal", 'get') terminal: Api.create("/api/machines/{id}/terminal", 'get'),
recDirNames: Api.create("/machines/rec/names", 'get')
} }

View File

@@ -5,7 +5,7 @@
<el-button type="primary" icon="edit" :disabled="currentId == null" @click="editMongo(false)" plain>编辑</el-button> <el-button type="primary" icon="edit" :disabled="currentId == null" @click="editMongo(false)" plain>编辑</el-button>
<el-button type="danger" icon="delete" :disabled="currentId == null" @click="deleteMongo" plain>删除</el-button> <el-button type="danger" icon="delete" :disabled="currentId == null" @click="deleteMongo" plain>删除</el-button>
<div style="float: right"> <div style="float: right">
<el-select v-model="query.projectId" placeholder="请选择项目" filterable clearable> <el-select @focus="getProjects" v-model="query.projectId" placeholder="请选择项目" filterable clearable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option> <el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select> </el-select>
<el-button class="ml5" @click="search" type="success" icon="search"></el-button> <el-button class="ml5" @click="search" type="success" icon="search"></el-button>
@@ -250,7 +250,6 @@ export default defineComponent({
onMounted(async () => { onMounted(async () => {
search(); search();
state.projects = await projectApi.accountProjects.request(null);
}); });
const handlePageChange = (curPage: number) => { const handlePageChange = (curPage: number) => {
@@ -371,7 +370,12 @@ export default defineComponent({
state.total = res.total; state.total = res.total;
}; };
const editMongo = (isAdd = false) => { const getProjects = async () => {
state.projects = await projectApi.accountProjects.request(null);
};
const editMongo = async (isAdd = false) => {
await getProjects();
if (isAdd) { if (isAdd) {
state.mongoEditDialog.data = null; state.mongoEditDialog.data = null;
state.mongoEditDialog.title = '新增mongo'; state.mongoEditDialog.title = '新增mongo';
@@ -390,6 +394,7 @@ export default defineComponent({
return { return {
...toRefs(state), ...toRefs(state),
getProjects,
search, search,
handlePageChange, handlePageChange,
choose, choose,

View File

@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<el-table :data="projects" @current-change="choose" ref="table" style="width: 100%"> <el-table :data="projects" @current-change="choose" ref="table" style="width: 100%">
<el-table-column label="选择" width="50px"> <el-table-column label="选择" width="55px">
<template #default="scope"> <template #default="scope">
<el-radio v-model="chooseId" :label="scope.row.id"> <el-radio v-model="chooseId" :label="scope.row.id">
<i></i> <i></i>
@@ -80,9 +80,18 @@
<el-dialog width="500px" :title="showEnvDialog.title" v-model="showEnvDialog.visible"> <el-dialog width="500px" :title="showEnvDialog.title" v-model="showEnvDialog.visible">
<div class="toolbar"> <div class="toolbar">
<el-button @click="showAddEnvDialog" v-auth="permissions.saveMember" type="primary" icon="plus">添加</el-button> <el-button @click="showAddEnvDialog" v-auth="permissions.saveMember" type="primary" icon="plus">添加</el-button>
<!-- <el-button v-auth="'role:update'" :disabled="chooseId == null" type="danger" icon="delete">删除</el-button> --> <el-button @click="deleteEnv" v-auth="permissions.delProject" :disabled="showEnvDialog.chooseId == null" type="danger" icon="delete"
>删除</el-button
>
</div> </div>
<el-table border :data="showEnvDialog.envs"> <el-table @current-change="chooseEnv" border :data="showEnvDialog.envs">
<el-table-column label="选择" width="50px">
<template #default="scope">
<el-radio v-model="showEnvDialog.chooseId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column property="name" label="环境名" width="125"></el-table-column> <el-table-column property="name" label="环境名" width="125"></el-table-column>
<el-table-column property="remark" label="描述" width="125"></el-table-column> <el-table-column property="remark" label="描述" width="125"></el-table-column>
<el-table-column property="createTime" label="创建时间"> <el-table-column property="createTime" label="创建时间">
@@ -152,7 +161,7 @@
:remote-method="getAccount" :remote-method="getAccount"
v-model="showMemDialog.memForm.accountId" v-model="showMemDialog.memForm.accountId"
filterable filterable
placeholder="请选择" placeholder="请输入账号模糊搜索并选择"
> >
<el-option v-for="item in showMemDialog.accounts" :key="item.id" :label="item.username" :value="item.id"> </el-option> <el-option v-for="item in showMemDialog.accounts" :key="item.id" :label="item.username" :value="item.id"> </el-option>
</el-select> </el-select>
@@ -207,6 +216,8 @@ export default defineComponent({
}, },
showEnvDialog: { showEnvDialog: {
visible: false, visible: false,
chooseId: null,
chooseData: null,
envs: [], envs: [],
title: '', title: '',
addVisible: false, addVisible: false,
@@ -340,6 +351,21 @@ export default defineComponent({
state.showEnvDialog.visible = true; state.showEnvDialog.visible = true;
}; };
const chooseEnv = (item: any) => {
if (!item) {
return;
}
state.showEnvDialog.chooseData = item;
state.showEnvDialog.chooseId = item.id;
};
const deleteEnv = async () => {
notNull(state.showEnvDialog.chooseData, '请选选择环境');
await projectApi.delProjectEnvs.request({ id: state.showEnvDialog.chooseId });
ElMessage.success('删除成功');
state.showEnvDialog.envs = await projectApi.projectEnvs.request({ projectId: state.chooseId });
};
const showAddMemberDialog = () => { const showAddMemberDialog = () => {
state.showMemDialog.addVisible = true; state.showMemDialog.addVisible = true;
}; };
@@ -398,12 +424,14 @@ export default defineComponent({
showMembers, showMembers,
setMemebers, setMemebers,
showEnv, showEnv,
deleteEnv,
showAddMemberDialog, showAddMemberDialog,
addMember, addMember,
chooseMember, chooseMember,
deleteMember, deleteMember,
cancelAddMember, cancelAddMember,
showAddEnvDialog, showAddEnvDialog,
chooseEnv,
addEnv, addEnv,
cancelAddEnv, cancelAddEnv,
getAccount, getAccount,

View File

@@ -8,6 +8,7 @@ export const projectApi = {
delProject: Api.create("/projects", 'delete'), delProject: Api.create("/projects", 'delete'),
// 获取项目下的环境信息 // 获取项目下的环境信息
projectEnvs: Api.create("/projects/{projectId}/envs", 'get'), projectEnvs: Api.create("/projects/{projectId}/envs", 'get'),
delProjectEnvs: Api.create("/projects/envs", 'delete'),
saveProjectEnv: Api.create("/projects/{projectId}/envs", 'post'), saveProjectEnv: Api.create("/projects/{projectId}/envs", 'post'),
// 获取项目下的成员信息 // 获取项目下的成员信息
projectMems: Api.create("/projects/{projectId}/members", 'get'), projectMems: Api.create("/projects/{projectId}/members", 'get'),

View File

@@ -1,313 +0,0 @@
<template>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :show-close="false" width="750px" :destroy-on-close="true">
<el-form label-width="85px">
<el-form-item prop="key" label="key:">
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
</el-form-item>
<el-form-item prop="timed" label="过期时间:">
<el-input v-model.number="key.timed" type="number"></el-input>
</el-form-item>
<el-form-item prop="dataType" label="数据类型:">
<el-select :disabled="operationType == 2" style="width: 100%" v-model="key.type" placeholder="请选择数据类型">
<el-option key="string" label="string" value="string"> </el-option>
<el-option key="hash" label="hash" value="hash"> </el-option>
<el-option key="set" label="set" value="set"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="key.type == 'string'" prop="value" label="内容:">
<div id="string-value-text" style="width: 100%">
<el-input class="json-text" v-model="string.value" type="textarea" :autosize="{ minRows: 10, maxRows: 20 }"></el-input>
<el-select class="text-type-select" @change="onChangeTextType" v-model="string.type">
<el-option key="text" label="text" value="text"> </el-option>
<el-option key="json" label="json" value="json"> </el-option>
</el-select>
</div>
</el-form-item>
<span v-if="key.type == 'hash'">
<el-button @click="onAddHashValue" icon="plus" size="small" plain class="mt10">添加</el-button>
<el-table :data="hash.value" stripe style="width: 100%">
<el-table-column prop="key" label="key" width>
<template #default="scope">
<el-input v-model="scope.row.key" clearable size="small"></el-input>
</template>
</el-table-column>
<el-table-column prop="value" label="value" min-width="200">
<template #default="scope">
<el-input
v-model="scope.row.value"
clearable
type="textarea"
:autosize="{ minRows: 2, maxRows: 10 }"
size="small"
></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="scope">
<el-button type="danger" @click="hash.value.splice(scope.$index, 1)" icon="delete" size="small" plain>删除</el-button>
</template>
</el-table-column>
</el-table>
</span>
<span v-if="key.type == 'set'">
<el-button @click="onAddSetValue" icon="plus" size="small" plain class="mt10">添加</el-button>
<el-table :data="set.value" stripe style="width: 100%">
<el-table-column prop="value" label="value" min-width="200">
<template #default="scope">
<el-input
v-model="scope.row.value"
clearable
type="textarea"
:autosize="{ minRows: 2, maxRows: 10 }"
size="small"
></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="scope">
<el-button type="danger" @click="set.value.splice(scope.$index, 1)" icon="delete" size="small" plain>删除</el-button>
</template>
</el-table-column>
</el-table>
</span>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { isTrue, notEmpty } from '@/common/assert';
import { formatJsonString } from '@/common/utils/format';
export default defineComponent({
name: 'DateEdit',
components: {},
props: {
visible: {
type: Boolean,
},
title: {
type: String,
},
redisId: {
type: [Number],
require: true,
},
keyInfo: {
type: [Object],
},
// 操作类型1新增2修改
operationType: {
type: [Number],
},
stringValue: {
type: [String],
},
setValue: {
type: [Array, Object],
},
hashValue: {
type: [Array, Object],
},
},
emits: ['valChange', 'cancel', 'update:visible'],
setup(props: any, { emit }) {
const state = reactive({
dialogVisible: false,
operationType: 1,
redisId: '',
key: {
key: '',
type: 'string',
timed: -1,
},
string: {
type: 'text',
value: '',
},
hash: {
value: [
{
key: '',
value: '',
},
],
},
set: {
value: [{ value: '' }],
},
});
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
state.key = {
key: '',
type: 'string',
timed: -1,
};
state.string.value = '';
state.string.type = 'text';
state.hash.value = [
{
key: '',
value: '',
},
];
}, 500);
};
watch(
() => props.visible,
(val) => {
state.dialogVisible = val;
}
);
watch(
() => props.redisId,
(val) => {
state.redisId = val;
}
);
watch(
() => props.operationType,
(val) => {
state.operationType = val;
}
);
watch(
() => props.keyInfo,
(val) => {
if (val) {
state.key = { ...val };
}
},
{
deep: true, // 深度监听的参数
}
);
watch(
() => props.stringValue,
(val) => {
if (val) {
state.string.value = val;
}
},
{
deep: true, // 深度监听的参数
}
);
watch(
() => props.setValue,
(val) => {
if (val) {
state.set.value = val;
}
},
{
deep: true, // 深度监听的参数
}
);
watch(
() => props.hashValue,
(val) => {
if (val) {
state.hash.value = val;
}
},
{
deep: true, // 深度监听的参数
}
);
const saveValue = async () => {
notEmpty(state.key.key, 'key不能为空');
if (state.key.type == 'string') {
notEmpty(state.string.value, 'value不能为空');
const sv = { value: formatJsonString(state.string.value, true), id: state.redisId };
Object.assign(sv, state.key);
await redisApi.saveStringValue.request(sv);
}
if (state.key.type == 'hash') {
isTrue(state.hash.value.length > 0, 'hash内容不能为空');
const sv = { value: state.hash.value, id: state.redisId };
Object.assign(sv, state.key);
await redisApi.saveHashValue.request(sv);
}
if (state.key.type == 'set') {
isTrue(state.set.value.length > 0, 'set内容不能为空');
const sv = { value: state.set.value.map((x) => x.value), id: state.redisId };
Object.assign(sv, state.key);
await redisApi.saveSetValue.request(sv);
}
ElMessage.success('数据保存成功');
cancel();
emit('valChange');
};
const onAddHashValue = () => {
state.hash.value.push({ key: '', value: '' });
};
const onAddSetValue = () => {
state.set.value.push({ value: '' });
};
// 更改文本类型
const onChangeTextType = (val: string) => {
if (val == 'json') {
state.string.value = formatJsonString(state.string.value, false);
return;
}
if (val == 'text') {
state.string.value = formatJsonString(state.string.value, true);
}
};
return {
...toRefs(state),
saveValue,
cancel,
onAddHashValue,
onAddSetValue,
onChangeTextType,
};
},
});
</script>
<style lang="scss">
#string-value-text {
flex-grow: 1;
display: flex;
position: relative;
.text-type-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 70px;
}
}
</style>

View File

@@ -23,7 +23,7 @@
<el-form class="search-form" label-position="right" :inline="true" label-width="60px"> <el-form class="search-form" label-position="right" :inline="true" label-width="60px">
<el-form-item label="key" label-width="40px"> <el-form-item label="key" label-width="40px">
<el-input <el-input
placeholder="支持*模糊key" placeholder="match 支持*模糊key"
style="width: 240px" style="width: 240px"
v-model="scanParam.match" v-model="scanParam.match"
@clear="clear()" @clear="clear()"
@@ -36,7 +36,14 @@
<el-form-item> <el-form-item>
<el-button @click="searchKey()" type="success" icon="search" plain></el-button> <el-button @click="searchKey()" type="success" icon="search" plain></el-button>
<el-button @click="scan()" icon="bottom" plain>scan</el-button> <el-button @click="scan()" icon="bottom" plain>scan</el-button>
<el-button type="primary" icon="plus" @click="onAddData(false)" plain></el-button> <el-popover placement="right" :width="200" trigger="click">
<template #reference>
<el-button type="primary" icon="plus" plain></el-button>
</template>
<el-tag @click="onAddData('string')" :color="getTypeColor('string')" style="cursor: pointer">string</el-tag>
<el-tag @click="onAddData('hash')" :color="getTypeColor('hash')" class="ml5" style="cursor: pointer">hash</el-tag>
<el-tag @click="onAddData('set')" :color="getTypeColor('set')" class="ml5" style="cursor: pointer">set</el-tag>
</el-popover>
</el-form-item> </el-form-item>
<div style="float: right"> <div style="float: right">
<span>keys: {{ dbsize }}</span> <span>keys: {{ dbsize }}</span>
@@ -69,17 +76,32 @@
<div style="text-align: center; margin-top: 10px"></div> <div style="text-align: center; margin-top: 10px"></div>
<!-- <value-dialog v-model:visible="valueDialog.visible" :keyValue="valueDialog.value" /> --> <hash-value
v-model:visible="hashValueDialog.visible"
:operationType="dataEdit.operationType"
:title="dataEdit.title"
:keyInfo="dataEdit.keyInfo"
:redisId="scanParam.id"
@cancel="onCancelDataEdit"
@valChange="searchKey"
/>
<data-edit <string-value
v-model:visible="dataEdit.visible" v-model:visible="stringValueDialog.visible"
:operationType="dataEdit.operationType"
:title="dataEdit.title"
:keyInfo="dataEdit.keyInfo"
:redisId="scanParam.id"
@cancel="onCancelDataEdit"
@valChange="searchKey"
/>
<set-value
v-model:visible="setValueDialog.visible"
:title="dataEdit.title" :title="dataEdit.title"
:keyInfo="dataEdit.keyInfo" :keyInfo="dataEdit.keyInfo"
:redisId="scanParam.id" :redisId="scanParam.id"
:operationType="dataEdit.operationType" :operationType="dataEdit.operationType"
:stringValue="dataEdit.stringValue"
:setValue="dataEdit.setValue"
:hashValue="dataEdit.hashValue"
@valChange="searchKey" @valChange="searchKey"
@cancel="onCancelDataEdit" @cancel="onCancelDataEdit"
/> />
@@ -91,13 +113,17 @@ import { redisApi } from './api';
import { toRefs, reactive, defineComponent } from 'vue'; import { toRefs, reactive, defineComponent } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import ProjectEnvSelect from '../component/ProjectEnvSelect.vue'; import ProjectEnvSelect from '../component/ProjectEnvSelect.vue';
import DataEdit from './DataEdit.vue'; import HashValue from './HashValue.vue';
import StringValue from './StringValue.vue';
import SetValue from './SetValue.vue';
import { isTrue, notBlank, notNull } from '@/common/assert'; import { isTrue, notBlank, notNull } from '@/common/assert';
export default defineComponent({ export default defineComponent({
name: 'DataOperation', name: 'DataOperation',
components: { components: {
DataEdit, StringValue,
HashValue,
SetValue,
ProjectEnvSelect, ProjectEnvSelect,
}, },
setup() { setup() {
@@ -113,10 +139,6 @@ export default defineComponent({
count: 10, count: 10,
cursor: {}, cursor: {},
}, },
valueDialog: {
visible: false,
value: {},
},
dataEdit: { dataEdit: {
visible: false, visible: false,
title: '新增数据', title: '新增数据',
@@ -126,9 +148,15 @@ export default defineComponent({
timed: -1, timed: -1,
key: '', key: '',
}, },
stringValue: '', },
hashValue: [{ key: '', value: '' }], hashValueDialog: {
setValue: [{ value: '' }], visible: false,
},
stringValueDialog: {
visible: false,
},
setValueDialog: {
visible: false,
}, },
keys: [], keys: [],
dbsize: 0, dbsize: 0,
@@ -158,10 +186,15 @@ export default defineComponent({
const scan = async () => { const scan = async () => {
isTrue(state.scanParam.id != null, '请先选择redis'); isTrue(state.scanParam.id != null, '请先选择redis');
notBlank(state.scanParam.count, 'count不能为空'); notBlank(state.scanParam.count, 'count不能为空');
isTrue(state.scanParam.count < 20001, 'count不能超过20000');
const match = state.scanParam.match;
if (!match || match == '*') {
isTrue(state.scanParam.count <= 200, 'match为空或者*时, count不能超过200');
} else {
isTrue(state.scanParam.count <= 20000, 'count不能超过20000');
}
state.loading = true; state.loading = true;
try { try {
const res = await redisApi.scan.request(state.scanParam); const res = await redisApi.scan.request(state.scanParam);
state.keys = res.keys; state.keys = res.keys;
@@ -207,60 +240,43 @@ export default defineComponent({
const getValue = async (row: any) => { const getValue = async (row: any) => {
const type = row.type; const type = row.type;
const key = row.key;
let res: any;
const reqParam = {
key: row.key,
id: state.scanParam.id,
};
switch (type) {
case 'string':
res = await redisApi.getStringValue.request(reqParam);
break;
case 'hash':
res = await redisApi.getHashValue.request(reqParam);
break;
case 'set':
res = await redisApi.getSetValue.request(reqParam);
break;
default:
res = null;
break;
}
notNull(res, '暂不支持该类型数据查看');
if (type == 'string') {
state.dataEdit.stringValue = res;
}
if (type == 'set') {
state.dataEdit.setValue = res.map((x: any) => {
return {
value: x,
};
});
}
if (type == 'hash') {
const hash = [];
//遍历key和value
const keys = Object.keys(res);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = res[key];
hash.push({
key,
value,
});
}
state.dataEdit.hashValue = hash;
}
state.dataEdit.keyInfo.type = type; state.dataEdit.keyInfo.type = type;
state.dataEdit.keyInfo.timed = row.ttl; state.dataEdit.keyInfo.timed = row.ttl;
state.dataEdit.keyInfo.key = key; state.dataEdit.keyInfo.key = row.key;
state.dataEdit.operationType = 2; state.dataEdit.operationType = 2;
state.dataEdit.title = '修改数据'; state.dataEdit.title = '查看数据';
state.dataEdit.visible = true;
if (type == 'hash') {
state.hashValueDialog.visible = true;
} else if (type == 'string') {
state.stringValueDialog.visible = true;
} else if (type == 'set') {
state.setValueDialog.visible = true;
} else {
ElMessage.warning('暂不支持该类型');
}
};
const onAddData = (type: string) => {
notNull(state.scanParam.id, '请先选择redis');
state.dataEdit.operationType = 1;
state.dataEdit.title = '新增数据';
state.dataEdit.keyInfo.type = type;
state.dataEdit.keyInfo.timed = -1;
if (type == 'hash') {
state.hashValueDialog.visible = true;
} else if (type == 'string') {
state.stringValueDialog.visible = true;
} else if (type == 'set') {
state.setValueDialog.visible = true;
} else {
ElMessage.warning('暂不支持该类型');
}
};
const onCancelDataEdit = () => {
state.dataEdit.keyInfo = {} as any;
}; };
const del = (key: string) => { const del = (key: string) => {
@@ -331,20 +347,6 @@ export default defineComponent({
} }
}; };
const onAddData = () => {
notNull(state.scanParam.id, '请先选择redis');
state.dataEdit.operationType = 1;
state.dataEdit.title = '新增数据';
state.dataEdit.visible = true;
};
const onCancelDataEdit = () => {
state.dataEdit.keyInfo = {} as any;
state.dataEdit.stringValue = '';
state.dataEdit.setValue = [];
state.dataEdit.hashValue = [];
};
return { return {
...toRefs(state), ...toRefs(state),
changeProjectEnv, changeProjectEnv,

View File

@@ -0,0 +1,254 @@
<template>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
<el-form label-width="85px">
<el-form-item prop="key" label="key:">
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
</el-form-item>
<el-form-item prop="timed" label="过期时间:">
<el-input v-model.number="key.timed" type="number"></el-input>
</el-form-item>
<el-form-item prop="dataType" label="数据类型:">
<el-input v-model="key.type" disabled></el-input>
</el-form-item>
<el-row class="mt10">
<el-form label-position="right" :inline="true">
<el-form-item label="field" label-width="40px" v-if="operationType == 2">
<el-input placeholder="支持*模糊field" style="width: 140px" v-model="scanParam.match" clearable size="small"></el-input>
</el-form-item>
<el-form-item label="count" v-if="operationType == 2">
<el-input placeholder="count" style="width: 62px" v-model.number="scanParam.count" size="small"></el-input>
</el-form-item>
<el-form-item>
<el-button v-if="operationType == 2" @click="reHscan()" type="success" icon="search" plain size="small"></el-button>
<el-button v-if="operationType == 2" @click="hscan()" icon="bottom" plain size="small">scan</el-button>
<el-button @click="onAddHashValue" icon="plus" size="small" plain>添加</el-button>
</el-form-item>
<div v-if="operationType == 2" class="mt10" style="float: right">
<span>fieldSize: {{ keySize }}</span>
</div>
</el-form>
</el-row>
<el-table :data="hashValues" stripe style="width: 100%">
<el-table-column prop="field" label="field" width>
<template #default="scope">
<el-input v-model="scope.row.field" clearable size="small"></el-input>
</template>
</el-table-column>
<el-table-column prop="value" label="value" min-width="200">
<template #default="scope">
<el-input v-model="scope.row.value" clearable type="textarea" :autosize="{ minRows: 2, maxRows: 10 }" size="small"></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button v-if="operationType == 2" type="success" @click="hset(scope.row)" icon="check" size="small" plain></el-button>
<el-button type="danger" @click="hdel(scope.row.field, scope.$index)" icon="delete" size="small" plain></el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer v-if="operationType == 1">
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
import { isTrue, notEmpty } from '@/common/assert';
export default defineComponent({
name: 'HashValue',
components: {},
props: {
visible: {
type: Boolean,
},
title: {
type: String,
},
// 操作类型1新增2修改
operationType: {
type: [Number],
require: true,
},
redisId: {
type: [Number],
require: true,
},
keyInfo: {
type: [Object],
},
hashValue: {
type: [Array, Object],
},
},
emits: ['valChange', 'cancel', 'update:visible'],
setup(props: any, { emit }) {
const state = reactive({
dialogVisible: false,
operationType: 1,
redisId: 0,
key: {
key: '',
type: 'hash',
timed: -1,
},
scanParam: {
key: '',
id: 0,
cursor: 0,
match: '',
count: 10,
},
keySize: 0,
hashValues: [
{
field: '',
value: '',
},
],
});
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
state.hashValues = [];
state.key = {} as any;
}, 500);
};
watch(props, async (newValue) => {
const visible = newValue.visible;
state.redisId = newValue.redisId;
state.key = newValue.keyInfo;
state.operationType = newValue.operationType;
if (visible && state.operationType == 2) {
state.scanParam.id = props.redisId;
state.scanParam.key = state.key.key;
await reHscan();
}
state.dialogVisible = visible;
});
const reHscan = async () => {
state.scanParam.id = state.redisId;
state.scanParam.cursor = 0;
hscan();
};
const hscan = async () => {
const match = state.scanParam.match;
if (!match || match == '' || match == '*') {
if (state.scanParam.count > 100) {
ElMessage.error('match为空或者*时, count不能超过100');
return;
}
} else {
if (state.scanParam.count > 1000) {
ElMessage.error('count不能超过1000');
return;
}
}
const scanRes = await redisApi.hscan.request(state.scanParam);
state.scanParam.cursor = scanRes.cursor;
state.keySize = scanRes.keySize;
const keys = scanRes.keys;
const hashValue = [];
const fieldCount = keys.length / 2;
let nextFieldIndex = 0;
for (let i = 0; i < fieldCount; i++) {
hashValue.push({ field: keys[nextFieldIndex++], value: keys[nextFieldIndex++] });
}
state.hashValues = hashValue;
};
const hdel = async (field: any, index: any) => {
// 如果是新增操作,则直接数组移除即可
if (state.operationType == 1) {
state.hashValues.splice(index, 1);
return;
}
await ElMessageBox.confirm(`确定删除[${field}]?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await redisApi.hdel.request({
id: state.redisId,
key: state.key.key,
field,
});
ElMessage.success('删除成功');
reHscan();
};
const hset = async (row: any) => {
await redisApi.saveHashValue.request({
id: state.redisId,
key: state.key.key,
timed: state.key.timed,
value: [
{
field: row.field,
value: row.value,
},
],
});
ElMessage.success('保存成功');
};
const onAddHashValue = () => {
state.hashValues.unshift({ field: '', value: '' });
};
const saveValue = async () => {
notEmpty(state.key.key, 'key不能为空');
isTrue(state.hashValues.length > 0, 'hash内容不能为空');
const sv = { value: state.hashValues, id: state.redisId };
Object.assign(sv, state.key);
await redisApi.saveHashValue.request(sv);
ElMessage.success('保存成功');
cancel();
emit('valChange');
};
return {
...toRefs(state),
reHscan,
hscan,
cancel,
hdel,
hset,
onAddHashValue,
saveValue,
};
},
});
</script>
<style lang="scss">
#string-value-text {
flex-grow: 1;
display: flex;
position: relative;
.text-type-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 70px;
}
}
</style>

View File

@@ -17,12 +17,13 @@
<el-select style="width: 100%" v-model="form.mode" placeholder="请选择模式"> <el-select style="width: 100%" v-model="form.mode" placeholder="请选择模式">
<el-option label="standalone" value="standalone"> </el-option> <el-option label="standalone" value="standalone"> </el-option>
<el-option label="cluster" value="cluster"> </el-option> <el-option label="cluster" value="cluster"> </el-option>
<el-option label="sentinel" value="sentinel"> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item prop="host" label="host:" required> <el-form-item prop="host" label="host:" required>
<el-input <el-input
v-model.trim="form.host" v-model.trim="form.host"
placeholder="请输入host:port,集群模式用','分割" placeholder="请输入host:portsentinel模式为: mastername=sentinelhost:port若集群或哨兵需设多个节点可使用','分割"
auto-complete="off" auto-complete="off"
type="textarea" type="textarea"
></el-input> ></el-input>
@@ -113,7 +114,7 @@ export default defineComponent({
id: null, id: null,
name: null, name: null,
mode: 'standalone', mode: 'standalone',
host: null, host: '',
password: null, password: null,
project: null, project: null,
projectId: null, projectId: null,
@@ -219,6 +220,10 @@ export default defineComponent({
redisForm.value.validate(async (valid: boolean) => { redisForm.value.validate(async (valid: boolean) => {
if (valid) { if (valid) {
const reqForm = { ...state.form }; const reqForm = { ...state.form };
if (reqForm.mode == 'sentinel' && reqForm.host.split('=').length != 2) {
ElMessage.error('sentinel模式host需为: mastername=sentinelhost:sentinelport模式');
return;
}
reqForm.password = await RsaEncrypt(reqForm.password); reqForm.password = await RsaEncrypt(reqForm.password);
redisApi.saveRedis.request(reqForm).then(() => { redisApi.saveRedis.request(reqForm).then(() => {
ElMessage.success('保存成功'); ElMessage.success('保存成功');

View File

@@ -5,7 +5,7 @@
<el-button type="primary" icon="edit" :disabled="currentId == null" @click="editRedis(false)" plain>编辑</el-button> <el-button type="primary" icon="edit" :disabled="currentId == null" @click="editRedis(false)" plain>编辑</el-button>
<el-button type="danger" icon="delete" :disabled="currentId == null" @click="deleteRedis" plain>删除</el-button> <el-button type="danger" icon="delete" :disabled="currentId == null" @click="deleteRedis" plain>删除</el-button>
<div style="float: right"> <div style="float: right">
<el-select v-model="query.projectId" placeholder="请选择项目" filterable clearable> <el-select @focus="getProjects" v-model="query.projectId" placeholder="请选择项目" filterable clearable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option> <el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
</el-select> </el-select>
<el-button class="ml5" @click="search" type="success" icon="search"></el-button> <el-button class="ml5" @click="search" type="success" icon="search"></el-button>
@@ -31,7 +31,13 @@
<el-table-column prop="creator" label="创建人" min-width="100"></el-table-column> <el-table-column prop="creator" label="创建人" min-width="100"></el-table-column>
<el-table-column label="更多" min-width="130" fixed="right"> <el-table-column label="更多" min-width="130" fixed="right">
<template #default="scope"> <template #default="scope">
<el-link v-if="scope.row.mode == 'standalone'" type="primary" @click="info(scope.row)" :underline="false">单机信息</el-link> <el-link
v-if="scope.row.mode == 'standalone' || scope.row.mode == 'sentinel'"
type="primary"
@click="info(scope.row)"
:underline="false"
>单机信息</el-link
>
<el-link @click="onShowClusterInfo(scope.row)" v-if="scope.row.mode == 'cluster'" type="success" :underline="false" <el-link @click="onShowClusterInfo(scope.row)" v-if="scope.row.mode == 'cluster'" type="success" :underline="false"
>集群信息</el-link >集群信息</el-link
> >
@@ -202,7 +208,6 @@ export default defineComponent({
onMounted(async () => { onMounted(async () => {
search(); search();
state.projects = await projectApi.accountProjects.request(null);
}); });
const handlePageChange = (curPage: number) => { const handlePageChange = (curPage: number) => {
@@ -258,7 +263,12 @@ export default defineComponent({
state.total = res.total; state.total = res.total;
}; };
const editRedis = (isAdd = false) => { const getProjects = async () => {
state.projects = await projectApi.accountProjects.request(null);
};
const editRedis = async (isAdd = false) => {
await getProjects();
if (isAdd) { if (isAdd) {
state.redisEditDialog.data = null; state.redisEditDialog.data = null;
state.redisEditDialog.title = '新增redis'; state.redisEditDialog.title = '新增redis';
@@ -277,6 +287,7 @@ export default defineComponent({
return { return {
...toRefs(state), ...toRefs(state),
getProjects,
search, search,
handlePageChange, handlePageChange,
choose, choose,

View File

@@ -0,0 +1,157 @@
<template>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
<el-form label-width="85px">
<el-form-item prop="key" label="key:">
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
</el-form-item>
<el-form-item prop="timed" label="过期时间:">
<el-input v-model.number="key.timed" type="number"></el-input>
</el-form-item>
<el-form-item prop="dataType" label="数据类型:">
<el-input v-model="key.type" disabled></el-input>
</el-form-item>
<el-button @click="onAddSetValue" icon="plus" size="small" plain class="mt10">添加</el-button>
<el-table :data="value" stripe style="width: 100%">
<el-table-column prop="value" label="value" min-width="200">
<template #default="scope">
<el-input v-model="scope.row.value" clearable type="textarea" :autosize="{ minRows: 2, maxRows: 10 }" size="small"></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="scope">
<el-button type="danger" @click="set.value.splice(scope.$index, 1)" icon="delete" size="small" plain>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { isTrue, notEmpty } from '@/common/assert';
export default defineComponent({
name: 'SetValue',
components: {},
props: {
visible: {
type: Boolean,
},
title: {
type: String,
},
redisId: {
type: [Number],
require: true,
},
keyInfo: {
type: [Object],
},
// 操作类型1新增2修改
operationType: {
type: [Number],
},
setValue: {
type: [Array, Object],
},
},
emits: ['valChange', 'cancel', 'update:visible'],
setup(props: any, { emit }) {
const state = reactive({
dialogVisible: false,
operationType: 1,
redisId: '',
key: {
key: '',
type: 'string',
timed: -1,
},
value: [{ value: '' }],
});
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
state.key = {
key: '',
type: 'string',
timed: -1,
};
state.value = [];
}, 500);
};
watch(props, async (newValue) => {
state.dialogVisible = newValue.visible;
state.key = newValue.key;
state.redisId = newValue.redisId;
state.key = newValue.keyInfo;
state.operationType = newValue.operationType;
// 如果是查看编辑操作,则获取值
if (state.dialogVisible && state.operationType == 2) {
getSetValue();
}
});
const getSetValue = async () => {
const res = await redisApi.getSetValue.request({
id: state.redisId,
key: state.key.key,
});
state.value = res.map((x: any) => {
return {
value: x,
};
});
};
const saveValue = async () => {
notEmpty(state.key.key, 'key不能为空');
isTrue(state.value.length > 0, 'set内容不能为空');
const sv = { value: state.value.map((x) => x.value), id: state.redisId };
Object.assign(sv, state.key);
await redisApi.saveSetValue.request(sv);
ElMessage.success('数据保存成功');
cancel();
emit('valChange');
};
const onAddSetValue = () => {
state.value.unshift({ value: '' });
};
return {
...toRefs(state),
saveValue,
cancel,
onAddSetValue,
};
},
});
</script>
<style lang="scss">
#string-value-text {
flex-grow: 1;
display: flex;
position: relative;
.text-type-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 70px;
}
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
<el-form label-width="85px">
<el-form-item prop="key" label="key:">
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
</el-form-item>
<el-form-item prop="timed" label="过期时间:">
<el-input v-model.number="key.timed" type="number"></el-input>
</el-form-item>
<el-form-item prop="dataType" label="数据类型:">
<el-input v-model="key.type" disabled></el-input>
</el-form-item>
<div id="string-value-text" style="width: 100%">
<el-input class="json-text" v-model="string.value" type="textarea" :autosize="{ minRows: 10, maxRows: 20 }"></el-input>
<el-select class="text-type-select" @change="onChangeTextType" v-model="string.type">
<el-option key="text" label="text" value="text"> </el-option>
<el-option key="json" label="json" value="json"> </el-option>
</el-select>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, watch, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { notEmpty } from '@/common/assert';
import { formatJsonString } from '@/common/utils/format';
export default defineComponent({
name: 'StringValue',
components: {},
props: {
visible: {
type: Boolean,
},
title: {
type: String,
},
redisId: {
type: [Number],
require: true,
},
keyInfo: {
type: [Object],
},
// 操作类型1新增2修改
operationType: {
type: [Number],
},
},
emits: ['valChange', 'cancel', 'update:visible'],
setup(props: any, { emit }) {
const state = reactive({
dialogVisible: false,
operationType: 1,
redisId: '',
key: {
key: '',
type: 'string',
timed: -1,
},
string: {
type: 'text',
value: '',
},
});
const cancel = () => {
emit('update:visible', false);
emit('cancel');
setTimeout(() => {
state.key = {
key: '',
type: 'string',
timed: -1,
};
state.string.value = '';
state.string.type = 'text';
}, 500);
};
watch(
() => props.visible,
(val) => {
state.dialogVisible = val;
}
);
watch(
() => props.redisId,
(val) => {
state.redisId = val;
}
);
watch(props, async (newValue) => {
state.dialogVisible = newValue.visible;
state.key = newValue.key;
state.redisId = newValue.redisId;
state.key = newValue.keyInfo;
state.operationType = newValue.operationType
// 如果是查看编辑操作,则获取值
if (state.dialogVisible && state.operationType == 2) {
getStringValue();
}
});
const getStringValue = async () => {
state.string.value = await redisApi.getStringValue.request({
id: state.redisId,
key: state.key.key,
});
};
const saveValue = async () => {
notEmpty(state.key.key, 'key不能为空');
notEmpty(state.string.value, 'value不能为空');
const sv = { value: formatJsonString(state.string.value, true), id: state.redisId };
Object.assign(sv, state.key);
await redisApi.saveStringValue.request(sv);
ElMessage.success('数据保存成功');
cancel();
emit('valChange');
};
// 更改文本类型
const onChangeTextType = (val: string) => {
if (val == 'json') {
state.string.value = formatJsonString(state.string.value, false);
return;
}
if (val == 'text') {
state.string.value = formatJsonString(state.string.value, true);
}
};
return {
...toRefs(state),
saveValue,
cancel,
onChangeTextType,
};
},
});
</script>
<style lang="scss">
#string-value-text {
flex-grow: 1;
display: flex;
position: relative;
.text-type-select {
position: absolute;
z-index: 2;
right: 10px;
top: 10px;
max-width: 70px;
}
}
</style>

View File

@@ -12,6 +12,9 @@ export const redisApi = {
getStringValue: Api.create("/redis/{id}/string-value", 'get'), getStringValue: Api.create("/redis/{id}/string-value", 'get'),
saveStringValue: Api.create("/redis/{id}/string-value", 'post'), saveStringValue: Api.create("/redis/{id}/string-value", 'post'),
getHashValue: Api.create("/redis/{id}/hash-value", 'get'), getHashValue: Api.create("/redis/{id}/hash-value", 'get'),
hscan: Api.create("/redis/{id}/hscan", 'get'),
hget: Api.create("/redis/{id}/hget", 'get'),
hdel: Api.create("/redis/{id}/hdel", 'delete'),
saveHashValue: Api.create("/redis/{id}/hash-value", 'post'), saveHashValue: Api.create("/redis/{id}/hash-value", 'post'),
getSetValue: Api.create("/redis/{id}/set-value", 'get'), getSetValue: Api.create("/redis/{id}/set-value", 'get'),
saveSetValue: Api.create("/redis/{id}/set-value", 'post'), saveSetValue: Api.create("/redis/{id}/set-value", 'post'),

View File

@@ -5,12 +5,9 @@
<el-form-item prop="username" label="用户名:" required> <el-form-item prop="username" label="用户名:" required>
<el-input :disabled="edit" v-model.trim="form.username" placeholder="请输入账号用户名,密码默认与账号名一致" auto-complete="off"></el-input> <el-input :disabled="edit" v-model.trim="form.username" placeholder="请输入账号用户名,密码默认与账号名一致" auto-complete="off"></el-input>
</el-form-item> </el-form-item>
<!-- <el-form-item prop="password" label="密码:" required> <el-form-item v-if="edit" prop="password" label="密码:" required>
<el-input type="password" v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password"></el-input> <el-input type="password" v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password"></el-input>
</el-form-item> </el-form-item>
<el-form-item v-if="!edit" label="确认密码:" required>
<el-input type="password" v-model.trim="form.repassword" placeholder="请输入确认密码" autocomplete="new-password"></el-input>
</el-form-item> -->
</el-form> </el-form>
<template #footer> <template #footer>
@@ -74,6 +71,7 @@ export default defineComponent({
watch(props, (newValue) => { watch(props, (newValue) => {
if (newValue.account) { if (newValue.account) {
state.form = { ...newValue.account }; state.form = { ...newValue.account };
state.edit = true;
} else { } else {
state.form = {} as any; state.form = {} as any;
} }
@@ -81,11 +79,9 @@ export default defineComponent({
}); });
const btnOk = async () => { const btnOk = async () => {
let p = state.form.id ? accountApi.update : accountApi.save;
accountForm.value.validate((valid: boolean) => { accountForm.value.validate((valid: boolean) => {
if (valid) { if (valid) {
p.request(state.form).then(() => { accountApi.save.request(state.form).then(() => {
ElMessage.success('操作成功'); ElMessage.success('操作成功');
emit('val-change', state.form); emit('val-change', state.form);
state.btnLoading = true; state.btnLoading = true;

View File

@@ -2,7 +2,7 @@
<div class="role-list"> <div class="role-list">
<el-card> <el-card>
<el-button v-auth="'account:add'" type="primary" icon="plus" @click="editAccount(true)">添加</el-button> <el-button v-auth="'account:add'" type="primary" icon="plus" @click="editAccount(true)">添加</el-button>
<!-- <el-button v-auth="'account:update'" :disabled="chooseId == null" @click="editAccount(false)" type="primary" icon="edit">编辑</el-button> --> <el-button v-auth="'account:add'" :disabled="chooseId == null" @click="editAccount(false)" type="primary" icon="edit">编辑</el-button>
<el-button v-auth="'account:saveRoles'" :disabled="chooseId == null" @click="roleEdit()" type="success" icon="setting" <el-button v-auth="'account:saveRoles'" :disabled="chooseId == null" @click="roleEdit()" type="success" icon="setting"
>角色分配</el-button >角色分配</el-button
> >
@@ -20,7 +20,7 @@
<el-button @click="search()" type="success" icon="search" size="small"></el-button> <el-button @click="search()" type="success" icon="search" size="small"></el-button>
</div> </div>
<el-table :data="datas" ref="table" @current-change="choose" show-overflow-tooltip> <el-table :data="datas" ref="table" @current-change="choose" show-overflow-tooltip>
<el-table-column label="选择" width="50px"> <el-table-column label="选择" width="55px">
<template #default="scope"> <template #default="scope">
<el-radio v-model="chooseId" :label="scope.row.id"> <el-radio v-model="chooseId" :label="scope.row.id">
<i></i> <i></i>
@@ -29,20 +29,20 @@
</el-table-column> </el-table-column>
<el-table-column prop="username" label="用户名" min-width="115"></el-table-column> <el-table-column prop="username" label="用户名" min-width="115"></el-table-column>
<el-table-column align="center" prop="status" label="状态" min-width="63"> <el-table-column align="center" prop="status" label="状态" min-width="65">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.status == 1" type="success">正常</el-tag> <el-tag v-if="scope.row.status == 1" type="success">正常</el-tag>
<el-tag v-if="scope.row.status == -1" type="danger">禁用</el-tag> <el-tag v-if="scope.row.status == -1" type="danger">禁用</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column min-width="160" prop="lastLoginTime" label="最后登录时间"> <el-table-column min-width="160" prop="lastLoginTime" label="最后登录时间" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
{{ $filters.dateFormat(scope.row.lastLoginTime) }} {{ $filters.dateFormat(scope.row.lastLoginTime) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column min-width="115" prop="creator" label="创建账号"></el-table-column> <el-table-column min-width="115" prop="creator" label="创建账号"></el-table-column>
<el-table-column min-width="160" prop="createTime" label="创建时间"> <el-table-column min-width="160" prop="createTime" label="创建时间" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }} {{ $filters.dateFormat(scope.row.createTime) }}
</template> </template>

View File

@@ -32,6 +32,12 @@ export const accountApi = {
saveRoles: Api.create("/sys/accounts/roles", 'post') saveRoles: Api.create("/sys/accounts/roles", 'post')
} }
export const configApi = {
list: Api.create("/sys/configs", 'get'),
save: Api.create("/sys/configs", 'post'),
getValue: Api.create("/sys/configs/value", 'get'),
}
export const logApi = { export const logApi = {
list: Api.create("/syslogs", "get") list: Api.create("/syslogs", "get")
} }

View File

@@ -0,0 +1,99 @@
<template>
<div>
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" :destroy-on-close="true">
<el-form ref="configForm" :model="form" label-width="90px">
<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-input :disabled="form.id != null" v-model="form.key"></el-input>
</el-form-item>
<el-form-item prop="value" label="配置值:" required>
<el-input v-model="form.value"></el-input>
</el-form-item>
<el-form-item label="备注:">
<el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="btnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { ref, toRefs, reactive, watch, defineComponent } from 'vue';
import { configApi } from '../api';
export default defineComponent({
name: 'ConfigEdit',
props: {
visible: {
type: Boolean,
},
data: {
type: [Boolean, Object],
},
title: {
type: String,
},
},
setup(props: any, { emit }) {
const configForm: any = ref(null);
const state = reactive({
dvisible: false,
form: {
id: null,
name: '',
key: '',
value: '',
remark: '',
},
btnLoading: false,
});
watch(props, (newValue) => {
state.dvisible = newValue.visible;
if (newValue.data) {
state.form = { ...newValue.data };
} else {
state.form = {} as any;
}
});
const cancel = () => {
// 更新父组件visible prop对应的值为false
emit('update:visible', false);
// 若父组件有取消事件,则调用
emit('cancel');
};
const btnOk = async () => {
configForm.value.validate(async (valid: boolean) => {
if (valid) {
await configApi.save.request(state.form);
emit('val-change', state.form);
cancel();
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
}
});
};
return {
...toRefs(state),
configForm,
btnOk,
cancel,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,124 @@
<template>
<div class="role-list">
<el-card>
<el-button type="primary" icon="plus" @click="editConfig(false)">添加</el-button>
<el-button :disabled="chooseId == null" @click="editConfig(chooseData)" type="primary" icon="edit">编辑</el-button>
<el-table :data="configs" @current-change="choose" ref="table" style="width: 100%">
<el-table-column label="选择" width="55px">
<template #default="scope">
<el-radio v-model="chooseId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="name" label="配置项"></el-table-column>
<el-table-column prop="key" label="配置key"></el-table-column>
<el-table-column prop="value" label="配置值" min-width="100px" show-overflow-tooltip></el-table-column>
<el-table-column prop="remark" label="备注" min-width="100px" show-overflow-tooltip></el-table-column>
<el-table-column prop="updateTime" label="更新时间">
<template #default="scope">
{{ $filters.dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="modifier" label="修改者" show-overflow-tooltip></el-table-column>
</el-table>
<el-row style="margin-top: 20px" type="flex" justify="end">
<el-pagination
style="text-align: right"
@current-change="handlePageChange"
:total="total"
layout="prev, pager, next, total, jumper"
v-model:current-page="query.pageNum"
:page-size="query.pageSize"
></el-pagination>
</el-row>
</el-card>
<config-edit :title="configEdit.title" v-model:visible="configEdit.visible" :data="configEdit.config" @val-change="configEditChange" />
</div>
</template>
<script lang="ts">
import { toRefs, reactive, onMounted, defineComponent } from 'vue';
import ConfigEdit from './ConfigEdit.vue';
import { configApi } from '../api';
import { ElMessage, ElMessageBox } from 'element-plus';
export default defineComponent({
name: 'ConfigList',
components: {
ConfigEdit,
},
setup() {
const state = reactive({
dialogFormVisible: false,
currentEditPermissions: false,
query: {
pageNum: 1,
pageSize: 10,
name: null,
},
total: 0,
configs: [],
chooseId: null,
chooseData: null,
configEdit: {
title: '配置修改',
visible: false,
config: {},
},
});
onMounted(() => {
search();
});
const search = async () => {
let res = await configApi.list.request(state.query);
state.configs = res.list;
state.total = res.total;
};
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const choose = (item: any) => {
if (!item) {
return;
}
state.chooseId = item.id;
state.chooseData = item;
};
const configEditChange = () => {
ElMessage.success('修改成功!');
state.chooseId = null;
state.chooseData = null;
search();
};
const editConfig = (data: any) => {
if (data) {
state.configEdit.config = data;
} else {
state.configEdit.config = false;
}
state.configEdit.visible = true;
};
return {
...toRefs(state),
search,
handlePageChange,
choose,
configEditChange,
editConfig,
};
},
});
</script>
<style lang="scss">
</style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="role-dialog"> <div class="role-dialog">
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" :destroy-on-close="true"> <el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" :destroy-on-close="true">
<el-form :model="form" label-width="90px"> <el-form ref="roleForm" :model="form" label-width="90px">
<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-input v-model="form.name" auto-complete="off"></el-input>
</el-form-item> </el-form-item>
@@ -28,7 +28,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { toRefs, reactive, watch, defineComponent } from 'vue'; import { ref, toRefs, reactive, watch, defineComponent } from 'vue';
import { roleApi } from '../api'; import { roleApi } from '../api';
export default defineComponent({ export default defineComponent({
@@ -45,6 +45,7 @@ export default defineComponent({
}, },
}, },
setup(props: any, { emit }) { setup(props: any, { emit }) {
const roleForm: any = ref(null);
const state = reactive({ const state = reactive({
dvisible: false, dvisible: false,
form: { form: {
@@ -73,18 +74,22 @@ export default defineComponent({
}; };
const btnOk = async () => { const btnOk = async () => {
// let p = state.form.id ? roleApi.update : roleApi.save; roleForm.value.validate(async (valid: boolean) => {
await roleApi.save.request(state.form); if (valid) {
emit('val-change', state.form); await roleApi.save.request(state.form);
cancel(); emit('val-change', state.form);
state.btnLoading = true; cancel();
setTimeout(() => { state.btnLoading = true;
state.btnLoading = false; setTimeout(() => {
}, 1000); state.btnLoading = false;
}, 1000);
}
});
}; };
return { return {
...toRefs(state), ...toRefs(state),
roleForm,
btnOk, btnOk,
cancel, cancel,
}; };

View File

@@ -20,7 +20,7 @@
<el-button @click="search" type="success" icon="search"></el-button> <el-button @click="search" type="success" icon="search"></el-button>
</div> </div>
<el-table :data="roles" @current-change="choose" ref="table" style="width: 100%"> <el-table :data="roles" @current-change="choose" ref="table" style="width: 100%">
<el-table-column label="选择" width="50px"> <el-table-column label="选择" width="55px">
<template #default="scope"> <template #default="scope">
<el-radio v-model="chooseId" :label="scope.row.id"> <el-radio v-model="chooseId" :label="scope.row.id">
<i></i> <i></i>

View File

@@ -7,6 +7,13 @@
resolved "https://registry.npmmirror.com/@babel/parser/download/@babel/parser-7.16.6.tgz" resolved "https://registry.npmmirror.com/@babel/parser/download/@babel/parser-7.16.6.tgz"
integrity sha512-Gr86ujcNuPDnNOY8mi383Hvi8IYrJVJYuf3XcuBM/Dgd+bINn/7tHqsj+tKkoreMbmGsFLsltI/JJd8fOFWGDQ== integrity sha512-Gr86ujcNuPDnNOY8mi383Hvi8IYrJVJYuf3XcuBM/Dgd+bINn/7tHqsj+tKkoreMbmGsFLsltI/JJd8fOFWGDQ==
"@babel/runtime@^7.15.4":
version "7.18.9"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
dependencies:
regenerator-runtime "^0.13.4"
"@ctrl/tinycolor@^3.4.1": "@ctrl/tinycolor@^3.4.1":
version "3.4.1" version "3.4.1"
resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz" resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz"
@@ -17,6 +24,11 @@
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.0.6.tgz" resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.0.6.tgz"
integrity sha512-lPpG8hYkjL/Z97DH5Ei6w6o22Z4YdNglWCNYOPcB33JCF2A4wye6HFgSI7hEt9zdLyxlSpiqtgf9XcYU+m5mew== integrity sha512-lPpG8hYkjL/Z97DH5Ei6w6o22Z4YdNglWCNYOPcB33JCF2A4wye6HFgSI7hEt9zdLyxlSpiqtgf9XcYU+m5mew==
"@element-plus/icons-vue@^2.0.9":
version "2.0.9"
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.0.9.tgz#b7777c57534522e387303d194451d50ff549d49a"
integrity sha512-okdrwiVeKBmW41Hkl0eMrXDjzJwhQMuKiBOu17rOszqM+LS/yBYpNQNV5Jvoh06Wc+89fMmb/uhzf8NZuDuUaQ==
"@eslint/eslintrc@^1.0.5": "@eslint/eslintrc@^1.0.5":
version "1.0.5" version "1.0.5"
resolved "https://registry.npmmirror.com/@eslint/eslintrc/download/@eslint/eslintrc-1.0.5.tgz" resolved "https://registry.npmmirror.com/@eslint/eslintrc/download/@eslint/eslintrc-1.0.5.tgz"
@@ -32,17 +44,17 @@
minimatch "^3.0.4" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@floating-ui/core@^0.7.3": "@floating-ui/core@^1.0.1":
version "0.7.3" version "1.0.1"
resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-0.7.3.tgz" resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-1.0.1.tgz#00e64d74e911602c8533957af0cce5af6b2e93c8"
integrity sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg== integrity sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==
"@floating-ui/dom@^0.5.4": "@floating-ui/dom@^1.0.1":
version "0.5.4" version "1.0.1"
resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-0.5.4.tgz" resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.0.1.tgz#3321d4e799d6ac2503e729131d07ad0e714aabeb"
integrity sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg== integrity sha512-wBDiLUKWU8QNPNOTAFHiIAkBv1KlHauG2AhqjSeh2H+wR8PX+AArXfz8NkRexH5PgMJMmSOS70YS89AbWYh5dA==
dependencies: dependencies:
"@floating-ui/core" "^0.7.3" "@floating-ui/core" "^1.0.1"
"@humanwhocodes/config-array@^0.9.2": "@humanwhocodes/config-array@^0.9.2":
version "0.9.2" version "0.9.2"
@@ -126,10 +138,10 @@
resolved "https://registry.npmmirror.com/@types/sortablejs/download/@types/sortablejs-1.10.7.tgz" resolved "https://registry.npmmirror.com/@types/sortablejs/download/@types/sortablejs-1.10.7.tgz"
integrity sha1-q5A5yFQp8FFpVextvAuyATlBexU= integrity sha1-q5A5yFQp8FFpVextvAuyATlBexU=
"@types/web-bluetooth@^0.0.14": "@types/web-bluetooth@^0.0.15":
version "0.0.14" version "0.0.15"
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz" resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz#d60330046a6ed8a13b4a53df3813c44942ebdf72"
integrity sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A== integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==
"@typescript-eslint/eslint-plugin@^4.23.0": "@typescript-eslint/eslint-plugin@^4.23.0":
version "4.33.0" version "4.33.0"
@@ -364,25 +376,25 @@
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.37.tgz" resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.37.tgz"
integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw== integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==
"@vueuse/core@^8.7.5": "@vueuse/core@^9.1.0":
version "8.7.5" version "9.1.0"
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-8.7.5.tgz" resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.1.0.tgz#f0fb13fd99768c0eb617169a2d2c1cbd5f5a52eb"
integrity sha512-tqgzeZGoZcXzoit4kOGLWJibDMLp0vdm6ZO41SSUQhkhtrPhAg6dbIEPiahhUu6sZAmSYvVrZgEr5aKD51nrLA== integrity sha512-BIroqvXEqt826aE9r3K5cox1zobuPuAzdYJ36kouC2TVhlXvFKIILgFVWrpp9HZPwB3aLzasmG3K87q7TSyXZg==
dependencies: dependencies:
"@types/web-bluetooth" "^0.0.14" "@types/web-bluetooth" "^0.0.15"
"@vueuse/metadata" "8.7.5" "@vueuse/metadata" "9.1.0"
"@vueuse/shared" "8.7.5" "@vueuse/shared" "9.1.0"
vue-demi "*" vue-demi "*"
"@vueuse/metadata@8.7.5": "@vueuse/metadata@9.1.0":
version "8.7.5" version "9.1.0"
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-8.7.5.tgz" resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.1.0.tgz#194d4bd47f7acb91e348c0f436e678ddf7ee235b"
integrity sha512-emJZKRQSaEnVqmlu39NpNp8iaW+bPC2kWykWoWOZMSlO/0QVEmO/rt8A5VhOEJTKLX3vwTevqbiRy9WJRwVOQg== integrity sha512-8OEhlog1iaAGTD3LICZ8oBGQdYeMwByvXetOtAOZCJOzyCRSwqwdggTsmVZZ1rkgYIEqgUBk942AsAPwM21s6A==
"@vueuse/shared@8.7.5": "@vueuse/shared@9.1.0":
version "8.7.5" version "9.1.0"
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-8.7.5.tgz" resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.1.0.tgz#d8459a45324f32fb05a2a56ed754637c3d0efaeb"
integrity sha512-THXPvMBFmg6Gf6AwRn/EdTh2mhqwjGsB2Yfp374LNQSQVKRHtnJ0I42bsZTn7nuEliBxqUrGQm/lN6qUHmhJLw== integrity sha512-pB/3njQu4tfJJ78ajELNda0yMG6lKfpToQW7Soe09CprF1k3QuyoNi1tBNvo75wBDJWD+LOnr+c4B5HZ39jY/Q==
dependencies: dependencies:
vue-demi "*" vue-demi "*"
@@ -446,6 +458,14 @@ 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" 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= integrity sha1-t5hCCtvrHego2ErNii4j0+/oXo0=
asciinema-player@^3.0.1:
version "3.0.1"
resolved "https://registry.npmmirror.com/asciinema-player/-/asciinema-player-3.0.1.tgz#4ea9e6ee2cc5c1f956bfb562950279dcfad3e737"
integrity sha512-plm/C/MhOtZWysrfcT/rzxOuu8vxvvDSvF50pqZS6KpJUDmATedAhO54zktbE/g7RiaaYfzgX8xjRhlQdgISwA==
dependencies:
"@babel/runtime" "^7.15.4"
solid-js "^1.3.0"
async-validator@^4.2.5: async-validator@^4.2.5:
version "4.2.5" version "4.2.5"
resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz" resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz"
@@ -579,6 +599,11 @@ csstype@^2.6.8:
resolved "https://registry.npmmirror.com/csstype/download/csstype-2.6.19.tgz?cache=0&sync_timestamp=1637224514674&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcsstype%2Fdownload%2Fcsstype-2.6.19.tgz" resolved "https://registry.npmmirror.com/csstype/download/csstype-2.6.19.tgz?cache=0&sync_timestamp=1637224514674&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcsstype%2Fdownload%2Fcsstype-2.6.19.tgz"
integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ== integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==
csstype@^3.1.0:
version "3.1.0"
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
dayjs@^1.11.3: dayjs@^1.11.3:
version "1.11.3" version "1.11.3"
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.3.tgz" resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.3.tgz"
@@ -633,18 +658,18 @@ echarts@^5.3.3:
tslib "2.3.0" tslib "2.3.0"
zrender "5.3.2" zrender "5.3.2"
element-plus@^2.2.12: element-plus@^2.2.16:
version "2.2.12" version "2.2.16"
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.2.12.tgz#b6c4e298e02ba9b904d70daa54def27b2de8c43c" resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.2.16.tgz#84c00fc4a2d84031ec18d0f28820593c6e451513"
integrity sha512-g/hIHj3b+dND2R3YRvyvCJtJhQvR7lWvXqhJaoxaQmajjNWedoe4rttxG26fOSv9YCC2wN4iFDcJHs70YFNgrA== integrity sha512-rvaTMFIujec9YDC5lyaiQv2XVUCHuhVDq2k+9vQxP78N8Wd07iEOGa9pvEVOO2uYc75l4rSl2RE/IWPH/6Mdzw==
dependencies: dependencies:
"@ctrl/tinycolor" "^3.4.1" "@ctrl/tinycolor" "^3.4.1"
"@element-plus/icons-vue" "^2.0.6" "@element-plus/icons-vue" "^2.0.6"
"@floating-ui/dom" "^0.5.4" "@floating-ui/dom" "^1.0.1"
"@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7" "@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
"@types/lodash" "^4.14.182" "@types/lodash" "^4.14.182"
"@types/lodash-es" "^4.17.6" "@types/lodash-es" "^4.17.6"
"@vueuse/core" "^8.7.5" "@vueuse/core" "^9.1.0"
async-validator "^4.2.5" async-validator "^4.2.5"
dayjs "^1.11.3" dayjs "^1.11.3"
escape-html "^1.0.3" escape-html "^1.0.3"
@@ -1472,6 +1497,11 @@ readdirp@~3.6.0:
dependencies: dependencies:
picomatch "^2.2.1" picomatch "^2.2.1"
regenerator-runtime@^0.13.4:
version "0.13.9"
resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
regexpp@^3.1.0, regexpp@^3.2.0: regexpp@^3.1.0, regexpp@^3.2.0:
version "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" 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"
@@ -1534,10 +1564,10 @@ sass@^1.45.1:
immutable "^4.0.0" immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0" source-map-js ">=0.6.2 <2.0.0"
screenfull@^5.1.0: screenfull@^6.0.2:
version "5.2.0" version "6.0.2"
resolved "https://registry.npmmirror.com/screenfull/download/screenfull-5.2.0.tgz?cache=0&sync_timestamp=1635923453416&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fscreenfull%2Fdownload%2Fscreenfull-5.2.0.tgz" resolved "https://registry.npmmirror.com/screenfull/-/screenfull-6.0.2.tgz#3dbe4b8c4f8f49fb8e33caa8f69d0bca730ab238"
integrity sha1-ZTPVJNMGIfwSg7lpIUbz8TqT0bo= integrity sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==
select@^1.1.2: select@^1.1.2:
version "1.1.2" version "1.1.2"
@@ -1568,6 +1598,13 @@ slash@^3.0.0:
resolved "https://registry.nlark.com/slash/download/slash-3.0.0.tgz" resolved "https://registry.nlark.com/slash/download/slash-3.0.0.tgz"
integrity sha1-ZTm+hwwWWtvVJAIg2+Nh8bxNRjQ= integrity sha1-ZTm+hwwWWtvVJAIg2+Nh8bxNRjQ=
solid-js@^1.3.0:
version "1.5.1"
resolved "https://registry.npmmirror.com/solid-js/-/solid-js-1.5.1.tgz#b4281cf92cb00b235e62ef890d87ea996a9b9fa3"
integrity sha512-Y6aKystIxnrB0quV5nhqNuJV+l2Fk3/PQy1mMya/bzxlGiMHAym7v1NaqEgqDIvctbkxOi5dBj0ER/ewrH060g==
dependencies:
csstype "^3.1.0"
sortablejs@^1.13.0: sortablejs@^1.13.0:
version "1.14.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" 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"
@@ -1593,10 +1630,10 @@ sourcemap-codec@^1.4.4:
resolved "https://registry.npm.taobao.org/sourcemap-codec/download/sourcemap-codec-1.4.8.tgz" resolved "https://registry.npm.taobao.org/sourcemap-codec/download/sourcemap-codec-1.4.8.tgz"
integrity sha1-6oBL2UhXQC5pktBaOO8a41qatMQ= integrity sha1-6oBL2UhXQC5pktBaOO8a41qatMQ=
sql-formatter@^8.2.0: sql-formatter@^9.2.0:
version "8.2.0" version "9.2.0"
resolved "https://registry.npmmirror.com/sql-formatter/-/sql-formatter-8.2.0.tgz#2b664f02bb6b7bb6fcad1346e850b8f583303469" resolved "https://registry.npmmirror.com/sql-formatter/-/sql-formatter-9.2.0.tgz#18a398ae71436dc1936a45e6f230236b4347231b"
integrity sha512-5hQOSOk8jfhPkNgUmpm+9Fn2aaLWcf4vKL/dIvUN5q9rsamKHSyN/gL79xpkETNOyL+Zv5BMQfA7z9Rmz/DJJg== integrity sha512-Dn4lEpUeAhfNDR2LnEs9Uaq92TSHjhcNrzhllPuMnp188P4sLU7UcdcB9UqIfMfcN62gWXJlJ3KocaAf/SOzXQ==
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"

View File

@@ -1,7 +1,3 @@
app:
name: mayfly-go
version: 1.2.3
server: server:
# debug release test # debug release test
model: release model: release
@@ -11,19 +7,8 @@ server:
enable: false enable: false
key-file: ./default.key key-file: ./default.key
cert-file: ./default.pem cert-file: ./default.pem
# 静态资源 # 机器终端操作回放文件存储路径
static: machine-rec-path: ./rec
- relative-path: /assets
root: ./static/assets
# 静态文件
static-file:
- relative-path: /
filepath: ./static/index.html
- relative-path: /favicon.ico
filepath: ./static/favicon.ico
- relative-path: /config.js
filepath: ./static/config.js
jwt: jwt:
# jwt key不设置默认使用随机字符串 # jwt key不设置默认使用随机字符串
key: key:
@@ -39,7 +24,6 @@ mysql:
db-name: mayfly-go db-name: mayfly-go
config: charset=utf8&loc=Local&parseTime=true config: charset=utf8&loc=Local&parseTime=true
max-idle-conns: 5 max-idle-conns: 5
log: log:
# 日志等级, trace, debug, info, warn, error, fatal # 日志等级, trace, debug, info, warn, error, fatal
level: info level: info

View File

@@ -1,6 +1,6 @@
module mayfly-go module mayfly-go
go 1.18 go 1.19
require ( require (
github.com/gin-gonic/gin v1.8.1 github.com/gin-gonic/gin v1.8.1
@@ -15,7 +15,8 @@ require (
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.0
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
go.mongodb.org/mongo-driver v1.9.1 // mongo go.mongodb.org/mongo-driver v1.9.1 // mongo
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // ssh golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
// ssh
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
// gorm // gorm
gorm.io/driver/mysql v1.3.5 gorm.io/driver/mysql v1.3.5

View File

@@ -2,16 +2,25 @@ package initialize
import ( import (
"fmt" "fmt"
"io/fs"
common_router "mayfly-go/internal/common/router" common_router "mayfly-go/internal/common/router"
devops_router "mayfly-go/internal/devops/router" devops_router "mayfly-go/internal/devops/router"
sys_router "mayfly-go/internal/sys/router" sys_router "mayfly-go/internal/sys/router"
"mayfly-go/pkg/config" "mayfly-go/pkg/config"
"mayfly-go/pkg/middleware" "mayfly-go/pkg/middleware"
"mayfly-go/static"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func WrapStaticHandler(h http.Handler) gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Cache-Control", `public, max-age=31536000`)
h.ServeHTTP(c.Writer, c.Request)
}
}
func InitRouter() *gin.Engine { func InitRouter() *gin.Engine {
// server配置 // server配置
serverConfig := config.Conf.Server serverConfig := config.Conf.Server
@@ -25,12 +34,21 @@ func InitRouter() *gin.Engine {
g.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": fmt.Sprintf("not found '%s:%s'", g.Request.Method, g.Request.URL.Path)}) g.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": fmt.Sprintf("not found '%s:%s'", g.Request.Method, g.Request.URL.Path)})
}) })
// 使用embed打包静态资源至二进制文件中
fsys, _ := fs.Sub(static.Static, "static")
fileServer := http.FileServer(http.FS(fsys))
handler := WrapStaticHandler(fileServer)
router.GET("/", handler)
router.GET("/favicon.ico", handler)
router.GET("/config.js", handler)
// 所有/assets/**开头的都是静态资源文件
router.GET("/assets/*file", handler)
// 设置静态资源 // 设置静态资源
if staticConfs := serverConfig.Static; staticConfs != nil { if staticConfs := serverConfig.Static; staticConfs != nil {
for _, scs := range *staticConfs { for _, scs := range *staticConfs {
router.Static(scs.RelativePath, scs.Root) router.StaticFS(scs.RelativePath, http.Dir(scs.Root))
} }
} }
// 设置静态文件 // 设置静态文件
if staticFileConfs := serverConfig.StaticFile; staticFileConfs != nil { if staticFileConfs := serverConfig.StaticFile; staticFileConfs != nil {
@@ -38,6 +56,7 @@ func InitRouter() *gin.Engine {
router.StaticFile(sfs.RelativePath, sfs.Filepath) router.StaticFile(sfs.RelativePath, sfs.Filepath)
} }
} }
// 是否允许跨域 // 是否允许跨域
if serverConfig.Cors { if serverConfig.Cors {
router.Use(middleware.Cors()) router.Use(middleware.Cors())
@@ -55,6 +74,7 @@ func InitRouter() *gin.Engine {
sys_router.InitRoleRouter(api) sys_router.InitRoleRouter(api)
sys_router.InitSystemRouter(api) sys_router.InitSystemRouter(api)
sys_router.InitSyslogRouter(api) sys_router.InitSyslogRouter(api)
sys_router.InitSysConfigRouter(api)
devops_router.InitProjectRouter(api) devops_router.InitProjectRouter(api)
devops_router.InitDbRouter(api) devops_router.InitDbRouter(api)

View File

@@ -21,6 +21,9 @@ func PwdAesEncrypt(password string) string {
// 使用config.yml的aes.key进行密码解密 // 使用config.yml的aes.key进行密码解密
func PwdAesDecrypt(encryptPwd string) string { func PwdAesDecrypt(encryptPwd string) string {
if encryptPwd == "" {
return ""
}
aes := config.Conf.Aes aes := config.Conf.Aes
if aes == nil { if aes == nil {
return encryptPwd return encryptPwd

View File

@@ -97,19 +97,19 @@ func (d *Db) DeleteDb(rc *ctx.ReqCtx) {
} }
func (d *Db) TableInfos(rc *ctx.ReqCtx) { func (d *Db) TableInfos(rc *ctx.ReqCtx) {
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetTableInfos() rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta().GetTableInfos()
} }
func (d *Db) TableIndex(rc *ctx.ReqCtx) { func (d *Db) TableIndex(rc *ctx.ReqCtx) {
tn := rc.GinCtx.Query("tableName") tn := rc.GinCtx.Query("tableName")
biz.NotEmpty(tn, "tableName不能为空") biz.NotEmpty(tn, "tableName不能为空")
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetTableIndex(tn) rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta().GetTableIndex(tn)
} }
func (d *Db) GetCreateTableDdl(rc *ctx.ReqCtx) { func (d *Db) GetCreateTableDdl(rc *ctx.ReqCtx) {
tn := rc.GinCtx.Query("tableName") tn := rc.GinCtx.Query("tableName")
biz.NotEmpty(tn, "tableName不能为空") biz.NotEmpty(tn, "tableName不能为空")
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetCreateTableDdl(tn) rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta().GetCreateTableDdl(tn)
} }
func (d *Db) ExecSql(rc *ctx.ReqCtx) { func (d *Db) ExecSql(rc *ctx.ReqCtx) {
@@ -237,11 +237,12 @@ func (d *Db) DumpSql(rc *ctx.ReqCtx) {
writer.WriteString(fmt.Sprintf("\n-- 导出数据库: %s ", db)) writer.WriteString(fmt.Sprintf("\n-- 导出数据库: %s ", db))
writer.WriteString("\n-- ----------------------------\n") writer.WriteString("\n-- ----------------------------\n")
dbmeta := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta()
for _, table := range tables { for _, table := range tables {
if needStruct { if needStruct {
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表结构: %s \n-- ----------------------------\n", table)) 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", table))
writer.WriteString(dbInstance.GetCreateTableDdl(table)[0]["Create Table"].(string) + ";\n") writer.WriteString(dbmeta.GetCreateTableDdl(table)[0]["Create Table"].(string) + ";\n")
} }
if !needData { if !needData {
@@ -254,7 +255,13 @@ func (d *Db) DumpSql(rc *ctx.ReqCtx) {
countSql := fmt.Sprintf("SELECT COUNT(*) count FROM %s", table) countSql := fmt.Sprintf("SELECT COUNT(*) count FROM %s", table)
_, countRes, _ := dbInstance.SelectData(countSql) _, countRes, _ := dbInstance.SelectData(countSql)
// 查询出所有列信息总数,手动分页获取所有数据 // 查询出所有列信息总数,手动分页获取所有数据
maCount := int(countRes[0]["count"].(int64)) maCount := 0
// 查询出所有列信息总数,手动分页获取所有数据
if count64, is64 := countRes[0]["maNum"].(int64); is64 {
maCount = int(count64)
} else {
maCount = countRes[0]["maNum"].(int)
}
// 计算需要查询的页数 // 计算需要查询的页数
pageNum := maCount / DEFAULT_COLUMN_SIZE pageNum := maCount / DEFAULT_COLUMN_SIZE
if maCount%DEFAULT_COLUMN_SIZE > 0 { if maCount%DEFAULT_COLUMN_SIZE > 0 {
@@ -303,7 +310,7 @@ func (d *Db) DumpSql(rc *ctx.ReqCtx) {
func (d *Db) TableMA(rc *ctx.ReqCtx) { func (d *Db) TableMA(rc *ctx.ReqCtx) {
dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)) dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx))
biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, dbi.ProjectId), "%s") biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, dbi.ProjectId), "%s")
rc.ResData = dbi.GetTableMetedatas() rc.ResData = dbi.GetMeta().GetTables()
} }
// @router /api/db/:dbId/c-metadata [get] // @router /api/db/:dbId/c-metadata [get]
@@ -314,16 +321,17 @@ func (d *Db) ColumnMA(rc *ctx.ReqCtx) {
dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)) dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx))
biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, dbi.ProjectId), "%s") biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, dbi.ProjectId), "%s")
rc.ResData = dbi.GetColumnMetadatas(tn) rc.ResData = dbi.GetMeta().GetColumns(tn)
} }
// @router /api/db/:dbId/hint-tables [get] // @router /api/db/:dbId/hint-tables [get]
func (d *Db) HintTables(rc *ctx.ReqCtx) { func (d *Db) HintTables(rc *ctx.ReqCtx) {
dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)) dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx))
biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, dbi.ProjectId), "%s") biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, dbi.ProjectId), "%s")
// 获取所有表
tables := dbi.GetTableMetedatas()
dm := dbi.GetMeta()
// 获取所有表
tables := dm.GetTables()
tableNames := make([]string, 0) tableNames := make([]string, 0)
for _, v := range tables { for _, v := range tables {
tableNames = append(tableNames, v["tableName"].(string)) tableNames = append(tableNames, v["tableName"].(string))
@@ -338,7 +346,7 @@ func (d *Db) HintTables(rc *ctx.ReqCtx) {
} }
// 获取所有表下的所有列信息 // 获取所有表下的所有列信息
columnMds := dbi.GetColumnMetadatas(tableNames...) columnMds := dm.GetColumns(tableNames...)
for _, v := range columnMds { for _, v := range columnMds {
tName := v["tableName"].(string) tName := v["tableName"].(string)
if res[tName] == nil { if res[tName] == nil {

View File

@@ -13,6 +13,7 @@ type MachineForm struct {
Remark string `json:"remark"` Remark string `json:"remark"`
EnableSshTunnel int8 `json:"enableSshTunnel"` // 是否启用ssh隧道 EnableSshTunnel int8 `json:"enableSshTunnel"` // 是否启用ssh隧道
SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id
EnableRecorder int8 `json:"enableRecorder"` // 是否启用终端回放记录
} }
type MachineRunForm struct { type MachineRunForm struct {

View File

@@ -1,6 +1,7 @@
package api package api
import ( import (
"encoding/base64"
"fmt" "fmt"
"mayfly-go/internal/devops/api/form" "mayfly-go/internal/devops/api/form"
"mayfly-go/internal/devops/api/vo" "mayfly-go/internal/devops/api/vo"
@@ -8,11 +9,16 @@ import (
"mayfly-go/internal/devops/domain/entity" "mayfly-go/internal/devops/domain/entity"
"mayfly-go/internal/devops/infrastructure/machine" "mayfly-go/internal/devops/infrastructure/machine"
"mayfly-go/pkg/biz" "mayfly-go/pkg/biz"
"mayfly-go/pkg/config"
"mayfly-go/pkg/ctx" "mayfly-go/pkg/ctx"
"mayfly-go/pkg/ginx" "mayfly-go/pkg/ginx"
"mayfly-go/pkg/utils" "mayfly-go/pkg/utils"
"mayfly-go/pkg/ws" "mayfly-go/pkg/ws"
"os"
"path"
"sort"
"strconv" "strconv"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@@ -138,43 +144,76 @@ func (m *Machine) KillProcess(rc *ctx.ReqCtx) {
cli := m.MachineApp.GetCli(GetMachineId(rc.GinCtx)) cli := m.MachineApp.GetCli(GetMachineId(rc.GinCtx))
biz.ErrIsNilAppendErr(m.ProjectApp.CanAccess(rc.LoginAccount.Id, cli.GetMachine().ProjectId), "%s") biz.ErrIsNilAppendErr(m.ProjectApp.CanAccess(rc.LoginAccount.Id, cli.GetMachine().ProjectId), "%s")
_, err := cli.Run("kill -9 " + pid) _, err := cli.Run("sudo kill -9 " + pid)
biz.ErrIsNilAppendErr(err, "终止进程失败: %s") biz.ErrIsNilAppendErr(err, "终止进程失败: %s")
} }
func (m *Machine) WsSSH(g *gin.Context) { func (m *Machine) WsSSH(g *gin.Context) {
wsConn, err := ws.Upgrader.Upgrade(g.Writer, g.Request, nil) wsConn, err := ws.Upgrader.Upgrade(g.Writer, g.Request, nil)
defer func() { defer func() {
if err := recover(); err != nil { if wsConn != nil {
wsConn.WriteMessage(websocket.TextMessage, []byte(err.(error).Error())) if err := recover(); err != nil {
wsConn.WriteMessage(websocket.TextMessage, []byte(err.(error).Error()))
}
wsConn.Close() wsConn.Close()
} }
}() }()
if err != nil { biz.ErrIsNilAppendErr(err, "升级websocket失败: %s")
panic(biz.NewBizErr("升级websocket失败"))
}
// 权限校验 // 权限校验
rc := ctx.NewReqCtxWithGin(g).WithRequiredPermission(ctx.NewPermission("machine:terminal")) rc := ctx.NewReqCtxWithGin(g).WithRequiredPermission(ctx.NewPermission("machine:terminal"))
if err = ctx.PermissionHandler(rc); err != nil { if err = ctx.PermissionHandler(rc); err != nil {
panic(biz.NewBizErr("\033[1;31m您没有权限操作该机器终端,请重新登录后再试~\033[0m")) panic(biz.NewBizErr("\033[1;31m您没有权限操作该机器终端,请重新登录后再试~\033[0m"))
} }
cols := ginx.QueryInt(g, "cols", 80)
rows := ginx.QueryInt(g, "rows", 40)
cli := m.MachineApp.GetCli(GetMachineId(g)) cli := m.MachineApp.GetCli(GetMachineId(g))
biz.ErrIsNilAppendErr(m.ProjectApp.CanAccess(rc.LoginAccount.Id, cli.GetMachine().ProjectId), "%s") biz.ErrIsNilAppendErr(m.ProjectApp.CanAccess(rc.LoginAccount.Id, cli.GetMachine().ProjectId), "%s")
sws, err := machine.NewLogicSshWsSession(cols, rows, cli, wsConn) cols := ginx.QueryInt(g, "cols", 80)
biz.ErrIsNilAppendErr(err, "\033[1;31m连接失败%s\033[0m") rows := ginx.QueryInt(g, "rows", 40)
defer sws.Close()
quitChan := make(chan bool, 3) var recorder *machine.Recorder
sws.Start(quitChan) if cli.GetMachine().EnableRecorder == 1 {
go sws.Wait(quitChan) now := time.Now()
// 回放文件路径为: 基础配置路径/机器id/操作日期/操作者账号/操作时间.cast
recPath := fmt.Sprintf("%s/%d/%s/%s", config.Conf.Server.GetMachineRecPath(), cli.GetMachine().Id, now.Format("20060102"), rc.LoginAccount.Username)
os.MkdirAll(recPath, 0766)
fileName := path.Join(recPath, fmt.Sprintf("%s.cast", now.Format("20060102_150405")))
f, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0766)
biz.ErrIsNilAppendErr(err, "创建终端回放记录文件失败: %s")
defer f.Close()
recorder = machine.NewRecorder(f)
}
<-quitChan mts, err := machine.NewTerminalSession(utils.RandString(16), wsConn, cli, rows, cols, recorder)
biz.ErrIsNilAppendErr(err, "\033[1;31m连接失败: %s\033[0m")
mts.Start()
defer mts.Stop()
}
// 获取机器终端回放记录的相应文件夹名或文件内容
func (m *Machine) MachineRecDirNames(rc *ctx.ReqCtx) {
readPath := rc.GinCtx.Query("path")
biz.NotEmpty(readPath, "path不能为空")
path_ := path.Join(config.Conf.Server.GetMachineRecPath(), readPath)
// 如果是读取文件内容,则读取对应回放记录文件内容,否则读取文件夹名列表。小小偷懒一会不想再加个接口
isFile := rc.GinCtx.Query("isFile")
if isFile == "1" {
bytes, err := os.ReadFile(path_)
biz.ErrIsNilAppendErr(err, "还未有相应终端操作记录: %s")
rc.ResData = base64.StdEncoding.EncodeToString(bytes)
return
}
files, err := os.ReadDir(path_)
biz.ErrIsNilAppendErr(err, "还未有相应终端操作记录: %s")
var names []string
for _, f := range files {
names = append(names, f.Name())
}
sort.Sort(sort.Reverse(sort.StringSlice(names)))
rc.ResData = names
} }
func GetMachineId(g *gin.Context) uint64 { func GetMachineId(g *gin.Context) uint64 {

View File

@@ -2,8 +2,8 @@ package api
import ( import (
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"io/ioutil"
"mayfly-go/internal/devops/api/form" "mayfly-go/internal/devops/api/form"
"mayfly-go/internal/devops/api/vo" "mayfly-go/internal/devops/api/vo"
"mayfly-go/internal/devops/application" "mayfly-go/internal/devops/application"
@@ -98,7 +98,7 @@ func (m *MachineFile) ReadFileContent(rc *ctx.ReqCtx) {
path := strings.Split(readPath, "/") path := strings.Split(readPath, "/")
rc.Download(sftpFile, path[len(path)-1]) rc.Download(sftpFile, path[len(path)-1])
} else { } else {
datas, err := ioutil.ReadAll(sftpFile) datas, err := io.ReadAll(sftpFile)
biz.ErrIsNilAppendErr(err, "读取文件内容失败: %s") biz.ErrIsNilAppendErr(err, "读取文件内容失败: %s")
rc.ResData = string(datas) rc.ResData = string(datas)
} }
@@ -116,11 +116,12 @@ func (m *MachineFile) GetDirEntry(rc *ctx.ReqCtx) {
fisVO := make([]vo.MachineFileInfo, 0) fisVO := make([]vo.MachineFileInfo, 0)
for _, fi := range fis { for _, fi := range fis {
fisVO = append(fisVO, vo.MachineFileInfo{ fisVO = append(fisVO, vo.MachineFileInfo{
Name: fi.Name(), Name: fi.Name(),
Size: fi.Size(), Size: fi.Size(),
Path: readPath + fi.Name(), Path: readPath + fi.Name(),
Type: getFileType(fi.Mode()), Type: getFileType(fi.Mode()),
Mode: fi.Mode().String(), Mode: fi.Mode().String(),
ModTime: fi.ModTime().Format("2006-01-02 15:04:05"),
}) })
} }
rc.ResData = fisVO rc.ResData = fisVO

View File

@@ -9,6 +9,7 @@ import (
"mayfly-go/pkg/ctx" "mayfly-go/pkg/ctx"
"mayfly-go/pkg/ginx" "mayfly-go/pkg/ginx"
"mayfly-go/pkg/utils" "mayfly-go/pkg/utils"
"regexp"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -34,11 +35,16 @@ func (m *Mongo) Save(rc *ctx.ReqCtx) {
form := &form.Mongo{} form := &form.Mongo{}
ginx.BindJsonAndValid(rc.GinCtx, form) ginx.BindJsonAndValid(rc.GinCtx, form)
rc.ReqParam = form
mongo := new(entity.Mongo) mongo := new(entity.Mongo)
utils.Copy(mongo, form) utils.Copy(mongo, form)
// 密码脱敏记录日志
form.Uri = func(str string) string {
reg := regexp.MustCompile(`(^mongodb://.+?:)(.+)(@.+$)`)
return reg.ReplaceAllString(str, `${1}****${3}`)
}(form.Uri)
rc.ReqParam = form
mongo.SetBaseInfo(rc.LoginAccount) mongo.SetBaseInfo(rc.LoginAccount)
m.MongoApp.Save(mongo) m.MongoApp.Save(mongo)
} }

View File

@@ -54,6 +54,10 @@ func (p *Project) DelProject(rc *ctx.ReqCtx) {
p.ProjectApp.DelProject(uint64(ginx.QueryInt(rc.GinCtx, "id", 0))) p.ProjectApp.DelProject(uint64(ginx.QueryInt(rc.GinCtx, "id", 0)))
} }
func (p *Project) DelProjectEnv(rc *ctx.ReqCtx) {
p.ProjectApp.DelProjectEnv(uint64(ginx.QueryInt(rc.GinCtx, "id", 0)))
}
// 获取项目下的环境信息 // 获取项目下的环境信息
func (p *Project) GetProjectEnvs(rc *ctx.ReqCtx) { func (p *Project) GetProjectEnvs(rc *ctx.ReqCtx) {
projectEnvs := &[]entity.ProjectEnv{} projectEnvs := &[]entity.ProjectEnv{}
@@ -61,7 +65,7 @@ func (p *Project) GetProjectEnvs(rc *ctx.ReqCtx) {
rc.ResData = projectEnvs rc.ResData = projectEnvs
} }
//保存项目下的环境信息 // 保存项目下的环境信息
func (p *Project) SaveProjectEnvs(rc *ctx.ReqCtx) { func (p *Project) SaveProjectEnvs(rc *ctx.ReqCtx) {
projectEnv := &entity.ProjectEnv{} projectEnv := &entity.ProjectEnv{}
ginx.BindJsonAndValid(rc.GinCtx, projectEnv) ginx.BindJsonAndValid(rc.GinCtx, projectEnv)
@@ -78,7 +82,7 @@ func (p *Project) GetProjectMembers(rc *ctx.ReqCtx) {
ginx.GetPageParam(rc.GinCtx), projectMems) ginx.GetPageParam(rc.GinCtx), projectMems)
} }
//保存项目的成员信息 // 保存项目的成员信息
func (p *Project) SaveProjectMember(rc *ctx.ReqCtx) { func (p *Project) SaveProjectMember(rc *ctx.ReqCtx) {
projectMem := &entity.ProjectMember{} projectMem := &entity.ProjectMember{}
ginx.BindJsonAndValid(rc.GinCtx, projectMem) ginx.BindJsonAndValid(rc.GinCtx, projectMem)
@@ -95,7 +99,7 @@ func (p *Project) SaveProjectMember(rc *ctx.ReqCtx) {
p.ProjectApp.SaveProjectMember(projectMem) p.ProjectApp.SaveProjectMember(projectMem)
} }
//删除项目成员 // 删除项目成员
func (p *Project) DelProjectMember(rc *ctx.ReqCtx) { func (p *Project) DelProjectMember(rc *ctx.ReqCtx) {
g := rc.GinCtx g := rc.GinCtx
pid := ginx.PathParamInt(g, "projectId") pid := ginx.PathParamInt(g, "projectId")

View File

@@ -193,7 +193,7 @@ func (r *Redis) Scan(rc *ctx.ReqCtx) {
kis := make([]*vo.KeyInfo, 0) kis := make([]*vo.KeyInfo, 0)
var cursorRes map[string]uint64 = make(map[string]uint64) var cursorRes map[string]uint64 = make(map[string]uint64)
if ri.Mode == "" || ri.Mode == entity.RedisModeStandalone { if ri.Mode == "" || ri.Mode == entity.RedisModeStandalone || ri.Mode == entity.RedisModeSentinel {
redisAddr := ri.Cli.Options().Addr redisAddr := ri.Cli.Options().Addr
keys, cursor := ri.Scan(form.Cursor[redisAddr], form.Match, form.Count) keys, cursor := ri.Scan(form.Cursor[redisAddr], form.Match, form.Count)
cursorRes[redisAddr] = cursor cursorRes[redisAddr] = cursor
@@ -285,13 +285,6 @@ func (r *Redis) GetStringValue(rc *ctx.ReqCtx) {
rc.ResData = str rc.ResData = str
} }
func (r *Redis) GetHashValue(rc *ctx.ReqCtx) {
ri, key := r.checkKey(rc)
res, err := ri.GetCmdable().HGetAll(context.TODO(), key).Result()
biz.ErrIsNilAppendErr(err, "获取hash值失败: %s")
rc.ResData = res
}
func (r *Redis) SetStringValue(rc *ctx.ReqCtx) { func (r *Redis) SetStringValue(rc *ctx.ReqCtx) {
g := rc.GinCtx g := rc.GinCtx
keyValue := new(form.StringValue) keyValue := new(form.StringValue)
@@ -305,6 +298,45 @@ func (r *Redis) SetStringValue(rc *ctx.ReqCtx) {
rc.ResData = str rc.ResData = str
} }
func (r *Redis) Hscan(rc *ctx.ReqCtx) {
ri, key := r.checkKey(rc)
g := rc.GinCtx
count := ginx.QueryInt(g, "count", 10)
match := g.Query("match")
cursor := ginx.QueryInt(g, "cursor", 0)
contextTodo := context.TODO()
cmdable := ri.GetCmdable()
keys, nextCursor, err := cmdable.HScan(contextTodo, key, uint64(cursor), match, int64(count)).Result()
biz.ErrIsNilAppendErr(err, "hcan err: %s")
keySize, err := cmdable.HLen(contextTodo, key).Result()
biz.ErrIsNilAppendErr(err, "hlen err: %s")
rc.ResData = map[string]interface{}{
"keys": keys,
"cursor": nextCursor,
"keySize": keySize,
}
}
func (r *Redis) Hdel(rc *ctx.ReqCtx) {
ri, key := r.checkKey(rc)
field := rc.GinCtx.Query("field")
delRes, err := ri.GetCmdable().HDel(context.TODO(), key, field).Result()
biz.ErrIsNilAppendErr(err, "hdel err: %s")
rc.ResData = delRes
}
func (r *Redis) Hget(rc *ctx.ReqCtx) {
ri, key := r.checkKey(rc)
field := rc.GinCtx.Query("field")
res, err := ri.GetCmdable().HGet(context.TODO(), key, field).Result()
biz.ErrIsNilAppendErr(err, "hget err: %s")
rc.ResData = res
}
func (r *Redis) SetHashValue(rc *ctx.ReqCtx) { func (r *Redis) SetHashValue(rc *ctx.ReqCtx) {
g := rc.GinCtx g := rc.GinCtx
hashValue := new(form.HashValue) hashValue := new(form.HashValue)
@@ -315,13 +347,12 @@ func (r *Redis) SetHashValue(rc *ctx.ReqCtx) {
cmd := ri.GetCmdable() cmd := ri.GetCmdable()
key := hashValue.Key key := hashValue.Key
// 简单处理->先删除,后新增 contextTodo := context.TODO()
cmd.Del(context.TODO(), key)
for _, v := range hashValue.Value { for _, v := range hashValue.Value {
res := cmd.HSet(context.TODO(), key, v["key"].(string), v["value"]) res := cmd.HSet(contextTodo, key, v["field"].(string), v["value"])
biz.ErrIsNilAppendErr(res.Err(), "保存hash值失败: %s") biz.ErrIsNilAppendErr(res.Err(), "保存hash值失败: %s")
} }
if hashValue.Timed != -1 { if hashValue.Timed != 0 && hashValue.Timed != -1 {
cmd.Expire(context.TODO(), key, time.Second*time.Duration(hashValue.Timed)) cmd.Expire(context.TODO(), key, time.Second*time.Duration(hashValue.Timed))
} }
} }

View File

@@ -34,6 +34,7 @@ type MachineVO struct {
ModifierId *int64 `json:"modifierId"` ModifierId *int64 `json:"modifierId"`
HasCli bool `json:"hasCli" gorm:"-"` HasCli bool `json:"hasCli" gorm:"-"`
Remark *string `json:"remark"` Remark *string `json:"remark"`
EnableRecorder int8 `json:"enableRecorder"`
} }
type MachineScriptVO struct { type MachineScriptVO struct {
@@ -55,11 +56,12 @@ type MachineFileVO struct {
} }
type MachineFileInfo struct { type MachineFileInfo struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
Size int64 `json:"size"` Size int64 `json:"size"`
Type string `json:"type"` Type string `json:"type"`
Mode string `json:"mode"` Mode string `json:"mode"`
ModTime string `json:"modTime"`
} }
type RoleVO struct { type RoleVO struct {

View File

@@ -147,6 +147,7 @@ func (d *dbAppImpl) Delete(id uint64) {
} }
func (d *dbAppImpl) GetDatabases(ed *entity.Db) []string { func (d *dbAppImpl) GetDatabases(ed *entity.Db) []string {
ed.Network = ed.GetNetwork()
databases := make([]string, 0) databases := make([]string, 0)
var dbConn *sql.DB var dbConn *sql.DB
var metaDb string var metaDb string
@@ -218,6 +219,9 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// 单次最大查询数据集
const Max_Rows = 2000
// 客户端连接缓存,指定时间内没有访问则会被关闭, key为数据库实例id:数据库 // 客户端连接缓存,指定时间内没有访问则会被关闭, key为数据库实例id:数据库
var dbCache = cache.NewTimedCache(constant.DbConnExpireTime, 5*time.Second). var dbCache = cache.NewTimedCache(constant.DbConnExpireTime, 5*time.Second).
WithUpdateAccessTime(true). WithUpdateAccessTime(true).
@@ -314,7 +318,11 @@ func SelectDataByDb(db *sql.DB, selectSql string) ([]string, []map[string]interf
colNames := make([]string, 0) colNames := make([]string, 0)
// 是否第一次遍历,列名数组只需第一次遍历时加入 // 是否第一次遍历,列名数组只需第一次遍历时加入
isFirst := true isFirst := true
rowNum := 0
for rows.Next() { for rows.Next() {
rowNum++
biz.IsTrue(rowNum <= Max_Rows, "结果集 > 2000, 请完善条件或分页信息")
// 不Scan也会导致等待该链接实际处于未工作的状态然后也会导致连接数迅速达到最大 // 不Scan也会导致等待该链接实际处于未工作的状态然后也会导致连接数迅速达到最大
err := rows.Scan(scans...) err := rows.Scan(scans...)
if err != nil { if err != nil {
@@ -328,6 +336,7 @@ func SelectDataByDb(db *sql.DB, selectSql string) ([]string, []map[string]interf
colName := colType.Name() colName := colType.Name()
// 字段类型名 // 字段类型名
colScanType := colType.ScanType().Name() colScanType := colType.ScanType().Name()
// 如果是第一行则将列名加入到列信息中由于map是无序的所有需要返回列名的有序数组
if isFirst { if isFirst {
colNames = append(colNames, colName) colNames = append(colNames, colName)
} }
@@ -422,6 +431,18 @@ func (d *DbInstance) Exec(sql string) (int64, error) {
return res.RowsAffected() return res.RowsAffected()
} }
// 获取数据库元信息实现接口
func (di *DbInstance) GetMeta() DbMetadata {
dbType := di.Type
if dbType == entity.DbTypeMysql {
return &MysqlMetadata{di: di}
}
if dbType == entity.DbTypePostgres {
return &PgsqlMetadata{di: di}
}
return nil
}
// 关闭连接 // 关闭连接
func (d *DbInstance) Close() { func (d *DbInstance) Close() {
if d.db != nil { if d.db != nil {
@@ -458,8 +479,32 @@ func CloseDb(dbId uint64, db string) {
dbCache.Delete(GetDbCacheKey(dbId, db)) dbCache.Delete(GetDbCacheKey(dbId, db))
} }
//-----------------------------------元数据------------------------------------------- // -----------------------------------元数据-------------------------------------------
// 数据库元信息接口(表、列等元信息)
type DbMetadata interface {
// 获取表基础元信息, 如表名等
GetTables() []map[string]interface{}
// 获取列元信息, 如列名等
GetColumns(tableNames ...string) []map[string]interface{}
// 获取表主键字段名,默认第一个字段
GetPrimaryKey(tablename string) string
// 获取表信息比GetTables获取更详细的表信息
GetTableInfos() []map[string]interface{}
// 获取表索引信息
GetTableIndex(tableName string) []map[string]interface{}
// 获取建表ddl
GetCreateTableDdl(tableName string) []map[string]interface{}
}
// 默认每次查询列元信息数量
const DEFAULT_COLUMN_SIZE = 2000
// ---------------------------------- mysql元数据 -----------------------------------
const ( const (
// mysql 表信息元数据 // mysql 表信息元数据
MYSQL_TABLE_MA = `SELECT table_name tableName, engine, table_comment tableComment, MYSQL_TABLE_MA = `SELECT table_name tableName, engine, table_comment tableComment,
@@ -478,9 +523,6 @@ const (
FROM information_schema.STATISTICS FROM information_schema.STATISTICS
WHERE table_schema = (SELECT database()) AND table_name = '%s' LIMIT 500` WHERE table_schema = (SELECT database()) AND table_name = '%s' LIMIT 500`
// 默认每次查询列元信息数量
DEFAULT_COLUMN_SIZE = 2000
// mysql 列信息元数据 // mysql 列信息元数据
MYSQL_COLUMN_MA = `SELECT table_name tableName, column_name columnName, column_type columnType, MYSQL_COLUMN_MA = `SELECT table_name tableName, column_name columnName, column_type columnType,
column_comment columnComment, column_key columnKey, extra, is_nullable nullable from information_schema.columns column_comment columnComment, column_key columnKey, extra, is_nullable nullable from information_schema.columns
@@ -491,6 +533,80 @@ const (
WHERE table_name in (%s) AND table_schema = (SELECT database())` WHERE table_name in (%s) AND table_schema = (SELECT database())`
) )
type MysqlMetadata struct {
di *DbInstance
}
// 获取表基础元信息, 如表名等
func (mm *MysqlMetadata) GetTables() []map[string]interface{} {
_, res, _ := mm.di.SelectData(MYSQL_TABLE_MA)
return res
}
// 获取列元信息, 如列名等
func (mm *MysqlMetadata) GetColumns(tableNames ...string) []map[string]interface{} {
var sql, tableName string
for i := 0; i < len(tableNames); i++ {
if i != 0 {
tableName = tableName + ", "
}
tableName = tableName + "'" + tableNames[i] + "'"
}
pageNum := 1
// 如果大于一个表,则统计列数并分页获取
if len(tableNames) > 1 {
countSql := fmt.Sprintf(MYSQL_COLOUMN_MA_COUNT, tableName)
_, countRes, _ := mm.di.SelectData(countSql)
maCount := 0
// 查询出所有列信息总数,手动分页获取所有数据
if count64, is64 := countRes[0]["maNum"].(int64); is64 {
maCount = int(count64)
} else {
maCount = countRes[0]["maNum"].(int)
}
// 计算需要查询的页数
pageNum = maCount / DEFAULT_COLUMN_SIZE
if maCount%DEFAULT_COLUMN_SIZE > 0 {
pageNum++
}
}
res := make([]map[string]interface{}, 0)
for index := 0; index < pageNum; index++ {
sql = fmt.Sprintf(MYSQL_COLUMN_MA, tableName, index*DEFAULT_COLUMN_SIZE, DEFAULT_COLUMN_SIZE)
_, result, err := mm.di.SelectData(sql)
biz.ErrIsNilAppendErr(err, "获取数据库列信息失败: %s")
res = append(res, result...)
}
return res
}
// 获取表主键字段名,默认第一个字段
func (mm *MysqlMetadata) GetPrimaryKey(tablename string) string {
return mm.GetColumns(tablename)[0]["columnName"].(string)
}
// 获取表信息比GetTableMetedatas获取更详细的表信息
func (mm *MysqlMetadata) GetTableInfos() []map[string]interface{} {
_, res, _ := mm.di.SelectData(MYSQL_TABLE_INFO)
return res
}
// 获取表索引信息
func (mm *MysqlMetadata) GetTableIndex(tableName string) []map[string]interface{} {
_, res, _ := mm.di.SelectData(MYSQL_INDEX_INFO)
return res
}
// 获取建表ddl
func (mm *MysqlMetadata) GetCreateTableDdl(tableName string) []map[string]interface{} {
_, res, _ := mm.di.SelectData(fmt.Sprintf("show create table %s ", tableName))
return res
}
// ---------------------------------- pgsql元数据 -----------------------------------
const ( const (
// postgres 表信息元数据 // postgres 表信息元数据
PGSQL_TABLE_MA = `SELECT obj_description(c.oid) AS "tableComment", c.relname AS "tableName" FROM pg_class c PGSQL_TABLE_MA = `SELECT obj_description(c.oid) AS "tableComment", c.relname AS "tableName" FROM pg_class c
@@ -537,18 +653,18 @@ const (
` `
) )
func (d *DbInstance) GetTableMetedatas() []map[string]interface{} { type PgsqlMetadata struct {
var sql string di *DbInstance
if d.Type == entity.DbTypeMysql { }
sql = MYSQL_TABLE_MA
} else if d.Type == "postgres" { // 获取表基础元信息, 如表名等
sql = PGSQL_TABLE_MA func (pm *PgsqlMetadata) GetTables() []map[string]interface{} {
} _, res, _ := pm.di.SelectData(PGSQL_TABLE_MA)
_, res, _ := d.SelectData(sql)
return res return res
} }
func (d *DbInstance) GetColumnMetadatas(tableNames ...string) []map[string]interface{} { // 获取列元信息, 如列名等
func (pm *PgsqlMetadata) GetColumns(tableNames ...string) []map[string]interface{} {
var sql, tableName string var sql, tableName string
for i := 0; i < len(tableNames); i++ { for i := 0; i < len(tableNames); i++ {
if i != 0 { if i != 0 {
@@ -557,68 +673,53 @@ func (d *DbInstance) GetColumnMetadatas(tableNames ...string) []map[string]inter
tableName = tableName + "'" + tableNames[i] + "'" tableName = tableName + "'" + tableNames[i] + "'"
} }
var countSqlTmp string pageNum := 1
var sqlTmp string // 如果大于一个表,则统计列数并分页获取
if d.Type == entity.DbTypeMysql { if len(tableNames) > 1 {
countSqlTmp = MYSQL_COLOUMN_MA_COUNT countSql := fmt.Sprintf(PGSQL_COLUMN_MA_COUNT, tableName)
sqlTmp = MYSQL_COLUMN_MA _, countRes, _ := pm.di.SelectData(countSql)
} else if d.Type == entity.DbTypePostgres { maCount := 0
countSqlTmp = PGSQL_COLUMN_MA_COUNT // 查询出所有列信息总数,手动分页获取所有数据
sqlTmp = PGSQL_COLUMN_MA if count64, is64 := countRes[0]["maNum"].(int64); is64 {
} maCount = int(count64)
} else {
countSql := fmt.Sprintf(countSqlTmp, tableName) maCount = countRes[0]["maNum"].(int)
_, countRes, _ := d.SelectData(countSql) }
// 查询出所有列信息总数,手动分页获取所有数据 // 计算需要查询的页数
maCount := int(countRes[0]["maNum"].(int64)) pageNum = maCount / DEFAULT_COLUMN_SIZE
// 计算需要查询的页数 if maCount%DEFAULT_COLUMN_SIZE > 0 {
pageNum := maCount / DEFAULT_COLUMN_SIZE pageNum++
if maCount%DEFAULT_COLUMN_SIZE > 0 { }
pageNum++
} }
res := make([]map[string]interface{}, 0) res := make([]map[string]interface{}, 0)
for index := 0; index < pageNum; index++ { for index := 0; index < pageNum; index++ {
sql = fmt.Sprintf(sqlTmp, tableName, index*DEFAULT_COLUMN_SIZE, DEFAULT_COLUMN_SIZE) sql = fmt.Sprintf(PGSQL_COLUMN_MA, tableName, index*DEFAULT_COLUMN_SIZE, DEFAULT_COLUMN_SIZE)
_, result, err := d.SelectData(sql) _, result, err := pm.di.SelectData(sql)
biz.ErrIsNilAppendErr(err, "获取数据库列信息失败: %s") biz.ErrIsNilAppendErr(err, "获取数据库列信息失败: %s")
res = append(res, result...) res = append(res, result...)
} }
return res return res
} }
// 获取表主键,目前默认第一列为主键 // 获取表主键字段名,默认第一个字段
func (d *DbInstance) GetPrimaryKey(tablename string) string { func (pm *PgsqlMetadata) GetPrimaryKey(tablename string) string {
return d.GetColumnMetadatas(tablename)[0]["columnName"].(string) return pm.GetColumns(tablename)[0]["columnName"].(string)
} }
func (d *DbInstance) GetTableInfos() []map[string]interface{} { // 获取表信息比GetTables获取更详细的表信息
var sql string func (pm *PgsqlMetadata) GetTableInfos() []map[string]interface{} {
if d.Type == entity.DbTypeMysql { _, res, _ := pm.di.SelectData(PGSQL_TABLE_INFO)
sql = MYSQL_TABLE_INFO
} else if d.Type == entity.DbTypePostgres {
sql = PGSQL_TABLE_INFO
}
_, res, _ := d.SelectData(sql)
return res return res
} }
func (d *DbInstance) GetTableIndex(tableName string) []map[string]interface{} { // 获取表索引信息
var sql string func (pm *PgsqlMetadata) GetTableIndex(tableName string) []map[string]interface{} {
if d.Type == entity.DbTypeMysql { _, res, _ := pm.di.SelectData(PGSQL_INDEX_INFO)
sql = fmt.Sprintf(MYSQL_INDEX_INFO, tableName)
} else if d.Type == entity.DbTypePostgres {
sql = fmt.Sprintf(PGSQL_INDEX_INFO, tableName)
}
_, res, _ := d.SelectData(sql)
return res return res
} }
func (d *DbInstance) GetCreateTableDdl(tableName string) []map[string]interface{} { // 获取建表ddl
var sql string func (mm *PgsqlMetadata) GetCreateTableDdl(tableName string) []map[string]interface{} {
if d.Type == entity.DbTypeMysql { return nil
sql = fmt.Sprintf("show create table %s ", tableName)
}
_, res, _ := d.SelectData(sql)
return res
} }

View File

@@ -3,6 +3,7 @@ package application
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"mayfly-go/internal/devops/domain/entity" "mayfly-go/internal/devops/domain/entity"
"mayfly-go/internal/devops/domain/repository" "mayfly-go/internal/devops/domain/repository"
"mayfly-go/internal/devops/infrastructure/persistence" "mayfly-go/internal/devops/infrastructure/persistence"
@@ -89,7 +90,7 @@ func doUpdate(update *sqlparser.Update, dbInstance *DbInstance, dbSqlExec *entit
} }
// 获取表主键列名,排除使用别名 // 获取表主键列名,排除使用别名
primaryKey := dbInstance.GetPrimaryKey(tableName) primaryKey := dbInstance.GetMeta().GetPrimaryKey(tableName)
updateColumnsAndPrimaryKey := strings.Join(updateColumns, ",") + "," + primaryKey updateColumnsAndPrimaryKey := strings.Join(updateColumns, ",") + "," + primaryKey
// 查询要更新字段数据的旧值,以及主键值 // 查询要更新字段数据的旧值,以及主键值

View File

@@ -56,7 +56,11 @@ func (m *machineAppImpl) Count(condition *entity.Machine) int64 {
func (m *machineAppImpl) Save(me *entity.Machine) { func (m *machineAppImpl) Save(me *entity.Machine) {
// ’修改机器信息且密码不为空‘ or ‘新增’需要测试是否可连接 // ’修改机器信息且密码不为空‘ or ‘新增’需要测试是否可连接
if (me.Id != 0 && me.Password != "") || me.Id == 0 { if (me.Id != 0 && me.Password != "") || me.Id == 0 {
biz.ErrIsNilAppendErr(machine.TestConn(*me, func(u uint64) *entity.Machine { return m.GetById(u) }), "该机器无法连接: %s") biz.ErrIsNilAppendErr(machine.TestConn(*me, func(u uint64) *entity.Machine {
me := m.GetById(u)
me.PwdDecrypt()
return me
}), "该机器无法连接: %s")
} }
oldMachine := &entity.Machine{Ip: me.Ip, Port: me.Port, Username: me.Username} oldMachine := &entity.Machine{Ip: me.Ip, Port: me.Port, Username: me.Username}

View File

@@ -13,6 +13,7 @@ import (
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils" "mayfly-go/pkg/utils"
"net" "net"
"regexp"
"time" "time"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
@@ -95,7 +96,7 @@ func (d *mongoAppImpl) GetMongoCli(id uint64) *mongo.Client {
// ----------------------------------------------------------- // -----------------------------------------------------------
//mongo客户端连接缓存指定时间内没有访问则会被关闭 // mongo客户端连接缓存指定时间内没有访问则会被关闭
var mongoCliCache = cache.NewTimedCache(constant.MongoConnExpireTime, 5*time.Second). var mongoCliCache = cache.NewTimedCache(constant.MongoConnExpireTime, 5*time.Second).
WithUpdateAccessTime(true). WithUpdateAccessTime(true).
OnEvicted(func(key interface{}, value interface{}) { OnEvicted(func(key interface{}, value interface{}) {
@@ -177,7 +178,10 @@ func connect(me *entity.Mongo) (*MongoInstance, error) {
return nil, err return nil, err
} }
global.Log.Infof("连接mongo: %s", me.Uri) global.Log.Infof("连接mongo: %s", func(str string) string {
reg := regexp.MustCompile(`(^mongodb://.+?:)(.+)(@.+$)`)
return reg.ReplaceAllString(str, `${1}****${3}`)
}(me.Uri))
mongoInstance.Cli = client mongoInstance.Cli = client
return mongoInstance, err return mongoInstance, err
} }

View File

@@ -20,6 +20,8 @@ type Project interface {
DelProject(id uint64) DelProject(id uint64)
DelProjectEnv(id uint64)
// 根据项目id获取所有该项目下的环境信息列表 // 根据项目id获取所有该项目下的环境信息列表
ListEnvByProjectId(projectId uint64, listPtr interface{}) ListEnvByProjectId(projectId uint64, listPtr interface{})
@@ -100,6 +102,13 @@ func (p *projectAppImpl) SaveProjectEnv(projectEnv *entity.ProjectEnv) {
p.projectEnvRepo.Save(projectEnv) p.projectEnvRepo.Save(projectEnv)
} }
// 删除项目环境信息
func (p *projectAppImpl) DelProjectEnv(id uint64) {
biz.IsTrue(p.redisRepo.Count(&entity.Redis{EnvId: id}) == 0, "请先删除该项目环境关联的redis信息")
biz.IsTrue(p.dbRepo.Count(&entity.Db{EnvId: id}) == 0, "请先删除该项目环境关联的数据库信息")
p.projectEnvRepo.DeleteEnv(id)
}
// 根据条件获取项目成员信息 // 根据条件获取项目成员信息
func (p *projectAppImpl) ListMember(condition *entity.ProjectMember, toEntity interface{}, orderBy ...string) { func (p *projectAppImpl) ListMember(condition *entity.ProjectMember, toEntity interface{}, orderBy ...string) {
p.projectMemberRepo.ListMemeber(condition, toEntity, orderBy...) p.projectMemberRepo.ListMemeber(condition, toEntity, orderBy...)

View File

@@ -133,6 +133,14 @@ func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
ri.Close() ri.Close()
panic(biz.NewBizErr(fmt.Sprintf("redis集群连接失败: %s", e.Error()))) panic(biz.NewBizErr(fmt.Sprintf("redis集群连接失败: %s", e.Error())))
} }
} else if redisMode == entity.RedisModeSentinel {
ri = getRedisSentinelCient(re)
// 测试连接
_, e := ri.Cli.Ping(context.Background()).Result()
if e != nil {
ri.Close()
panic(biz.NewBizErr(fmt.Sprintf("redis sentinel连接失败: %s", e.Error())))
}
} }
global.Log.Infof("连接redis: %s", re.Host) global.Log.Infof("连接redis: %s", re.Host)
@@ -177,6 +185,27 @@ func getRedisClusterClient(re *entity.Redis) *RedisInstance {
return ri return ri
} }
func getRedisSentinelCient(re *entity.Redis) *RedisInstance {
ri := &RedisInstance{Id: re.Id, ProjectId: re.ProjectId, Mode: re.Mode}
// sentinel模式host为 masterName=host:port,host:port
masterNameAndHosts := strings.Split(re.Host, "=")
sentinelOptions := &redis.FailoverOptions{
MasterName: masterNameAndHosts[0],
SentinelAddrs: strings.Split(masterNameAndHosts[1], ","),
Password: re.Password, // no password set
DB: re.Db, // use default DB
DialTimeout: 8 * time.Second,
ReadTimeout: -1, // Disable timeouts, because SSH does not support deadlines.
WriteTimeout: -1,
}
if re.EnableSshTunnel == 1 {
ri.sshTunnelMachineId = re.SshTunnelMachineId
sentinelOptions.Dialer = getRedisDialer(re.SshTunnelMachineId)
}
ri.Cli = redis.NewFailoverClient(sentinelOptions)
return ri
}
func getRedisDialer(machineId uint64) func(ctx context.Context, network, addr string) (net.Conn, error) { func getRedisDialer(machineId uint64) func(ctx context.Context, network, addr string) (net.Conn, error) {
sshTunnel := MachineApp.GetSshTunnelMachine(machineId) sshTunnel := MachineApp.GetSshTunnelMachine(machineId)
return func(_ context.Context, network, addr string) (net.Conn, error) { return func(_ context.Context, network, addr string) (net.Conn, error) {
@@ -227,6 +256,10 @@ func TestRedisConnection(re *entity.Redis) {
ccli := getRedisClusterClient(re) ccli := getRedisClusterClient(re)
defer ccli.Close() defer ccli.Close()
cmd = ccli.ClusterCli cmd = ccli.ClusterCli
} else if re.Mode == entity.RedisModeSentinel {
rcli := getRedisSentinelCient(re)
defer rcli.Close()
cmd = rcli.Cli
} }
// 测试连接 // 测试连接
@@ -247,10 +280,10 @@ type RedisInstance struct {
// 获取命令执行接口的具体实现 // 获取命令执行接口的具体实现
func (r *RedisInstance) GetCmdable() redis.Cmdable { func (r *RedisInstance) GetCmdable() redis.Cmdable {
redisMode := r.Mode redisMode := r.Mode
if redisMode == "" || redisMode == entity.RedisModeStandalone { if redisMode == "" || redisMode == entity.RedisModeStandalone || r.Mode == entity.RedisModeSentinel {
return r.Cli return r.Cli
} }
if r.Mode == entity.RedisModeCluster { if redisMode == entity.RedisModeCluster {
return r.ClusterCli return r.ClusterCli
} }
return nil return nil
@@ -263,7 +296,7 @@ func (r *RedisInstance) Scan(cursor uint64, match string, count int64) ([]string
} }
func (r *RedisInstance) Close() { func (r *RedisInstance) Close() {
if r.Mode == entity.RedisModeStandalone { if r.Mode == entity.RedisModeStandalone || r.Mode == entity.RedisModeSentinel {
if err := r.Cli.Close(); err != nil { if err := r.Cli.Close(); err != nil {
global.Log.Errorf("关闭redis单机实例[%d]连接失败: %s", r.Id, err.Error()) global.Log.Errorf("关闭redis单机实例[%d]连接失败: %s", r.Id, err.Error())
} }

View File

@@ -19,6 +19,7 @@ type Machine struct {
Remark string `json:"remark"` // 备注 Remark string `json:"remark"` // 备注
EnableSshTunnel int8 `json:"enableSshTunnel"` // 是否启用ssh隧道 EnableSshTunnel int8 `json:"enableSshTunnel"` // 是否启用ssh隧道
SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id
EnableRecorder int8 `json:"enableRecorder"` // 是否启用终端回放记录
} }
const ( const (

View File

@@ -24,6 +24,7 @@ type Redis struct {
const ( const (
RedisModeStandalone = "standalone" RedisModeStandalone = "standalone"
RedisModeCluster = "cluster" RedisModeCluster = "cluster"
RedisModeSentinel = "sentinel"
) )
func (r *Redis) PwdEncrypt() { func (r *Redis) PwdEncrypt() {

View File

@@ -9,4 +9,6 @@ type ProjectEnv interface {
Save(entity *entity.ProjectEnv) Save(entity *entity.ProjectEnv)
DeleteEnvs(projectId uint64) DeleteEnvs(projectId uint64)
DeleteEnv(envId uint64)
} }

View File

@@ -27,7 +27,7 @@ type Cli struct {
sshTunnelMachineId uint64 sshTunnelMachineId uint64
} }
//连接 // 连接
func (c *Cli) connect() error { func (c *Cli) connect() error {
// 如果已经有client则直接返回 // 如果已经有client则直接返回
if c.client != nil { if c.client != nil {
@@ -90,8 +90,8 @@ func (c *Cli) GetSession() (*ssh.Session, error) {
return c.client.NewSession() return c.client.NewSession()
} }
//执行shell // 执行shell
//@param shell shell脚本命令 // @param shell shell脚本命令
func (c *Cli) Run(shell string) (*string, error) { func (c *Cli) Run(shell string) (*string, error) {
session, err := c.GetSession() session, err := c.GetSession()
if err != nil { if err != nil {
@@ -236,7 +236,7 @@ func GetSshClient(m *entity.Machine) (*ssh.Client, error) {
return sshClient, nil return sshClient, nil
} }
//根据机器信息创建客户端对象 // 根据机器信息创建客户端对象
func newClient(machine *entity.Machine) (*Cli, error) { func newClient(machine *entity.Machine) (*Cli, error) {
if machine == nil { if machine == nil {
return nil, errors.New("机器不存在") return nil, errors.New("机器不存在")

View File

@@ -0,0 +1,67 @@
package machine
import (
"encoding/json"
"io"
"sync"
"time"
)
type RecType string
const (
InputType RecType = "i"
OutPutType RecType = "o"
)
type RecHeader struct {
Version int `json:"version"`
Width int `json:"width"`
Height int `json:"height"`
Timestamp int64 `json:"timestamp"`
Env struct {
Shell string `json:"SHELL"`
Term string `json:"TERM"`
} `json:"env"`
}
func defaultRecHeader() *RecHeader {
recHeader := new(RecHeader)
recHeader.Version = 2
recHeader.Env.Shell = "/bin/bash"
recHeader.Env.Term = "xterm-256color"
return recHeader
}
type Recorder struct {
StartTime time.Time
Writer io.Writer
sync.Mutex
}
func NewRecorder(writer io.Writer) *Recorder {
return &Recorder{
StartTime: time.Now(),
Writer: writer,
}
}
func (rec *Recorder) WriteHeader(height, width int) {
header := defaultRecHeader()
header.Timestamp = rec.StartTime.Unix()
header.Height = height
header.Width = width
b, _ := json.Marshal(header)
rec.Writer.Write(b)
rec.Writer.Write([]byte("\r\n"))
}
func (rec *Recorder) WriteData(rectype RecType, data string) {
recData := make([]interface{}, 3)
recData[0] = float64(time.Since(rec.StartTime).Microseconds()) / float64(1000000)
recData[1] = rectype
recData[2] = data
b, _ := json.Marshal(recData)
rec.Writer.Write(b)
rec.Writer.Write([]byte("\r\n"))
}

View File

@@ -1,19 +0,0 @@
package machine
const StatsShell = `
cat /proc/uptime
echo '-----'
/bin/hostname -f
echo '-----'
cat /proc/loadavg
echo '-----'
cat /proc/meminfo
echo '-----'
df -B1
echo '-----'
/sbin/ip -o addr
echo '-----'
/bin/cat /proc/net/dev
echo '-----'
top -b -n 1 | grep Cpu
`

View File

@@ -53,6 +53,24 @@ type Stats struct {
CPU CPUInfo // or []CPUInfo to get all the cpu-core's stats? CPU CPUInfo // or []CPUInfo to get all the cpu-core's stats?
} }
const StatsShell = `
cat /proc/uptime
echo '-----'
/bin/hostname -f
echo '-----'
cat /proc/loadavg
echo '-----'
cat /proc/meminfo
echo '-----'
df -B1
echo '-----'
/sbin/ip -o addr
echo '-----'
/bin/cat /proc/net/dev
echo '-----'
top -b -n 1 | grep Cpu
`
func (c *Cli) GetAllStats() *Stats { func (c *Cli) GetAllStats() *Stats {
res, _ := c.Run(StatsShell) res, _ := c.Run(StatsShell)
infos := strings.Split(*res, "-----") infos := strings.Split(*res, "-----")

View File

@@ -0,0 +1,74 @@
package machine
import (
"bufio"
"io"
"golang.org/x/crypto/ssh"
)
type Terminal struct {
SshSession *ssh.Session
StdinPipe io.WriteCloser
StdoutReader *bufio.Reader
}
// 新建机器ssh终端
func NewTerminal(cli *Cli) (*Terminal, error) {
sshSession, err := cli.GetSession()
if err != nil {
return nil, err
}
stdoutPipe, err := sshSession.StdoutPipe()
if err != nil {
return nil, err
}
stdoutReader := bufio.NewReader(stdoutPipe)
stdinPipe, err := sshSession.StdinPipe()
if err != nil {
return nil, err
}
terminal := Terminal{
SshSession: sshSession,
StdinPipe: stdinPipe,
StdoutReader: stdoutReader,
}
return &terminal, nil
}
func (t *Terminal) Write(p []byte) (int, error) {
return t.StdinPipe.Write(p)
}
func (t *Terminal) ReadRune() (r rune, size int, err error) {
return t.StdoutReader.ReadRune()
}
func (t *Terminal) Close() error {
if t.SshSession != nil {
return t.SshSession.Close()
}
return nil
}
func (t *Terminal) WindowChange(h int, w int) error {
return t.SshSession.WindowChange(h, w)
}
func (t *Terminal) RequestPty(term string, h, w int) error {
modes := ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
return t.SshSession.RequestPty(term, h, w, modes)
}
func (t *Terminal) Shell() error {
return t.SshSession.Shell()
}

View File

@@ -0,0 +1,184 @@
package machine
import (
"context"
"encoding/json"
"io"
"mayfly-go/pkg/global"
"time"
"unicode/utf8"
"github.com/gorilla/websocket"
)
const (
Resize = 1
Data = 2
Ping = 3
)
type TerminalSession struct {
ID string
wsConn *websocket.Conn
terminal *Terminal
recorder *Recorder
ctx context.Context
cancel context.CancelFunc
dataChan chan rune
tick *time.Ticker
}
func NewTerminalSession(sessionId string, ws *websocket.Conn, cli *Cli, rows, cols int, recorder *Recorder) (*TerminalSession, error) {
terminal, err := NewTerminal(cli)
if err != nil {
return nil, err
}
err = terminal.RequestPty("xterm-256color", rows, cols)
if err != nil {
return nil, err
}
err = terminal.Shell()
if err != nil {
return nil, err
}
if recorder != nil {
recorder.WriteHeader(rows-5, cols)
}
ctx, cancel := context.WithCancel(context.Background())
tick := time.NewTicker(time.Millisecond * time.Duration(60))
ts := &TerminalSession{
ID: sessionId,
wsConn: ws,
terminal: terminal,
recorder: recorder,
ctx: ctx,
cancel: cancel,
dataChan: make(chan rune),
tick: tick,
}
return ts, nil
}
func (r TerminalSession) Start() {
go r.readFormTerminal()
go r.writeToWebsocket()
r.receiveWsMsg()
}
func (r TerminalSession) Stop() {
global.Log.Debug("close machine ssh terminal session")
r.tick.Stop()
r.cancel()
if r.terminal != nil {
if err := r.terminal.Close(); err != nil {
global.Log.Errorf("关闭机器ssh终端失败: %s", err.Error())
}
}
}
func (ts TerminalSession) readFormTerminal() {
for {
select {
case <-ts.ctx.Done():
return
default:
rn, size, err := ts.terminal.ReadRune()
if err != nil {
if err != io.EOF {
global.Log.Error("机器ssh终端读取消息失败: ", err)
}
return
}
if size > 0 {
ts.dataChan <- rn
}
}
}
}
func (ts TerminalSession) writeToWebsocket() {
var buf []byte
for {
select {
case <-ts.ctx.Done():
return
case <-ts.tick.C:
if len(buf) > 0 {
s := string(buf)
if err := WriteMessage(ts.wsConn, s); err != nil {
global.Log.Error("机器ssh终端发送消息至websocket失败: ", err)
return
}
// 如果记录器存在,则记录操作回放信息
if ts.recorder != nil {
ts.recorder.Lock()
ts.recorder.WriteData(OutPutType, s)
ts.recorder.Unlock()
}
buf = []byte{}
}
case data := <-ts.dataChan:
if data != utf8.RuneError {
p := make([]byte, utf8.RuneLen(data))
utf8.EncodeRune(p, data)
buf = append(buf, p...)
} else {
buf = append(buf, []byte("@")...)
}
}
}
}
type WsMsg struct {
Type int `json:"type"`
Msg string `json:"msg"`
Cols int `json:"cols"`
Rows int `json:"rows"`
}
func (ts *TerminalSession) receiveWsMsg() {
wsConn := ts.wsConn
for {
select {
case <-ts.ctx.Done():
return
default:
// read websocket msg
_, wsData, err := wsConn.ReadMessage()
if err != nil {
global.Log.Debug("机器ssh终端读取websocket消息失败: ", err)
return
}
// 解析消息
msgObj := WsMsg{}
if err := json.Unmarshal(wsData, &msgObj); err != nil {
global.Log.Error("机器ssh终端消息解析失败: ", err)
}
switch msgObj.Type {
case Resize:
if msgObj.Cols > 0 && msgObj.Rows > 0 {
if err := ts.terminal.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
global.Log.Error("ssh pty change windows size failed")
}
}
case Data:
_, err := ts.terminal.Write([]byte(msgObj.Msg))
if err != nil {
global.Log.Debug("机器ssh终端写入消息失败: %s", err)
}
case Ping:
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
if err != nil {
WriteMessage(wsConn, "\r\n\033[1;31m提示: 终端连接已断开...\033[0m")
return
}
}
}
}
}
func WriteMessage(ws *websocket.Conn, msg string) error {
return ws.WriteMessage(websocket.TextMessage, []byte(msg))
}

View File

@@ -1,195 +0,0 @@
package machine
import (
"bytes"
"encoding/json"
"io"
"mayfly-go/pkg/global"
"sync"
"time"
"github.com/gorilla/websocket"
"golang.org/x/crypto/ssh"
)
type safeBuffer struct {
buffer bytes.Buffer
mu sync.Mutex
}
func (w *safeBuffer) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.buffer.Write(p)
}
func (w *safeBuffer) Bytes() []byte {
w.mu.Lock()
defer w.mu.Unlock()
return w.buffer.Bytes()
}
func (w *safeBuffer) Reset() {
w.mu.Lock()
defer w.mu.Unlock()
w.buffer.Reset()
}
const (
wsMsgCmd = "cmd"
wsMsgResize = "resize"
)
type WsMsg struct {
Type string `json:"type"`
Msg string `json:"msg"`
Cols int `json:"cols"`
Rows int `json:"rows"`
}
type LogicSshWsSession struct {
stdinPipe io.WriteCloser
comboOutput *safeBuffer //ssh 终端混合输出
inputFilterBuff *safeBuffer //用来过滤输入的命令和ssh_filter配置对比的
session *ssh.Session
wsConn *websocket.Conn
}
func NewLogicSshWsSession(cols, rows int, cli *Cli, wsConn *websocket.Conn) (*LogicSshWsSession, error) {
sshSession, err := cli.GetSession()
if err != nil {
return nil, err
}
stdinP, err := sshSession.StdinPipe()
if err != nil {
return nil, err
}
comboWriter := new(safeBuffer)
inputBuf := new(safeBuffer)
//ssh.stdout and stderr will write output into comboWriter
sshSession.Stdout = comboWriter
sshSession.Stderr = comboWriter
modes := ssh.TerminalModes{
ssh.ECHO: 1, // disable echo
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
// Request pseudo terminal
if err := sshSession.RequestPty("xterm-256color", rows, cols, modes); err != nil {
return nil, err
}
// Start remote shell
if err := sshSession.Shell(); err != nil {
return nil, err
}
return &LogicSshWsSession{
stdinPipe: stdinP,
comboOutput: comboWriter,
inputFilterBuff: inputBuf,
session: sshSession,
wsConn: wsConn,
}, nil
}
//Close 关闭
func (sws *LogicSshWsSession) Close() {
if sws.session != nil {
sws.session.Close()
}
if sws.comboOutput != nil {
sws.comboOutput = nil
}
}
func (sws *LogicSshWsSession) Start(quitChan chan bool) {
go sws.receiveWsMsg(quitChan)
go sws.sendComboOutput(quitChan)
}
//receiveWsMsg receive websocket msg do some handling then write into ssh.session.stdin
func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
wsConn := sws.wsConn
//tells other go routine quit
defer setQuit(exitCh)
for {
select {
case <-exitCh:
return
default:
//read websocket msg
_, wsData, err := wsConn.ReadMessage()
if err != nil {
if websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
return
}
global.Log.Error("reading webSocket message failed: ", err)
return
}
//unmashal bytes into struct
msgObj := WsMsg{}
if err := json.Unmarshal(wsData, &msgObj); err != nil {
global.Log.Error("unmarshal websocket message failed", err)
}
switch msgObj.Type {
case wsMsgResize:
//handle xterm.js size change
if msgObj.Cols > 0 && msgObj.Rows > 0 {
if err := sws.session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
global.Log.Error("ssh pty change windows size failed")
}
}
case wsMsgCmd:
sws.sendWebsocketInputCommandToSshSessionStdinPipe([]byte(msgObj.Msg))
}
}
}
}
//sendWebsocketInputCommandToSshSessionStdinPipe
func (sws *LogicSshWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) {
if _, err := sws.stdinPipe.Write(cmdBytes); err != nil {
global.Log.Error("ws cmd bytes write to ssh.stdin pipe failed")
}
}
func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) {
wsConn := sws.wsConn
//todo 优化成一个方法
//tells other go routine quit
defer setQuit(exitCh)
//every 120ms write combine output bytes into websocket response
tick := time.NewTicker(time.Millisecond * time.Duration(60))
//for range time.Tick(120 * time.Millisecond){}
defer tick.Stop()
for {
select {
case <-tick.C:
if sws.comboOutput == nil {
return
}
bs := sws.comboOutput.Bytes()
if len(bs) > 0 {
err := wsConn.WriteMessage(websocket.TextMessage, bs)
if err != nil {
global.Log.Error("ssh sending combo output to webSocket failed")
}
sws.comboOutput.buffer.Reset()
}
case <-exitCh:
return
}
}
}
func (sws *LogicSshWsSession) Wait(quitChan chan bool) {
if err := sws.session.Wait(); err != nil {
setQuit(quitChan)
}
}
func setQuit(ch chan bool) {
ch <- true
}

View File

@@ -22,3 +22,7 @@ func (p *projectEnvRepo) Save(entity *entity.ProjectEnv) {
func (p *projectEnvRepo) DeleteEnvs(projectId uint64) { func (p *projectEnvRepo) DeleteEnvs(projectId uint64) {
model.DeleteByCondition(&entity.ProjectEnv{ProjectId: projectId}) model.DeleteByCondition(&entity.ProjectEnv{ProjectId: projectId})
} }
func (p *projectEnvRepo) DeleteEnv(envId uint64) {
model.DeleteById(new(entity.ProjectEnv), envId)
}

View File

@@ -43,9 +43,11 @@ func InitMachineRouter(router *gin.RouterGroup) {
}) })
saveMachine := ctx.NewLogInfo("保存机器信息").WithSave(true) saveMachine := ctx.NewLogInfo("保存机器信息").WithSave(true)
saveMachineP := ctx.NewPermission("machine:update")
machines.POST("", func(c *gin.Context) { machines.POST("", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c). ctx.NewReqCtxWithGin(c).
WithLog(saveMachine). WithLog(saveMachine).
WithRequiredPermission(saveMachineP).
Handle(m.SaveMachine) Handle(m.SaveMachine)
}) })
@@ -69,5 +71,12 @@ func InitMachineRouter(router *gin.RouterGroup) {
}) })
machines.GET(":machineId/terminal", m.WsSSH) machines.GET(":machineId/terminal", m.WsSSH)
// 获取机器终端回放记录的相应文件夹名或文件名,目前具有保存机器信息的权限标识才有权限查看终端回放
machines.GET("rec/names", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).
WithRequiredPermission(saveMachineP).
Handle(m.MachineRecDirNames)
})
} }
} }

View File

@@ -57,6 +57,13 @@ func InitProjectRouter(router *gin.RouterGroup) {
Handle(m.SaveProjectEnvs) Handle(m.SaveProjectEnvs)
}) })
delProjectEnvLog := ctx.NewLogInfo("删除项目环境信息").WithSave(true)
project.DELETE("/envs", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).WithLog(delProjectEnvLog).
WithRequiredPermission(delPP).
Handle(m.DelProjectEnv)
})
// 获取项目下的成员信息列表 // 获取项目下的成员信息列表
project.GET("/:projectId/members", func(c *gin.Context) { project.GET("/:projectId/members", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(m.GetProjectMembers) ctx.NewReqCtxWithGin(c).Handle(m.GetProjectMembers)

View File

@@ -64,9 +64,17 @@ func InitRedisRouter(router *gin.RouterGroup) {
ctx.NewReqCtxWithGin(c).Handle(rs.SetStringValue) ctx.NewReqCtxWithGin(c).Handle(rs.SetStringValue)
}) })
// 获取hash类型值 // hscan
redis.GET(":id/hash-value", func(c *gin.Context) { redis.GET(":id/hscan", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(rs.GetHashValue) ctx.NewReqCtxWithGin(c).Handle(rs.Hscan)
})
redis.GET(":id/hget", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(rs.Hget)
})
redis.DELETE(":id/hdel", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(rs.Hdel)
}) })
// 设置hash类型值 // 设置hash类型值

View File

@@ -23,6 +23,7 @@ type Account struct {
ResourceApp application.Resource ResourceApp application.Resource
RoleApp application.Role RoleApp application.Role
MsgApp application.Msg MsgApp application.Msg
ConfigApp application.Config
} }
/** 登录者个人相关操作 **/ /** 登录者个人相关操作 **/
@@ -32,8 +33,11 @@ func (a *Account) Login(rc *ctx.ReqCtx) {
loginForm := &form.LoginForm{} loginForm := &form.LoginForm{}
ginx.BindJsonAndValid(rc.GinCtx, loginForm) ginx.BindJsonAndValid(rc.GinCtx, loginForm)
// 校验验证码 // 判断是否有开启登录验证码校验
biz.IsTrue(captcha.Verify(loginForm.Cid, loginForm.Captcha), "验证码错误") if a.ConfigApp.GetConfig(entity.ConfigKeyUseLoginCaptcha).BoolValue(true) {
// 校验验证码
biz.IsTrue(captcha.Verify(loginForm.Cid, loginForm.Captcha), "验证码错误")
}
originPwd, err := utils.DefaultRsaDecrypt(loginForm.Password, true) originPwd, err := utils.DefaultRsaDecrypt(loginForm.Password, true)
biz.ErrIsNilAppendErr(err, "解密密码错误: %s") biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
@@ -145,17 +149,6 @@ func (a *Account) saveLogin(account *entity.Account, ip string) {
loginMsg.Creator = account.Username loginMsg.Creator = account.Username
loginMsg.CreatorId = account.Id loginMsg.CreatorId = account.Id
a.MsgApp.Create(loginMsg) a.MsgApp.Create(loginMsg)
// bodyMap, err := httpclient.NewRequest(fmt.Sprintf("http://ip-api.com/json/%s?lang=zh-CN", ip)).Get().BodyToMap()
// if err != nil {
// global.Log.Errorf("获取客户端ip地址信息失败%s", err.Error())
// return
// }
// if bodyMap["status"].(string) == "fail" {
// return
// }
// msg := fmt.Sprintf("%s于%s-%s登录", account.Username, bodyMap["regionName"], bodyMap["city"])
// global.Log.Info(msg)
} }
// 获取个人账号信息 // 获取个人账号信息
@@ -203,8 +196,8 @@ func (a *Account) Accounts(rc *ctx.ReqCtx) {
rc.ResData = a.AccountApp.GetPageList(condition, ginx.GetPageParam(rc.GinCtx), new([]vo.AccountManageVO)) rc.ResData = a.AccountApp.GetPageList(condition, ginx.GetPageParam(rc.GinCtx), new([]vo.AccountManageVO))
} }
// @router /accounts [get] // @router /accounts
func (a *Account) CreateAccount(rc *ctx.ReqCtx) { func (a *Account) SaveAccount(rc *ctx.ReqCtx) {
form := &form.AccountCreateForm{} form := &form.AccountCreateForm{}
ginx.BindJsonAndValid(rc.GinCtx, form) ginx.BindJsonAndValid(rc.GinCtx, form)
rc.ReqParam = form rc.ReqParam = form
@@ -212,7 +205,16 @@ func (a *Account) CreateAccount(rc *ctx.ReqCtx) {
account := &entity.Account{} account := &entity.Account{}
utils.Copy(account, form) utils.Copy(account, form)
account.SetBaseInfo(rc.LoginAccount) account.SetBaseInfo(rc.LoginAccount)
a.AccountApp.Create(account)
if account.Id == 0 {
a.AccountApp.Create(account)
} else {
if account.Password != "" {
biz.IsTrue(CheckPasswordLever(account.Password), "密码强度必须8位以上且包含字⺟⼤⼩写+数字+特殊符号")
account.Password = utils.PwdHash(account.Password)
}
a.AccountApp.Update(account)
}
} }
func (a *Account) ChangeStatus(rc *ctx.ReqCtx) { func (a *Account) ChangeStatus(rc *ctx.ReqCtx) {

View File

@@ -0,0 +1,39 @@
package api
import (
"mayfly-go/internal/sys/api/form"
"mayfly-go/internal/sys/application"
"mayfly-go/internal/sys/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/utils"
)
type Config struct {
ConfigApp application.Config
}
func (c *Config) Configs(rc *ctx.ReqCtx) {
g := rc.GinCtx
condition := &entity.Config{Key: g.Query("key")}
rc.ResData = c.ConfigApp.GetPageList(condition, ginx.GetPageParam(g), new([]entity.Config))
}
func (c *Config) GetConfigValueByKey(rc *ctx.ReqCtx) {
key := rc.GinCtx.Query("key")
biz.NotEmpty(key, "key不能为空")
rc.ResData = c.ConfigApp.GetConfig(key).Value
}
func (c *Config) SaveConfig(rc *ctx.ReqCtx) {
g := rc.GinCtx
form := &form.ConfigForm{}
ginx.BindJsonAndValid(g, form)
rc.ReqParam = form
config := new(entity.Config)
utils.Copy(config, form)
config.SetBaseInfo(rc.LoginAccount)
c.ConfigApp.Save(config)
}

View File

@@ -1,7 +1,9 @@
package form package form
type AccountCreateForm struct { type AccountCreateForm struct {
Id uint64
Username *string `json:"username" binding:"required,min=4,max=16"` Username *string `json:"username" binding:"required,min=4,max=16"`
Password *string `json:"password"`
} }
type AccountUpdateForm struct { type AccountUpdateForm struct {

View File

@@ -0,0 +1,9 @@
package form
type ConfigForm struct {
Id int
Name string `binding:"required"`
Key string `binding:"required"`
Value string
Remark string `json:"remark"`
}

View File

@@ -4,6 +4,6 @@ package form
type LoginForm struct { type LoginForm struct {
Username string `json:"username" binding:"required"` Username string `json:"username" binding:"required"`
Password string `binding:"required"` Password string `binding:"required"`
Captcha string `binding:"required"` Captcha string
Cid string `binding:"required"` Cid string
} }

View File

@@ -0,0 +1,46 @@
package application
import (
"mayfly-go/internal/sys/domain/entity"
"mayfly-go/internal/sys/domain/repository"
"mayfly-go/internal/sys/infrastructure/persistence"
"mayfly-go/pkg/global"
"mayfly-go/pkg/model"
)
type Config interface {
GetPageList(condition *entity.Config, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
Save(config *entity.Config)
// 获取指定key的配置信息, 不会返回nil, 若不存在则值都默认值即空字符串
GetConfig(key string) *entity.Config
}
type configAppImpl struct {
configRepo repository.Config
}
var ConfigApp Config = &configAppImpl{
configRepo: persistence.ConfigDao,
}
func (a *configAppImpl) GetPageList(condition *entity.Config, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
return a.configRepo.GetPageList(condition, pageParam, toEntity)
}
func (a *configAppImpl) Save(config *entity.Config) {
if config.Id == 0 {
a.configRepo.Insert(config)
} else {
a.configRepo.Update(config)
}
}
func (a *configAppImpl) GetConfig(key string) *entity.Config {
config := &entity.Config{Key: key}
if err := a.configRepo.GetConfig(config, "Id", "Key", "Value"); err != nil {
global.Log.Warnf("不存在key = [%s] 的系统配置", key)
}
return config
}

View File

@@ -0,0 +1,29 @@
package entity
import "mayfly-go/pkg/model"
const (
ConfigKeyUseLoginCaptcha string = "UseLoginCaptcha" // 是否使用登录验证码
)
type Config struct {
model.Model
Name string `json:"name"` // 配置名
Key string `json:"key"` // 配置key
Value string `json:"value"`
Remark string `json:"remark"`
}
func (a *Config) TableName() string {
return "t_sys_config"
}
// 若配置信息不存在, 则返回传递的默认值.
// 否则只有value == "1"为true其他为false
func (c *Config) BoolValue(defaultValue bool) bool {
// 如果值不存在,则返回默认值
if c.Id == 0 {
return defaultValue
}
return c.Value == "1"
}

View File

@@ -0,0 +1,18 @@
package repository
import (
"mayfly-go/internal/sys/domain/entity"
"mayfly-go/pkg/model"
)
type Config interface {
GetPageList(condition *entity.Config, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
Insert(config *entity.Config)
Update(config *entity.Config)
GetConfig(config *entity.Config, cols ...string) error
GetByCondition(condition *entity.Config, cols ...string) error
}

View File

@@ -0,0 +1,32 @@
package persistence
import (
"mayfly-go/internal/sys/domain/entity"
"mayfly-go/internal/sys/domain/repository"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/model"
)
type configRepo struct{}
var ConfigDao repository.Config = &configRepo{}
func (m *configRepo) GetPageList(condition *entity.Config, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
return model.GetPage(pageParam, condition, toEntity)
}
func (m *configRepo) Insert(config *entity.Config) {
biz.ErrIsNil(model.Insert(config), "新增系统配置失败")
}
func (m *configRepo) Update(config *entity.Config) {
biz.ErrIsNil(model.UpdateById(config), "更新系统配置失败")
}
func (m *configRepo) GetConfig(condition *entity.Config, cols ...string) error {
return model.GetBy(condition, cols...)
}
func (r *configRepo) GetByCondition(condition *entity.Config, cols ...string) error {
return model.GetBy(condition, cols...)
}

View File

@@ -15,6 +15,7 @@ func InitAccountRouter(router *gin.RouterGroup) {
ResourceApp: application.ResourceApp, ResourceApp: application.ResourceApp,
RoleApp: application.RoleApp, RoleApp: application.RoleApp,
MsgApp: application.MsgApp, MsgApp: application.MsgApp,
ConfigApp: application.ConfigApp,
} }
{ {
// 用户登录 // 用户登录
@@ -61,7 +62,7 @@ func InitAccountRouter(router *gin.RouterGroup) {
ctx.NewReqCtxWithGin(c). ctx.NewReqCtxWithGin(c).
WithRequiredPermission(addAccountPermission). WithRequiredPermission(addAccountPermission).
WithLog(createAccount). WithLog(createAccount).
Handle(a.CreateAccount) Handle(a.SaveAccount)
}) })
changeStatus := ctx.NewLogInfo("修改账号状态").WithSave(true) changeStatus := ctx.NewLogInfo("修改账号状态").WithSave(true)

View File

@@ -0,0 +1,30 @@
package router
import (
"mayfly-go/internal/sys/api"
"mayfly-go/internal/sys/application"
"mayfly-go/pkg/ctx"
"github.com/gin-gonic/gin"
)
func InitSysConfigRouter(router *gin.RouterGroup) {
r := &api.Config{ConfigApp: application.ConfigApp}
db := router.Group("sys/configs")
{
db.GET("", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(r.Configs)
})
db.GET("/value", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).WithNeedToken(false).Handle(r.GetConfigValueByKey)
})
saveConfig := ctx.NewLogInfo("保存系统配置信息").WithSave(true)
db.POST("", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).
WithLog(saveConfig).
Handle(r.SaveConfig)
})
}
}

View File

@@ -5,7 +5,5 @@ import (
) )
func main() { func main() {
starter.PrintBanner()
starter.InitDb()
starter.RunWebServer() starter.RunWebServer()
} }

View File

@@ -113,6 +113,7 @@ CREATE TABLE `t_machine` (
`password` varchar(3200) COLLATE utf8mb4_bin DEFAULT NULL, `password` varchar(3200) COLLATE utf8mb4_bin DEFAULT NULL,
`enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道', `enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
`ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id', `ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
`enable_recorder` tinyint(2) DEFAULT NULL COMMENT '是否启用终端回放记录',
`status` tinyint(2) NOT NULL COMMENT '状态: 1:启用; -1:禁用', `status` tinyint(2) NOT NULL COMMENT '状态: 1:启用; -1:禁用',
`remark` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `remark` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
`need_monitor` tinyint(2) DEFAULT NULL, `need_monitor` tinyint(2) DEFAULT NULL,
@@ -260,7 +261,7 @@ DROP TABLE IF EXISTS `t_redis`;
CREATE TABLE `t_redis` ( CREATE TABLE `t_redis` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`host` varchar(255) COLLATE utf8mb4_bin NOT NULL, `host` varchar(255) COLLATE utf8mb4_bin NOT NULL,
`password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL, `password` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL,
`db` int(32) DEFAULT NULL, `db` int(32) DEFAULT NULL,
`mode` varchar(32) DEFAULT NULL, `mode` varchar(32) DEFAULT NULL,
`enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道', `enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
@@ -286,7 +287,7 @@ CREATE TABLE `t_redis` (
DROP TABLE IF EXISTS `t_sys_account`; DROP TABLE IF EXISTS `t_sys_account`;
CREATE TABLE `t_sys_account` ( CREATE TABLE `t_sys_account` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(12) COLLATE utf8mb4_bin NOT NULL, `username` varchar(30) COLLATE utf8mb4_bin NOT NULL,
`password` varchar(64) COLLATE utf8mb4_bin NOT NULL, `password` varchar(64) COLLATE utf8mb4_bin NOT NULL,
`status` tinyint(4) DEFAULT NULL, `status` tinyint(4) DEFAULT NULL,
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间', `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
@@ -304,7 +305,7 @@ CREATE TABLE `t_sys_account` (
-- Records of t_sys_account -- Records of t_sys_account
-- ---------------------------- -- ----------------------------
BEGIN; BEGIN;
INSERT INTO `t_sys_account` VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, '2021-11-17 16:30:02', '12.0.216.228', '2020-01-01 19:00:00', 1, 'admin', '2020-01-01 19:00:00', 1, 'admin'); INSERT INTO `t_sys_account` VALUES (1, 'admin', '$2a$10$w3Wky2U.tinvR7c/s0aKPuwZsIu6pM1/DMJalwBDMbE6niHIxVrrm', 1, '2021-11-17 16:30:02', '12.0.216.228', '2020-01-01 19:00:00', 1, 'admin', '2020-01-01 19:00:00', 1, 'admin');
COMMIT; COMMIT;
-- ---------------------------- -- ----------------------------
@@ -437,6 +438,8 @@ INSERT INTO `t_sys_resource`(`id`, `pid`, `type`, `status`, `name`, `code`, `wei
INSERT INTO `t_sys_resource`(`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`) VALUES (83, 82, 2, 1, '基本权限', 'mongo:manage:base', 1, 'null', 1, 'admin', 1, 'admin', '2022-05-16 18:13:25', '2022-05-16 18:13:25'); INSERT INTO `t_sys_resource`(`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`) VALUES (83, 82, 2, 1, '基本权限', 'mongo:manage:base', 1, 'null', 1, 'admin', 1, 'admin', '2022-05-16 18:13:25', '2022-05-16 18:13:25');
INSERT INTO `t_sys_resource`(`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`) VALUES (84, 4, 1, 1, '操作日志', 'syslogs', 4, '{\"component\":\"SyslogList\",\"icon\":\"Tickets\",\"routeName\":\"SyslogList\"}', 1, 'admin', 1, 'admin', '2022-07-13 19:57:07', '2022-07-13 22:58:19'); INSERT INTO `t_sys_resource`(`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`) VALUES (84, 4, 1, 1, '操作日志', 'syslogs', 4, '{\"component\":\"SyslogList\",\"icon\":\"Tickets\",\"routeName\":\"SyslogList\"}', 1, 'admin', 1, 'admin', '2022-07-13 19:57:07', '2022-07-13 22:58:19');
INSERT INTO `t_sys_resource`(`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`) VALUES (85, 84, 2, 1, '操作日志基本权限', 'syslog', 1, 'null', 1, 'admin', 1, 'admin', '2022-07-13 19:57:55', '2022-07-13 19:57:55'); INSERT INTO `t_sys_resource`(`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`) VALUES (85, 84, 2, 1, '操作日志基本权限', 'syslog', 1, 'null', 1, 'admin', 1, 'admin', '2022-07-13 19:57:55', '2022-07-13 19:57:55');
INSERT INTO `t_sys_resource`(`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`) VALUES (86, 4, 1, 1, '系统配置', 'configs', 5, '{\"component\":\"ConfigList\",\"icon\":\"Setting\",\"isKeepAlive\":true,\"routeName\":\"ConfigList\"}', 1, 'admin', 1, 'admin', '2022-08-25 22:18:55', '2022-08-25 22:19:18');
INSERT INTO `t_sys_resource`(`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`) VALUES (87, 86, 2, 1, '基本权限', 'config:base', 1, 'null', 1, 'admin', 1, 'admin', '2022-08-25 22:19:35', '2022-08-25 22:19:35');
COMMIT; COMMIT;
-- ---------------------------- -- ----------------------------
@@ -642,6 +645,8 @@ INSERT INTO `t_sys_role_resource`(`id`, `role_id`, `resource_id`, `creator_id`,
INSERT INTO `t_sys_role_resource`(`id`, `role_id`, `resource_id`, `creator_id`, `creator`, `create_time`) VALUES (506, 1, 83, 1, 'admin', '2022-07-14 11:03:09'); INSERT INTO `t_sys_role_resource`(`id`, `role_id`, `resource_id`, `creator_id`, `creator`, `create_time`) VALUES (506, 1, 83, 1, 'admin', '2022-07-14 11:03:09');
INSERT INTO `t_sys_role_resource`(`id`, `role_id`, `resource_id`, `creator_id`, `creator`, `create_time`) VALUES (507, 1, 84, 1, 'admin', '2022-07-14 11:10:11'); INSERT INTO `t_sys_role_resource`(`id`, `role_id`, `resource_id`, `creator_id`, `creator`, `create_time`) VALUES (507, 1, 84, 1, 'admin', '2022-07-14 11:10:11');
INSERT INTO `t_sys_role_resource`(`id`, `role_id`, `resource_id`, `creator_id`, `creator`, `create_time`) VALUES (508, 1, 85, 1, 'admin', '2022-07-14 11:10:11'); INSERT INTO `t_sys_role_resource`(`id`, `role_id`, `resource_id`, `creator_id`, `creator`, `create_time`) VALUES (508, 1, 85, 1, 'admin', '2022-07-14 11:10:11');
INSERT INTO `t_sys_role_resource`(`id`, `role_id`, `resource_id`, `creator_id`, `creator`, `create_time`) VALUES (509, 1, 86, 1, 'admin', '2022-07-14 11:10:11');
INSERT INTO `t_sys_role_resource`(`id`, `role_id`, `resource_id`, `creator_id`, `creator`, `create_time`) VALUES (510, 1, 87, 1, 'admin', '2022-07-14 11:10:11');
COMMIT; COMMIT;
-- ---------------------------- -- ----------------------------
@@ -685,5 +690,32 @@ CREATE TABLE `t_mongo` (
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for t_sys_config
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_config`;
CREATE TABLE `t_sys_config` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) COLLATE utf8mb4_bin NOT NULL COMMENT '配置名',
`key` varchar(120) COLLATE utf8mb4_bin NOT NULL COMMENT '配置key',
`value` varchar(500) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '配置value',
`remark` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL,
`creator_id` bigint(20) NOT NULL,
`creator` varchar(36) COLLATE utf8mb4_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint(20) NOT NULL,
`modifier` varchar(36) COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Records of t_sys_config
-- ----------------------------
BEGIN;
INSERT INTO `t_sys_config` VALUES (1, '是否启用登录验证码', 'UseLoginCaptcha', '1', '1: 启用、0: 不启用', '2022-08-25 22:27:17', 1, 'admin', '2022-08-26 10:26:56', 1, 'admin');
INSERT INTO `t_sys_config` VALUES (2, '是否启用水印', 'UseWartermark', '1', '1: 启用、0: 不启用', '2022-08-25 23:36:35', 1, 'admin', '2022-08-26 10:02:52', 1, 'admin');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -2,11 +2,11 @@ package config
import "fmt" import "fmt"
type App struct { const (
Name string `yaml:"name"` AppName = "mayfly-go"
Version string `yaml:"version"` Version = "v1.2.10"
} )
func (a *App) GetAppInfo() string { func GetAppInfo() string {
return fmt.Sprintf("[%s:%s]", a.Name, a.Version) return fmt.Sprintf("[%s:%s]", AppName, Version)
} }

View File

@@ -11,12 +11,12 @@ import (
// 配置文件映射对象 // 配置文件映射对象
var Conf *Config var Conf *Config
func init() { func Init() {
configFilePath := flag.String("e", "./config.yml", "配置文件路径,默认为可执行文件目录") configFilePath := flag.String("e", "./config.yml", "配置文件路径,默认为可执行文件目录")
flag.Parse() flag.Parse()
// 获取启动参数中,配置文件的绝对路径 // 获取启动参数中,配置文件的绝对路径
path, _ := filepath.Abs(*configFilePath) path, _ := filepath.Abs(*configFilePath)
startConfigParam = &CmdConfigParam{ConfigFilePath: path} startConfigParam := &CmdConfigParam{ConfigFilePath: path}
// 读取配置文件信息 // 读取配置文件信息
yc := &Config{} yc := &Config{}
if err := utils.LoadYml(startConfigParam.ConfigFilePath, yc); err != nil { if err := utils.LoadYml(startConfigParam.ConfigFilePath, yc); err != nil {
@@ -32,12 +32,8 @@ type CmdConfigParam struct {
ConfigFilePath string // -e 配置文件路径 ConfigFilePath string // -e 配置文件路径
} }
// 启动可执行文件时的参数
var startConfigParam *CmdConfigParam
// yaml配置文件映射对象 // yaml配置文件映射对象
type Config struct { type Config struct {
App *App `yaml:"app"`
Server *Server `yaml:"server"` Server *Server `yaml:"server"`
Jwt *Jwt `yaml:"jwt"` Jwt *Jwt `yaml:"jwt"`
Aes *Aes `yaml:"aes"` Aes *Aes `yaml:"aes"`

View File

@@ -3,18 +3,28 @@ package config
import "fmt" import "fmt"
type Server struct { type Server struct {
Port int `yaml:"port"` Port int `yaml:"port"`
Model string `yaml:"model"` Model string `yaml:"model"`
Cors bool `yaml:"cors"` Cors bool `yaml:"cors"`
Tls *Tls `yaml:"tls"` Tls *Tls `yaml:"tls"`
Static *[]*Static `yaml:"static"` Static *[]*Static `yaml:"static"`
StaticFile *[]*StaticFile `yaml:"static-file"` StaticFile *[]*StaticFile `yaml:"static-file"`
MachineRecPath string `yaml:"machine-rec-path"` // 机器终端操作回放文件存储路径
} }
func (s *Server) GetPort() string { func (s *Server) GetPort() string {
return fmt.Sprintf(":%d", s.Port) return fmt.Sprintf(":%d", s.Port)
} }
// 获取终端回访记录存放基础路径, 如果配置文件未配置,则默认为./rec
func (s *Server) GetMachineRecPath() string {
path := s.MachineRecPath
if path == "" {
return "./rec"
}
return path
}
type Static struct { type Static struct {
RelativePath string `yaml:"relative-path"` RelativePath string `yaml:"relative-path"`
Root string `yaml:"root"` Root string `yaml:"root"`

View File

@@ -13,9 +13,14 @@ import (
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
) )
var ( func InitTokenConfig() {
JwtKey = config.Conf.Jwt.Key JwtKey = config.Conf.Jwt.Key
ExpTime = config.Conf.Jwt.ExpireTime ExpTime = config.Conf.Jwt.ExpireTime
}
var (
JwtKey string
ExpTime uint64
) )
// 创建用户token // 创建用户token
@@ -27,7 +32,8 @@ func CreateToken(userId uint64, username string) string {
"username": username, "username": username,
"exp": time.Now().Add(time.Minute * time.Duration(ExpTime)).Unix(), "exp": time.Now().Add(time.Minute * time.Duration(ExpTime)).Unix(),
}) })
// 如果jwt key为空则随机生成字符串
// 如果配置文件中的jwt key为空则随机生成字符串
if JwtKey == "" { if JwtKey == "" {
JwtKey = utils.RandString(32) JwtKey = utils.RandString(32)
global.Log.Infof("config.yml未配置jwt.key, 随机生成key为: %s", JwtKey) global.Log.Infof("config.yml未配置jwt.key, 随机生成key为: %s", JwtKey)

View File

@@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os" "os"
@@ -204,7 +203,7 @@ func request(rw *RequestWrapper) *ResponseWrapper {
return wrapper return wrapper
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
wrapper.Body = []byte(fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error())) wrapper.Body = []byte(fmt.Sprintf("读取HTTP请求返回值失败-%s", err.Error()))
return wrapper return wrapper

View File

@@ -13,7 +13,7 @@ import (
var Log = logrus.New() var Log = logrus.New()
func init() { func Init() {
Log.SetFormatter(new(LogFormatter)) Log.SetFormatter(new(LogFormatter))
Log.SetReportCaller(true) Log.SetReportCaller(true)

View File

@@ -1,16 +1,17 @@
package starter package starter
import ( import (
"fmt"
"mayfly-go/pkg/config"
"mayfly-go/pkg/global" "mayfly-go/pkg/global"
) )
func PrintBanner() { func printBanner() {
global.Log.Print(` global.Log.Print(fmt.Sprintf(`
__ _ __ _
_ __ ___ __ _ _ _ / _| |_ _ __ _ ___ _ __ ___ __ _ _ _ / _| |_ _ __ _ ___
| '_ ' _ \ / _' | | | | |_| | | | |_____ / _' |/ _ \ | '_ ' _ \ / _' | | | | |_| | | | |_____ / _' |/ _ \
| | | | | | (_| | |_| | _| | |_| |_____| (_| | (_) | | | | | | | (_| | |_| | _| | |_| |_____| (_| | (_) | version: %s
|_| |_| |_|\__,_|\__, |_| |_|\__, | \__, |\___/ |_| |_| |_|\__,_|\__, |_| |_|\__, | \__, |\___/
|___/ |___/ |___/ |___/ |___/ |___/ `, config.Version))
`)
} }

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