refactor: 前端统一使用prettier格式化&枚举值统一管理

This commit is contained in:
meilin.huang
2023-07-06 20:59:22 +08:00
parent f25bdb07ce
commit 5463ae9d7e
125 changed files with 3932 additions and 3735 deletions

View File

@@ -1,6 +1,6 @@
module.exports = { module.exports = {
// 一行最多多少个字符 // 一行最多多少个字符
printWidth: 150, printWidth: 160,
// 指定每个缩进级别的空格数 // 指定每个缩进级别的空格数
tabWidth: 4, tabWidth: 4,
// 使用制表符而不是空格缩进行 // 使用制表符而不是空格缩进行

View File

@@ -44,7 +44,7 @@ onMounted(() => {
}); });
// 获取缓存中的布局配置 // 获取缓存中的布局配置
if (getLocal('themeConfig')) { if (getLocal('themeConfig')) {
themeConfigStores.setThemeConfig({ themeConfig: getLocal('themeConfig') }) themeConfigStores.setThemeConfig({ themeConfig: getLocal('themeConfig') });
document.documentElement.style.cssText = getLocal('themeConfigStyle'); document.documentElement.style.cssText = getLocal('themeConfigStyle');
} }
}); });

View File

@@ -1,4 +1,4 @@
import request from './request' import request from './request';
/** /**
* 可用于各模块定义各自api请求 * 可用于各模块定义各自api请求
@@ -27,23 +27,13 @@ class Api {
} }
/** /**
* 操作该权限,即请求对应的url * 请求对应的该api
* @param {Object} param 请求该api的参数 * @param {Object} param 请求该api的参数
*/ */
request(param: any = null, options: any = null): Promise<any> { request(param: any = null, options: any = null, headers: any = null): Promise<any> {
return request.send(this, param, options); return request.request(this.method, this.url, param, headers, options);
} }
/**
* 操作该权限即请求对应的url
* @param {Object} param 请求该api的参数
* @param headers headers
*/
requestWithHeaders(param: any, headers: any): Promise<any> {
return request.sendWithHeaders(this, param, headers);
}
/** 静态方法 **/ /** 静态方法 **/
/** /**
@@ -88,5 +78,4 @@ class Api {
} }
} }
export default Api;
export default Api

View File

@@ -1,37 +1,90 @@
/** export interface EnumValueTag {
* 枚举类 color?: string;
* @author meilin.huang type?: string;
*/
export class Enum {
/**
* 添加枚举字段
*
* @param {string} field 枚举字段名
* @param {string} label 枚举名称
* @param {Object} value 枚举值
*/
add(field: string, label: string, value: any) {
this[field] = { label, value }
return this
} }
/** /**
* 根据枚举value获取其label * 枚举值
*
* @param {Object} value
*/ */
getLabelByValue(value: any) { export class EnumValue {
// 字段不存在返回‘’ /**
if (value === undefined || value === null) { * 枚举值
return '' */
value: any;
/**
* 枚举描述
*/
label: string;
/**
* 展示的标签信息
*/
tag: EnumValueTag;
constructor(value: any, label: string) {
this.value = value;
this.label = label;
} }
for (const i in this) {
const e: any = this[i] setTagType(type: string = 'primary'): EnumValue {
if (e && e.value === value) { this.tag = { type };
return e.label return this;
}
tagTypeSuccess(): EnumValue {
return this.setTagType('success');
}
tagTypeDanger(): EnumValue {
return this.setTagType('danger');
}
tagTypeWarning(): EnumValue {
return this.setTagType('warning');
}
setTagColor(color: string): EnumValue {
this.tag = { color };
return this;
}
public static of(value: any, label: string): EnumValue {
return new EnumValue(value, label);
}
/**
* 根据枚举值获取指定枚举值对象
*
* @param enumValues 所有枚举值
* @param value 需要匹配的枚举值
* @returns 枚举值对象
*/
static getEnumByValue(enumValues: EnumValue[], value: any): EnumValue | null {
for (let enumValue of enumValues) {
if (enumValue.value == value) {
return enumValue;
}
}
return null;
}
/**
* 根据枚举值获取枚举描述
*
* @param enums 枚举对象
* @param value 枚举值
* @returns 枚举描述
*/
static getLabelByValue(enums: any, value: any) {
const enumValues = Object.values(enums) as any;
for (let enumValue of enumValues) {
if (enumValue['value'] == value) {
return enumValue['label'];
}
}
return '';
} }
} }
return '' export default EnumValue;
}
}

View File

@@ -1,9 +1,8 @@
class SocketBuilder { class SocketBuilder {
websocket: WebSocket; websocket: WebSocket;
constructor(url: string) { constructor(url: string) {
if (typeof (WebSocket) === "undefined") { if (typeof WebSocket === 'undefined') {
throw new Error('不支持websocket'); throw new Error('不支持websocket');
} }
if (!url) { if (!url) {
@@ -41,6 +40,4 @@ class SocketBuilder {
} }
} }
export default SocketBuilder; export default SocketBuilder;

View File

@@ -5,7 +5,7 @@ class AssertError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);
// 错误类名 // 错误类名
this.name = "AssertError"; this.name = 'AssertError';
} }
} }
@@ -28,7 +28,7 @@ class AssertError extends Error {
* @param msg 错误消息 * @param msg 错误消息
*/ */
export function notBlank(obj: any, msg: string) { export function notBlank(obj: any, msg: string) {
isTrue(obj, msg) isTrue(obj, msg);
} }
/** /**
@@ -50,7 +50,7 @@ class AssertError extends Error {
*/ */
export function notNull(obj: any, msg: string) { export function notNull(obj: any, msg: string) {
if (obj == null || obj == undefined) { if (obj == null || obj == undefined) {
throw new AssertError(msg) throw new AssertError(msg);
} }
} }

View File

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

View File

@@ -1,26 +1,19 @@
// import * as echarts from 'echarts' // import * as echarts from 'echarts'
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。 // 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from "echarts/core"; import * as echarts from 'echarts/core';
/** 图表后缀都为 Chart */ /** 图表后缀都为 Chart */
import { PieChart } from "echarts/charts"; import { PieChart } from 'echarts/charts';
// 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component // 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
import { import { TitleComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent, LegendComponent } from 'echarts/components';
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LegendComponent,
} from "echarts/components";
// 标签自动布局,全局过渡动画等特性 // 标签自动布局,全局过渡动画等特性
import { LabelLayout, UniversalTransition } from "echarts/features"; import { LabelLayout, UniversalTransition } from 'echarts/features';
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步 // 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from "echarts/renderers"; import { CanvasRenderer } from 'echarts/renderers';
// 注册必须的组件 // 注册必须的组件
echarts.use([ echarts.use([

View File

@@ -1,27 +1,7 @@
interface BaseEnum {
name: string
value: any
}
const success: BaseEnum = {
name: 'success',
value: 200
}
export enum ResultEnum { export enum ResultEnum {
SUCCESS = 200, SUCCESS = 200,
ERROR = 400, ERROR = 400,
PARAM_ERROR = 405, PARAM_ERROR = 405,
SERVER_ERROR = 500, SERVER_ERROR = 500,
NO_PERMISSION = 501 NO_PERMISSION = 501,
} }
// /**
// * 全局公共枚举类
// */
// export default {
// // uri请求方法
// requestMethod: new Enum().add('GET', 'GET', 1).add('POST', 'POST', 2).add('PUT', 'PUT', 3).add('DELETE', 'DELETE', 4),
// // 结果枚举
// ResultEnum: new Enum().add('SUCCESS', '操作成功', 200).add('ERROR', '操作失败', 400).add('PARAM_ERROR', '参数错误', 405).add('SERVER_ERROR', '服务器异常', 500)
// .add('NO_PERMISSION', '没有权限', 501)
// }

View File

@@ -1,12 +1,12 @@
import Api from './Api' import request from './request';
export default { export default {
login: Api.newPost("/sys/accounts/login"), login: (param: any) => request.post('/sys/accounts/login', param),
otpVerify: Api.newPost("/sys/accounts/otp-verify"), otpVerify: (param: any) => request.post('/sys/accounts/otp-verify', param),
changePwd: Api.newPost("/sys/accounts/change-pwd"), changePwd: (param: any) => request.post('/sys/accounts/change-pwd', param),
getPublicKey: Api.newGet("/common/public-key"), getPublicKey: () => request.get('/common/public-key'),
getConfigValue: Api.newGet("/sys/configs/value"), getConfigValue: (params: any) => request.get('/sys/configs/value', params),
captcha: Api.newGet("/sys/captcha"), captcha: () => request.get('/sys/captcha'),
logout: Api.newPost("/sys/accounts/logout/{token}"), logout: () => request.post('/sys/accounts/logout/{token}'),
getPermissions: Api.newGet("/sys/accounts/permissions") getPermissions: () => request.get('/sys/accounts/permissions'),
} };

View File

@@ -1,7 +1,6 @@
import router from "../router"; import router from '../router';
import Axios from 'axios'; import Axios from 'axios';
import { ResultEnum } from './enums' import { ResultEnum } from './enums';
import Api from './Api';
import config from './config'; import config from './config';
import { getSession } from './utils/storage'; import { getSession } from './utils/storage';
import { templateResolve } from './utils/string'; import { templateResolve } from './utils/string';
@@ -22,8 +21,8 @@ export interface Result {
data?: any; data?: any;
} }
const baseUrl: string = config.baseApiUrl const baseUrl: string = config.baseApiUrl;
const baseWsUrl: string = config.baseWsUrl const baseWsUrl: string = config.baseWsUrl;
/** /**
* 通知错误消息 * 通知错误消息
@@ -37,28 +36,28 @@ function notifyErrorMsg(msg: string) {
// create an axios instance // create an axios instance
const service = Axios.create({ const service = Axios.create({
baseURL: baseUrl, // url = base url + request url baseURL: baseUrl, // url = base url + request url
timeout: 20000 // request timeout timeout: 20000, // request timeout
}) });
// request interceptor // request interceptor
service.interceptors.request.use( service.interceptors.request.use(
(config: any) => { (config: any) => {
// do something before request is sent // do something before request is sent
const token = getSession("token") const token = getSession('token');
if (token) { if (token) {
// 设置token // 设置token
config.headers['Authorization'] = token config.headers['Authorization'] = token;
} }
return config return config;
}, },
error => { (error) => {
return Promise.reject(error) return Promise.reject(error);
} }
) );
// response interceptor // response interceptor
service.interceptors.response.use( service.interceptors.response.use(
response => { (response) => {
// 获取请求返回结果 // 获取请求返回结果
const data: Result = response.data; const data: Result = response.data;
// 如果提示没有权限则移除token使其重新登录 // 如果提示没有权限则移除token使其重新登录
@@ -88,9 +87,9 @@ service.interceptors.response.use(
} }
} }
return Promise.reject(e) return Promise.reject(e);
} }
) );
/** /**
* 请求uri * 请求uri
@@ -101,19 +100,18 @@ service.interceptors.response.use(
* @param {Object} params 参数 * @param {Object} params 参数
*/ */
function request(method: string, url: string, params: any = null, headers: any = null, options: any = null): Promise<any> { function request(method: string, url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
if (!url) if (!url) throw new Error('请求url不能为空');
throw new Error('请求url不能为空');
// 简单判断该url是否是restful风格 // 简单判断该url是否是restful风格
if (url.indexOf("{") != -1) { if (url.indexOf('{') != -1) {
url = templateResolve(url, params); url = templateResolve(url, params);
} }
const query: any = { const query: any = {
method, method,
url: url, url: url,
...options ...options,
}; };
if (headers) { if (headers) {
query.headers = headers query.headers = headers;
} }
// post和put使用json格式传参 // post和put使用json格式传参
@@ -122,32 +120,39 @@ function request(method: string, url: string, params: any = null, headers: any =
} else { } else {
query.params = params; query.params = params;
} }
return service.request(query).then(res => res) return service
.catch(e => { .request(query)
.then((res) => res)
.catch((e) => {
// 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可 // 如果返回的code不为成功则会返回对应的错误msg则直接统一通知即可
if (e.msg) { if (e.msg) {
notifyErrorMsg(e.msg) notifyErrorMsg(e.msg);
} }
return Promise.reject(e); return Promise.reject(e);
}); });
} }
/** /**
* 根据api执行对应接口 * get请求uri
* @param api Api实例 * 该方法已处理请求结果中code != 200的message提示,如需其他错误处理(取消加载状态,重置对象状态等等),可catch继续处理
* @param params 请求参数 *
* @param {Object} url uri
* @param {Object} params 参数
*/ */
function send(api: Api, params: any, options: any): Promise<any> { function get(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request(api.method, api.url, params, null, options); return request('get', url, params, headers, options);
} }
/** function post(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
* 根据api执行对应接口 return request('post', url, params, headers, options);
* @param api Api实例 }
* @param params 请求参数
*/ function put(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
function sendWithHeaders(api: Api, params: any, headers: any): Promise<any> { return request('put', url, params, headers, options);
return request(api.method, api.url, params, headers, null); }
function del(url: string, params: any = null, headers: any = null, options: any = null): Promise<any> {
return request('delete', url, params, headers, options);
} }
function getApiUrl(url: string) { function getApiUrl(url: string) {
@@ -155,10 +160,11 @@ function getApiUrl(url: string) {
return baseUrl + url + '?token=' + getSession('token'); return baseUrl + url + '?token=' + getSession('token');
} }
export default { export default {
request, request,
send, get,
sendWithHeaders, post,
getApiUrl put,
} del,
getApiUrl,
};

View File

@@ -1,17 +1,17 @@
import openApi from './openApi'; import openApi from './openApi';
import JSEncrypt from 'jsencrypt' import JSEncrypt from 'jsencrypt';
import { notBlank } from './assert'; import { notBlank } from './assert';
var encryptor: any = null var encryptor: any = null;
export async function getRsaPublicKey() { export async function getRsaPublicKey() {
let publicKey = sessionStorage.getItem('RsaPublicKey') let publicKey = sessionStorage.getItem('RsaPublicKey');
if (publicKey) { if (publicKey) {
return publicKey return publicKey;
} }
publicKey = await openApi.getPublicKey.request() as string publicKey = (await openApi.getPublicKey()) as string;
sessionStorage.setItem('RsaPublicKey', publicKey) sessionStorage.setItem('RsaPublicKey', publicKey);
return publicKey return publicKey;
} }
/** /**
@@ -23,14 +23,14 @@ export async function getRsaPublicKey() {
export async function RsaEncrypt(value: any) { export async function RsaEncrypt(value: any) {
// 不存在则返回空值 // 不存在则返回空值
if (!value) { if (!value) {
return "" return '';
} }
if (encryptor != null) { if (encryptor != null) {
return encryptor.encrypt(value) return encryptor.encrypt(value);
} }
encryptor = new JSEncrypt() encryptor = new JSEncrypt();
const publicKey = await getRsaPublicKey() as string; const publicKey = (await getRsaPublicKey()) as string;
notBlank(publicKey, "获取公钥失败") notBlank(publicKey, '获取公钥失败');
encryptor.setPublicKey(publicKey)//设置公钥 encryptor.setPublicKey(publicKey); //设置公钥
return encryptor.encrypt(value) return encryptor.encrypt(value);
} }

View File

@@ -1,8 +1,7 @@
import Config from './config';
import Config from './config' import { ElNotification } from 'element-plus';
import { ElNotification } from 'element-plus'
import SocketBuilder from './SocketBuilder'; import SocketBuilder from './SocketBuilder';
import { getSession } from '@/common/utils/storage.ts'; import { getSession } from '@/common/utils/storage';
export default { export default {
/** /**
@@ -38,8 +37,9 @@ export default {
title: message.title, title: message.title,
message: message.msg, message: message.msg,
type: mtype as any, type: mtype as any,
});
}) })
}) .open((event: any) => console.log(event))
.open((event: any) => console.log(event)).build(); .build();
} },
} };

View File

@@ -1,9 +1,9 @@
import openApi from './openApi'; import openApi from './openApi';
// 登录是否使用验证码配置key // 登录是否使用验证码配置key
const AccountLoginSecurity = "AccountLoginSecurity" const AccountLoginSecurity = 'AccountLoginSecurity';
const UseLoginCaptchaConfigKey = "UseLoginCaptcha" const UseLoginCaptchaConfigKey = 'UseLoginCaptcha';
const UseWartermarkConfigKey = "UseWartermark" const UseWartermarkConfigKey = 'UseWartermark';
/** /**
* 获取系统配置值 * 获取系统配置值
@@ -12,7 +12,7 @@ const UseWartermarkConfigKey = "UseWartermark"
* @returns 配置值 * @returns 配置值
*/ */
export async function getConfigValue(key: string): Promise<string> { export async function getConfigValue(key: string): Promise<string> {
return await openApi.getConfigValue.request({key}) as string return (await openApi.getConfigValue({ key })) as string;
} }
/** /**
@@ -49,7 +49,7 @@ export async function getAccountLoginSecurity() : Promise<any> {
* @returns * @returns
*/ */
export async function useLoginCaptcha(): Promise<boolean> { export async function useLoginCaptcha(): Promise<boolean> {
return await getBoolConfigValue(UseLoginCaptchaConfigKey, true) return await getBoolConfigValue(UseLoginCaptchaConfigKey, true);
} }
/** /**
@@ -58,12 +58,12 @@ export async function useLoginCaptcha() : Promise<boolean> {
* @returns * @returns
*/ */
export async function useWartermark(): Promise<boolean> { export async function useWartermark(): Promise<boolean> {
return await getBoolConfigValue(UseWartermarkConfigKey, true) return await getBoolConfigValue(UseWartermarkConfigKey, true);
} }
function convertBool(value: string, defaultValue: boolean) { function convertBool(value: string, defaultValue: boolean) {
if (!value) { if (!value) {
return defaultValue; return defaultValue;
} }
return value == "1" || value == "true"; return value == '1' || value == 'true';
} }

View File

@@ -1,27 +1,27 @@
export function dateFormat2(fmt: string, date: Date) { export function dateFormat2(fmt: string, date: Date) {
let ret; let ret;
const opt = { const opt = {
"y+": date.getFullYear().toString(), // 年 'y+': date.getFullYear().toString(), // 年
"M+": (date.getMonth() + 1).toString(), // 月 'M+': (date.getMonth() + 1).toString(), // 月
"d+": date.getDate().toString(), // 日 'd+': date.getDate().toString(), // 日
"H+": date.getHours().toString(), // 时 'H+': date.getHours().toString(), // 时
"m+": date.getMinutes().toString(), // 分 'm+': date.getMinutes().toString(), // 分
"s+": date.getSeconds().toString() // 秒 's+': date.getSeconds().toString(), // 秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串 // 有其他格式化字符需求可以继续添加,必须转化成字符串
}; };
for (const k in opt) { for (const k in opt) {
ret = new RegExp("(" + k + ")").exec(fmt); ret = new RegExp('(' + k + ')').exec(fmt);
if (ret) { if (ret) {
fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0"))) fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0'));
} }
} }
return fmt; return fmt;
} }
export function dateStrFormat(fmt: string, dateStr: string) { export function dateStrFormat(fmt: string, dateStr: string) {
return dateFormat2(fmt, new Date(dateStr)) return dateFormat2(fmt, new Date(dateStr));
} }
export function dateFormat(dateStr: string) { export function dateFormat(dateStr: string) {
return dateFormat2('yyyy-MM-dd HH:mm:ss',new Date(dateStr)) return dateFormat2('yyyy-MM-dd HH:mm:ss', new Date(dateStr));
} }

View File

@@ -11,7 +11,7 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
if (val.indexOf(',') != -1) { if (val.indexOf(',') != -1) {
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误 // 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
if (val.indexOf('"') != -1) { if (val.indexOf('"') != -1) {
val = val.replace(/\"/g, "\"\""); val = val.replace(/\"/g, '""');
} }
// 再将逗号转义 // 再将逗号转义
val = `"${val}"`; val = `"${val}"`;
@@ -20,7 +20,6 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
} else { } else {
dataValueArr.push(val); dataValueArr.push(val);
} }
} }
cvsData.push(dataValueArr); cvsData.push(dataValueArr);
} }

View File

@@ -116,8 +116,7 @@ export function formatDate(date: Date, format: string) {
'3': '三', '3': '三',
'4': '四', '4': '四',
}; };
if (/(W+)/.test(format)) if (/(W+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
format = format.replace(RegExp.$1, RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]);
if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]); if (/(Q+)/.test(format)) format = format.replace(RegExp.$1, RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]);
for (let k in opt) { for (let k in opt) {
let r = new RegExp('(' + k + ')').exec(format); let r = new RegExp('(' + k + ')').exec(format);

View File

@@ -1,5 +1,5 @@
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import loadingCss from "@/theme/loading.scss?inline" import loadingCss from '@/theme/loading.scss?inline';
// 定义方法 // 定义方法
export const NextLoading = { export const NextLoading = {

View File

@@ -1,7 +1,5 @@
// 字体图标 url // 字体图标 url
const cssCdnUrlList: Array<string> = [ const cssCdnUrlList: Array<string> = [];
];
// 第三方 js url // 第三方 js url
const jsCdnUrlList: Array<string> = []; const jsCdnUrlList: Array<string> = [];

View File

@@ -36,21 +36,19 @@ export function clearSession() {
window.sessionStorage.clear(); window.sessionStorage.clear();
} }
export function getUserInfo4Session() { export function getUserInfo4Session() {
return getSession("userInfo") return getSession('userInfo');
} }
export function setUserInfo2Session(userinfo: any) { export function setUserInfo2Session(userinfo: any) {
setSession("userInfo", userinfo) setSession('userInfo', userinfo);
} }
// 获取是否开启水印 // 获取是否开启水印
export function getUseWatermark4Session() { export function getUseWatermark4Session() {
return getSession("useWatermark") return getSession('useWatermark');
} }
export function setUseWatermark2Session(useWatermark: boolean) { export function setUseWatermark2Session(useWatermark: boolean) {
setSession("useWatermark", useWatermark) setSession('useWatermark', useWatermark);
} }

View File

@@ -12,7 +12,7 @@ export function templateResolve(template: string, param: any) {
if (value != null || value != undefined) { if (value != null || value != undefined) {
return value; return value;
} }
return ""; return '';
}); });
} }
@@ -21,11 +21,34 @@ export function letterAvatar(name: string, size = 60, color = '') {
name = name || ''; name = name || '';
size = size || 60; size = size || 60;
var colours = [ var colours = [
"#1abc9c", "#2ecc71", "#3498db", "#9b59b6", "#34495e", "#16a085", "#27ae60", "#2980b9", "#8e44ad", "#2c3e50", '#1abc9c',
"#f1c40f", "#e67e22", "#e74c3c", "#00bcd4", "#95a5a6", "#f39c12", "#d35400", "#c0392b", "#bdc3c7", "#7f8c8d" '#2ecc71',
'#3498db',
'#9b59b6',
'#34495e',
'#16a085',
'#27ae60',
'#2980b9',
'#8e44ad',
'#2c3e50',
'#f1c40f',
'#e67e22',
'#e74c3c',
'#00bcd4',
'#95a5a6',
'#f39c12',
'#d35400',
'#c0392b',
'#bdc3c7',
'#7f8c8d',
], ],
nameSplit = String(name).split(' '), nameSplit = String(name).split(' '),
initials, charIndex, colourIndex, canvas, context, dataURI; initials,
charIndex,
colourIndex,
canvas,
context,
dataURI;
if (nameSplit.length == 1) { if (nameSplit.length == 1) {
initials = nameSplit[0] ? nameSplit[0].charAt(0) : '?'; initials = nameSplit[0] ? nameSplit[0].charAt(0) : '?';
@@ -33,26 +56,26 @@ export function letterAvatar(name: string, size = 60, color = '') {
initials = nameSplit[0].charAt(0) + nameSplit[1].charAt(0); initials = nameSplit[0].charAt(0) + nameSplit[1].charAt(0);
} }
if (window.devicePixelRatio) { if (window.devicePixelRatio) {
size = (size * window.devicePixelRatio); size = size * window.devicePixelRatio;
} }
initials = initials.toLocaleUpperCase() initials = initials.toLocaleUpperCase();
charIndex = (initials == '?' ? 72 : initials.charCodeAt(0)) - 64; charIndex = (initials == '?' ? 72 : initials.charCodeAt(0)) - 64;
colourIndex = charIndex % 20; colourIndex = charIndex % 20;
canvas = document.createElement('canvas'); canvas = document.createElement('canvas');
canvas.width = size; canvas.width = size;
canvas.height = size; canvas.height = size;
context = canvas.getContext("2d") as any; context = canvas.getContext('2d') as any;
context.fillStyle = color ? color : colours[colourIndex - 1]; context.fillStyle = color ? color : colours[colourIndex - 1];
context.fillRect(0, 0, canvas.width, canvas.height); context.fillRect(0, 0, canvas.width, canvas.height);
context.font = Math.round(canvas.width / 2) + "px 'Microsoft Yahei'"; context.font = Math.round(canvas.width / 2) + "px 'Microsoft Yahei'";
context.textAlign = "center"; context.textAlign = 'center';
context.fillStyle = "#FFF"; context.fillStyle = '#FFF';
context.fillText(initials, size / 2, size / 1.5); context.fillText(initials, size / 2, size / 1.5);
dataURI = canvas.toDataURL(); dataURI = canvas.toDataURL();
canvas = null; canvas = null;
return dataURI; return dataURI;
}; }
/** /**
* 计算文本所占用的宽度px -> 该种方式较为准确 * 计算文本所占用的宽度px -> 该种方式较为准确
@@ -105,4 +128,4 @@ export function getContentWidth(content: any): number {
// flexWidth = 450; // flexWidth = 450;
// } // }
return flexWidth; return flexWidth;
}; }

View File

@@ -1,6 +1,6 @@
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import * as svg from '@element-plus/icons-vue'; import * as svg from '@element-plus/icons-vue';
import iconfontJson from '@/assets/iconfont/iconfont.json' import iconfontJson from '@/assets/iconfont/iconfont.json';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
/** /**
@@ -52,7 +52,7 @@ const getLocalAliIconfont = () => {
resolve(iconfontJson.glyphs.map((x: any) => prefix + x.font_class)); resolve(iconfontJson.glyphs.map((x: any) => prefix + x.font_class));
}); });
}); });
} };
// 初始化获取 css 样式,获取 element plus 自带图标 // 初始化获取 css 样式,获取 element plus 自带图标
const elementPlusIconfont = () => { const elementPlusIconfont = () => {

View File

@@ -163,8 +163,7 @@ export function verifyPasswordStrength(val: string) {
// 中:字母+数字,字母+特殊字符,数字+特殊字符 // 中:字母+数字,字母+特殊字符,数字+特殊字符
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '中'; if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '中';
// 强:字母+数字+特殊字符 // 强:字母+数字+特殊字符
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '强';
v = '强';
// 返回结果 // 返回结果
return v; return v;
} }
@@ -172,11 +171,7 @@ export function verifyPasswordStrength(val: string) {
// IP地址 // IP地址
export function verifyIPAddress(val: string) { export function verifyIPAddress(val: string) {
// false: IP地址不正确 // false: IP地址不正确
if ( if (!/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/.test(val))
!/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/.test(
val
)
)
return false; return false;
// true: IP地址正确 // true: IP地址正确
else return true; else return true;

View File

@@ -1,5 +1,5 @@
import { getUseWatermark4Session, getUserInfo4Session } from '@/common/utils/storage.ts'; import { getUseWatermark4Session, getUserInfo4Session } from '@/common/utils/storage';
import { dateFormat2 } from '@/common/utils/date.ts' import { dateFormat2 } from '@/common/utils/date';
// 页面添加水印效果 // 页面添加水印效果
const setWatermark = (str: any) => { const setWatermark = (str: any) => {
@@ -44,17 +44,17 @@ function del() {
const watermark = { const watermark = {
use: () => { use: () => {
setTimeout(() => { setTimeout(() => {
const userinfo = getUserInfo4Session() const userinfo = getUserInfo4Session();
if (userinfo && getUseWatermark4Session()) { if (userinfo && getUseWatermark4Session()) {
set(`${userinfo.username} ${dateFormat2('yyyy-MM-dd HH:mm:ss', new Date())}`) set(`${userinfo.username} ${dateFormat2('yyyy-MM-dd HH:mm:ss', new Date())}`);
} else { } else {
del(); del();
} }
}, 1500) }, 1500);
}, },
// 设置水印 // 设置水印
set: (str: any) => { set: (str: any) => {
set(str) set(str);
}, },
// 删除水印 // 删除水印
del: () => { del: () => {

View File

@@ -1,12 +1,24 @@
<template> <template>
<transition name="el-zoom-in-center"> <transition name="el-zoom-in-center">
<div aria-hidden="true" class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu" role="tooltip" <div
data-popper-placement="bottom" :style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`" :key="Math.random()" aria-hidden="true"
v-show="state.isShow"> class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
role="tooltip"
data-popper-placement="bottom"
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
:key="Math.random()"
v-show="state.isShow"
>
<ul class="el-dropdown-menu"> <ul class="el-dropdown-menu">
<template v-for="(v, k) in state.dropdownList"> <template v-for="(v, k) in state.dropdownList">
<li class="el-dropdown-menu__item" aria-disabled="false" tabindex="-1" :key="k" v-if="!v.affix" <li
@click="onCurrentContextmenuClick(v.contextMenuClickId)"> class="el-dropdown-menu__item"
aria-disabled="false"
tabindex="-1"
:key="k"
v-if="!v.affix"
@click="onCurrentContextmenuClick(v.contextMenuClickId)"
>
<SvgIcon :name="v.icon" /> <SvgIcon :name="v.icon" />
<span>{{ v.txt }}</span> <span>{{ v.txt }}</span>
</li> </li>
@@ -43,9 +55,7 @@ const emit = defineEmits(['currentContextmenuClick']);
// 定义变量内容 // 定义变量内容
const state = reactive({ const state = reactive({
isShow: false, isShow: false,
dropdownList: [ dropdownList: [{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' }],
{ contextMenuClickId: 0, txt: '刷新', affix: false, icon: 'RefreshRight' },
],
item: {} as any, item: {} as any,
arrowLeft: 10, arrowLeft: 10,
}); });
@@ -100,7 +110,7 @@ watch(
watch( watch(
() => props.items, () => props.items,
(x: any) => { (x: any) => {
state.dropdownList = x state.dropdownList = x;
}, },
{ {
deep: true, deep: true,

View File

@@ -0,0 +1,56 @@
<template>
<el-tag v-bind="$attrs" :type="type" :color="color">{{ enumLabel }}</el-tag>
</template>
<script lang="ts" setup>
import { toRefs, watch, reactive, onMounted } from 'vue';
import EnumValue from '@/common/Enum';
const props = defineProps({
enums: {
type: Object, // 需要为EnumValue类型
default: {},
},
value: {
type: Object,
default: {},
},
});
const state = reactive({
type: 'primary',
color: '',
enumLabel: '',
});
const { type, color, enumLabel } = toRefs(state);
// 监听该值是否改变,改变则需要将其枚举值与标签进行调整
watch(
() => props.value,
(newValue: any) => {
convert(newValue);
}
);
onMounted(() => {
convert(props.value);
});
const convert = (value: any) => {
const enumValue = EnumValue.getEnumByValue(Object.values(props.enums) as any, value) as any;
if (!enumValue) {
state.enumLabel = '-';
state.type = 'danger';
state.color = '';
return;
}
state.enumLabel = enumValue?.label || '';
if (enumValue.tag) {
state.color = enumValue.tag.color;
state.type = enumValue.tag.type;
}
};
</script>
<style scoped lang="scss"></style>

View File

@@ -12,10 +12,7 @@
@blur="onIconBlur" @blur="onIconBlur"
> >
<template #prepend> <template #prepend>
<SvgIcon <SvgIcon :name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14" />
:name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix"
class="font14"
/>
</template> </template>
</el-input> </el-input>
<el-popover <el-popover

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="monaco-editor" style="border: 1px solid #ccc;"> <div class="monaco-editor" style="border: 1px solid #ccc">
<div class="monaco-editor-content" ref="monacoTextarea" :style="{ height: height }"></div> <div class="monaco-editor-content" ref="monacoTextarea" :style="{ height: height }"></div>
<el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage"> <el-select v-if="canChangeMode" class="code-mode-select" v-model="languageMode" @change="changeLanguage">
<el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option> <el-option v-for="mode in languageArr" :key="mode.value" :label="mode.label" :value="mode.value"> </el-option>
@@ -12,7 +12,7 @@ import { ref, watch, toRefs, reactive, onMounted, onBeforeUnmount } from 'vue';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker'; import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
import * as monaco from 'monaco-editor'; import * as monaco from 'monaco-editor';
import { editor, languages } from 'monaco-editor'; import { editor, languages } from 'monaco-editor';
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker' import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
// 主题仓库 https://github.com/brijeshb42/monaco-themes // 主题仓库 https://github.com/brijeshb42/monaco-themes
// 主题例子 https://editor.bitwiser.in/ // 主题例子 https://editor.bitwiser.in/
// import Monokai from 'monaco-themes/themes/Monokai.json' // import Monokai from 'monaco-themes/themes/Monokai.json'
@@ -21,7 +21,7 @@ import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
// import bop from 'monaco-themes/themes/Birds of Paradise.json' // import bop from 'monaco-themes/themes/Birds of Paradise.json'
// import krTheme from 'monaco-themes/themes/krTheme.json' // import krTheme from 'monaco-themes/themes/krTheme.json'
// import Dracula from 'monaco-themes/themes/Dracula.json' // import Dracula from 'monaco-themes/themes/Dracula.json'
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json' import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js'; import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
import { ElOption, ElSelect } from 'element-plus'; import { ElOption, ElSelect } from 'element-plus';
@@ -49,10 +49,10 @@ const props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
}) });
//定义事件 //定义事件
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue']);
const languageArr = [ const languageArr = [
{ {
@@ -125,15 +125,13 @@ const options = {
minimap: { minimap: {
enabled: false, // 不要小地图 enabled: false, // 不要小地图
}, },
} };
const state = reactive({ const state = reactive({
languageMode: 'shell', languageMode: 'shell',
}) });
const { const { languageMode } = toRefs(state);
languageMode,
} = toRefs(state)
onMounted(() => { onMounted(() => {
state.languageMode = props.language; state.languageMode = props.language;
@@ -149,19 +147,24 @@ onBeforeUnmount(() => {
if (completionItemProvider) { if (completionItemProvider) {
completionItemProvider.dispose(); completionItemProvider.dispose();
} }
}) });
watch(() => props.modelValue, (newValue: any) => { watch(
() => props.modelValue,
(newValue: any) => {
if (!monacoEditorIns.hasTextFocus()) { if (!monacoEditorIns.hasTextFocus()) {
state.languageMode = props.language; state.languageMode = props.language;
monacoEditorIns?.setValue(newValue); monacoEditorIns?.setValue(newValue);
} }
}) }
);
watch(() => props.language, (newValue: any) => { watch(
() => props.language,
(newValue: any) => {
changeLanguage(newValue); changeLanguage(newValue);
}) }
);
const monacoTextarea: any = ref(null); const monacoTextarea: any = ref(null);
@@ -171,14 +174,14 @@ let completionItemProvider: any = null;
self.MonacoEnvironment = { self.MonacoEnvironment = {
getWorker(_: any, label: string) { getWorker(_: any, label: string) {
if (label === 'json') { if (label === 'json') {
return new JsonWorker() return new JsonWorker();
} }
return new EditorWorker(); return new EditorWorker();
} },
}; };
const initMonacoEditorIns = () => { const initMonacoEditorIns = () => {
console.log('初始化monaco编辑器') console.log('初始化monaco编辑器');
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language // options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
// 初始化一些主题 // 初始化一些主题
monaco.editor.defineTheme('SolarizedLight', SolarizedLight); monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
@@ -190,7 +193,7 @@ const initMonacoEditorIns = () => {
// 监听内容改变,双向绑定 // 监听内容改变,双向绑定
monacoEditorIns.onDidChangeModelContent(() => { monacoEditorIns.onDidChangeModelContent(() => {
emit('update:modelValue', monacoEditorIns.getModel()?.getValue()); emit('update:modelValue', monacoEditorIns.getModel()?.getValue());
}) });
// 动态设置主题 // 动态设置主题
// monaco.editor.setTheme('hc-black'); // monaco.editor.setTheme('hc-black');
@@ -199,25 +202,25 @@ const initMonacoEditorIns = () => {
const changeLanguage = (value: any) => { const changeLanguage = (value: any) => {
console.log('change lan'); console.log('change lan');
// 获取当前的文档模型 // 获取当前的文档模型
let oldModel = monacoEditorIns.getModel() let oldModel = monacoEditorIns.getModel();
if (!oldModel) { if (!oldModel) {
return; return;
} }
// 创建一个新的文档模型 // 创建一个新的文档模型
let newModel = monaco.editor.createModel(oldModel.getValue(), value) let newModel = monaco.editor.createModel(oldModel.getValue(), value);
// 设置成新的 // 设置成新的
monacoEditorIns.setModel(newModel) monacoEditorIns.setModel(newModel);
// 销毁旧的模型 // 销毁旧的模型
if (oldModel) { if (oldModel) {
oldModel.dispose() oldModel.dispose();
} }
registerCompletionItemProvider(); registerCompletionItemProvider();
} };
const setEditorValue = (value: any) => { const setEditorValue = (value: any) => {
monacoEditorIns.getModel()?.setValue(value) monacoEditorIns.getModel()?.setValue(value);
} };
/** /**
* 注册联想补全提示 * 注册联想补全提示
@@ -227,44 +230,43 @@ const registerCompletionItemProvider = () => {
completionItemProvider.dispose(); completionItemProvider.dispose();
} }
if (state.languageMode == 'shell') { if (state.languageMode == 'shell') {
registeShell() registeShell();
}
} }
};
const registeShell = () => { const registeShell = () => {
completionItemProvider = monaco.languages.registerCompletionItemProvider('shell', { completionItemProvider = monaco.languages.registerCompletionItemProvider('shell', {
provideCompletionItems: async () => { provideCompletionItems: async () => {
let suggestions: languages.CompletionItem[] = [] let suggestions: languages.CompletionItem[] = [];
shellLan.keywords.forEach((item: any) => { shellLan.keywords.forEach((item: any) => {
suggestions.push({ suggestions.push({
label: item, label: item,
kind: monaco.languages.CompletionItemKind.Keyword, kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item, insertText: item,
} as any); } as any);
}) });
shellLan.builtins.forEach((item: any) => { shellLan.builtins.forEach((item: any) => {
suggestions.push({ suggestions.push({
label: item, label: item,
kind: monaco.languages.CompletionItemKind.Property, kind: monaco.languages.CompletionItemKind.Property,
insertText: item, insertText: item,
} as any); } as any);
}) });
return { return {
suggestions: suggestions suggestions: suggestions,
}; };
} },
}) });
}; };
const format = () => { const format = () => {
/* /*
触发自动格式化; 触发自动格式化;
*/ */
monacoEditorIns.trigger('', 'editor.action.formatDocument', '') monacoEditorIns.trigger('', 'editor.action.formatDocument', '');
} };
defineExpose({ format })
defineExpose({ format });
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -12,28 +12,55 @@
<div> <div>
<div v-if="props.query.length > 0"> <div v-if="props.query.length > 0">
<el-form :model="props.queryForm" label-width="auto" :size="props.size"> <el-form :model="props.queryForm" label-width="auto" :size="props.size">
<el-row v-for="i in Math.ceil((props.query.length + 1) / (defaultQueryCount + 1))" :key="i" <el-row
v-show="i == 1 || isOpenMoreQuery" :class="i > 1 && isOpenMoreQuery ? 'is-open' : ''"> v-for="i in Math.ceil((props.query.length + 1) / (defaultQueryCount + 1))"
:key="i"
<el-form-item :label="item.label" style="margin-right: 12px; margin-bottom: 0px;" v-show="i == 1 || isOpenMoreQuery"
v-for="item in getRowQueryItem(i)" :key="item.prop"> :class="i > 1 && isOpenMoreQuery ? 'is-open' : ''"
>
<el-form-item
:label="item.label"
style="margin-right: 12px; margin-bottom: 0px"
v-for="item in getRowQueryItem(i)"
:key="item.prop"
>
<!-- 这里只获取指定个数的筛选条件 --> <!-- 这里只获取指定个数的筛选条件 -->
<el-input v-model="queryForm[item.prop]" :placeholder="'输入' + item.label + '关键字'" <el-input
clearable v-if="item.type == 'text'"></el-input> v-model="queryForm[item.prop]"
:placeholder="'输入' + item.label + '关键字'"
clearable
v-if="item.type == 'text'"
></el-input>
<el-select-v2 v-model="queryForm[item.prop]" :options="item.options" clearable <el-select-v2
:placeholder="'选择' + item.label + '关键字'" v-else-if="item.type == 'select'" /> v-model="queryForm[item.prop]"
:options="item.options"
clearable
:placeholder="'选择' + item.label + '关键字'"
v-else-if="item.type == 'select'"
/>
<el-date-picker v-model="queryForm[item.prop]" clearable type="datetimerange" <el-date-picker
format="YYYY-MM-DD hh:mm:ss" value-format="x" range-separator="" v-model="queryForm[item.prop]"
start-placeholder="开始时间" end-placeholder="结束时间" v-else-if="item.type == 'date'" /> clearable
type="datetimerange"
format="YYYY-MM-DD hh:mm:ss"
value-format="x"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
v-else-if="item.type == 'date'"
/>
<template v-else-if="item.slot == 'queryBtns'"> <template v-else-if="item.slot == 'queryBtns'">
<template v-if="props.query?.length > defaultQueryCount"> <template v-if="props.query?.length > defaultQueryCount">
<el-button @click="isOpenMoreQuery = !isOpenMoreQuery" v-if="!isOpenMoreQuery" <el-button
icon="ArrowDownBold" circle></el-button> @click="isOpenMoreQuery = !isOpenMoreQuery"
<el-button @click="isOpenMoreQuery = !isOpenMoreQuery" v-else icon="ArrowUpBold" v-if="!isOpenMoreQuery"
circle></el-button> icon="ArrowDownBold"
circle
></el-button>
<el-button @click="isOpenMoreQuery = !isOpenMoreQuery" v-else icon="ArrowUpBold" circle></el-button>
</template> </template>
<el-button @click="queryData()" type="primary" icon="search" plain>查询</el-button> <el-button @click="queryData()" type="primary" icon="search" plain>查询</el-button>
@@ -42,7 +69,6 @@
<slot :name="item.slot"></slot> <slot :name="item.slot"></slot>
</el-form-item> </el-form-item>
</el-row> </el-row>
</el-form> </el-form>
</div> </div>
@@ -56,8 +82,13 @@
动态表头显示根据表格每条配置项中的show字段来决定改列是否显示或者隐藏 动态表头显示根据表格每条配置项中的show字段来决定改列是否显示或者隐藏
columns 就是我们表格配置的数组对象 columns 就是我们表格配置的数组对象
--> -->
<el-popover placement="bottom" title="表格配置" <el-popover
popper-style="max-height: 550px; overflow: auto; max-width: 450px" width="auto" trigger="click"> placement="bottom"
title="表格配置"
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
width="auto"
trigger="click"
>
<div v-for="(item, index) in props.columns" :key="index"> <div v-for="(item, index) in props.columns" :key="index">
<el-checkbox v-model="item.show" :label="item.label" :true-label="true" :false-label="false" /> <el-checkbox v-model="item.show" :label="item.label" :true-label="true" :false-label="false" />
</div> </div>
@@ -69,42 +100,68 @@
</div> </div>
</div> </div>
<el-table v-bind="$attrs" max-height="700" @selection-change="handleSelectionChange" :data="props.data" <el-table
highlight-current-row v-loading="loadingData" :size="props.size"> v-bind="$attrs"
max-height="700"
@selection-change="handleSelectionChange"
:data="props.data"
highlight-current-row
v-loading="loadingData"
:size="props.size"
>
<el-table-column v-if="props.showSelection" type="selection" width="40" /> <el-table-column v-if="props.showSelection" type="selection" width="40" />
<template v-for="(item, index) in columns"> <template v-for="(item, index) in columns">
<el-table-column :key="index" v-if="item.show" :prop="item.prop" :label="item.label" :fixed="item.fixed" <el-table-column
:align="item.align" :show-overflow-tooltip="item.showOverflowTooltip" :min-width="item.minWidth" :key="index"
:sortable="item.sortable || false" :type="item.type" :width="item.width"> v-if="item.show"
:prop="item.prop"
:label="item.label"
:fixed="item.fixed"
:align="item.align"
:show-overflow-tooltip="item.showOverflowTooltip"
:min-width="item.minWidth"
:sortable="item.sortable || false"
:type="item.type"
:width="item.width"
>
<!-- 插槽预留功能 --> <!-- 插槽预留功能 -->
<template #default="scope" v-if="item.slot"> <template #default="scope" v-if="item.slot">
<slot :name="item.prop" :data="scope.row"></slot> <slot :name="item.prop" :data="scope.row"></slot>
</template> </template>
<template #default="scope" v-else-if="item.type == 'tag'">
<enum-tag :size="props.size" :enums="item.typeParam" :value="scope.row[item.prop]"></enum-tag>
</template>
<template #default="scope" v-else> <template #default="scope" v-else>
<span>{{ item.getValueByData(scope.row) }}</span> <span>{{ item.getValueByData(scope.row) }}</span>
</template> </template>
</el-table-column> </el-table-column>
</template> </template>
</el-table> </el-table>
<el-row style="margin-top: 20px" type="flex" justify="end"> <el-row style="margin-top: 20px" type="flex" justify="end">
<el-pagination :small="props.size == 'small'" @current-change="handlePageChange" <el-pagination
@size-change="handleSizeChange" style="text-align: right" :small="props.size == 'small'"
layout="prev, pager, next, total, sizes, jumper" :total="props.total" @current-change="handlePageChange"
v-model:current-page="state.pageNum" v-model:page-size="state.pageSize" :page-sizes="pageSizes" /> @size-change="handleSizeChange"
style="text-align: right"
layout="prev, pager, next, total, sizes, jumper"
:total="props.total"
v-model:current-page="state.pageNum"
v-model:page-size="state.pageSize"
:page-sizes="pageSizes"
/>
</el-row> </el-row>
</el-card> </el-card>
</div> </div>
</template> </template>
<script lang='ts' setup> <script lang="ts" setup>
import { toRefs, watch, reactive, onMounted } from 'vue'; import { toRefs, watch, reactive, onMounted } from 'vue';
import { TableColumn, TableQuery } from './index'; import { TableColumn, TableQuery } from './index';
import EnumTag from '@/components/enumtag/EnumTag.vue';
const emit = defineEmits(['update:queryForm', 'update:pageNum', 'update:pageSize', 'update:selectionData', 'pageChange']) const emit = defineEmits(['update:queryForm', 'update:pageNum', 'update:pageSize', 'update:selectionData', 'pageChange'])
@@ -283,7 +340,6 @@ const loading = (loading: boolean) => {
} }
defineExpose({ loading }) defineExpose({ loading })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.page-table { .page-table {

View File

@@ -1,8 +1,8 @@
import EnumValue from '@/common/Enum';
import { dateFormat } from '@/common/utils/date'; import { dateFormat } from '@/common/utils/date';
import { getTextWidth } from '@/common/utils/string'; import { getTextWidth } from '@/common/utils/string';
export class TableColumn { export class TableColumn {
/** /**
* 属性字段 * 属性字段
*/ */
@@ -19,7 +19,7 @@ export class TableColumn {
autoWidth: boolean = true; autoWidth: boolean = true;
/** /**
* 自动计算宽度时,累加该值(可能列值会进行转换添加图标等,宽度需要比计算出来的更宽些) * 自动计算宽度时,累加该值(可能列值会进行转换添加图标等,宽度需要比计算出来的更宽些)
*/ */
addWidth: number = 0; addWidth: number = 0;
@@ -37,24 +37,36 @@ export class TableColumn {
sortable: boolean = false; sortable: boolean = false;
/**
* 官方:对应列的类型。 如果设置了selection则显示多选框
* 如果设置了 index 则显示该行的索引(从 1 开始计算);
*
* 新增 tag类型用于枚举值转换后用tag进行展示
*
*/
type: string; type: string;
/**
* 类型展示需要的额外参数如枚举转换的EnumValue值等
*/
typeParam: any;
width: number | string; width: number | string;
fixed: any; fixed: any;
align: string = "left" align: string = 'left';
/** /**
* 指定格式化函数对原始值进行格式化,如时间格式化等 * 指定格式化函数对原始值进行格式化,如时间格式化等
* param1: data, param2: prop * param1: data, param2: prop
*/ */
formatFunc: Function formatFunc: Function;
/** /**
* 是否显示该列 * 是否显示该列
*/ */
show: boolean = true show: boolean = true;
constructor(prop: string, label: string) { constructor(prop: string, label: string) {
this.prop = prop; this.prop = prop;
@@ -74,22 +86,22 @@ export class TableColumn {
} }
static new(prop: string, label: string): TableColumn { static new(prop: string, label: string): TableColumn {
return new TableColumn(prop, label) return new TableColumn(prop, label);
} }
noShowOverflowTooltip(): TableColumn { noShowOverflowTooltip(): TableColumn {
this.showOverflowTooltip = false this.showOverflowTooltip = false;
return this; return this;
} }
setMinWidth(minWidth: number | string): TableColumn { setMinWidth(minWidth: number | string): TableColumn {
this.minWidth = minWidth this.minWidth = minWidth;
this.autoWidth = false; this.autoWidth = false;
return this; return this;
} }
setAddWidth(addWidth: number): TableColumn { setAddWidth(addWidth: number): TableColumn {
this.addWidth = addWidth this.addWidth = addWidth;
return this; return this;
} }
@@ -98,7 +110,18 @@ export class TableColumn {
* @returns this * @returns this
*/ */
alignCenter(): TableColumn { alignCenter(): TableColumn {
this.align = "center"; this.align = 'center';
return this;
}
/**
* 使用标签类型展示该列(用于枚举值友好展示)
* @param param 枚举对象
* @returns this
*/
typeTag(param: any): TableColumn {
this.type = 'tag';
this.typeParam = param;
return this; return this;
} }
@@ -107,7 +130,7 @@ export class TableColumn {
* @returns this * @returns this
*/ */
isSlot(): TableColumn { isSlot(): TableColumn {
this.slot = true this.slot = true;
return this; return this;
} }
@@ -127,22 +150,32 @@ export class TableColumn {
*/ */
isTime(): TableColumn { isTime(): TableColumn {
this.setFormatFunc((data: any, prop: string) => { this.setFormatFunc((data: any, prop: string) => {
return dateFormat(data[prop]) return dateFormat(data[prop]);
}) });
return this;
}
/**
* 标识该列枚举类,需进行枚举值转换
* @returns this
*/
isEnum(enums: any): TableColumn {
this.setFormatFunc((data: any, prop: string) => {
return EnumValue.getLabelByValue(enums, data[prop]);
});
return this; return this;
} }
fixedRight(): TableColumn { fixedRight(): TableColumn {
this.fixed = "right"; this.fixed = 'right';
return this; return this;
} }
fixedLeft(): TableColumn { fixedLeft(): TableColumn {
this.fixed = "left"; this.fixed = 'left';
return this; return this;
} }
/** /**
* 自动计算最小宽度 * 自动计算最小宽度
* @param str 字符串 * @param str 字符串
@@ -151,26 +184,26 @@ export class TableColumn {
* @returns 列宽度 * @returns 列宽度
*/ */
autoCalculateMinWidth = (tableData: any) => { autoCalculateMinWidth = (tableData: any) => {
const prop = this.prop const prop = this.prop;
const label = this.label const label = this.label;
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) { if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return 0; return 0;
} }
let maxWidthText = "" let maxWidthText = '';
let maxWidthValue let maxWidthValue;
// 为了兼容formatFunc格式化回调函数 // 为了兼容formatFunc格式化回调函数
let maxData let maxData;
// 获取该列中最长的数据(内容) // 获取该列中最长的数据(内容)
for (let i = 0; i < tableData.length; i++) { for (let i = 0; i < tableData.length; i++) {
let nowData = tableData[i] let nowData = tableData[i];
let nowValue = nowData[prop] let nowValue = nowData[prop];
if (!nowValue) { if (!nowValue) {
continue; continue;
} }
// 转为字符串比较长度 // 转为字符串比较长度
let nowText = nowValue + ""; let nowText = nowValue + '';
if (nowText.length > maxWidthText.length) { if (nowText.length > maxWidthText.length) {
maxWidthText = nowText; maxWidthText = nowText;
maxWidthValue = nowValue; maxWidthValue = nowValue;
@@ -178,7 +211,7 @@ export class TableColumn {
} }
} }
if (this.formatFunc && maxWidthValue) { if (this.formatFunc && maxWidthValue) {
maxWidthText = this.formatFunc(maxData, prop) + "" maxWidthText = this.formatFunc(maxData, prop) + '';
} }
// 需要加上表格的内间距等,视情况加 // 需要加上表格的内间距等,视情况加
const contentWidth: number = getTextWidth(maxWidthText) + 30; const contentWidth: number = getTextWidth(maxWidthText) + 30;
@@ -191,7 +224,6 @@ export class TableColumn {
} }
export class TableQuery { export class TableQuery {
/** /**
* 属性字段 * 属性字段
*/ */
@@ -217,42 +249,41 @@ export class TableQuery {
*/ */
slot: string; slot: string;
constructor(prop: string, label: string) { constructor(prop: string, label: string) {
this.prop = prop; this.prop = prop;
this.label = label; this.label = label;
} }
static new(prop: string, label: string): TableQuery { static new(prop: string, label: string): TableQuery {
return new TableQuery(prop, label) return new TableQuery(prop, label);
} }
static text(prop: string, label: string): TableQuery { static text(prop: string, label: string): TableQuery {
const tq = new TableQuery(prop, label) const tq = new TableQuery(prop, label);
tq.type = 'text'; tq.type = 'text';
return tq; return tq;
} }
static select(prop: string, label: string): TableQuery { static select(prop: string, label: string): TableQuery {
const tq = new TableQuery(prop, label) const tq = new TableQuery(prop, label);
tq.type = 'select'; tq.type = 'select';
return tq; return tq;
} }
static date(prop: string, label: string): TableQuery { static date(prop: string, label: string): TableQuery {
const tq = new TableQuery(prop, label) const tq = new TableQuery(prop, label);
tq.type = 'date'; tq.type = 'date';
return tq; return tq;
} }
static slot(prop: string, label: string, slotName: string): TableQuery { static slot(prop: string, label: string, slotName: string): TableQuery {
const tq = new TableQuery(prop, label) const tq = new TableQuery(prop, label);
tq.slot = slotName; tq.slot = slotName;
return tq; return tq;
} }
setOptions(options: any): TableQuery { setOptions(options: any): TableQuery {
this.options = options; this.options = options;
return this return this;
} }
} }

View File

@@ -35,7 +35,7 @@ const props = defineProps({
isEle: { isEle: {
type: Boolean, type: Boolean,
default: true, default: true,
} },
}); });
// 在线链接、本地引入地址前缀 // 在线链接、本地引入地址前缀
@@ -48,7 +48,7 @@ const getIconName = computed(() => {
// 用于判断 element plus 自带 svg 图标的显示、隐藏。不存在 空格分隔的icon name即为element plus自带icon // 用于判断 element plus 自带 svg 图标的显示、隐藏。不存在 空格分隔的icon name即为element plus自带icon
const isShowIconSvg = computed(() => { const isShowIconSvg = computed(() => {
const ss = props?.name?.split(" ") const ss = props?.name?.split(' ');
if (!ss) { if (!ss) {
return true; return true;
} }
@@ -56,13 +56,13 @@ const isShowIconSvg = computed(() => {
}); });
const isIconfont = () => { const isIconfont = () => {
return props?.name?.startsWith("iconfont") return props?.name?.startsWith('iconfont');
} };
const getIconfontName = () => { const getIconfontName = () => {
// iconfont icon-xxxx 获取icon-xxx即可 // iconfont icon-xxxx 获取icon-xxx即可
return props?.name?.split(" ")[1] return props?.name?.split(' ')[1];
} };
// 用于判断在线链接、本地引入等图标显示、隐藏 // 用于判断在线链接、本地引入等图标显示、隐藏
const isShowIconImg = computed(() => { const isShowIconImg = computed(() => {

View File

@@ -10,13 +10,13 @@ export function authDirective(app: App) {
mounted(el, binding) { mounted(el, binding) {
if (!hasPerm(binding.value)) { if (!hasPerm(binding.value)) {
parseNoAuth(el, binding); parseNoAuth(el, binding);
}; }
}, },
}); });
// 多个权限验证满足一个则显示v-auths="[xxx,xxx]" // 多个权限验证满足一个则显示v-auths="[xxx,xxx]"
app.directive('auths', { app.directive('auths', {
mounted(el, binding) { mounted(el, binding) {
const value = binding.value const value = binding.value;
let flag = false; let flag = false;
useUserInfo().userInfo.permissions.map((val: any) => { useUserInfo().userInfo.permissions.map((val: any) => {
value.map((v: any) => { value.map((v: any) => {
@@ -33,7 +33,7 @@ export function authDirective(app: App) {
mounted(el, binding) { mounted(el, binding) {
if (!judementSameArr(binding.value, useUserInfo().userInfo.permissions)) { if (!judementSameArr(binding.value, useUserInfo().userInfo.permissions)) {
parseNoAuth(el, binding); parseNoAuth(el, binding);
}; }
}, },
}); });
} }
@@ -55,8 +55,8 @@ const parseNoAuth = (el: any, binding: any) => {
// 移除该元素 // 移除该元素
el.parentNode.removeChild(el); el.parentNode.removeChild(el);
} }
} };
const disableClickFn = (event: any) => { const disableClickFn = (event: any) => {
event && event.stopImmediatePropagation(); event && event.stopImmediatePropagation();
} };

View File

@@ -9,22 +9,19 @@ import { registElSvgIcon } from '@/common/utils/svgIcons';
import ElementPlus from 'element-plus'; import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css'; import 'element-plus/dist/index.css';
import zhCn from 'element-plus/es/locale/lang/zh-cn' import zhCn from 'element-plus/es/locale/lang/zh-cn';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import '@/theme/index.scss'; import '@/theme/index.scss';
import '@/assets/font/font.css' import '@/assets/font/font.css';
import '@/assets/iconfont/iconfont.js' import '@/assets/iconfont/iconfont.js';
const app = createApp(App); const app = createApp(App);
registElSvgIcon(app); registElSvgIcon(app);
directive(app); directive(app);
app.use(pinia) app.use(pinia).use(router).use(ElementPlus, { size: globalComponentSize, locale: zhCn }).mount('#app');
.use(router)
.use(ElementPlus, { size: globalComponentSize, locale: zhCn })
.mount('#app');
// 屏蔽警告信息 // 屏蔽警告信息
app.config.warnHandler = () => null; app.config.warnHandler = () => null;
@@ -32,8 +29,8 @@ app.config.warnHandler = () => null;
app.config.errorHandler = function (err: any, vm, info) { app.config.errorHandler = function (err: any, vm, info) {
// 如果是断言错误,则进行提示即可 // 如果是断言错误,则进行提示即可
if (err.name == 'AssertError') { if (err.name == 'AssertError') {
ElMessage.error(err.message) ElMessage.error(err.message);
} else { } else {
console.error(err, info) console.error(err, info);
}
} }
};

View File

@@ -2,9 +2,9 @@ import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
import { getSession, clearSession } from '@/common/utils/storage'; import { getSession, clearSession } from '@/common/utils/storage';
import { templateResolve } from '@/common/utils/string' import { templateResolve } from '@/common/utils/string';
import { NextLoading } from '@/common/utils/loading'; import { NextLoading } from '@/common/utils/loading';
import { dynamicRoutes, staticRoutes, pathMatch } from './route' import { dynamicRoutes, staticRoutes, pathMatch } from './route';
import openApi from '@/common/openApi'; import openApi from '@/common/openApi';
import sockets from '@/common/sockets'; import sockets from '@/common/sockets';
import pinia from '@/store/index'; import pinia from '@/store/index';
@@ -33,17 +33,17 @@ export function initAllFun() {
const token = getSession('token'); // 获取浏览器缓存 token 值 const token = getSession('token'); // 获取浏览器缓存 token 值
if (!token) { if (!token) {
// 无 token 停止执行下一步 // 无 token 停止执行下一步
return false return false;
} }
useUserInfo().setUserInfo({}); useUserInfo().setUserInfo({});
router.addRoute(pathMatch); // 添加404界面 router.addRoute(pathMatch); // 添加404界面
resetRoute(); // 删除/重置路由 resetRoute(); // 删除/重置路由
// 添加动态路由 // 添加动态路由
setFilterRouteEnd().forEach((route: any) => { setFilterRouteEnd().forEach((route: any) => {
router.addRoute((route as unknown) as RouteRecordRaw); router.addRoute(route as unknown as RouteRecordRaw);
}); });
// 过滤权限菜单 // 过滤权限菜单
useRoutesList().setRoutesList(setFilterMenuFun(dynamicRoutes[0].children, useUserInfo().userInfo.menus)) useRoutesList().setRoutesList(setFilterMenuFun(dynamicRoutes[0].children, useUserInfo().userInfo.menus));
} }
// 后端控制路由:执行路由数据初始化 // 后端控制路由:执行路由数据初始化
@@ -52,7 +52,7 @@ export async function initBackEndControlRoutesFun() {
const token = getSession('token'); // 获取浏览器缓存 token 值 const token = getSession('token'); // 获取浏览器缓存 token 值
if (!token) { if (!token) {
// 无 token 停止执行下一步 // 无 token 停止执行下一步
return false return false;
} }
useUserInfo().setUserInfo({}); useUserInfo().setUserInfo({});
// 获取路由 // 获取路由
@@ -63,59 +63,59 @@ export async function initBackEndControlRoutesFun() {
resetRoute(); // 删除/重置路由 resetRoute(); // 删除/重置路由
// 添加动态路由 // 添加动态路由
formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes)).forEach((route: any) => { formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes)).forEach((route: any) => {
router.addRoute((route as unknown) as RouteRecordRaw); router.addRoute(route as unknown as RouteRecordRaw);
}); });
useRoutesList().setRoutesList(dynamicRoutes[0].children) useRoutesList().setRoutesList(dynamicRoutes[0].children);
} }
// 后端控制路由isRequestRoutes 为 true则开启后端控制路由 // 后端控制路由isRequestRoutes 为 true则开启后端控制路由
export async function getBackEndControlRoutes() { export async function getBackEndControlRoutes() {
try { try {
const menuAndPermission = await openApi.getPermissions.request(); const menuAndPermission = await openApi.getPermissions();
// 赋值权限码,用于控制按钮等 // 赋值权限码,用于控制按钮等
useUserInfo().userInfo.permissions = menuAndPermission.permissions; useUserInfo().userInfo.permissions = menuAndPermission.permissions;
return menuAndPermission.menus; return menuAndPermission.menus;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
return [] return [];
} }
} }
// 后端控制路由,后端返回路由 转换为vue route // 后端控制路由,后端返回路由 转换为vue route
export function backEndRouterConverter(routes: any, parentPath: string = "/") { export function backEndRouterConverter(routes: any, parentPath: string = '/') {
if (!routes) return; if (!routes) return;
return routes.map((item: any) => { return routes.map((item: any) => {
if (!item.meta) { if (!item.meta) {
return item return item;
} }
// 将json字符串的meta转为对象 // 将json字符串的meta转为对象
item.meta = JSON.parse(item.meta) item.meta = JSON.parse(item.meta);
// 将meta.comoponet 解析为route.component // 将meta.comoponet 解析为route.component
if (item.meta.component) { if (item.meta.component) {
item.component = dynamicImport(dynamicViewsModules, item.meta.component) item.component = dynamicImport(dynamicViewsModules, item.meta.component);
delete item.meta['component'] delete item.meta['component'];
} }
// route.path == resource.code // route.path == resource.code
let path = item.code let path = item.code;
// 如果不是以 / 开头,则路径需要拼接父路径 // 如果不是以 / 开头,则路径需要拼接父路径
if (!path.startsWith("/")) { if (!path.startsWith('/')) {
path = parentPath + "/" + path; path = parentPath + '/' + path;
} }
item.path = path item.path = path;
delete item['code'] delete item['code'];
// route.meta.title == resource.name // route.meta.title == resource.name
item.meta.title = item.name item.meta.title = item.name;
delete item['name'] delete item['name'];
// route.name == resource.meta.routeName // route.name == resource.meta.routeName
item.name = item.meta.routeName item.name = item.meta.routeName;
delete item.meta['routeName'] delete item.meta['routeName'];
// route.redirect == resource.meta.redirect // route.redirect == resource.meta.redirect
if (item.meta.redirect) { if (item.meta.redirect) {
item.redirect = item.meta.redirect item.redirect = item.meta.redirect;
delete item.meta['redirect'] delete item.meta['redirect'];
} }
item.children && backEndRouterConverter(item.children, item.path); item.children && backEndRouterConverter(item.children, item.path);
return item; return item;
@@ -178,9 +178,9 @@ export function formatTwoStageRoutes(arr: any) {
// 判断路由code 是否包含当前登录用户menus字段中menus为字符串code数组 // 判断路由code 是否包含当前登录用户menus字段中menus为字符串code数组
export function hasAnth(menus: any, route: any) { export function hasAnth(menus: any, route: any) {
if (route.meta && route.meta.code) { if (route.meta && route.meta.code) {
return menus.includes(route.meta.code) return menus.includes(route.meta.code);
} }
return true return true;
} }
// 递归过滤有权限的路由 // 递归过滤有权限的路由
@@ -190,7 +190,7 @@ export function setFilterMenuFun(routes: any, menus: any) {
const item = { ...route }; const item = { ...route };
if (hasAnth(menus, item)) { if (hasAnth(menus, item)) {
if (item.children) { if (item.children) {
item.children = setFilterMenuFun(item.children, menus) item.children = setFilterMenuFun(item.children, menus);
} }
menu.push(item); menu.push(item);
} }
@@ -206,11 +206,11 @@ export function setFilterRoute(chil: any) {
if (route.meta.code) { if (route.meta.code) {
useUserInfo().userInfo.menus.forEach((m: any) => { useUserInfo().userInfo.menus.forEach((m: any) => {
if (route.meta.code == m) { if (route.meta.code == m) {
filterRoute.push({ ...route }) filterRoute.push({ ...route });
} }
}) });
} else { } else {
filterRoute.push({ ...route }) filterRoute.push({ ...route });
} }
}); });
return filterRoute; return filterRoute;
@@ -253,7 +253,7 @@ router.beforeEach(async (to, from, next) => {
// 如果有标题参数,则再原标题后加上参数来区别 // 如果有标题参数,则再原标题后加上参数来区别
if (to.meta.titleRename) { if (to.meta.titleRename) {
to.meta.title = templateResolve(to.meta.title as string, to.query) to.meta.title = templateResolve(to.meta.title as string, to.query);
} }
const token = getSession('token'); const token = getSession('token');

View File

@@ -1,5 +1,5 @@
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import Layout from '@/views/layout/index.vue' import Layout from '@/views/layout/index.vue';
// 定义动态路由 // 定义动态路由
export const dynamicRoutes = [ export const dynamicRoutes = [
@@ -11,7 +11,7 @@ export const dynamicRoutes = [
meta: { meta: {
isKeepAlive: true, isKeepAlive: true,
}, },
children: [] children: [],
// children: [ // children: [
// { // {
// path: '/home', // path: '/home',

View File

@@ -116,7 +116,6 @@ export const useThemeConfig = defineStore('themeConfig', {
// 编辑器主题 // 编辑器主题
editorTheme: 'vs', editorTheme: 'vs',
/* 后端控制路由 /* 后端控制路由
------------------------------- */ ------------------------------- */
// 是否开启后端控制路由 // 是否开启后端控制路由
@@ -140,4 +139,4 @@ export const useThemeConfig = defineStore('themeConfig', {
this.themeConfig = data.themeConfig; this.themeConfig = data.themeConfig;
}, },
}, },
}) });

View File

@@ -8,12 +8,12 @@ export const useUserInfo = defineStore('userInfo', {
actions: { actions: {
// 设置用户信息 // 设置用户信息
async setUserInfo(data: object) { async setUserInfo(data: object) {
const ui = getSession('userInfo') const ui = getSession('userInfo');
if (ui) { if (ui) {
this.userInfo = ui; this.userInfo = ui;
} else { } else {
this.userInfo = data; this.userInfo = data;
} }
}, },
} },
}) });

View File

@@ -2,30 +2,42 @@
<div> <div>
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" class="login-content-form" size="large"> <el-form ref="loginFormRef" :model="loginForm" :rules="rules" class="login-content-form" size="large">
<el-form-item prop="username"> <el-form-item prop="username">
<el-input type="text" placeholder="请输入用户名" prefix-icon="user" v-model="loginForm.username" clearable <el-input type="text" placeholder="请输入用户名" prefix-icon="user" v-model="loginForm.username" clearable autocomplete="off"> </el-input>
autocomplete="off">
</el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<el-input type="password" placeholder="请输入密码" prefix-icon="lock" v-model="loginForm.password" <el-input
autocomplete="off" @keyup.enter="login" show-password> type="password"
placeholder="请输入密码"
prefix-icon="lock"
v-model="loginForm.password"
autocomplete="off"
@keyup.enter="login"
show-password
>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item v-if="accountLoginSecurity.useCaptcha" prop="captcha"> <el-form-item v-if="accountLoginSecurity.useCaptcha" prop="captcha">
<el-row :gutter="15"> <el-row :gutter="15">
<el-col :span="16"> <el-col :span="16">
<el-input type="text" maxlength="6" placeholder="请输入验证码" prefix-icon="position" <el-input
v-model="loginForm.captcha" clearable autocomplete="off" @keyup.enter="login"></el-input> type="text"
maxlength="6"
placeholder="请输入验证码"
prefix-icon="position"
v-model="loginForm.captcha"
clearable
autocomplete="off"
@keyup.enter="login"
></el-input>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<div class="login-content-code"> <div class="login-content-code">
<img class="login-content-code-img" @click="getCaptcha" width="130px" height="40px" <img class="login-content-code-img" @click="getCaptcha" width="130px" height="40px" :src="captchaImage" style="cursor: pointer" />
:src="captchaImage" style="cursor: pointer" />
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
</el-form-item> </el-form-item>
<span v-if="showLoginFailTips" style="color: #f56c6c;font-size: 12px;"> <span v-if="showLoginFailTips" style="color: #f56c6c; font-size: 12px">
提示登录失败超过{{ accountLoginSecurity.loginFailCount }}次后将被限制{{ accountLoginSecurity.loginFailMin }}分钟内不可再次登录 提示登录失败超过{{ accountLoginSecurity.loginFailCount }}次后将被限制{{ accountLoginSecurity.loginFailMin }}分钟内不可再次登录
</span> </span>
<el-form-item> <el-form-item>
@@ -35,19 +47,21 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-dialog title="修改密码" v-model="changePwdDialog.visible" :close-on-click-modal="false" width="450px" <el-dialog title="修改密码" v-model="changePwdDialog.visible" :close-on-click-modal="false" width="450px" :destroy-on-close="true">
:destroy-on-close="true"> <el-form :model="changePwdDialog.form" :rules="changePwdDialog.rules" ref="changePwdFormRef" label-width="auto">
<el-form :model="changePwdDialog.form" :rules="changePwdDialog.rules" ref="changePwdFormRef" label-width="65px">
<el-form-item prop="username" label="用户名" required> <el-form-item prop="username" label="用户名" required>
<el-input v-model.trim="changePwdDialog.form.username" disabled></el-input> <el-input v-model.trim="changePwdDialog.form.username" disabled></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="oldPassword" label="旧密码" required> <el-form-item prop="oldPassword" label="旧密码" required>
<el-input v-model.trim="changePwdDialog.form.oldPassword" autocomplete="new-password" <el-input v-model.trim="changePwdDialog.form.oldPassword" autocomplete="new-password" type="password"></el-input>
type="password"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="newPassword" label="新密码" required> <el-form-item prop="newPassword" label="新密码" required>
<el-input v-model.trim="changePwdDialog.form.newPassword" placeholder="须为8位以上且包含字⺟⼤⼩写+数字+特殊符号" <el-input
type="password" autocomplete="new-password"></el-input> v-model.trim="changePwdDialog.form.newPassword"
placeholder="须为8位以上且包含字⺟⼤⼩写+数字+特殊符号"
type="password"
autocomplete="new-password"
></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -59,16 +73,28 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog title="OTP校验" v-model="otpDialog.visible" @close="loading.signIn = false" :close-on-click-modal="false" <el-dialog
width="350px" :destroy-on-close="true"> title="OTP校验"
<el-form ref="otpFormRef" :model="otpDialog.form" :rules="otpDialog.rules" @submit.native.prevent label-width="65px"> v-model="otpDialog.visible"
@close="loading.signIn = false"
:close-on-click-modal="false"
width="350px"
:destroy-on-close="true"
>
<el-form ref="otpFormRef" :model="otpDialog.form" :rules="otpDialog.rules" @submit.native.prevent label-width="auto">
<el-form-item v-if="otpDialog.otpUrl" label="二维码"> <el-form-item v-if="otpDialog.otpUrl" label="二维码">
<qrcode-vue :value="otpDialog.otpUrl" :size="200" level="H" /> <qrcode-vue :value="otpDialog.otpUrl" :size="200" level="H" />
</el-form-item> </el-form-item>
<el-form-item prop="code" label="OTP" required> <el-form-item prop="code" label="OTP" required>
<el-input style="width:220px" ref="otpCodeInputRef" v-model.trim="otpDialog.form.code" clearable @keyup.enter="otpVerify" <el-input
placeholder="请输入令牌APP中显示的授权码"></el-input> style="width: 220px"
ref="otpCodeInputRef"
v-model.trim="otpDialog.form.code"
clearable
@keyup.enter="otpVerify"
placeholder="请输入令牌APP中显示的授权码"
></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -93,13 +119,13 @@ import { RsaEncrypt } from '@/common/rsa';
import { getAccountLoginSecurity, useWartermark } from '@/common/sysconfig'; import { getAccountLoginSecurity, useWartermark } from '@/common/sysconfig';
import { letterAvatar } from '@/common/utils/string'; import { letterAvatar } from '@/common/utils/string';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import QrcodeVue from 'qrcode.vue' import QrcodeVue from 'qrcode.vue';
const rules = { const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }], password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }], captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
} };
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -143,15 +169,13 @@ const state = reactive({
}, },
otpDialog: { otpDialog: {
visible: false, visible: false,
otpUrl: "", otpUrl: '',
form: { form: {
code: '', code: '',
otpToken: '', otpToken: '',
}, },
rules: { rules: {
code: [ code: [{ required: true, message: '请输入OTP授权码', trigger: 'blur' }],
{ required: true, message: '请输入OTP授权码', trigger: 'blur' },
],
}, },
}, },
loading: { loading: {
@@ -161,15 +185,7 @@ const state = reactive({
}, },
}); });
const { const { accountLoginSecurity, showLoginFailTips, captchaImage, loginForm, changePwdDialog, otpDialog, loading } = toRefs(state);
accountLoginSecurity,
showLoginFailTips,
captchaImage,
loginForm,
changePwdDialog,
otpDialog,
loading,
} = toRefs(state)
onMounted(async () => { onMounted(async () => {
nextTick(async () => { nextTick(async () => {
@@ -187,7 +203,7 @@ const getCaptcha = async () => {
if (!state.accountLoginSecurity.useCaptcha) { if (!state.accountLoginSecurity.useCaptcha) {
return; return;
} }
let res: any = await openApi.captcha.request(); let res: any = await openApi.captcha();
state.captchaImage = res.base64Captcha; state.captchaImage = res.base64Captcha;
state.loginForm.cid = res.cid; state.loginForm.cid = res.cid;
}; };
@@ -215,14 +231,14 @@ const otpVerify = async () => {
} }
try { try {
state.loading.otpConfirm = true; state.loading.otpConfirm = true;
const accessToken = await openApi.otpVerify.request(state.otpDialog.form); const accessToken = await openApi.otpVerify(state.otpDialog.form);
await signInSuccess(accessToken); await signInSuccess(accessToken);
state.otpDialog.visible = false; state.otpDialog.visible = false;
} finally { } finally {
state.loading.otpConfirm = false; state.loading.otpConfirm = false;
} }
}); });
} };
// 登录 // 登录
const onSignIn = async () => { const onSignIn = async () => {
@@ -232,7 +248,7 @@ const onSignIn = async () => {
try { try {
const loginReq = { ...state.loginForm }; const loginReq = { ...state.loginForm };
loginReq.password = await RsaEncrypt(originPwd); loginReq.password = await RsaEncrypt(originPwd);
loginRes = await openApi.login.request(loginReq); loginRes = await openApi.login(loginReq);
} catch (e: any) { } catch (e: any) {
state.loading.signIn = false; state.loading.signIn = false;
state.loginForm.captcha = ''; state.loginForm.captcha = '';
@@ -274,7 +290,7 @@ const onSignIn = async () => {
} }
state.otpDialog.form.otpToken = token; state.otpDialog.form.otpToken = token;
state.otpDialog.otpUrl = loginRes.otpUrl state.otpDialog.otpUrl = loginRes.otpUrl;
state.otpDialog.visible = true; state.otpDialog.visible = true;
setTimeout(() => { setTimeout(() => {
otpCodeInputRef.value.focus(); otpCodeInputRef.value.focus();
@@ -282,7 +298,7 @@ const onSignIn = async () => {
}; };
// 登录成功后的跳转 // 登录成功后的跳转
const signInSuccess = async (accessToken: string = "") => { const signInSuccess = async (accessToken: string = '') => {
// 存储 token 到浏览器缓存 // 存储 token 到浏览器缓存
setSession('token', accessToken); setSession('token', accessToken);
// 初始化路由 // 初始化路由
@@ -315,7 +331,7 @@ const changePwd = () => {
const changePwdReq: any = { ...form }; const changePwdReq: any = { ...form };
changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword); changePwdReq.oldPassword = await RsaEncrypt(form.oldPassword);
changePwdReq.newPassword = await RsaEncrypt(form.newPassword); changePwdReq.newPassword = await RsaEncrypt(form.newPassword);
await openApi.changePwd.request(changePwdReq); await openApi.changePwd(changePwdReq);
ElMessage.success('密码修改成功, 新密码已填充至登录密码框'); ElMessage.success('密码修改成功, 新密码已填充至登录密码框');
state.loginForm.password = state.changePwdDialog.form.newPassword; state.loginForm.password = state.changePwdDialog.form.newPassword;
state.changePwdDialog.visible = false; state.changePwdDialog.visible = false;

View File

@@ -1,8 +1,7 @@
<template> <template>
<el-form class="login-content-form"> <el-form class="login-content-form">
<el-form-item> <el-form-item>
<el-input type="text" placeholder="请输入手机号" prefix-icon="el-icon-user" v-model="ruleForm.userName" clearable autocomplete="off"> <el-input type="text" placeholder="请输入手机号" prefix-icon="el-icon-user" v-model="ruleForm.userName" clearable autocomplete="off"> </el-input>
</el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-row :gutter="15"> <el-row :gutter="15">

View File

@@ -43,11 +43,7 @@ const state = reactive({
isTabPaneShow: true, isTabPaneShow: true,
}); });
const { const { isTabPaneShow, tabsActiveName } = toRefs(state);
isTabPaneShow,
tabsActiveName,
} = toRefs(state)
// 切换密码、手机登录 // 切换密码、手机登录
const onTabsClick = () => { const onTabsClick = () => {

View File

@@ -1,10 +1,15 @@
<template> <template>
<div style="width: 100%"> <div style="width: 100%">
<el-select @focus="getSshTunnelMachines" @change="change" style="width: 100%" v-model="sshTunnelMachineId" <el-select
@clear="clear" placeholder="请选择SSH隧道机器" clearable> @focus="getSshTunnelMachines"
<el-option v-for="item in sshTunnelMachineList" :key="item.id" :label="`${item.ip}:${item.port} [${item.name}]`" @change="change"
:value="item.id"> style="width: 100%"
</el-option> v-model="sshTunnelMachineId"
@clear="clear"
placeholder="请选择SSH隧道机器"
clearable
>
<el-option v-for="item in sshTunnelMachineList" :key="item.id" :label="`${item.ip}:${item.port} [${item.name}]`" :value="item.id"> </el-option>
</el-select> </el-select>
</div> </div>
</template> </template>
@@ -17,10 +22,10 @@ const props = defineProps({
modelValue: { modelValue: {
type: Number, type: Number,
}, },
}) });
//定义事件 //定义事件
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue']);
const state = reactive({ const state = reactive({
// 单选则为id多选为id数组 // 单选则为id多选为id数组
@@ -28,10 +33,7 @@ const state = reactive({
sshTunnelMachineList: [] as any, sshTunnelMachineList: [] as any,
}); });
const { const { sshTunnelMachineId, sshTunnelMachineList } = toRefs(state);
sshTunnelMachineId,
sshTunnelMachineList,
} = toRefs(state)
onMounted(async () => { onMounted(async () => {
if (!props.modelValue || props.modelValue <= 0) { if (!props.modelValue || props.modelValue <= 0) {
@@ -52,7 +54,7 @@ const getSshTunnelMachines = async () => {
const clear = () => { const clear = () => {
state.sshTunnelMachineId = null; state.sshTunnelMachineId = null;
change(); change();
} };
const change = () => { const change = () => {
emit('update:modelValue', state.sshTunnelMachineId); emit('update:modelValue', state.sshTunnelMachineId);

View File

@@ -1,6 +1,5 @@
<template> <template>
<div <div style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer; vertical-align: middle">
style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer;vertical-align: middle;">
<el-popover @show="showTagInfo" placement="top-start" title="标签信息" :width="300" trigger="hover"> <el-popover @show="showTagInfo" placement="top-start" title="标签信息" :width="300" trigger="hover">
<template #reference> <template #reference>
<el-icon> <el-icon>
@@ -25,20 +24,18 @@ const props = defineProps({
type: [String], type: [String],
required: true, required: true,
}, },
}) });
const state = reactive({ const state = reactive({
tagPath: '', tagPath: '',
tags: [] as any, tags: [] as any,
}) });
const { const { tags } = toRefs(state);
tags,
} = toRefs(state)
onMounted(async () => { onMounted(async () => {
state.tagPath = props.tagPath; state.tagPath = props.tagPath;
}) });
const showTagInfo = async () => { const showTagInfo = async () => {
if (state.tags && state.tags.length > 0) { if (state.tags && state.tags.length > 0) {
@@ -59,10 +56,7 @@ const showTagInfo = async () => {
tagPaths.push(nowTag); tagPaths.push(nowTag);
} }
state.tags = await tagApi.listByQuery.request({ tagPaths: tagPaths.join(',') }); state.tags = await tagApi.listByQuery.request({ tagPaths: tagPaths.join(',') });
} };
</script> </script>
<style lang="scss"> <style lang="scss"></style>
</style>

View File

@@ -1,12 +1,22 @@
<template> <template>
<div> <div>
<el-tree-select v-bind="$attrs" @check="changeTag" style="width: 100%" :data="tags" placeholder="请选择关联标签" <el-tree-select
:render-after-expand="true" :default-expanded-keys="[selectTags]" show-checkbox check-strictly node-key="id" v-bind="$attrs"
@check="changeTag"
style="width: 100%"
:data="tags"
placeholder="请选择关联标签"
:render-after-expand="true"
:default-expanded-keys="[selectTags]"
show-checkbox
check-strictly
node-key="id"
:props="{ :props="{
value: 'id', value: 'id',
label: 'codePath', label: 'codePath',
children: 'children', children: 'children',
}"> }"
>
<template #default="{ data }"> <template #default="{ data }">
<span class="custom-tree-node"> <span class="custom-tree-node">
<span style="font-size: 13px"> <span style="font-size: 13px">
@@ -26,9 +36,9 @@
import { useAttrs, toRefs, reactive, onMounted } from 'vue'; import { useAttrs, toRefs, reactive, onMounted } from 'vue';
import { tagApi } from '../tag/api'; import { tagApi } from '../tag/api';
const attrs = useAttrs() const attrs = useAttrs();
//定义事件 //定义事件
const emit = defineEmits(['changeTag', 'update:tagPath']) const emit = defineEmits(['changeTag', 'update:tagPath']);
const state = reactive({ const state = reactive({
tags: [], tags: [],
@@ -36,10 +46,7 @@ const state = reactive({
selectTags: null as any, selectTags: null as any,
}); });
const { const { tags, selectTags } = toRefs(state);
tags,
selectTags,
} = toRefs(state)
onMounted(async () => { onMounted(async () => {
if (attrs.modelValue) { if (attrs.modelValue) {
@@ -57,6 +64,4 @@ const changeTag = (tag: any, checkInfo: any) => {
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss"></style>
</style>

View File

@@ -4,10 +4,21 @@
<el-col :span="24" class="el-scrollbar flex-auto" style="overflow: auto"> <el-col :span="24" class="el-scrollbar flex-auto" style="overflow: auto">
<el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5" /> <el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5" />
<el-tree ref="treeRef" :style="{ maxHeight: state.height, height: state.height, overflow: 'auto' }" <el-tree
:highlight-current="true" :indent="7" :load="loadNode" :props="treeProps" lazy node-key="key" ref="treeRef"
:expand-on-click-node="true" :filter-node-method="filterNode" @node-click="treeNodeClick" :style="{ maxHeight: state.height, height: state.height, overflow: 'auto' }"
@node-expand="treeNodeClick" @node-contextmenu="nodeContextmenu"> :highlight-current="true"
:indent="7"
:load="loadNode"
:props="treeProps"
lazy
node-key="key"
:expand-on-click-node="true"
:filter-node-method="filterNode"
@node-click="treeNodeClick"
@node-expand="treeNodeClick"
@node-contextmenu="nodeContextmenu"
>
<template #default="{ node, data }"> <template #default="{ node, data }">
<span> <span>
<span v-if="data.type == TagTreeNode.TagPath"> <span v-if="data.type == TagTreeNode.TagPath">
@@ -24,8 +35,7 @@
</el-tree> </el-tree>
</el-col> </el-col>
</el-row> </el-row>
<contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef" <contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
@currentContextmenuClick="onCurrentContextmenuClick" />
</div> </div>
</template> </template>
@@ -38,7 +48,7 @@ import Contextmenu from '@/components/contextmenu/index.vue';
const props = defineProps({ const props = defineProps({
height: { height: {
type: [Number, String], type: [Number, String],
default: 0 default: 0,
}, },
load: { load: {
type: Function, type: Function,
@@ -47,17 +57,17 @@ const props = defineProps({
loadContextmenuItems: { loadContextmenuItems: {
type: Function, type: Function,
required: false, required: false,
} },
}) });
const treeProps = { const treeProps = {
label: 'name', label: 'name',
children: 'zones', children: 'zones',
isLeaf: 'isLeaf', isLeaf: 'isLeaf',
} };
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']) const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
const treeRef: any = ref(null) const treeRef: any = ref(null);
const contextmenuRef = ref(); const contextmenuRef = ref();
const state = reactive({ const state = reactive({
@@ -69,8 +79,8 @@ const state = reactive({
}, },
contextmenuItems: [], contextmenuItems: [],
opend: {}, opend: {},
}) });
const { filterText } = toRefs(state) const { filterText } = toRefs(state);
onMounted(async () => { onMounted(async () => {
if (!props.height) { if (!props.height) {
@@ -78,16 +88,16 @@ onMounted(async () => {
} else { } else {
state.height = props.height; state.height = props.height;
} }
}) });
watch(filterText, (val) => { watch(filterText, (val) => {
treeRef.value?.filter(val) treeRef.value?.filter(val);
}) });
const filterNode = (value: string, data: any) => { const filterNode = (value: string, data: any) => {
if (!value) return true if (!value) return true;
return data.label.includes(value) return data.label.includes(value);
} };
/** /**
* 加载树节点 * 加载树节点
@@ -98,20 +108,20 @@ const loadNode = async (node: any, resolve: any) => {
if (typeof resolve !== 'function') { if (typeof resolve !== 'function') {
return; return;
} }
let nodes = [] let nodes = [];
try { try {
nodes = await props.load(node) nodes = await props.load(node);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
} }
return resolve(nodes) return resolve(nodes);
}; };
const treeNodeClick = (data: any) => { const treeNodeClick = (data: any) => {
emit('nodeClick', data); emit('nodeClick', data);
// 关闭可能存在的右击菜单 // 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu(); contextmenuRef.value.closeContextmenu();
} };
// 树节点右击事件 // 树节点右击事件
const nodeContextmenu = (event: any, data: any) => { const nodeContextmenu = (event: any, data: any) => {
@@ -119,7 +129,7 @@ const nodeContextmenu = (event: any, data: any) => {
return; return;
} }
// 加载当前节点是否需要显示右击菜单 // 加载当前节点是否需要显示右击菜单
const items = props.loadContextmenuItems(data) const items = props.loadContextmenuItems(data);
if (!items || items.length == 0) { if (!items || items.length == 0) {
return; return;
} }
@@ -128,17 +138,17 @@ const nodeContextmenu = (event: any, data: any) => {
state.dropdown.x = clientX; state.dropdown.x = clientX;
state.dropdown.y = clientY; state.dropdown.y = clientY;
contextmenuRef.value.openContextmenu(data); contextmenuRef.value.openContextmenu(data);
} };
const onCurrentContextmenuClick = (clickData: any) => { const onCurrentContextmenuClick = (clickData: any) => {
emit('currentContextmenuClick', clickData); emit('currentContextmenuClick', clickData);
} };
const reloadNode = (nodeKey: any) => { const reloadNode = (nodeKey: any) => {
let node = getNode(nodeKey); let node = getNode(nodeKey);
node.loaded = false; node.loaded = false;
node.expand(); node.expand();
} };
const getNode = (nodeKey: any) => { const getNode = (nodeKey: any) => {
let node = treeRef.value.getNode(nodeKey); let node = treeRef.value.getNode(nodeKey);
@@ -146,11 +156,11 @@ const getNode = (nodeKey: any) => {
throw new Error('未找到节点: ' + nodeKey); throw new Error('未找到节点: ' + nodeKey);
} }
return node; return node;
} };
defineExpose({ defineExpose({
reloadNode, reloadNode,
}) });
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -2,17 +2,17 @@ export class TagTreeNode {
/** /**
* 节点id * 节点id
*/ */
key: any key: any;
/** /**
* 节点名称 * 节点名称
*/ */
label: string label: string;
/** /**
* 树节点类型 * 树节点类型
*/ */
type: any type: any;
isLeaf: boolean = false; isLeaf: boolean = false;

View File

@@ -16,17 +16,19 @@
<el-col :span="12"> <el-col :span="12">
<el-form-item prop="characterSet" label="charset"> <el-form-item prop="characterSet" label="charset">
<el-select filterable style="width: 80%" v-model="tableData.characterSet" size="small"> <el-select filterable style="width: 80%" v-model="tableData.characterSet" size="small">
<el-option v-for="item in characterSetNameList" :key="item" :label="item" :value="item"> <el-option v-for="item in characterSetNameList" :key="item" :label="item" :value="item"> </el-option>
</el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item prop="characterSet" label="collation"> <el-form-item prop="characterSet" label="collation">
<el-select filterable style="width: 80%" v-model="tableData.collation" size="small"> <el-select filterable style="width: 80%" v-model="tableData.collation" size="small">
<el-option v-for="item in collationNameList" :key="item" <el-option
v-for="item in collationNameList"
:key="item"
:label="tableData.characterSet + '_' + item" :label="tableData.characterSet + '_' + item"
:value="tableData.characterSet + '_' + item"> :value="tableData.characterSet + '_' + item"
>
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -36,38 +38,35 @@
<el-tabs v-model="activeName"> <el-tabs v-model="activeName">
<el-tab-pane label="字段" name="1"> <el-tab-pane label="字段" name="1">
<el-table :data="tableData.fields.res" :max-height="tableData.height"> <el-table :data="tableData.fields.res" :max-height="tableData.height">
<el-table-column :prop="item.prop" :label="item.label" <el-table-column :prop="item.prop" :label="item.label" v-for="item in tableData.fields.colNames" :key="item.prop">
v-for="item in tableData.fields.colNames" :key="item.prop">
<template #default="scope"> <template #default="scope">
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name"> <el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name"> </el-input>
</el-input>
<el-select v-if="item.prop === 'type'" filterable size="small" <el-select v-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
v-model="scope.row.type"> <el-option v-for="typeValue in columnTypeList" :key="typeValue" :value="typeValue">{{ typeValue }}</el-option>
<el-option v-for="typeValue in columnTypeList" :key="typeValue"
:value="typeValue">{{ typeValue }}</el-option>
</el-select> </el-select>
<el-input v-if="item.prop === 'value'" size="small" v-model="scope.row.value"> <el-input v-if="item.prop === 'value'" size="small" v-model="scope.row.value"> </el-input>
</el-input>
<el-input v-if="item.prop === 'length'" size="small" v-model="scope.row.length"> <el-input v-if="item.prop === 'length'" size="small" v-model="scope.row.length"> </el-input>
</el-input>
<el-checkbox v-if="item.prop === 'notNull'" size="small" <el-checkbox v-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull"> </el-checkbox>
v-model="scope.row.notNull"> </el-checkbox>
<el-checkbox v-if="item.prop === 'pri'" size="small" v-model="scope.row.pri"> <el-checkbox v-if="item.prop === 'pri'" size="small" v-model="scope.row.pri"> </el-checkbox>
</el-checkbox>
<el-checkbox v-if="item.prop === 'auto_increment'" size="small" <el-checkbox v-if="item.prop === 'auto_increment'" size="small" v-model="scope.row.auto_increment"> </el-checkbox>
v-model="scope.row.auto_increment"> </el-checkbox>
<el-input v-if="item.prop === 'remark'" size="small" v-model="scope.row.remark"> <el-input v-if="item.prop === 'remark'" size="small" v-model="scope.row.remark"> </el-input>
</el-input>
<el-link v-if="item.prop === 'action'" type="danger" plain size="small" <el-link
:underline="false" @click.prevent="deleteRow(scope.$index)">删除</el-link> v-if="item.prop === 'action'"
type="danger"
plain
size="small"
:underline="false"
@click.prevent="deleteRow(scope.$index)"
>删除</el-link
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -78,19 +77,22 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="索引" name="2"> <el-tab-pane label="索引" name="2">
<el-table :data="tableData.indexs.res" :max-height="tableData.height"> <el-table :data="tableData.indexs.res" :max-height="tableData.height">
<el-table-column :prop="item.prop" :label="item.label" <el-table-column :prop="item.prop" :label="item.label" v-for="item in tableData.indexs.colNames" :key="item.prop">
v-for="item in tableData.indexs.colNames" :key="item.prop">
<template #default="scope"> <template #default="scope">
<el-input v-if="item.prop === 'indexName'" size="small" disabled v-model="scope.row.indexName"></el-input>
<el-input v-if="item.prop === 'indexName'" size="small" disabled <el-select
v-model="scope.row.indexName"></el-input> v-if="item.prop === 'columnNames'"
v-model="scope.row.columnNames"
<el-select v-if="item.prop === 'columnNames'" v-model="scope.row.columnNames" multiple
multiple collapse-tags collapse-tags-tooltip filterable placeholder="请选择字段" collapse-tags
collapse-tags-tooltip
filterable
placeholder="请选择字段"
@change="indexChanges(scope.row)" @change="indexChanges(scope.row)"
style="width: 100%"> style="width: 100%"
<el-option v-for="cl in tableData.indexs.columns" :key="cl.name" >
:label="cl.name" :value="cl.name"> <el-option v-for="cl in tableData.indexs.columns" :key="cl.name" :label="cl.name" :value="cl.name">
{{ cl.name + ' - ' + (cl.remark || '') }} {{ cl.name + ' - ' + (cl.remark || '') }}
</el-option> </el-option>
</el-select> </el-select>
@@ -98,17 +100,21 @@
<el-checkbox v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)"> <el-checkbox v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)">
</el-checkbox> </el-checkbox>
<el-select v-if="item.prop === 'indexType'" filterable size="small" <el-select v-if="item.prop === 'indexType'" filterable size="small" v-model="scope.row.indexType">
v-model="scope.row.indexType"> <el-option v-for="typeValue in indexTypeList" :key="typeValue" :value="typeValue">{{ typeValue }}</el-option>
<el-option v-for="typeValue in indexTypeList" :key="typeValue"
:value="typeValue">{{ typeValue }}</el-option>
</el-select> </el-select>
<el-input v-if="item.prop === 'indexComment'" size="small" <el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
v-model="scope.row.indexComment"> </el-input>
<el-link v-if="item.prop === 'action'" type="danger" plain size="small" <el-link
:underline="false" @click.prevent="deleteIndex(scope.$index)">删除</el-link> v-if="item.prop === 'action'"
type="danger"
plain
size="small"
:underline="false"
@click.prevent="deleteIndex(scope.$index)"
>删除</el-link
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -117,7 +123,6 @@
<el-button @click="addIndex()" link type="primary" icon="plus">添加索引</el-button> <el-button @click="addIndex()" link type="primary" icon="plus">添加索引</el-button>
</el-row> </el-row>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -127,7 +132,6 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, toRefs, reactive, ref } from 'vue'; import { watch, toRefs, reactive, ref } from 'vue';
import { TYPE_LIST, CHARACTER_SET_NAME_LIST, COLLATION_SUFFIX_LIST } from './service'; import { TYPE_LIST, CHARACTER_SET_NAME_LIST, COLLATION_SUFFIX_LIST } from './service';
@@ -149,11 +153,11 @@ const props = defineProps({
}, },
db: { db: {
type: String, type: String,
} },
}) });
//定义事件 //定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']) const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
const formRef: any = ref(); const formRef: any = ref();
const state = reactive({ const state = reactive({
@@ -260,20 +264,11 @@ const state = reactive({
collation: 'utf8mb4_general_ci', collation: 'utf8mb4_general_ci',
tableName: '', tableName: '',
tableComment: '', tableComment: '',
height: 550 height: 550,
}, },
}); });
const { const { dialogVisible, btnloading, activeName, columnTypeList, indexTypeList, characterSetNameList, collationNameList, tableData } = toRefs(state);
dialogVisible,
btnloading,
activeName,
columnTypeList,
indexTypeList,
characterSetNameList,
collationNameList,
tableData,
} = toRefs(state)
watch(props, async (newValue) => { watch(props, async (newValue) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
@@ -315,7 +310,7 @@ const addDefaultRows = () => {
{ name: 'create_time', type: 'datetime', length: '', value: 'CURRENT_TIMESTAMP', notNull: true, pri: false, auto_increment: false, remark: '创建时间' }, { name: 'create_time', type: 'datetime', length: '', value: 'CURRENT_TIMESTAMP', notNull: true, pri: false, auto_increment: false, remark: '创建时间' },
{ name: 'updator_id', type: 'bigint', length: '20', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' }, { name: 'updator_id', type: 'bigint', length: '20', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
{ name: 'updator', type: 'varchar', length: '100', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人姓名' }, { name: 'updator', type: 'varchar', length: '100', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人姓名' },
{ name: 'update_time', type: 'datetime', length: '', value: 'CURRENT_TIMESTAMP', notNull: true, pri: false, auto_increment: false, remark: '修改时间' }, { name: 'update_time', type: 'datetime', length: '', value: 'CURRENT_TIMESTAMP', notNull: true, pri: false, auto_increment: false, remark: '修改时间' }
); );
}; };
@@ -350,71 +345,75 @@ const submit = async () => {
* @param nowArr 修改后的对象数组 * @param nowArr 修改后的对象数组
* @param key 标志对象唯一属性 * @param key 标志对象唯一属性
*/ */
const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { del: any[], add: any[], upd: any[] } => { const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { del: any[]; add: any[]; upd: any[] } => {
let data = { let data = {
del: [] as object[], // 删除的数据 del: [] as object[], // 删除的数据
add: [] as object[], // 新增的数据 add: [] as object[], // 新增的数据
upd: [] as object[] // 修改的数据 upd: [] as object[], // 修改的数据
} };
// 旧数据为空 // 旧数据为空
if (oldArr && Array.isArray(oldArr) && oldArr.length === 0 if (oldArr && Array.isArray(oldArr) && oldArr.length === 0 && nowArr && Array.isArray(nowArr) && nowArr.length > 0) {
&& nowArr && Array.isArray(nowArr) && nowArr.length > 0) {
data.add = nowArr; data.add = nowArr;
return data; return data;
} }
// 新数据为空 // 新数据为空
if (nowArr && Array.isArray(nowArr) && nowArr.length === 0 if (nowArr && Array.isArray(nowArr) && nowArr.length === 0 && oldArr && Array.isArray(oldArr) && oldArr.length > 0) {
&& oldArr && Array.isArray(oldArr) && oldArr.length > 0) {
data.del = oldArr; data.del = oldArr;
return data; return data;
} }
let oldMap = {}, newMap = {}; let oldMap = {},
oldArr.forEach(a => oldMap[a[key]] = a) newMap = {};
oldArr.forEach((a) => (oldMap[a[key]] = a));
nowArr.forEach(a => { nowArr.forEach((a) => {
let k = a[key] let k = a[key];
newMap[k] = a; newMap[k] = a;
if (!oldMap.hasOwnProperty(k)) {// 新增 if (!oldMap.hasOwnProperty(k)) {
data.add.push(a) // 新增
data.add.push(a);
} }
}) });
oldArr.forEach(a => { oldArr.forEach((a) => {
let k = a[key]; let k = a[key];
let newData = newMap[k]; let newData = newMap[k];
if (!newData) { // 删除 if (!newData) {
data.del.push(a) // 删除
} else { // 判断每个字段是否相等,否则为修改 data.del.push(a);
} else {
// 判断每个字段是否相等,否则为修改
for (let f in a) { for (let f in a) {
let oldV = a[f] let oldV = a[f];
let newV = newData[f] let newV = newData[f];
if (oldV.toString() !== newV.toString()) { if (oldV.toString() !== newV.toString()) {
data.upd.push(newData) data.upd.push(newData);
break; break;
} }
} }
} }
}) });
return data; return data;
} };
const genSql = () => { const genSql = () => {
const genColumnBasicSql = (cl: any) => { const genColumnBasicSql = (cl: any) => {
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : '\'' + cl.value + '\'') : ''; let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : "'" + cl.value + "'") : '';
let defVal = `${val ? ('DEFAULT ' + val) : ''}`; let defVal = `${val ? 'DEFAULT ' + val : ''}`;
let length = cl.length ? `(${cl.length})` : ''; let length = cl.length ? `(${cl.length})` : '';
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '' let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${cl.auto_increment ? 'AUTO_INCREMENT' : ''} ${defVal} ${onUpdate} comment '${cl.remark || ''}' ` return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
} cl.auto_increment ? 'AUTO_INCREMENT' : ''
} ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
};
let data = state.tableData; let data = state.tableData;
// 创建表 // 创建表
if (!props.data?.edit) { if (!props.data?.edit) {
if (state.activeName === '1') {// 创建表结构 if (state.activeName === '1') {
// 创建表结构
let primary_key = ''; let primary_key = '';
let fields: string[] = []; let fields: string[] = [];
data.fields.res.forEach((item) => { data.fields.res.forEach((item) => {
@@ -428,45 +427,49 @@ const genSql = () => {
( ${fields.join(',')} ( ${fields.join(',')}
${primary_key ? `, PRIMARY KEY (${primary_key.slice(0, -1)})` : ''} ${primary_key ? `, PRIMARY KEY (${primary_key.slice(0, -1)})` : ''}
) ENGINE=InnoDB DEFAULT CHARSET=${data.characterSet} COLLATE =${data.collation} COMMENT='${data.tableComment}';`; ) ENGINE=InnoDB DEFAULT CHARSET=${data.characterSet} COLLATE =${data.collation} COMMENT='${data.tableComment}';`;
} else if (state.activeName === '2' && data.indexs.res.length > 0) {
} else if (state.activeName === '2' && data.indexs.res.length > 0) { // 创建索引 // 创建索引
let sql = `ALTER TABLE ${data.tableName}`; let sql = `ALTER TABLE ${data.tableName}`;
state.tableData.indexs.res.forEach(a => { state.tableData.indexs.res.forEach((a) => {
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${a.indexComment}',`; sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${a.indexComment}',`;
}) });
return sql.substring(0, sql.length - 1) + ';' return sql.substring(0, sql.length - 1) + ';';
} }
} else { // 修改 } else {
let addSql = '', updSql = '', delSql = ''; // 修改
if (state.activeName === '1') {// 修改列 let addSql = '',
let changeData = filterChangedData(oldData.fields, state.tableData.fields.res, 'name') updSql = '',
delSql = '';
if (state.activeName === '1') {
// 修改列
let changeData = filterChangedData(oldData.fields, state.tableData.fields.res, 'name');
if (changeData.add.length > 0) { if (changeData.add.length > 0) {
addSql = `ALTER TABLE ${data.tableName}` addSql = `ALTER TABLE ${data.tableName}`;
changeData.add.forEach(a => { changeData.add.forEach((a) => {
addSql += ` ADD ${genColumnBasicSql(a)},` addSql += ` ADD ${genColumnBasicSql(a)},`;
}) });
addSql = addSql.substring(0, addSql.length - 1) addSql = addSql.substring(0, addSql.length - 1);
addSql += ';' addSql += ';';
} }
if (changeData.upd.length > 0) { if (changeData.upd.length > 0) {
updSql = `ALTER TABLE ${data.tableName}`; updSql = `ALTER TABLE ${data.tableName}`;
changeData.upd.forEach(a => { changeData.upd.forEach((a) => {
updSql += ` MODIFY ${genColumnBasicSql(a)},` updSql += ` MODIFY ${genColumnBasicSql(a)},`;
}) });
updSql = updSql.substring(0, updSql.length - 1) updSql = updSql.substring(0, updSql.length - 1);
updSql += ';' updSql += ';';
} }
if (changeData.del.length > 0) { if (changeData.del.length > 0) {
changeData.del.forEach(a => { changeData.del.forEach((a) => {
delSql += ` ALTER TABLE ${data.tableName} DROP COLUMN ${a.name}; ` delSql += ` ALTER TABLE ${data.tableName} DROP COLUMN ${a.name}; `;
}) });
} }
return addSql + updSql + delSql; return addSql + updSql + delSql;
} else if (state.activeName === '2') {
} else if (state.activeName === '2') { // 修改索引 // 修改索引
let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName') let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName');
// 搜集修改和删除的索引添加到drop index xx // 搜集修改和删除的索引添加到drop index xx
// 收集新增和修改的索引添加到ADD xx // 收集新增和修改的索引添加到ADD xx
// ALTER TABLE `test1` // ALTER TABLE `test1`
@@ -479,41 +482,43 @@ const genSql = () => {
let addIndexs: any[] = []; let addIndexs: any[] = [];
if (changeData.upd.length > 0) { if (changeData.upd.length > 0) {
changeData.upd.forEach(a => { changeData.upd.forEach((a) => {
dropIndexNames.push(a.indexName) dropIndexNames.push(a.indexName);
addIndexs.push(a) addIndexs.push(a);
}) });
} }
if (changeData.del.length > 0) { if (changeData.del.length > 0) {
changeData.del.forEach(a => { changeData.del.forEach((a) => {
dropIndexNames.push(a.indexName) dropIndexNames.push(a.indexName);
}) });
} }
if (changeData.add.length > 0) { if (changeData.add.length > 0) {
changeData.add.forEach(a => { changeData.add.forEach((a) => {
addIndexs.push(a) addIndexs.push(a);
}) });
} }
if (dropIndexNames.length > 0 || addIndexs.length > 0) { if (dropIndexNames.length > 0 || addIndexs.length > 0) {
let sql = `ALTER TABLE ${data.tableName} `; let sql = `ALTER TABLE ${data.tableName} `;
if (dropIndexNames.length > 0) { if (dropIndexNames.length > 0) {
dropIndexNames.forEach(a => { dropIndexNames.forEach((a) => {
sql += `DROP INDEX ${a},` sql += `DROP INDEX ${a},`;
}) });
sql = sql.substring(0, sql.length - 1) sql = sql.substring(0, sql.length - 1);
} }
if (addIndexs.length > 0) { if (addIndexs.length > 0) {
if (dropIndexNames.length > 0) { if (dropIndexNames.length > 0) {
sql += ',' sql += ',';
} }
addIndexs.forEach(a => { addIndexs.forEach((a) => {
sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${a.indexComment}',`; sql += ` ADD ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')}) USING ${a.indexType} COMMENT '${
}) a.indexComment
sql = sql.substring(0, sql.length - 1) }',`;
});
sql = sql.substring(0, sql.length - 1);
} }
return sql; return sql;
} }
@@ -522,10 +527,10 @@ const genSql = () => {
}; };
const reset = () => { const reset = () => {
state.activeName = '1' state.activeName = '1';
formRef.value.resetFields() formRef.value.resetFields();
state.tableData.tableName = '' state.tableData.tableName = '';
state.tableData.tableComment = '' state.tableData.tableComment = '';
state.tableData.fields.res = [ state.tableData.fields.res = [
{ {
name: '', name: '',
@@ -538,46 +543,50 @@ const reset = () => {
remark: '', remark: '',
}, },
]; ];
state.tableData.indexs.res = [{ state.tableData.indexs.res = [
{
indexName: '', indexName: '',
columnNames: [], columnNames: [],
unique: false, unique: false,
indexType: 'BTREE', indexType: 'BTREE',
indexComment: '', indexComment: '',
},] },
];
}; };
const indexChanges = (row: any) => { const indexChanges = (row: any) => {
let name = ''; let name = '';
if (row.columnNames && row.columnNames.length > 0) { if (row.columnNames && row.columnNames.length > 0) {
for (const column of row.columnNames) { for (const column of row.columnNames) {
name += column.replace('_','').toLowerCase() + '_' name += column.replace('_', '').toLowerCase() + '_';
} }
name = name.substring(0, name.length-1) name = name.substring(0, name.length - 1);
} else { } else {
return; return;
} }
let prefix = row.unique ? 'udx_' : 'idx_'; let prefix = row.unique ? 'udx_' : 'idx_';
row.indexName = prefix + name; row.indexName = prefix + name;
} };
const oldData = { indexs: [] as any[], fields: [] as any[] } const oldData = { indexs: [] as any[], fields: [] as any[] };
watch(() => props.data, (newValue: any) => { watch(
() => props.data,
(newValue: any) => {
const { row, indexs, columns } = newValue; const { row, indexs, columns } = newValue;
// 回显表名表注释 // 回显表名表注释
state.tableData.tableName = row.tableName state.tableData.tableName = row.tableName;
state.tableData.tableComment = row.tableComment state.tableData.tableComment = row.tableComment;
// 回显列 // 回显列
if (columns && Array.isArray(columns) && columns.length > 0) { if (columns && Array.isArray(columns) && columns.length > 0) {
oldData.fields = []; oldData.fields = [];
state.tableData.fields.res = []; state.tableData.fields.res = [];
// 索引列下拉选 // 索引列下拉选
state.tableData.indexs.columns = []; state.tableData.indexs.columns = [];
columns.forEach(a => { columns.forEach((a) => {
let typeObj = a.columnType.replace(')', '').split('(') let typeObj = a.columnType.replace(')', '').split('(');
let type = typeObj[0]; let type = typeObj[0];
let length = typeObj.length > 1 && typeObj[1] || ''; let length = (typeObj.length > 1 && typeObj[1]) || '';
let data = { let data = {
name: a.columnName, name: a.columnName,
type, type,
@@ -588,29 +597,31 @@ watch(() => props.data, (newValue: any) => {
auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/, auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/,
remark: a.columnComment, remark: a.columnComment,
}; };
state.tableData.fields.res.push(data) state.tableData.fields.res.push(data);
oldData.fields.push(JSON.parse(JSON.stringify(data))) oldData.fields.push(JSON.parse(JSON.stringify(data)));
// 索引字段下拉选项 // 索引字段下拉选项
state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment }) state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
}) });
} }
// 回显索引 // 回显索引
if (indexs && Array.isArray(indexs) && indexs.length > 0) { if (indexs && Array.isArray(indexs) && indexs.length > 0) {
oldData.indexs = []; oldData.indexs = [];
state.tableData.indexs.res = []; state.tableData.indexs.res = [];
// 索引过滤掉主键 // 索引过滤掉主键
indexs.filter(a => a.indexName !== "PRIMARY").forEach(a => { indexs
.filter((a) => a.indexName !== 'PRIMARY')
.forEach((a) => {
let data = { let data = {
indexName: a.indexName, indexName: a.indexName,
columnNames: a.columnName?.split(','), columnNames: a.columnName?.split(','),
unique: a.nonUnique === 0 || false, unique: a.nonUnique === 0 || false,
indexType: a.indexType, indexType: a.indexType,
indexComment: a.indexComment, indexComment: a.indexComment,
};
state.tableData.indexs.res.push(data);
oldData.indexs.push(JSON.parse(JSON.stringify(data)));
});
} }
state.tableData.indexs.res.push(data)
oldData.indexs.push(JSON.parse(JSON.stringify(data)))
})
} }
}) );
</script> </script>

View File

@@ -1,7 +1,6 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
:destroy-on-close="true" width="38%">
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto"> <el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName"> <el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic"> <el-tab-pane label="基础信息" name="basic">
@@ -20,8 +19,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 :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" <el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
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">
@@ -32,14 +30,17 @@
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input> <el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password" label="密码:"> <el-form-item prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码,修改操作可不填" <el-input
autocomplete="new-password"> type="password"
show-password
v-model.trim="form.password"
placeholder="请输入密码,修改操作可不填"
autocomplete="new-password"
>
<template v-if="form.id && form.id != 0" #suffix> <template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" <el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
:content="pwd">
<template #reference> <template #reference>
<el-link @click="getDbPwd" :underline="false" type="primary" class="mr5">原密码 <el-link @click="getDbPwd" :underline="false" type="primary" class="mr5">原密码 </el-link>
</el-link>
</template> </template>
</el-popover> </el-popover>
</template> </template>
@@ -47,9 +48,18 @@
</el-form-item> </el-form-item>
<el-form-item prop="database" label="数据库名:" required> <el-form-item prop="database" label="数据库名:" required>
<el-col :span="19"> <el-col :span="19">
<el-select @change="changeDatabase" v-model="databaseList" multiple clearable collapse-tags <el-select
collapse-tags-tooltip filterable allow-create placeholder="请确保数据库实例信息填写完整后获取库名" @change="changeDatabase"
style="width: 100%"> v-model="databaseList"
multiple
clearable
collapse-tags
collapse-tags-tooltip
filterable
allow-create
placeholder="请确保数据库实例信息填写完整后获取库名"
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>
@@ -70,13 +80,18 @@
<el-form-item prop="params" label="连接参数:"> <el-form-item prop="params" label="连接参数:">
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2"> <el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
<template #suffix> <template #suffix>
<el-link target="_blank" href="https://github.com/go-sql-driver/mysql#parameters" <el-link
:underline="false" type="primary" class="mr5">参数参考</el-link> target="_blank"
href="https://github.com/go-sql-driver/mysql#parameters"
:underline="false"
type="primary"
class="mr5"
>参数参考</el-link
>
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:"> <el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" /> <ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item> </el-form-item>
@@ -113,10 +128,10 @@ const props = defineProps({
title: { title: {
type: String, type: String,
}, },
}) });
//定义事件 //定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']) const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const rules = { const rules = {
tagId: [ tagId: [
@@ -161,7 +176,7 @@ const rules = {
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
} };
const dbForm: any = ref(null); const dbForm: any = ref(null);
@@ -190,15 +205,7 @@ const state = reactive({
btnLoading: false, btnLoading: false,
}); });
const { const { dialogVisible, tabActiveName, allDatabases, databaseList, form, pwd, btnLoading } = toRefs(state);
dialogVisible,
tabActiveName,
allDatabases,
databaseList,
form,
pwd,
btnLoading,
} = toRefs(state)
watch(props, (newValue: any) => { watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;

View File

@@ -146,35 +146,16 @@
@pageChange="searchSqlExecLog()"> @pageChange="searchSqlExecLog()">
<template #dbSelect> <template #dbSelect>
<el-select v-model="sqlExecLogDialog.query.db" placeholder="请选择数据库" filterable clearable> <el-select v-model="sqlExecLogDialog.query.db" placeholder="请选择数据库" style="width: 200px" filterable
clearable>
<el-option v-for="item in sqlExecLogDialog.dbs" :key="item" :label="`${item}`" :value="item"> <el-option v-for="item in sqlExecLogDialog.dbs" :key="item" :label="`${item}`" :value="item">
</el-option> </el-option>
</el-select> </el-select>
</template> </template>
<template #typeSelect>
<el-select v-model="sqlExecLogDialog.query.type" placeholder="请选择操作类型" clearable>
<el-option v-for="item in enums.DbSqlExecTypeEnum as any" :key="item.value" :label="item.label"
:value="item.value"> </el-option>
</el-select>
</template>
<template #type="{ data }">
<el-tag v-if="data.type == enums.DbSqlExecTypeEnum['UPDATE'].value" color="#E4F5EB"
size="small">UPDATE</el-tag>
<el-tag v-if="data.type == enums.DbSqlExecTypeEnum['DELETE'].value" color="#F9E2AE"
size="small">DELETE</el-tag>
<el-tag v-if="data.type == enums.DbSqlExecTypeEnum['INSERT'].value" color="#A8DEE0"
size="small">INSERT</el-tag>
<el-tag v-if="data.type == enums.DbSqlExecTypeEnum['QUERY'].value" color="#A8DEE0"
size="small">QUERY</el-tag>
<el-tag v-if="data.type == enums.DbSqlExecTypeEnum['OTHER'].value" color="#F9E2AE"
size="small">OTHER</el-tag>
</template>
<template #action="{ data }"> <template #action="{ data }">
<el-link <el-link
v-if="data.type == enums.DbSqlExecTypeEnum['UPDATE'].value || data.type == enums.DbSqlExecTypeEnum['DELETE'].value" v-if="data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value"
type="primary" plain size="small" :underline="false" @click="onShowRollbackSql(data)"> type="primary" plain size="small" :underline="false" @click="onShowRollbackSql(data)">
还原SQL</el-link> 还原SQL</el-link>
</template> </template>
@@ -254,7 +235,7 @@ import { ref, toRefs, reactive, computed, onMounted, defineAsyncComponent } from
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 { dbApi } from './api'; import { dbApi } from './api';
import enums from './enums'; import { DbSqlExecTypeEnum } from './enums';
import SqlExecBox from './component/SqlExecBox'; import SqlExecBox from './component/SqlExecBox';
import config from '@/common/config'; import config from '@/common/config';
import { getSession } from '@/common/utils/storage'; import { getSession } from '@/common/utils/storage';
@@ -331,12 +312,12 @@ const state = reactive({
queryConfig: [ queryConfig: [
TableQuery.slot("db", "数据库", "dbSelect"), TableQuery.slot("db", "数据库", "dbSelect"),
TableQuery.text("table", "表名"), TableQuery.text("table", "表名"),
TableQuery.slot("type", "操作类型", "typeSelect"), TableQuery.select("type", "操作类型").setOptions(Object.values(DbSqlExecTypeEnum)),
], ],
columns: [ columns: [
TableColumn.new("db", "数据库"), TableColumn.new("db", "数据库"),
TableColumn.new("table", ""), TableColumn.new("table", ""),
TableColumn.new("type", "类型").isSlot().setAddWidth(10), TableColumn.new("type", "类型").typeTag(DbSqlExecTypeEnum).setAddWidth(10),
TableColumn.new("creator", "执行人"), TableColumn.new("creator", "执行人"),
TableColumn.new("sql", "SQL"), TableColumn.new("sql", "SQL"),
TableColumn.new("oldValue", "原值"), TableColumn.new("oldValue", "原值"),
@@ -566,7 +547,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
const oldValue = JSON.parse(sqlExecLog.oldValue); const oldValue = JSON.parse(sqlExecLog.oldValue);
const rollbackSqls = []; const rollbackSqls = [];
if (sqlExecLog.type == enums.DbSqlExecTypeEnum['UPDATE'].value) { if (sqlExecLog.type == DbSqlExecTypeEnum['UPDATE'].value) {
for (let ov of oldValue) { for (let ov of oldValue) {
const setItems = []; const setItems = [];
for (let key in ov) { for (let key in ov) {
@@ -577,7 +558,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
} }
rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`); rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
} }
} else if (sqlExecLog.type == enums.DbSqlExecTypeEnum['DELETE'].value) { } else if (sqlExecLog.type == DbSqlExecTypeEnum['DELETE'].value) {
const columnNames = columns.map((c: any) => c.columnName); const columnNames = columns.map((c: any) => c.columnName);
for (let ov of oldValue) { for (let ov of oldValue) {
const values = []; const values = [];

View File

@@ -2,9 +2,9 @@
<div> <div>
<el-row> <el-row>
<el-col :span="4"> <el-col :span="4">
<el-button type="primary" icon="plus" <el-button type="primary" icon="plus" @click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases }, state.db)" size="small"
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases }, state.db)" >新建查询</el-button
size="small">新建查询</el-button> >
</el-col> </el-col>
<el-col :span="20" v-if="state.db"> <el-col :span="20" v-if="state.db">
<el-descriptions :column="4" size="small" border style="height: 10px"> <el-descriptions :column="4" size="small" border style="height: 10px">
@@ -24,29 +24,29 @@
</el-row> </el-row>
<el-row type="flex"> <el-row type="flex">
<el-col :span="4" style="border-left: 1px solid #eee; margin-top: 10px"> <el-col :span="4" style="border-left: 1px solid #eee; margin-top: 10px">
<tag-tree ref="tagTreeRef" @node-click="nodeClick" :load="loadNode" <tag-tree
:load-contextmenu-items="getContextmenuItems" @current-contextmenu-click="onCurrentContextmenuClick" ref="tagTreeRef"
:height="state.tagTreeHeight"> @node-click="nodeClick"
:load="loadNode"
:load-contextmenu-items="getContextmenuItems"
@current-contextmenu-click="onCurrentContextmenuClick"
:height="state.tagTreeHeight"
>
<template #prefix="{ data }"> <template #prefix="{ data }">
<span v-if="data.type == NodeType.DbInst"> <span v-if="data.type == NodeType.DbInst">
<el-popover placement="right-start" title="数据库实例信息" trigger="hover" :width="210"> <el-popover placement="right-start" title="数据库实例信息" trigger="hover" :width="210">
<template #reference> <template #reference>
<SvgIcon v-if="data.params.type === 'mysql'" name="iconfont icon-op-mysql" :size="18" /> <SvgIcon v-if="data.params.type === 'mysql'" name="iconfont icon-op-mysql" :size="18" />
<SvgIcon v-if="data.params.type === 'postgres'" name="iconfont icon-op-postgres" <SvgIcon v-if="data.params.type === 'postgres'" name="iconfont icon-op-postgres" :size="18" />
:size="18" />
<SvgIcon name="InfoFilled" v-else /> <SvgIcon name="InfoFilled" v-else />
</template> </template>
<template #default> <template #default>
<el-form class="instances-pop-form" label-width="55px" :size="'small'"> <el-form class="instances-pop-form" label-width="55px" :size="'small'">
<el-form-item label="类型:">{{ data.params.type }}</el-form-item> <el-form-item label="类型:">{{ data.params.type }}</el-form-item>
<el-form-item label="链接:">{{ data.params.host }}:{{ <el-form-item label="链接:">{{ data.params.host }}:{{ data.params.port }}</el-form-item>
data.params.port
}}</el-form-item>
<el-form-item label="用户:">{{ data.params.username }}</el-form-item> <el-form-item label="用户:">{{ data.params.username }}</el-form-item>
<el-form-item v-if="data.params.remark" label="备注:">{{ <el-form-item v-if="data.params.remark" label="备注:">{{ data.params.remark }}</el-form-item>
data.params.remark
}}</el-form-item>
</el-form> </el-form>
</template> </template>
</el-popover> </el-popover>
@@ -56,28 +56,32 @@
<SvgIcon name="Calendar" v-if="data.type == NodeType.TableMenu" color="#409eff" /> <SvgIcon name="Calendar" v-if="data.type == NodeType.TableMenu" color="#409eff" />
<el-tooltip v-if="data.type == NodeType.Table" effect="customized" <el-tooltip v-if="data.type == NodeType.Table" effect="customized" :content="data.params.tableComment" placement="top-end">
:content="data.params.tableComment" placement="top-end">
<SvgIcon name="Calendar" color="#409eff" /> <SvgIcon name="Calendar" color="#409eff" />
</el-tooltip> </el-tooltip>
<SvgIcon name="Files" v-if="data.type == NodeType.SqlMenu || data.type == NodeType.Sql" <SvgIcon name="Files" v-if="data.type == NodeType.SqlMenu || data.type == NodeType.Sql" color="#f56c6c" />
color="#f56c6c" />
</template> </template>
</tag-tree> </tag-tree>
</el-col> </el-col>
<el-col :span="20"> <el-col :span="20">
<el-container id="data-exec" style="border-left: 1px solid #eee; margin-top: 10px"> <el-container id="data-exec" style="border-left: 1px solid #eee; margin-top: 10px">
<el-tabs @tab-remove="onRemoveTab" @tab-change="onTabChange" style="width: 100%" <el-tabs @tab-remove="onRemoveTab" @tab-change="onTabChange" style="width: 100%" v-model="state.activeName">
v-model="state.activeName"> <el-tab-pane closable v-for="dt in state.tabs.values()" :key="dt.key" :label="dt.key" :name="dt.key">
<table-data
v-if="dt.type === TabType.TableData"
@gen-insert-sql="onGenerateInsertSql"
:data="dt"
:table-height="state.dataTabsTableHeight"
></table-data>
<el-tab-pane closable v-for="dt in state.tabs.values()" :key="dt.key" :label="dt.key" <query
:name="dt.key"> v-else
<table-data v-if="dt.type === TabType.TableData" @gen-insert-sql="onGenerateInsertSql" @save-sql-success="reloadSqls"
:data="dt" :table-height="state.dataTabsTableHeight"></table-data> @delete-sql-success="deleteSqlScript(dt)"
:data="dt"
<query v-else @save-sql-success="reloadSqls" @delete-sql-success="deleteSqlScript(dt)" :editor-height="state.editorHeight"
:data="dt" :editor-height="state.editorHeight"> >
</query> </query>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@@ -85,8 +89,7 @@
</el-col> </el-col>
</el-row> </el-row>
<el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" title="SQL" <el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" title="SQL" width="1000px">
width="1000px">
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" /> <el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
</el-dialog> </el-dialog>
</div> </div>
@@ -100,7 +103,7 @@ import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/my
import * as monaco from 'monaco-editor'; import * as monaco from 'monaco-editor';
import { editor, languages, Position } from 'monaco-editor'; import { editor, languages, Position } from 'monaco-editor';
import { DbInst, TabInfo, TabType } from './db' import { DbInst, TabInfo, TabType } from './db';
import { TagTreeNode } from '../component/tag'; import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue'; import TagTree from '../component/TagTree.vue';
import { dbApi } from './api'; import { dbApi } from './api';
@@ -112,18 +115,18 @@ const TableData = defineAsyncComponent(() => import('./component/tab/TableData.v
* 树节点类型 * 树节点类型
*/ */
class NodeType { class NodeType {
static DbInst = 1 static DbInst = 1;
static Db = 2 static Db = 2;
static TableMenu = 3; static TableMenu = 3;
static SqlMenu = 4; static SqlMenu = 4;
static Table = 5; static Table = 5;
static Sql = 6; static Sql = 6;
} }
class ContextmenuClickId { class ContextmenuClickId {
static ReloadTable = 0 static ReloadTable = 0;
} }
const tagTreeRef: any = ref(null) const tagTreeRef: any = ref(null);
const tabs: Map<string, TabInfo> = new Map(); const tabs: Map<string, TabInfo> = new Map();
const state = reactive({ const state = reactive({
@@ -144,12 +147,10 @@ const state = reactive({
}, },
}); });
const { const { nowDbInst } = toRefs(state);
nowDbInst,
} = toRefs(state);
onMounted(() => { onMounted(() => {
self.completionItemProvider?.dispose() self.completionItemProvider?.dispose();
setHeight(); setHeight();
// 监听浏览器窗口大小变化,更新对应组件高度 // 监听浏览器窗口大小变化,更新对应组件高度
window.onresize = () => setHeight(); window.onresize = () => setHeight();
@@ -170,15 +171,15 @@ const setHeight = () => {
const instMap: Map<string, any[]> = new Map(); const instMap: Map<string, any[]> = new Map();
const getInsts = async () => { const getInsts = async () => {
const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000, }) const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000 });
if (!res.total) return if (!res.total) return;
for (const db of res.list) { for (const db of res.list) {
const tagPath = db.tagPath; const tagPath = db.tagPath;
let dbInsts = instMap.get(tagPath) || []; let dbInsts = instMap.get(tagPath) || [];
dbInsts.push(db); dbInsts.push(db);
instMap.set(tagPath, dbInsts?.sort()); instMap.set(tagPath, dbInsts?.sort());
} }
} };
/** /**
* 加载树节点 * 加载树节点
@@ -203,7 +204,7 @@ const loadNode = async (node: any) => {
// 点击tagPath -> 加载数据库实例信息列表 // 点击tagPath -> 加载数据库实例信息列表
if (nodeType === TagTreeNode.TagPath) { if (nodeType === TagTreeNode.TagPath) {
const dbInfos = instMap.get(data.key) const dbInfos = instMap.get(data.key);
return dbInfos?.map((x: any) => { return dbInfos?.map((x: any) => {
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.DbInst).withParams(x); return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.DbInst).withParams(x);
}); });
@@ -219,15 +220,17 @@ const loadNode = async (node: any) => {
name: params.name, name: params.name,
type: params.type, type: params.type,
dbs: dbs, dbs: dbs,
db: x db: x,
}) });
}) });
} }
// 点击数据库 -> 加载 表&Sql 菜单 // 点击数据库 -> 加载 表&Sql 菜单
if (nodeType === NodeType.Db) { if (nodeType === NodeType.Db) {
return [new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeType.TableMenu).withParams(params), return [
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeType.SqlMenu).withParams(params)]; new TagTreeNode(`${params.id}.${params.db}.table-menu`, '', NodeType.TableMenu).withParams(params),
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeType.SqlMenu).withParams(params),
];
} }
// 点击表菜单 -> 加载表列表 // 点击表菜单 -> 加载表列表
@@ -262,30 +265,28 @@ const nodeClick = async (data: any) => {
if (dataType === NodeType.Sql) { if (dataType === NodeType.Sql) {
await addQueryTab({ id: params.id, nodeKey: nodeKey, dbs: params.dbs }, params.db, params.sqlName); await addQueryTab({ id: params.id, nodeKey: nodeKey, dbs: params.dbs }, params.db, params.sqlName);
} }
} };
const getContextmenuItems = (data: any) => { const getContextmenuItems = (data: any) => {
const dataType = data.type; const dataType = data.type;
if (dataType === NodeType.TableMenu) { if (dataType === NodeType.TableMenu) {
return [ return [{ contextMenuClickId: ContextmenuClickId.ReloadTable, txt: '刷新', icon: 'RefreshRight' }];
{ contextMenuClickId: ContextmenuClickId.ReloadTable, txt: '刷新', icon: 'RefreshRight' }
]
} }
return []; return [];
} };
// 当前右击菜单点击事件 // 当前右击菜单点击事件
const onCurrentContextmenuClick = (clickData: any) => { const onCurrentContextmenuClick = (clickData: any) => {
const clickId = clickData.id; const clickId = clickData.id;
if (clickId == ContextmenuClickId.ReloadTable) { if (clickId == ContextmenuClickId.ReloadTable) {
reloadTables(clickData.item.key) reloadTables(clickData.item.key);
}
} }
};
const getTables = async (params: any) => { const getTables = async (params: any) => {
const { id, db } = params; const { id, db } = params;
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus); let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
state.reloadStatus = false state.reloadStatus = false;
return tables.map((x: any) => { return tables.map((x: any) => {
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeType.Table).withIsLeaf(true).withParams({ return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeType.Table).withIsLeaf(true).withParams({
id, id,
@@ -293,8 +294,8 @@ const getTables = async (params: any) => {
tableName: x.tableName, tableName: x.tableName,
tableComment: x.tableComment, tableComment: x.tableComment,
}); });
}) });
} };
/** /**
* 加载用户保存的sql脚本 * 加载用户保存的sql脚本
@@ -303,7 +304,7 @@ const getTables = async (params: any) => {
* @param schema * @param schema
*/ */
const loadSqls = async (id: any, db: string, dbs: any) => { const loadSqls = async (id: any, db: string, dbs: any) => {
const sqls = await dbApi.getSqlNames.request({ id: id, db: db, }) const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
return sqls.map((x: any) => { return sqls.map((x: any) => {
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeType.Sql).withIsLeaf(true).withParams({ return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeType.Sql).withIsLeaf(true).withParams({
id, id,
@@ -312,17 +313,17 @@ const loadSqls = async (id: any, db: string, dbs: any) => {
sqlName: x.name, sqlName: x.name,
}); });
}); });
} };
// 选择数据库 // 选择数据库
const changeSchema = (inst: any, schema: string) => { const changeSchema = (inst: any, schema: string) => {
state.nowDbInst = DbInst.getOrNewInst(inst); state.nowDbInst = DbInst.getOrNewInst(inst);
state.db = schema; state.db = schema;
} };
// 加载选中的表数据即新增表数据操作tab // 加载选中的表数据即新增表数据操作tab
const loadTableData = async (inst: any, schema: string, tableName: string) => { const loadTableData = async (inst: any, schema: string, tableName: string) => {
changeSchema(inst, schema) changeSchema(inst, schema);
if (tableName == '') { if (tableName == '') {
return; return;
} }
@@ -341,16 +342,16 @@ const loadTableData = async (inst: any, schema: string, tableName: string) => {
tab.db = schema; tab.db = schema;
tab.type = TabType.TableData; tab.type = TabType.TableData;
tab.params = { tab.params = {
table: tableName table: tableName,
} };
state.tabs.set(label, tab) state.tabs.set(label, tab);
} };
// 新建查询panel // 新建查询panel
const addQueryTab = async (inst: any, db: string, sqlName: string = '') => { const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
if (!db || !inst.id) { if (!db || !inst.id) {
ElMessage.warning('请选择数据库实例及对应的schema') ElMessage.warning('请选择数据库实例及对应的schema');
return return;
} }
const dbId = inst.id; const dbId = inst.id;
@@ -364,7 +365,7 @@ const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
if (v.type == TabType.Query && !v.params.sqlName) { if (v.type == TabType.Query && !v.params.sqlName) {
count++; count++;
} }
}) });
label = `新查询${count}:${dbId}:${db}`; label = `新查询${count}:${dbId}:${db}`;
} }
state.activeName = label; state.activeName = label;
@@ -381,16 +382,16 @@ const addQueryTab = async (inst: any, db: string, sqlName: string = '') => {
tab.params = { tab.params = {
sqlName: sqlName, sqlName: sqlName,
dbs: inst.dbs, dbs: inst.dbs,
} };
state.tabs.set(label, tab) state.tabs.set(label, tab);
registerSqlCompletionItemProvider(); registerSqlCompletionItemProvider();
} };
const onRemoveTab = (targetName: string) => { const onRemoveTab = (targetName: string) => {
let activeName = state.activeName; let activeName = state.activeName;
const tabNames = [...state.tabs.keys()] const tabNames = [...state.tabs.keys()];
for (let i = 0; i < tabNames.length; i++) { for (let i = 0; i < tabNames.length; i++) {
const tabName = tabNames[i] const tabName = tabNames[i];
if (tabName !== targetName) { if (tabName !== targetName) {
continue; continue;
} }
@@ -414,7 +415,7 @@ const onTabChange = () => {
const nowTab = state.tabs.get(state.activeName); const nowTab = state.tabs.get(state.activeName);
state.nowDbInst = DbInst.getInst(nowTab?.dbId); state.nowDbInst = DbInst.getInst(nowTab?.dbId);
state.db = nowTab?.db as string; state.db = nowTab?.db as string;
} };
const onGenerateInsertSql = async (sql: string) => { const onGenerateInsertSql = async (sql: string) => {
state.genSqlDialog.sql = sql; state.genSqlDialog.sql = sql;
@@ -423,25 +424,27 @@ const onGenerateInsertSql = async (sql: string) => {
const reloadSqls = (dbId: number, db: string) => { const reloadSqls = (dbId: number, db: string) => {
tagTreeRef.value.reloadNode(getSqlMenuNodeKey(dbId, db)); tagTreeRef.value.reloadNode(getSqlMenuNodeKey(dbId, db));
} };
const deleteSqlScript = (ti: TabInfo) => { const deleteSqlScript = (ti: TabInfo) => {
reloadSqls(ti.dbId, ti.db); reloadSqls(ti.dbId, ti.db);
onRemoveTab(ti.key); onRemoveTab(ti.key);
} };
const getSqlMenuNodeKey = (dbId: number, db: string) => { const getSqlMenuNodeKey = (dbId: number, db: string) => {
return `${dbId}.${db}.sql-menu` return `${dbId}.${db}.sql-menu`;
} };
const reloadTables = (nodeKey: string) => { const reloadTables = (nodeKey: string) => {
state.reloadStatus = true state.reloadStatus = true;
tagTreeRef.value.reloadNode(nodeKey); tagTreeRef.value.reloadNode(nodeKey);
} };
const registerSqlCompletionItemProvider = () => { const registerSqlCompletionItemProvider = () => {
// 参考 https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example // 参考 https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example
self.completionItemProvider = self.completionItemProvider || monaco.languages.registerCompletionItemProvider('sql', { self.completionItemProvider =
self.completionItemProvider ||
monaco.languages.registerCompletionItemProvider('sql', {
triggerCharacters: ['.', ' '], triggerCharacters: ['.', ' '],
provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => { provideCompletionItems: async (model: editor.ITextModel, position: Position): Promise<languages.CompletionList | null | undefined> => {
let word = model.getWordUntilPosition(position); let word = model.getWordUntilPosition(position);
@@ -451,14 +454,14 @@ const registerSqlCompletionItemProvider = () => {
} }
const { db, dbId } = nowTab; const { db, dbId } = nowTab;
const dbInst = DbInst.getInst(dbId); const dbInst = DbInst.getInst(dbId);
const { lineNumber, column } = position const { lineNumber, column } = position;
const { startColumn, endColumn } = word const { startColumn, endColumn } = word;
// 当前行文本 // 当前行文本
let lineContent = model.getLineContent(lineNumber); let lineContent = model.getLineContent(lineNumber);
// 注释行不需要代码提示 // 注释行不需要代码提示
if (lineContent.startsWith('--')) { if (lineContent.startsWith('--')) {
return { suggestions: [] } return { suggestions: [] };
} }
let range = { let range = {
@@ -473,97 +476,97 @@ const registerSqlCompletionItemProvider = () => {
startLineNumber: lineNumber, startLineNumber: lineNumber,
startColumn: 0, startColumn: 0,
endLineNumber: lineNumber, endLineNumber: lineNumber,
endColumn: column endColumn: column,
}) });
const textBeforePointerMulti = model.getValueInRange({ const textBeforePointerMulti = model.getValueInRange({
startLineNumber: 1, startLineNumber: 1,
startColumn: 0, startColumn: 0,
endLineNumber: lineNumber, endLineNumber: lineNumber,
endColumn: column endColumn: column,
}) });
// 光标后文本 // 光标后文本
const textAfterPointerMulti = model.getValueInRange({ const textAfterPointerMulti = model.getValueInRange({
startLineNumber: lineNumber, startLineNumber: lineNumber,
startColumn: column, startColumn: column,
endLineNumber: model.getLineCount(), endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()) endColumn: model.getLineMaxColumn(model.getLineCount()),
}) });
// // const nextTokens = textAfterPointer.trim().split(/\s+/) // // const nextTokens = textAfterPointer.trim().split(/\s+/)
// // const nextToken = nextTokens[0].toLowerCase() // // const nextToken = nextTokens[0].toLowerCase()
const tokens = textBeforePointer.trim().split(/\s+/) const tokens = textBeforePointer.trim().split(/\s+/);
let lastToken = tokens[tokens.length - 1].toLowerCase() let lastToken = tokens[tokens.length - 1].toLowerCase();
const secondToken = tokens.length > 2 && tokens[tokens.length - 2].toLowerCase() || '' const secondToken = (tokens.length > 2 && tokens[tokens.length - 2].toLowerCase()) || '';
// const dbs = nowTab.params?.dbs?.split(' ') || []; // const dbs = nowTab.params?.dbs?.split(' ') || [];
const dbs = nowTab.params && nowTab.params.dbs && nowTab.params.dbs.split(' ') || []; const dbs = (nowTab.params && nowTab.params.dbs && nowTab.params.dbs.split(' ')) || [];
// console.log("光标前文本:=>" + textBeforePointerMulti) // console.log("光标前文本:=>" + textBeforePointerMulti)
// console.log("最后输入的:=>" + lastToken) // console.log("最后输入的:=>" + lastToken)
let suggestions: languages.CompletionItem[] = [] let suggestions: languages.CompletionItem[] = [];
const tables = await dbInst.loadTables(db); const tables = await dbInst.loadTables(db);
async function hintTableColumns(tableName: any, db: any) { async function hintTableColumns(tableName: any, db: any) {
let dbHits = await dbInst.loadDbHints(db) let dbHits = await dbInst.loadDbHints(db);
let columns = dbHits[tableName] let columns = dbHits[tableName];
let suggestions: languages.CompletionItem[] = [] let suggestions: languages.CompletionItem[] = [];
columns?.forEach((a: string, index: any) => { columns?.forEach((a: string, index: any) => {
// 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间] // 字段数据格式 字段名 字段注释, 如: create_time [datetime][创建时间]
const nameAndComment = a.split(" ") const nameAndComment = a.split(' ');
const fieldName = nameAndComment[0] const fieldName = nameAndComment[0];
suggestions.push({ suggestions.push({
label: { label: {
label: a, label: a,
description: 'column' description: 'column',
}, },
kind: monaco.languages.CompletionItemKind.Property, kind: monaco.languages.CompletionItemKind.Property,
detail: '', // 不显示detail, 否则选中时备注等会被遮挡 detail: '', // 不显示detail, 否则选中时备注等会被遮挡
insertText: fieldName, // create_time insertText: fieldName, // create_time
range, range,
sortText: 100 + index + '' // 使用表字段声明顺序排序,排序需为字符串类型 sortText: 100 + index + '', // 使用表字段声明顺序排序,排序需为字符串类型
}); });
}) });
return suggestions return suggestions;
} }
if (lastToken.indexOf('.') > -1 || secondToken.indexOf('.') > -1) { if (lastToken.indexOf('.') > -1 || secondToken.indexOf('.') > -1) {
// 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】 // 如果是.触发代码提示,则进行【 库.表名联想 】 或 【 表别名.表字段联想 】
let str = lastToken.substring(0, lastToken.lastIndexOf('.')) let str = lastToken.substring(0, lastToken.lastIndexOf('.'));
if (lastToken.trim().startsWith('.')) { if (lastToken.trim().startsWith('.')) {
str = secondToken str = secondToken;
} }
// 如果字符串粘连起了如:'a.creator,a.',需要重新取出别名 // 如果字符串粘连起了如:'a.creator,a.',需要重新取出别名
let aliasArr = lastToken.split(','); let aliasArr = lastToken.split(',');
if (aliasArr.length > 1) { if (aliasArr.length > 1) {
lastToken = aliasArr[aliasArr.length - 1]; lastToken = aliasArr[aliasArr.length - 1];
str = lastToken.substring(0, lastToken.lastIndexOf('.')) str = lastToken.substring(0, lastToken.lastIndexOf('.'));
if (lastToken.trim().startsWith('.')) { if (lastToken.trim().startsWith('.')) {
str = secondToken str = secondToken;
} }
} }
// 库.表名联想 // 库.表名联想
if (dbs && dbs.filter((a: any) => a === str)?.length > 0) { if (dbs && dbs.filter((a: any) => a === str)?.length > 0) {
let tables = await dbInst.loadTables(str) let tables = await dbInst.loadTables(str);
let suggestions: languages.CompletionItem[] = [] let suggestions: languages.CompletionItem[] = [];
for (let item of tables) { for (let item of tables) {
const { tableName, tableComment } = item const { tableName, tableComment } = item;
suggestions.push({ suggestions.push({
label: { label: {
label: tableName + (tableComment ? ' - ' + tableComment : ''), label: tableName + (tableComment ? ' - ' + tableComment : ''),
description: 'table' description: 'table',
}, },
kind: monaco.languages.CompletionItemKind.File, kind: monaco.languages.CompletionItemKind.File,
insertText: tableName, insertText: tableName,
range range,
}); });
} }
return { suggestions } return { suggestions };
} }
let sql = textBeforePointerMulti.split(';')[textBeforePointerMulti.split(';').length - 1] + textAfterPointerMulti.split(';')[0]; let sql = textBeforePointerMulti.split(';')[textBeforePointerMulti.split(';').length - 1] + textAfterPointerMulti.split(';')[0];
// 表别名.表字段联想 // 表别名.表字段联想
let tableInfo = getTableByAlias(sql, db, str) let tableInfo = getTableByAlias(sql, db, str);
if (tableInfo.tableName) { if (tableInfo.tableName) {
let tableName = tableInfo.tableName let tableName = tableInfo.tableName;
let db = tableInfo.dbName; let db = tableInfo.dbName;
// 取出表名并提示 // 取出表名并提示
let suggestions = await hintTableColumns(tableName, db); let suggestions = await hintTableColumns(tableName, db);
@@ -571,16 +574,16 @@ const registerSqlCompletionItemProvider = () => {
return { suggestions }; return { suggestions };
} }
} }
return { suggestions: [] } return { suggestions: [] };
} else { } else {
// 如果sql里含有表名则提示表字段 // 如果sql里含有表名则提示表字段
let mat = textBeforePointerMulti.match(/[from|update]\n*\s+\n*(\w+)\n*\s+\n*/i) let mat = textBeforePointerMulti.match(/[from|update]\n*\s+\n*(\w+)\n*\s+\n*/i);
if (mat && mat.length > 1) { if (mat && mat.length > 1) {
let tableName = mat[1] let tableName = mat[1];
// 取出表名并提示 // 取出表名并提示
let addSuggestions = await hintTableColumns(tableName, db); let addSuggestions = await hintTableColumns(tableName, db);
if (addSuggestions.length > 0) { if (addSuggestions.length > 0) {
suggestions = suggestions.concat(addSuggestions) suggestions = suggestions.concat(addSuggestions);
} }
} }
} }
@@ -591,12 +594,12 @@ const registerSqlCompletionItemProvider = () => {
suggestions.push({ suggestions.push({
label: { label: {
label: tableName + ' - ' + tableComment, label: tableName + ' - ' + tableComment,
description: 'table' description: 'table',
}, },
kind: monaco.languages.CompletionItemKind.File, kind: monaco.languages.CompletionItemKind.File,
detail: tableComment, detail: tableComment,
insertText: tableName + ' ', insertText: tableName + ' ',
range range,
}); });
}); });
@@ -605,49 +608,49 @@ const registerSqlCompletionItemProvider = () => {
suggestions.push({ suggestions.push({
label: { label: {
label: item, label: item,
description: 'keyword' description: 'keyword',
}, },
kind: monaco.languages.CompletionItemKind.Keyword, kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item, insertText: item,
range range,
});
}); });
})
// 操作符 // 操作符
sqlLanguage.operators.forEach((item: any) => { sqlLanguage.operators.forEach((item: any) => {
suggestions.push({ suggestions.push({
label: { label: {
label: item, label: item,
description: 'opt' description: 'opt',
}, },
kind: monaco.languages.CompletionItemKind.Operator, kind: monaco.languages.CompletionItemKind.Operator,
insertText: item, insertText: item,
range range,
});
}); });
})
// 内置函数 // 内置函数
sqlLanguage.builtinFunctions.forEach((item: any) => { sqlLanguage.builtinFunctions.forEach((item: any) => {
suggestions.push({ suggestions.push({
label: { label: {
label: item, label: item,
description: 'func' description: 'func',
}, },
kind: monaco.languages.CompletionItemKind.Function, kind: monaco.languages.CompletionItemKind.Function,
insertText: item, insertText: item,
range range,
});
}); });
})
// 内置变量 // 内置变量
sqlLanguage.builtinVariables.forEach((item: string) => { sqlLanguage.builtinVariables.forEach((item: string) => {
suggestions.push({ suggestions.push({
label: { label: {
label: item, label: item,
description: 'var' description: 'var',
}, },
kind: monaco.languages.CompletionItemKind.Variable, kind: monaco.languages.CompletionItemKind.Variable,
insertText: item, insertText: item,
range range,
});
}); });
})
// 库名提示 // 库名提示
if (dbs && dbs.length > 0) { if (dbs && dbs.length > 0) {
@@ -655,22 +658,22 @@ const registerSqlCompletionItemProvider = () => {
suggestions.push({ suggestions.push({
label: { label: {
label: a, label: a,
description: 'schema' description: 'schema',
}, },
kind: monaco.languages.CompletionItemKind.Folder, kind: monaco.languages.CompletionItemKind.Folder,
insertText: a, insertText: a,
range range,
});
}); });
})
} }
// 默认提示 // 默认提示
return { return {
suggestions: suggestions suggestions: suggestions,
}; };
}, },
}); });
} };
/** /**
* 根据别名获取sql里的表名 * 根据别名获取sql里的表名
@@ -678,8 +681,7 @@ const registerSqlCompletionItemProvider = () => {
* @param db 默认数据库 * @param db 默认数据库
* @param alias 别名 * @param alias 别名
*/ */
const getTableByAlias = (sql: string, db: string, alias: string): { dbName: string, tableName: string } => { const getTableByAlias = (sql: string, db: string, alias: string): { dbName: string; tableName: string } => {
// 表别名:表名 // 表别名:表名
let result = {}; let result = {};
let defName = ''; let defName = '';
@@ -695,30 +697,32 @@ where l.name='kevin' and exsits(select 1 from pharmacywestpas pw where p.outvisi
unit all unit all
select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)/gi) select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)/gi)
*/ */
let match = sql.match(/(join|from)\n*\s+\n*(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)\n*/gi) let match = sql.match(/(join|from)\n*\s+\n*(\w*-?\w*\.?\w+)\s*(as)?\s*(\w*)\n*/gi);
if (match && match.length > 0) { if (match && match.length > 0) {
match.forEach(a => { match.forEach((a) => {
// 去掉前缀,取出 // 去掉前缀,取出
let t = a.substring(5, a.length) let t = a
.substring(5, a.length)
.replaceAll(/\s+/g, ' ') .replaceAll(/\s+/g, ' ')
.replaceAll(/\s+as\s+/gi, ' ') .replaceAll(/\s+as\s+/gi, ' ')
.replaceAll(/\r\n/g, ' ').trim() .replaceAll(/\r\n/g, ' ')
.trim()
.split(/\s+/); .split(/\s+/);
let withDb = t[0].split('.'); let withDb = t[0].split('.');
// 表名是 db名.表名 // 表名是 db名.表名
let tName = withDb.length > 1 ? withDb[1] : withDb[0] let tName = withDb.length > 1 ? withDb[1] : withDb[0];
let dbName = withDb.length > 1 ? withDb[0] : (db || '') let dbName = withDb.length > 1 ? withDb[0] : db || '';
if (t.length == 2) { if (t.length == 2) {
// 表别名:表名 // 表别名:表名
result[t[1]] = { tableName: tName, dbName } result[t[1]] = { tableName: tName, dbName };
} else { } else {
// 只有表名无别名 取第一个无别名的表为默认表 // 只有表名无别名 取第一个无别名的表为默认表
!defName && (defResult = { tableName: tName, dbName: db }) !defName && (defResult = { tableName: tName, dbName: db });
} }
}) });
}
return result[alias] || defResult
} }
return result[alias] || defResult;
};
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -762,7 +766,7 @@ select * from invisit v where`.match(/(join|from)\s+(\w*-?\w*\.?\w+)\s*(as)?\s*(
} }
.update_field_active { .update_field_active {
background-color: var(--el-color-success) background-color: var(--el-color-success);
} }
.instances-pop-form { .instances-pop-form {

View File

@@ -2,14 +2,12 @@
<div> <div>
<el-dialog :title="`${title} 详情`" v-model="dialogVisible" :before-close="cancel" width="90%"> <el-dialog :title="`${title} 详情`" v-model="dialogVisible" :before-close="cancel" width="90%">
<el-table @cell-click="cellClick" :data="data.res"> <el-table @cell-click="cellClick" :data="data.res">
<el-table-column :width="200" :prop="item" :label="item" v-for="item in data.colNames" :key="item"> <el-table-column :width="200" :prop="item" :label="item" v-for="item in data.colNames" :key="item"> </el-table-column>
</el-table-column>
</el-table> </el-table>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, toRefs, reactive } from 'vue'; import { watch, toRefs, reactive } from 'vue';
@@ -23,10 +21,10 @@ const props = defineProps({
data: { data: {
type: Object, type: Object,
}, },
}) });
//定义事件 //定义事件
const emit = defineEmits(['update:visible']) const emit = defineEmits(['update:visible']);
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
@@ -36,10 +34,7 @@ const state = reactive({
}, },
}); });
const { const { dialogVisible, data } = toRefs(state);
dialogVisible,
data,
} = toRefs(state)
watch(props, async (newValue: any) => { watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
@@ -66,6 +61,4 @@ const cellClick = (row: any, column: any, cell: any) => {
const cancel = () => { const cancel = () => {
emit('update:visible', false); emit('update:visible', false);
}; };
</script> </script>

View File

@@ -2,27 +2,27 @@ import Api from '@/common/Api';
export const dbApi = { export const dbApi = {
// 获取权限列表 // 获取权限列表
dbs: Api.newGet("/dbs"), dbs: Api.newGet('/dbs'),
saveDb: Api.newPost("/dbs"), saveDb: Api.newPost('/dbs'),
getAllDatabase: Api.newPost("/dbs/databases"), getAllDatabase: Api.newPost('/dbs/databases'),
getDbPwd: Api.newGet("/dbs/{id}/pwd"), getDbPwd: Api.newGet('/dbs/{id}/pwd'),
deleteDb: Api.newDelete("/dbs/{id}"), deleteDb: Api.newDelete('/dbs/{id}'),
dumpDb: Api.newPost("/dbs/{id}/dump"), dumpDb: Api.newPost('/dbs/{id}/dump'),
tableInfos: Api.newGet("/dbs/{id}/t-infos"), tableInfos: Api.newGet('/dbs/{id}/t-infos'),
tableIndex: Api.newGet("/dbs/{id}/t-index"), tableIndex: Api.newGet('/dbs/{id}/t-index'),
tableDdl: Api.newGet("/dbs/{id}/t-create-ddl"), tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
tableMetadata: Api.newGet("/dbs/{id}/t-metadata"), tableMetadata: Api.newGet('/dbs/{id}/t-metadata'),
columnMetadata: Api.newGet("/dbs/{id}/c-metadata"), columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
// 获取表即列提示 // 获取表即列提示
hintTables: Api.newGet("/dbs/{id}/hint-tables"), hintTables: Api.newGet('/dbs/{id}/hint-tables'),
sqlExec: Api.newPost("/dbs/{id}/exec-sql"), sqlExec: Api.newPost('/dbs/{id}/exec-sql'),
// 保存sql // 保存sql
saveSql: Api.newPost("/dbs/{id}/sql"), saveSql: Api.newPost('/dbs/{id}/sql'),
// 获取保存的sql // 获取保存的sql
getSql: Api.newGet("/dbs/{id}/sql"), getSql: Api.newGet('/dbs/{id}/sql'),
// 获取保存的sql names // 获取保存的sql names
getSqlNames: Api.newGet("/dbs/{id}/sql-names"), getSqlNames: Api.newGet('/dbs/{id}/sql-names'),
deleteDbSql: Api.newDelete("/dbs/{id}/sql"), deleteDbSql: Api.newDelete('/dbs/{id}/sql'),
// 获取数据库sql执行记录 // 获取数据库sql执行记录
getSqlExecs: Api.newGet("/dbs/{dbId}/sql-execs"), getSqlExecs: Api.newGet('/dbs/{dbId}/sql-execs'),
} };

View File

@@ -1,15 +1,34 @@
<template> <template>
<div> <div>
<el-table @cell-dblclick="(row: any, column: any, cell: any, event: any) => cellClick(row, column, cell)" <el-table
@sort-change="(sort: any) => onTableSortChange(sort)" @selection-change="onDataSelectionChange" :data="datas" @cell-dblclick="(row: any, column: any, cell: any, event: any) => cellClick(row, column, cell)"
size="small" :max-height="tableHeight" v-loading="loading" element-loading-text="查询中..." :empty-text="emptyText" @sort-change="(sort: any) => onTableSortChange(sort)"
highlight-current-row stripe border class="mt5"> @selection-change="onDataSelectionChange"
:data="datas"
size="small"
:max-height="tableHeight"
v-loading="loading"
element-loading-text="查询中..."
:empty-text="emptyText"
highlight-current-row
stripe
border
class="mt5"
>
<el-table-column v-if="datas.length > 0 && table" type="selection" width="35" /> <el-table-column v-if="datas.length > 0 && table" type="selection" width="35" />
<template v-for="(item, index) in columns"> <template v-for="(item, index) in columns">
<el-table-column min-width="100" :width="DbInst.flexColumnWidth(item.columnName, datas)" align="center" <el-table-column
v-if="item.show" :key="index" :prop="item.columnName" :label="item.columnName" show-overflow-tooltip min-width="100"
:sortable="sortable"> :width="DbInst.flexColumnWidth(item.columnName, datas)"
align="center"
v-if="item.show"
:key="index"
:prop="item.columnName"
:label="item.columnName"
show-overflow-tooltip
:sortable="sortable"
>
<template #header v-if="showColumnTip"> <template #header v-if="showColumnTip">
<el-tooltip raw-content placement="top" effect="customized"> <el-tooltip raw-content placement="top" effect="customized">
<template #content> {{ getColumnTip(item) }} </template> <template #content> {{ getColumnTip(item) }} </template>
@@ -18,7 +37,6 @@
</template> </template>
</el-table-column> </el-table-column>
</template> </template>
</el-table> </el-table>
</div> </div>
</template> </template>
@@ -307,11 +325,10 @@ defineExpose({
submitUpdateFields, submitUpdateFields,
cancelUpdateFields cancelUpdateFields
}) })
</script> </script>
<style lang="scss"> <style lang="scss">
.update_field_active { .update_field_active {
background-color: var(--el-color-success) background-color: var(--el-color-success);
} }
</style> </style>

View File

@@ -1,44 +1,44 @@
import { h, render, VNode } from 'vue' import { h, render, VNode } from 'vue';
import SqlExecDialog from './SqlExecDialog.vue' import SqlExecDialog from './SqlExecDialog.vue';
export type SqlExecProps = { export type SqlExecProps = {
sql: string sql: string;
dbId: number, dbId: number;
db: string, db: string;
runSuccessCallback?: Function, runSuccessCallback?: Function;
cancelCallback?: Function cancelCallback?: Function;
} };
const boxId = 'sql-exec-id' const boxId = 'sql-exec-id';
const renderBox = (): VNode => { const renderBox = (): VNode => {
const props: SqlExecProps = { const props: SqlExecProps = {
sql: '', sql: '',
dbId: 0, dbId: 0,
} as any } as any;
const container = document.createElement('div') const container = document.createElement('div');
container.id = boxId container.id = boxId;
// 创建 虚拟dom // 创建 虚拟dom
const boxVNode = h(SqlExecDialog, props) const boxVNode = h(SqlExecDialog, props);
// 将虚拟dom渲染到 container dom 上 // 将虚拟dom渲染到 container dom 上
render(boxVNode, container) render(boxVNode, container);
// 最后将 container 追加到 body 上 // 最后将 container 追加到 body 上
document.body.appendChild(container) document.body.appendChild(container);
return boxVNode return boxVNode;
} };
let boxInstance: any let boxInstance: any;
const SqlExecBox = (props: SqlExecProps): void => { const SqlExecBox = (props: SqlExecProps): void => {
if (boxInstance) { if (boxInstance) {
const boxVue = boxInstance.component const boxVue = boxInstance.component;
// 调用open方法显示弹框注意不能使用boxVue.ctx来调用组件函数build打包后ctx会获取不到 // 调用open方法显示弹框注意不能使用boxVue.ctx来调用组件函数build打包后ctx会获取不到
boxVue.exposed.open(props); boxVue.exposed.open(props);
} else { } else {
boxInstance = renderBox() boxInstance = renderBox();
SqlExecBox(props) SqlExecBox(props);
}
} }
};
export default SqlExecBox; export default SqlExecBox;

View File

@@ -36,7 +36,7 @@ const props = defineProps({
sql: { sql: {
type: String, type: String,
}, },
}) });
const remarkInputRef = ref<InputInstance>(); const remarkInputRef = ref<InputInstance>();
const state = reactive({ const state = reactive({
@@ -48,12 +48,7 @@ const state = reactive({
btnLoading: false, btnLoading: false,
}); });
const { const { dialogVisible, sqlValue, remark, btnLoading } = toRefs(state);
dialogVisible,
sqlValue,
remark,
btnLoading
} = toRefs(state)
state.sqlValue = props.sql as any; state.sqlValue = props.sql as any;
let runSuccessCallback: any; let runSuccessCallback: any;
@@ -81,7 +76,7 @@ const runSql = async () => {
for (let re of res.res) { for (let re of res.res) {
if (re.result !== 'success') { if (re.result !== 'success') {
ElMessage.error(`${re.sql} \n执行失败: ${re.result}`); ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
throw new Error(re.result) throw new Error(re.result);
} }
} }
@@ -129,7 +124,7 @@ const open = (props: SqlExecProps) => {
}); });
}; };
defineExpose({ open }) defineExpose({ open });
</script> </script>
<style lang="scss"> <style lang="scss">
.codesql { .codesql {

View File

@@ -3,25 +3,30 @@
<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 @click="onRunSql()" :underline="false" class="ml15" icon="VideoPlay"> </el-link>
</el-link>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="format sql" placement="top"> <el-tooltip class="box-item" effect="dark" content="format sql" placement="top">
<el-link @click="formatSql()" type="primary" :underline="false" icon="MagicStick"> <el-link @click="formatSql()" type="primary" :underline="false" icon="MagicStick"> </el-link>
</el-link>
</el-tooltip> </el-tooltip>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="commit" placement="top"> <el-tooltip class="box-item" effect="dark" content="commit" placement="top">
<el-link @click="onCommit()" type="success" :underline="false" icon="CircleCheck"> <el-link @click="onCommit()" type="success" :underline="false" icon="CircleCheck"> </el-link>
</el-link>
</el-tooltip> </el-tooltip>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-upload class="sql-file-exec" :before-upload="beforeUpload" :on-success="execSqlFileSuccess" <el-upload
:headers="{ Authorization: token }" :action="getUploadSqlFileUrl()" :show-file-list="false" class="sql-file-exec"
name="file" multiple :limit="100"> :before-upload="beforeUpload"
:on-success="execSqlFileSuccess"
:headers="{ Authorization: token }"
:action="getUploadSqlFileUrl()"
:show-file-list="false"
name="file"
multiple
:limit="100"
>
<el-tooltip class="box-item" effect="dark" content="SQL脚本执行" placement="top"> <el-tooltip class="box-item" effect="dark" content="SQL脚本执行" placement="top">
<el-link type="success" :underline="false" icon="Document"></el-link> <el-link type="success" :underline="false" icon="Document"></el-link>
</el-tooltip> </el-tooltip>
@@ -29,17 +34,14 @@
</div> </div>
<div style="float: right" class="fl"> <div style="float: right" class="fl">
<el-button @click="saveSql()" type="primary" icon="document-add" plain size="small">保存SQL <el-button @click="saveSql()" type="primary" icon="document-add" plain size="small">保存SQL </el-button>
</el-button> <el-button v-if="sqlName" @click="deleteSql()" type="danger" icon="delete" plain size="small">删除SQL </el-button>
<el-button v-if="sqlName" @click="deleteSql()" type="danger" icon="delete" plain size="small">删除SQL
</el-button>
</div> </div>
</div> </div>
</div> </div>
<div class="mt5 sqlEditor"> <div class="mt5 sqlEditor">
<div :id="'MonacoTextarea-' + ti.key" :style="{ height: editorHeight }"> <div :id="'MonacoTextarea-' + ti.key" :style="{ height: editorHeight }"></div>
</div>
</div> </div>
<div class="editor-move-resize" @mousedown="onDragSetHeight"> <div class="editor-move-resize" @mousedown="onDragSetHeight">
@@ -50,31 +52,35 @@
<div class="mt5"> <div class="mt5">
<el-row> <el-row>
<el-link v-if="table" @click="onDeleteData()" class="ml5" type="danger" icon="delete" <el-link v-if="table" @click="onDeleteData()" class="ml5" type="danger" icon="delete" :underline="false"></el-link>
:underline="false"></el-link>
<span v-if="execRes.data.length > 0"> <span v-if="execRes.data.length > 0">
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-link type="success" :underline="false" @click="exportData"><span <el-link type="success" :underline="false" @click="exportData"><span style="font-size: 12px">导出</span></el-link>
style="font-size: 12px">导出</span></el-link>
</span> </span>
<span v-if="hasUpdatedFileds"> <span v-if="hasUpdatedFileds">
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-link type="success" :underline="false" @click="submitUpdateFields()"><span <el-link type="success" :underline="false" @click="submitUpdateFields()"><span style="font-size: 12px">提交</span></el-link>
style="font-size: 12px">提交</span></el-link>
</span> </span>
<span v-if="hasUpdatedFileds"> <span v-if="hasUpdatedFileds">
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-link type="warning" :underline="false" @click="cancelUpdateFields"><span <el-link type="warning" :underline="false" @click="cancelUpdateFields"><span style="font-size: 12px">取消</span></el-link>
style="font-size: 12px">取消</span></el-link>
</span> </span>
</el-row> </el-row>
<db-table ref="dbTableRef" :db-id="state.ti.dbId" :db="state.ti.db" :data="execRes.data" :table="state.table" <db-table
:columns="execRes.tableColumn" :loading="loading" :height="tableDataHeight" ref="dbTableRef"
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改" @selection-change="onDataSelectionChange" :db-id="state.ti.dbId"
@change-updated-field="changeUpdatedField"></db-table> :db="state.ti.db"
:data="execRes.data"
:table="state.table"
:columns="execRes.tableColumn"
:loading="loading"
:height="tableDataHeight"
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
@selection-change="onDataSelectionChange"
@change-updated-field="changeUpdatedField"
></db-table>
</div> </div>
</div> </div>
</template> </template>
@@ -94,18 +100,18 @@ import { editor } from 'monaco-editor';
// 主题仓库 https://github.com/brijeshb42/monaco-themes // 主题仓库 https://github.com/brijeshb42/monaco-themes
// 主题例子 https://editor.bitwiser.in/ // 主题例子 https://editor.bitwiser.in/
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json'; import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
import DbTable from '../DbTable.vue' import DbTable from '../DbTable.vue';
import { TabInfo } from '../../db'; import { TabInfo } from '../../db';
import { exportCsv } from '@/common/utils/export'; import { exportCsv } from '@/common/utils/export';
import { dateStrFormat } from '@/common/utils/date'; import { dateStrFormat } from '@/common/utils/date';
import { dbApi } from '../../api'; import { dbApi } from '../../api';
const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess']) const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess']);
const props = defineProps({ const props = defineProps({
data: { data: {
type: TabInfo, type: TabInfo,
required: true required: true,
}, },
// sql脚本名若有则去加载该sql内容 // sql脚本名若有则去加载该sql内容
sqlName: { sqlName: {
@@ -114,9 +120,9 @@ const props = defineProps({
}, },
editorHeight: { editorHeight: {
type: String, type: String,
default: '600' default: '600',
} },
}) });
const { themeConfig } = storeToRefs(useThemeConfig()); const { themeConfig } = storeToRefs(useThemeConfig());
const token = getSession('token'); const token = getSession('token');
@@ -134,7 +140,7 @@ const state = reactive({
loading: false, // 是否在加载数据 loading: false, // 是否在加载数据
execRes: { execRes: {
data: [], data: [],
tableColumn: [] tableColumn: [],
}, },
selectionDatas: [] as any, selectionDatas: [] as any,
editorHeight: '500', editorHeight: '500',
@@ -142,20 +148,14 @@ const state = reactive({
hasUpdatedFileds: false, hasUpdatedFileds: false,
}); });
const { const { tableDataHeight, editorHeight, ti, execRes, table, sqlName, loading, hasUpdatedFileds } = toRefs(state);
tableDataHeight,
editorHeight,
ti,
execRes,
table,
sqlName,
loading,
hasUpdatedFileds,
} = toRefs(state);
watch(() => props.editorHeight, (newValue: any) => { watch(
() => props.editorHeight,
(newValue: any) => {
state.editorHeight = newValue; state.editorHeight = newValue;
}); }
);
onMounted(async () => { onMounted(async () => {
console.log('in query mounted'); console.log('in query mounted');
@@ -170,19 +170,19 @@ onMounted(async () => {
state.sql = res.sql; state.sql = res.sql;
} }
nextTick(() => { nextTick(() => {
setTimeout(() => initMonacoEditor(), 50) setTimeout(() => initMonacoEditor(), 50);
}) });
await state.ti.getNowDbInst().loadDbHints(state.ti.db); await state.ti.getNowDbInst().loadDbHints(state.ti.db);
}) });
self.MonacoEnvironment = { self.MonacoEnvironment = {
getWorker() { getWorker() {
return new EditorWorker(); return new EditorWorker();
} },
}; };
const initMonacoEditor = () => { const initMonacoEditor = () => {
let monacoTextarea = document.getElementById('MonacoTextarea-' + state.ti.key) as HTMLElement let monacoTextarea = document.getElementById('MonacoTextarea-' + state.ti.key) as HTMLElement;
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language // options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
// 初始化一些主题 // 初始化一些主题
monaco.editor.defineTheme('SolarizedLight', SolarizedLight); monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
@@ -219,7 +219,7 @@ const initMonacoEditor = () => {
keybindingContext: undefined, keybindingContext: undefined,
keybindings: [ keybindings: [
// chord // chord
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0) monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0),
], ],
contextMenuGroupId: 'navigation', contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5, contextMenuOrder: 1.5,
@@ -229,9 +229,9 @@ const initMonacoEditor = () => {
try { try {
await onRunSql(); await onRunSql();
} catch (e: any) { } catch (e: any) {
e.message && ElMessage.error(e.message) e.message && ElMessage.error(e.message);
}
} }
},
}); });
// 注册快捷键ctrl + shift + f 格式化sql // 注册快捷键ctrl + shift + f 格式化sql
@@ -246,7 +246,7 @@ const initMonacoEditor = () => {
keybindingContext: undefined, keybindingContext: undefined,
keybindings: [ keybindings: [
// chord // chord
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0) monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0),
], ],
contextMenuGroupId: 'navigation', contextMenuGroupId: 'navigation',
contextMenuOrder: 2, contextMenuOrder: 2,
@@ -256,9 +256,9 @@ const initMonacoEditor = () => {
try { try {
await formatSql(); await formatSql();
} catch (e: any) { } catch (e: any) {
e.message && ElMessage.error(e.message) e.message && ElMessage.error(e.message);
}
} }
},
}); });
// 动态设置主题 // 动态设置主题
@@ -277,13 +277,13 @@ const onDragSetHeight = () => {
document.onmousemove = (e) => { document.onmousemove = (e) => {
e.preventDefault(); e.preventDefault();
//得到鼠标拖动的宽高距离:取绝对值 //得到鼠标拖动的宽高距离:取绝对值
state.editorHeight = `${document.getElementById('MonacoTextarea-' + state.ti.key)!.offsetHeight + e.movementY}px` state.editorHeight = `${document.getElementById('MonacoTextarea-' + state.ti.key)!.offsetHeight + e.movementY}px`;
state.tableDataHeight -= e.movementY state.tableDataHeight -= e.movementY;
} };
document.onmouseup = () => { document.onmouseup = () => {
document.onmousemove = null; document.onmousemove = null;
} };
} };
/** /**
* 执行sql * 执行sql
@@ -324,7 +324,7 @@ const onRunSql = async () => {
const colAndData: any = await state.ti.getNowDbInst().runSql(state.ti.db, sql, execRemark); const colAndData: any = await state.ti.getNowDbInst().runSql(state.ti.db, sql, execRemark);
if (!colAndData.res || colAndData.res.length === 0) { if (!colAndData.res || colAndData.res.length === 0) {
ElMessage.warning('未查询到结果集') ElMessage.warning('未查询到结果集');
} }
state.execRes.data = colAndData.res; state.execRes.data = colAndData.res;
// 兼容表格字段配置 // 兼容表格字段配置
@@ -332,9 +332,9 @@ const onRunSql = async () => {
return { return {
columnName: x, columnName: x,
show: true, show: true,
} };
}); });
cancelUpdateFields() cancelUpdateFields();
} catch (e: any) { } catch (e: any) {
state.execRes.data = []; state.execRes.data = [];
state.execRes.tableColumn = []; state.execRes.tableColumn = [];
@@ -370,15 +370,15 @@ const getSql = () => {
return res; return res;
} }
// 选择选中的sql // 选择选中的sql
let selection = monacoEditor.getSelection() let selection = monacoEditor.getSelection();
if (selection) { if (selection) {
res = monacoEditor.getModel()?.getValueInRange(selection) res = monacoEditor.getModel()?.getValueInRange(selection);
} }
// 整个编辑器的sql // 整个编辑器的sql
if (!res) { if (!res) {
return monacoEditor.getModel()?.getValue() return monacoEditor.getModel()?.getValue();
} }
return res return res;
}; };
const saveSql = async () => { const saveSql = async () => {
@@ -392,8 +392,7 @@ const saveSql = async () => {
const input = await ElMessageBox.prompt('请输入SQL脚本名', 'SQL名', { const input = await ElMessageBox.prompt('请输入SQL脚本名', 'SQL名', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
inputPattern: inputPattern: /\w+/,
/\w+/,
inputErrorMessage: '请输入SQL脚本名', inputErrorMessage: '请输入SQL脚本名',
}); });
sqlName = input.value; sqlName = input.value;
@@ -411,7 +410,7 @@ const saveSql = async () => {
const deleteSql = async () => { const deleteSql = async () => {
const sqlName = state.sqlName; const sqlName = state.sqlName;
notBlank(sqlName, "该sql内容未保存"); notBlank(sqlName, '该sql内容未保存');
const { dbId, db } = state.ti; const { dbId, db } = state.ti;
try { try {
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', { await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
@@ -429,14 +428,14 @@ const deleteSql = async () => {
* 格式化sql * 格式化sql
*/ */
const formatSql = () => { const formatSql = () => {
let selection = monacoEditor.getSelection() let selection = monacoEditor.getSelection();
if (!selection) { if (!selection) {
return; return;
} }
let sql = monacoEditor.getModel()?.getValueInRange(selection) let sql = monacoEditor.getModel()?.getValueInRange(selection);
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容 // 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
if (sql) { if (sql) {
replaceSelection(sqlFormatter(sql), selection) replaceSelection(sqlFormatter(sql), selection);
return; return;
} }
monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue())); monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue()));
@@ -462,29 +461,29 @@ const replaceSelection = (str: string, selection: any) => {
model.setValue(str); model.setValue(str);
return; return;
} }
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
const textBeforeSelection = model.getValueInRange({ const textBeforeSelection = model.getValueInRange({
startLineNumber: 1, startLineNumber: 1,
startColumn: 0, startColumn: 0,
endLineNumber: startLineNumber, endLineNumber: startLineNumber,
endColumn: startColumn, endColumn: startColumn,
}) });
const textAfterSelection = model.getValueInRange({ const textAfterSelection = model.getValueInRange({
startLineNumber: endLineNumber, startLineNumber: endLineNumber,
startColumn: endColumn, startColumn: endColumn,
endLineNumber: model.getLineCount(), endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()), endColumn: model.getLineMaxColumn(model.getLineCount()),
}) });
monacoEditor.setValue(textBeforeSelection + str + textAfterSelection) monacoEditor.setValue(textBeforeSelection + str + textAfterSelection);
monacoEditor.focus() monacoEditor.focus();
monacoEditor.setPosition({ monacoEditor.setPosition({
lineNumber: startLineNumber, lineNumber: startLineNumber,
column: 0, column: 0,
}) });
} };
/** /**
* 导出当前页数据 * 导出当前页数据
@@ -492,7 +491,11 @@ const replaceSelection = (str: string, selection: any) => {
const exportData = () => { const exportData = () => {
const dataList = state.execRes.data as any; const dataList = state.execRes.data as any;
isTrue(dataList.length > 0, '没有数据可导出'); isTrue(dataList.length > 0, '没有数据可导出');
exportCsv(`数据查询导出-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, state.execRes.tableColumn.map((x: any) => x.columnName), dataList) exportCsv(
`数据查询导出-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`,
state.execRes.tableColumn.map((x: any) => x.columnName),
dataList
);
}; };
const beforeUpload = (file: File) => { const beforeUpload = (file: File) => {
@@ -511,7 +514,6 @@ const getUploadSqlFileUrl = () => {
return `${config.baseApiUrl}/dbs/${state.ti.dbId}/exec-sql-file?db=${state.ti.db}`; return `${config.baseApiUrl}/dbs/${state.ti.dbId}/exec-sql-file?db=${state.ti.db}`;
}; };
const onDataSelectionChange = (datas: []) => { const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas; state.selectionDatas = datas;
}; };
@@ -519,7 +521,7 @@ const onDataSelectionChange = (datas: []) => {
const changeUpdatedField = (updatedFields: []) => { const changeUpdatedField = (updatedFields: []) => {
// 如果存在要更新字段,则显示提交和取消按钮 // 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0; state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
} };
/** /**
* 执行删除数据事件 * 执行删除数据事件
@@ -528,7 +530,7 @@ const onDeleteData = async () => {
const deleteDatas = state.selectionDatas; const deleteDatas = state.selectionDatas;
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据'); isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
const { db } = state.ti; const { db } = state.ti;
const dbInst = state.ti.getNowDbInst() const dbInst = state.ti.getNowDbInst();
const primaryKey = await dbInst.loadTableColumn(db, state.table); const primaryKey = await dbInst.loadTableColumn(db, state.table);
const primaryKeyColumnName = primaryKey.columnName; const primaryKeyColumnName = primaryKey.columnName;
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => { dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
@@ -541,12 +543,11 @@ const onDeleteData = async () => {
const submitUpdateFields = () => { const submitUpdateFields = () => {
dbTableRef.value.submitUpdateFields(); dbTableRef.value.submitUpdateFields();
} };
const cancelUpdateFields = () => { const cancelUpdateFields = () => {
dbTableRef.value.cancelUpdateFields(); dbTableRef.value.cancelUpdateFields();
} };
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -566,9 +567,8 @@ const cancelUpdateFields = () => {
border: 1px solid #ccc; border: 1px solid #ccc;
} }
.update_field_active { .update_field_active {
background-color: var(--el-color-success) background-color: var(--el-color-success);
} }
.editor-move-resize { .editor-move-resize {

View File

@@ -2,16 +2,24 @@
<div> <div>
<el-row> <el-row>
<el-col :span="8"> <el-col :span="8">
<el-link @click="onRefresh()" icon="refresh" :underline="false" class="ml5"> <el-link @click="onRefresh()" icon="refresh" :underline="false" class="ml5"> </el-link>
</el-link>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-popover popper-style="max-height: 550px; overflow: auto; max-width: 450px" placement="bottom" <el-popover
width="auto" title="表格字段配置" trigger="click"> popper-style="max-height: 550px; overflow: auto; max-width: 450px"
placement="bottom"
width="auto"
title="表格字段配置"
trigger="click"
>
<div v-for="(item, index) in columns" :key="index"> <div v-for="(item, index) in columns" :key="index">
<el-checkbox v-model="item.show" <el-checkbox
v-model="item.show"
:label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`" :label="`${!item.columnComment ? item.columnName : item.columnName + ' [' + item.columnComment + ']'}`"
:true-label="true" :false-label="false" size="small" /> :true-label="true"
:false-label="false"
size="small"
/>
</div> </div>
<template #reference> <template #reference>
<el-link icon="Operation" size="small" :underline="false"></el-link> <el-link icon="Operation" size="small" :underline="false"></el-link>
@@ -26,8 +34,7 @@
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="commit" placement="top"> <el-tooltip class="box-item" effect="dark" content="commit" placement="top">
<el-link @click="onCommit()" type="success" icon="CircleCheck" :underline="false"> <el-link @click="onCommit()" type="success" icon="CircleCheck" :underline="false"> </el-link>
</el-link>
</el-tooltip> </el-tooltip>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
@@ -50,21 +57,31 @@
</el-tooltip> </el-tooltip>
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<el-input v-model="condition" placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容点击查询按钮即可" clearable <el-input
@clear="selectData" size="small" style="width: 100%"> v-model="condition"
placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容点击查询按钮即可"
clearable
@clear="selectData"
size="small"
style="width: 100%"
>
<template #prepend> <template #prepend>
<el-popover trigger="click" :width="320" placement="right"> <el-popover trigger="click" :width="320" placement="right">
<template #reference> <template #reference>
<el-link type="success" :underline="false">选择列</el-link> <el-link type="success" :underline="false">选择列</el-link>
</template> </template>
<el-table :data="columns" max-height="500" size="small" @row-click="(...event: any) => { <el-table
:data="columns"
max-height="500"
size="small"
@row-click="(...event: any) => {
onConditionRowClick(event); onConditionRowClick(event);
} }
" style="cursor: pointer"> "
<el-table-column property="columnName" label="列名" show-overflow-tooltip> style="cursor: pointer"
</el-table-column> >
<el-table-column property="columnComment" label="备注" show-overflow-tooltip> <el-table-column property="columnName" label="列名" show-overflow-tooltip> </el-table-column>
</el-table-column> <el-table-column property="columnComment" label="备注" show-overflow-tooltip> </el-table-column>
</el-table> </el-table>
</el-popover> </el-popover>
</template> </template>
@@ -76,16 +93,34 @@
</el-col> </el-col>
</el-row> </el-row>
<db-table ref="dbTableRef" :db-id="state.ti.dbId" :db="state.ti.db" :data="datas" :table="state.table" <db-table
:columns="columns" :loading="loading" :height="tableHeight" :show-column-tip="true" :sortable="'custom'" ref="dbTableRef"
@sort-change="(sort: any) => onTableSortChange(sort)" @selection-change="onDataSelectionChange" :db-id="state.ti.dbId"
@change-updated-field="changeUpdatedField"></db-table> :db="state.ti.db"
:data="datas"
:table="state.table"
:columns="columns"
:loading="loading"
:height="tableHeight"
:show-column-tip="true"
:sortable="'custom'"
@sort-change="(sort: any) => onTableSortChange(sort)"
@selection-change="onDataSelectionChange"
@change-updated-field="changeUpdatedField"
></db-table>
<el-row type="flex" class="mt5" justify="center"> <el-row type="flex" class="mt5" justify="center">
<el-pagination small :total="count" @current-change="pageChange()" layout="prev, pager, next, total, jumper" <el-pagination
v-model:current-page="pageNum" :page-size="DbInst.DefaultLimit"></el-pagination> small
:total="count"
@current-change="pageChange()"
layout="prev, pager, next, total, jumper"
v-model:current-page="pageNum"
:page-size="DbInst.DefaultLimit"
></el-pagination>
</el-row> </el-row>
<div style=" font-size: 12px; padding: 0 10px; color: #606266"><span>{{ state.sql }}</span> <div style="font-size: 12px; padding: 0 10px; color: #606266">
<span>{{ state.sql }}</span>
</div> </div>
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px"> <el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
@@ -114,14 +149,21 @@
<el-dialog v-model="addDataDialog.visible" :title="addDataDialog.title" :destroy-on-close="true" width="600px"> <el-dialog v-model="addDataDialog.visible" :title="addDataDialog.title" :destroy-on-close="true" width="600px">
<el-form ref="dataForm" :model="addDataDialog.data" label-width="auto" size="small"> <el-form ref="dataForm" :model="addDataDialog.data" label-width="auto" size="small">
<el-form-item v-for="column in columns" class="w100" :prop="column.columnName" :label="column.columnName" <el-form-item
:required="column.nullable != 'YES' && column.columnKey != 'PRI'"> v-for="column in columns"
<el-input-number v-if="DbInst.isNumber(column.columnType)" class="w100"
:prop="column.columnName"
:label="column.columnName"
:required="column.nullable != 'YES' && column.columnKey != 'PRI'"
>
<el-input-number
v-if="DbInst.isNumber(column.columnType)"
v-model="addDataDialog.data[`${column.columnName}`]" v-model="addDataDialog.data[`${column.columnName}`]"
:placeholder="`${column.columnType} ${column.columnComment}`" class="w100" /> :placeholder="`${column.columnType} ${column.columnComment}`"
class="w100"
/>
<el-input v-else v-model="addDataDialog.data[`${column.columnName}`]" <el-input v-else v-model="addDataDialog.data[`${column.columnName}`]" :placeholder="`${column.columnType} ${column.columnComment}`" />
:placeholder="`${column.columnType} ${column.columnComment}`" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -142,21 +184,21 @@ import { ElMessage } from 'element-plus';
import { DbInst, TabInfo } from '../../db'; import { DbInst, TabInfo } from '../../db';
import { exportCsv } from '@/common/utils/export'; import { exportCsv } from '@/common/utils/export';
import { dateStrFormat } from '@/common/utils/date'; import { dateStrFormat } from '@/common/utils/date';
import DbTable from '../DbTable.vue' import DbTable from '../DbTable.vue';
const emits = defineEmits(['genInsertSql']) const emits = defineEmits(['genInsertSql']);
const dataForm: any = ref(null); const dataForm: any = ref(null);
const props = defineProps({ const props = defineProps({
data: { data: {
type: TabInfo, type: TabInfo,
required: true required: true,
}, },
tableHeight: { tableHeight: {
type: [String], type: [String],
default: '600' default: '600',
} },
}) });
const dbTableRef = ref(null) as Ref; const dbTableRef = ref(null) as Ref;
@@ -179,7 +221,7 @@ const state = reactive({
dataTab: null, dataTab: null,
visible: false, visible: false,
condition: '=', condition: '=',
value: null value: null,
}, },
addDataDialog: { addDataDialog: {
data: {}, data: {},
@@ -191,43 +233,36 @@ const state = reactive({
hasUpdatedFileds: false, hasUpdatedFileds: false,
}); });
const { const { datas, condition, loading, columns, pageNum, count, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state);
datas,
condition,
loading,
columns,
pageNum,
count,
hasUpdatedFileds,
conditionDialog,
addDataDialog,
} = toRefs(state);
watch(() => props.tableHeight, (newValue: any) => { watch(
() => props.tableHeight,
(newValue: any) => {
state.tableHeight = newValue; state.tableHeight = newValue;
}); }
);
onMounted(async () => { onMounted(async () => {
console.log('in table data mounted'); console.log('in table data mounted');
state.ti = props.data; state.ti = props.data;
state.tableHeight = props.tableHeight; state.tableHeight = props.tableHeight;
state.table = state.ti.params.table; state.table = state.ti.params.table;
notBlank(state.table, "TableData组件params.table信息不能为空") notBlank(state.table, 'TableData组件params.table信息不能为空');
const columns = await state.ti.getNowDbInst().loadColumns(state.ti.db, state.table); const columns = await state.ti.getNowDbInst().loadColumns(state.ti.db, state.table);
columns.forEach((x: any) => { columns.forEach((x: any) => {
x.show = true; x.show = true;
}) });
state.columns = columns; state.columns = columns;
await onRefresh(); await onRefresh();
}) });
const onRefresh = async () => { const onRefresh = async () => {
// 查询条件置空 // 查询条件置空
state.condition = ''; state.condition = '';
state.pageNum = 1; state.pageNum = 1;
await selectData(); await selectData();
} };
/** /**
* 数据tab修改页数 * 数据tab修改页数
@@ -257,7 +292,7 @@ const selectData = async () => {
} finally { } finally {
state.loading = false; state.loading = false;
} }
} };
/** /**
* 导出当前页数据 * 导出当前页数据
@@ -271,10 +306,9 @@ const exportData = () => {
columnNames.push(column.columnName); columnNames.push(column.columnName);
} }
} }
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList) exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
}; };
/** /**
* 条件查询,点击列信息后显示输入对应的值 * 条件查询,点击列信息后显示输入对应的值
*/ */
@@ -320,7 +354,7 @@ const onSelectByCondition = async () => {
notEmpty(state.condition, '条件不能为空'); notEmpty(state.condition, '条件不能为空');
state.pageNum = 1; state.pageNum = 1;
await selectData(); await selectData();
} };
/** /**
* 表排序字段变更 * 表排序字段变更
@@ -341,7 +375,7 @@ const onDataSelectionChange = (datas: []) => {
const changeUpdatedField = (updatedFields: []) => { const changeUpdatedField = (updatedFields: []) => {
// 如果存在要更新字段,则显示提交和取消按钮 // 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0; state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
} };
/** /**
* 执行删除数据事件 * 执行删除数据事件
@@ -350,7 +384,7 @@ const onDeleteData = async () => {
const deleteDatas = state.selectionDatas; const deleteDatas = state.selectionDatas;
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据'); isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
const { db } = state.ti; const { db } = state.ti;
const dbInst = state.ti.getNowDbInst() const dbInst = state.ti.getNowDbInst();
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => { dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
onRefresh(); onRefresh();
}); });
@@ -363,21 +397,21 @@ const onGenerateInsertSql = async () => {
const submitUpdateFields = () => { const submitUpdateFields = () => {
dbTableRef.value.submitUpdateFields(); dbTableRef.value.submitUpdateFields();
} };
const cancelUpdateFields = () => { const cancelUpdateFields = () => {
dbTableRef.value.cancelUpdateFields(); dbTableRef.value.cancelUpdateFields();
} };
const onShowAddDataDialog = async () => { const onShowAddDataDialog = async () => {
state.addDataDialog.title = `添加'${state.table}'表数据` state.addDataDialog.title = `添加'${state.table}'表数据`;
state.addDataDialog.visible = true; state.addDataDialog.visible = true;
}; };
const closeAddDataDialog = () => { const closeAddDataDialog = () => {
state.addDataDialog.visible = false; state.addDataDialog.visible = false;
state.addDataDialog.data = {}; state.addDataDialog.data = {};
} };
// 添加新数据行 // 添加新数据行
const addRow = async () => { const addRow = async () => {
@@ -387,9 +421,9 @@ const addRow = async () => {
// key: 字段名value: 字段名提示 // key: 字段名value: 字段名提示
let obj: any = {}; let obj: any = {};
for (let item of state.columns) { for (let item of state.columns) {
const value = data[item.columnName] const value = data[item.columnName];
if (!value) { if (!value) {
continue continue;
} }
obj[`${item.columnName}`] = DbInst.wrapValueByType(value); obj[`${item.columnName}`] = DbInst.wrapValueByType(value);
} }
@@ -405,13 +439,11 @@ const addRow = async () => {
return false; return false;
} }
}); });
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.update_field_active { .update_field_active {
background-color: var(--el-color-success) background-color: var(--el-color-success);
} }
</style> </style>

View File

@@ -9,30 +9,30 @@ export class DbInst {
/** /**
* 标签路径 * 标签路径
*/ */
tagPath: string tagPath: string;
/** /**
* 实例id * 实例id
*/ */
id: number id: number;
/** /**
* 实例名 * 实例名
*/ */
name: string name: string;
/** /**
* 数据库类型, mysql postgres * 数据库类型, mysql postgres
*/ */
type: string type: string;
/** /**
* schema -> db * schema -> db
*/ */
dbs: Map<string, Db> = new Map() dbs: Map<string, Db> = new Map();
/** 数据库schema多个用空格隔开 */ /** 数据库schema多个用空格隔开 */
databases: string databases: string;
/** /**
* 默认查询分页数量 * 默认查询分页数量
@@ -46,9 +46,9 @@ export class DbInst {
*/ */
getDb(dbName: string) { getDb(dbName: string) {
if (!dbName) { if (!dbName) {
throw new Error('dbName不能为空') throw new Error('dbName不能为空');
} }
let db = this.dbs.get(dbName) let db = this.dbs.get(dbName);
if (db) { if (db) {
return db; return db;
} }
@@ -90,7 +90,6 @@ export class DbInst {
// 优先从 table map中获取 // 优先从 table map中获取
let columns = db.getColumns(table); let columns = db.getColumns(table);
if (columns) { if (columns) {
return columns; return columns;
} }
console.log(`load columns -> dbName: ${dbName}, table: ${table}`); console.log(`load columns -> dbName: ${dbName}, table: ${table}`);
@@ -122,7 +121,7 @@ export class DbInst {
return db.tableHints; return db.tableHints;
} }
console.log(`load db-hits -> dbName: ${dbName}`); console.log(`load db-hits -> dbName: ${dbName}`);
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name, }) const hits = await dbApi.hintTables.request({ id: this.id, db: db.name });
db.tableHints = hits; db.tableHints = hits;
return hits; return hits;
} }
@@ -176,7 +175,7 @@ export class DbInst {
} }
sqls.push(`INSERT INTO ${table} (${colNames.join(', ')}) VALUES(${values.join(', ')})`); sqls.push(`INSERT INTO ${table} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
} }
return sqls.join(';\n') + ';' return sqls.join(';\n') + ';';
} }
/** /**
@@ -196,7 +195,9 @@ export class DbInst {
*/ */
promptExeSql = (db: string, sql: string, cancelFunc: any = null, successFunc: any = null) => { promptExeSql = (db: string, sql: string, cancelFunc: any = null, successFunc: any = null) => {
SqlExecBox({ SqlExecBox({
sql, dbId: this.id, db, sql,
dbId: this.id,
db,
runSuccessCallback: successFunc, runSuccessCallback: successFunc,
cancelCallback: cancelFunc, cancelCallback: cancelFunc,
}); });
@@ -209,7 +210,7 @@ export class DbInst {
*/ */
static getOrNewInst(inst: any) { static getOrNewInst(inst: any) {
if (!inst) { if (!inst) {
throw new Error('inst不能为空') throw new Error('inst不能为空');
} }
let dbInst = dbInstCache.get(inst.id); let dbInst = dbInstCache.get(inst.id);
if (dbInst) { if (dbInst) {
@@ -284,7 +285,7 @@ export class DbInst {
return value; return value;
} }
return `'${value}'`; return `'${value}'`;
}; }
/** /**
* 判断字段类型是否为数字类型 * 判断字段类型是否为数字类型
@@ -293,7 +294,7 @@ export class DbInst {
*/ */
static isNumber(columnType: string) { static isNumber(columnType: string) {
return columnType.match(/int|double|float|nubmer|decimal|byte|bit/gi); return columnType.match(/int|double|float|nubmer|decimal|byte|bit/gi);
}; }
/** /**
* *
@@ -315,16 +316,16 @@ export class DbInst {
} }
// 获取该列中最长的数据(内容) // 获取该列中最长的数据(内容)
let maxWidthText = "" let maxWidthText = '';
let maxWidthValue let maxWidthValue;
// 获取该列中最长的数据(内容) // 获取该列中最长的数据(内容)
for (let i = 0; i < tableData.length; i++) { for (let i = 0; i < tableData.length; i++) {
let nowValue = tableData[i][prop] let nowValue = tableData[i][prop];
if (!nowValue) { if (!nowValue) {
continue; continue;
} }
// 转为字符串比较长度 // 转为字符串比较长度
let nowText = nowValue + ""; let nowText = nowValue + '';
if (nowText.length > maxWidthText.length) { if (nowText.length > maxWidthText.length) {
maxWidthText = nowText; maxWidthText = nowText;
maxWidthValue = nowValue; maxWidthValue = nowValue;
@@ -340,10 +341,10 @@ export class DbInst {
* 数据库实例信息 * 数据库实例信息
*/ */
class Db { class Db {
name: string // 库名 name: string; // 库名
tables: [] // 数据库实例表信息 tables: []; // 数据库实例表信息
columnsMap: Map<string, any> = new Map // table -> columns columnsMap: Map<string, any> = new Map(); // table -> columns
tableHints: any = null // 提示词 tableHints: any = null; // 提示词
/** /**
* 获取指定表列信息前提需要dbInst.loadColumns * 获取指定表列信息前提需要dbInst.loadColumns
@@ -384,32 +385,32 @@ export class TabInfo {
/** /**
* tab唯一key。与label、name都一致 * tab唯一key。与label、name都一致
*/ */
key: string key: string;
/** /**
* 菜单树节点key * 菜单树节点key
*/ */
treeNodeKey: string treeNodeKey: string;
/** /**
* 数据库实例id * 数据库实例id
*/ */
dbId: number dbId: number;
/** /**
* 库名 * 库名
*/ */
db: string = '' db: string = '';
/** /**
* tab 类型 * tab 类型
*/ */
type: TabType type: TabType;
/** /**
* tab需要的其他信息 * tab需要的其他信息
*/ */
params: any params: any;
getNowDbInst() { getNowDbInst() {
return DbInst.getInst(this.dbId); return DbInst.getInst(this.dbId);
@@ -423,26 +424,26 @@ export class TabInfo {
/** 修改表字段所需数据 */ /** 修改表字段所需数据 */
export type UpdateFieldsMeta = { export type UpdateFieldsMeta = {
// 主键值 // 主键值
primaryKey: string primaryKey: string;
// 主键名 // 主键名
primaryKeyName: string primaryKeyName: string;
// 主键类型 // 主键类型
primaryKeyType: string primaryKeyType: string;
// 新值 // 新值
fields: FieldsMeta[] fields: FieldsMeta[];
} };
export type FieldsMeta = { export type FieldsMeta = {
// 字段所在div // 字段所在div
div: HTMLElement div: HTMLElement;
// 字段名 // 字段名
fieldName: string fieldName: string;
// 字段所在的表格行数据 // 字段所在的表格行数据
row: any row: any;
// 字段类型 // 字段类型
fieldType: string fieldType: string;
// 原值 // 原值
oldValue: string oldValue: string;
// 新值 // 新值
newValue: string newValue: string;
} };

View File

@@ -1,13 +1,10 @@
import { Enum } from '@/common/Enum' import { EnumValue } from '@/common/Enum';
/**
* 枚举类
*/
export default {
// 数据库sql执行类型 // 数据库sql执行类型
DbSqlExecTypeEnum: new Enum().add('UPDATE', 'UPDATE', 1) export const DbSqlExecTypeEnum = {
.add('DELETE', 'DELETE', 2) Update: EnumValue.of(1, 'UPDATE').setTagColor('#E4F5EB'),
.add('INSERT', 'INSERT', 3) Delete: EnumValue.of(2, 'DELETE').setTagColor('#F9E2AE'),
.add('QUERY', 'QUERY', 4) Insert: EnumValue.of(3, 'INSERT').setTagColor('#A8DEE0'),
.add('OTHER', 'OTHER', -1), Query: EnumValue.of(4, 'QUERY').setTagColor('#A8DEE0'),
} Other: EnumValue.of(-1, 'OTHER').setTagColor('#F9E2AE'),
};

View File

@@ -1,5 +1,99 @@
export const TYPE_LIST = ['bigint', 'binary', 'blob', 'char', 'datetime', 'date', 'decimal', 'double', 'enum', 'float', 'int', 'json', 'longblob', 'longtext', 'mediumblob', 'mediumtext', 'set', 'smallint', 'text', 'time', 'timestamp', 'tinyint', 'varbinary', 'varchar'] export const TYPE_LIST = [
'bigint',
'binary',
'blob',
'char',
'datetime',
'date',
'decimal',
'double',
'enum',
'float',
'int',
'json',
'longblob',
'longtext',
'mediumblob',
'mediumtext',
'set',
'smallint',
'text',
'time',
'timestamp',
'tinyint',
'varbinary',
'varchar',
];
export const CHARACTER_SET_NAME_LIST = ['armscii8', 'ascii', 'big5', 'binary', 'cp1250', 'cp1251', 'cp1256', 'cp1257', 'cp850', 'cp852', 'cp866', 'cp932', 'dec8', 'eucjpms', 'euckr', 'gb18030', 'gb2312', 'gbk', 'geostd8', 'greek', 'hebrew', 'hp8', 'keybcs2', 'koi8r', 'koi8u', 'latin1', 'latin2', 'latin5', 'latin7', 'macce', 'macroman', 'sjis', 'swe7', 'tis620', 'ucs2', 'ujis', 'utf16', 'utf16le', 'utf32', 'utf8', 'utf8mb4'] export const CHARACTER_SET_NAME_LIST = [
'armscii8',
'ascii',
'big5',
'binary',
'cp1250',
'cp1251',
'cp1256',
'cp1257',
'cp850',
'cp852',
'cp866',
'cp932',
'dec8',
'eucjpms',
'euckr',
'gb18030',
'gb2312',
'gbk',
'geostd8',
'greek',
'hebrew',
'hp8',
'keybcs2',
'koi8r',
'koi8u',
'latin1',
'latin2',
'latin5',
'latin7',
'macce',
'macroman',
'sjis',
'swe7',
'tis620',
'ucs2',
'ujis',
'utf16',
'utf16le',
'utf32',
'utf8',
'utf8mb4',
];
export const COLLATION_SUFFIX_LIST = ['unicode_ci', 'bin', 'croatian_ci', 'czech_ci', 'danish_ci', 'esperanto_ci', 'estonian_ci', 'general_ci', 'german2_ci', 'hungarian_ci', 'icelandic_ci', 'latvian_ci', 'lithuanian_ci', 'persian_ci', 'polish_ci', 'roman_ci', 'romanian_ci', 'sinhala_ci', 'slovak_ci', 'slovenian_ci', 'spanish2_ci', 'spanish_ci', 'swedish_ci', 'turkish_ci', 'unicode_520_ci', 'vietnamese_ci'] export const COLLATION_SUFFIX_LIST = [
'unicode_ci',
'bin',
'croatian_ci',
'czech_ci',
'danish_ci',
'esperanto_ci',
'estonian_ci',
'general_ci',
'german2_ci',
'hungarian_ci',
'icelandic_ci',
'latvian_ci',
'lithuanian_ci',
'persian_ci',
'polish_ci',
'roman_ci',
'romanian_ci',
'sinhala_ci',
'slovak_ci',
'slovenian_ci',
'spanish2_ci',
'spanish_ci',
'swedish_ci',
'turkish_ci',
'unicode_520_ci',
'vietnamese_ci',
];

View File

@@ -3,56 +3,61 @@
<el-dialog :title="title" v-model="dialogVisible" :show-close="true" :before-close="handleClose" width="50%"> <el-dialog :title="title" v-model="dialogVisible" :show-close="true" :before-close="handleClose" width="50%">
<div class="toolbar"> <div class="toolbar">
<div style="float: right"> <div style="float: right">
<el-button v-auth="'machine:file:add'" type="primary" @click="add" icon="plus" plain>添加 <el-button v-auth="'machine:file:add'" type="primary" @click="add" icon="plus" plain>添加 </el-button>
</el-button>
</div> </div>
</div> </div>
<el-table :data="fileTable" stripe style="width: 100%" v-loading="loading"> <el-table :data="fileTable" stripe style="width: 100%" v-loading="loading">
<el-table-column prop="name" label="名称" min-width="70px"> <el-table-column prop="name" label="名称" min-width="70px">
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.name" :disabled="scope.row.id != null" clearable> <el-input v-model="scope.row.name" :disabled="scope.row.id != null" clearable> </el-input>
</el-input>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="name" label="类型" width="130px"> <el-table-column prop="name" label="类型" width="130px">
<template #default="scope"> <template #default="scope">
<el-select :disabled="scope.row.id != null" v-model="scope.row.type" style="width: 100px" <el-select :disabled="scope.row.id != null" v-model="scope.row.type" style="width: 100px" placeholder="请选择">
placeholder="请选择"> <el-option v-for="item in FileTypeEnum as any" :key="item.value" :label="item.label" :value="item.value"></el-option>
<el-option v-for="item in enums.FileTypeEnum as any" :key="item.value" :label="item.label"
:value="item.value"></el-option>
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="path" label="路径" min-width="150px" show-overflow-tooltip> <el-table-column prop="path" label="路径" min-width="150px" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> <el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input>
</el-input>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" min-wdith="180px"> <el-table-column label="操作" min-wdith="180px">
<template #default="scope"> <template #default="scope">
<el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" <el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button>
icon="success-filled" plain></el-button> <el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button>
<el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" <el-button v-auth="'machine:file:del'" type="danger" @click="deleteRow(scope.$index, scope.row)" icon="delete" plain></el-button>
plain></el-button>
<el-button v-auth="'machine:file:del'" type="danger" @click="deleteRow(scope.$index, scope.row)"
icon="delete" plain></el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-row style="margin-top: 10px" type="flex" justify="end"> <el-row style="margin-top: 10px" type="flex" justify="end">
<el-pagination style="text-align: center" :total="total" layout="prev, pager, next, total, jumper" <el-pagination
v-model:current-page="query.pageNum" :page-size="query.pageSize" @current-change="handlePageChange"> style="text-align: center"
:total="total"
layout="prev, pager, next, total, jumper"
v-model:current-page="query.pageNum"
:page-size="query.pageSize"
@current-change="handlePageChange"
>
</el-pagination> </el-pagination>
</el-row> </el-row>
</el-dialog> </el-dialog>
<el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="70%"> <el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="70%">
<el-progress v-if="uploadProgressShow" style="width: 90%; margin-left: 20px" :text-inside="true" <el-progress v-if="uploadProgressShow" style="width: 90%; margin-left: 20px" :text-inside="true" :stroke-width="20" :percentage="progressNum" />
:stroke-width="20" :percentage="progressNum" />
<div style="height: 55vh; overflow: auto"> <div style="height: 55vh; overflow: auto">
<el-tree v-if="tree.visible" ref="fileTree" :highlight-current="true" :load="loadNode" :props="treeProps" <el-tree
lazy node-key="id" :expand-on-click-node="false"> v-if="tree.visible"
ref="fileTree"
:highlight-current="true"
:load="loadNode"
:props="treeProps"
lazy
node-key="id"
:expand-on-click-node="false"
>
<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">
@@ -67,31 +72,38 @@
<SvgIcon :size="15" name="document" /> <SvgIcon :size="15" name="document" />
</span> </span>
<span class="ml5" style="font-weight: bold;"> <span class="ml5" style="font-weight: bold">
{{ node.label }} {{ node.label }}
</span> </span>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="getFileContent(tree.folder.id, data.path)" <el-dropdown-item
v-if="data.type == '-' && data.size < 1 * 1024 * 1024"> @click="getFileContent(tree.folder.id, data.path)"
v-if="data.type == '-' && data.size < 1 * 1024 * 1024"
>
<el-link type="info" icon="view" :underline="false">查看</el-link> <el-link type="info" icon="view" :underline="false">查看</el-link>
</el-dropdown-item> </el-dropdown-item>
<span v-auth="'machine:file:write'"> <span v-auth="'machine:file:write'">
<el-dropdown-item @click="showCreateFileDialog(node)" v-if="data.type == 'd'"> <el-dropdown-item @click="showCreateFileDialog(node)" v-if="data.type == 'd'">
<el-link type="primary" icon="document" :underline="false" <el-link type="primary" icon="document" :underline="false" style="margin-left: 2px">新建</el-link>
style="margin-left: 2px">新建</el-link>
</el-dropdown-item> </el-dropdown-item>
</span> </span>
<span v-auth="'machine:file:upload'"> <span v-auth="'machine:file:upload'">
<el-dropdown-item v-if="data.type == 'd'"> <el-dropdown-item v-if="data.type == 'd'">
<el-upload :before-upload="beforeUpload" :on-success="uploadSuccess" <el-upload
action="" :http-request="getUploadFile" :headers="{ token }" :before-upload="beforeUpload"
:show-file-list="false" name="file" :on-success="uploadSuccess"
style="display: inline-block; margin-left: 2px"> action=""
:http-request="getUploadFile"
:headers="{ token }"
:show-file-list="false"
name="file"
style="display: inline-block; margin-left: 2px"
>
<el-link icon="upload" :underline="false">上传</el-link> <el-link icon="upload" :underline="false">上传</el-link>
</el-upload> </el-upload>
</el-dropdown-item> </el-dropdown-item>
@@ -99,42 +111,32 @@
<span v-auth="'machine:file:write'"> <span v-auth="'machine:file:write'">
<el-dropdown-item @click="downloadFile(node, data)" v-if="data.type == '-'"> <el-dropdown-item @click="downloadFile(node, data)" v-if="data.type == '-'">
<el-link type="primary" icon="download" :underline="false" <el-link type="primary" icon="download" :underline="false" style="margin-left: 2px">下载</el-link>
style="margin-left: 2px">下载</el-link>
</el-dropdown-item> </el-dropdown-item>
</span> </span>
<span v-auth="'machine:file:rm'"> <span v-auth="'machine:file:rm'">
<el-dropdown-item @click="deleteFile(node, data)" v-if="!dontOperate(data)"> <el-dropdown-item @click="deleteFile(node, data)" v-if="!dontOperate(data)">
<el-link type="danger" icon="delete" :underline="false" <el-link type="danger" icon="delete" :underline="false" style="margin-left: 2px">删除</el-link>
style="margin-left: 2px">删除</el-link>
</el-dropdown-item> </el-dropdown-item>
</span> </span>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<span style="display: inline-block" class="ml15"> <span style="display: inline-block" class="ml15">
<span style="color: #67c23a;font-weight: bold;" v-if="data.type == '-'"> <span style="color: #67c23a; font-weight: bold" v-if="data.type == '-'"> [{{ formatFileSize(data.size) }}] </span>
[{{ formatFileSize(data.size) }}] <span style="color: #67c23a; font-weight: bold" v-if="data.type == 'd' && data.dirSize"> [{{ data.dirSize }}] </span>
</span> <span style="color: #67c23a; font-weight: bold" v-if="data.type == 'd' && !data.dirSize">
<span style="color: #67c23a;font-weight: bold;" v-if="data.type == 'd' && data.dirSize"> [<el-button @click="getDirSize(data)" type="primary" link :loading="data.loadingDirSize">size</el-button>]
[{{ data.dirSize }}]
</span>
<span style="color: #67c23a;font-weight: bold;" v-if="data.type == 'd' && !data.dirSize">
[<el-button @click="getDirSize(data)" type="primary" link
:loading="data.loadingDirSize">size</el-button>]
</span> </span>
<el-popover placement="top-start" :title="`${data.path}-文件详情`" :width="520" trigger="click" <el-popover placement="top-start" :title="`${data.path}-文件详情`" :width="520" trigger="click" @show="showFileStat(data)">
@show="showFileStat(data)">
<template #reference> <template #reference>
<span style="color: #67c23a;font-weight: bold;"> <span style="color: #67c23a; font-weight: bold">
[<el-button @click="showFileStat(data)" type="primary" link [<el-button @click="showFileStat(data)" type="primary" link :loading="data.loadingStat">stat</el-button>]
:loading="data.loadingStat">stat</el-button>]
</span> </span>
</template> </template>
<el-input :input-style="{ color: 'black' }" disabled autosize v-model="data.stat" <el-input :input-style="{ color: 'black' }" disabled autosize v-model="data.stat" type="textarea" />
type="textarea" />
</el-popover> </el-popover>
</span> </span>
</span> </span>
@@ -143,8 +145,15 @@
</div> </div>
</el-dialog> </el-dialog>
<el-dialog :destroy-on-close="true" title="新建文件" v-model="createFileDialog.visible" <el-dialog
:before-close="closeCreateFileDialog" :close-on-click-modal="false" top="5vh" width="400px"> :destroy-on-close="true"
title="新建文件"
v-model="createFileDialog.visible"
:before-close="closeCreateFileDialog"
:close-on-click-modal="false"
top="5vh"
width="400px"
>
<div> <div>
<el-form-item prop="name" label="名称:"> <el-form-item prop="name" label="名称:">
<el-input v-model.trim="createFileDialog.name" placeholder="请输入名称" auto-complete="off"></el-input> <el-input v-model.trim="createFileDialog.name" placeholder="请输入名称" auto-complete="off"></el-input>
@@ -165,8 +174,14 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog :destroy-on-close="true" :title="fileContent.dialogTitle" v-model="fileContent.contentVisible" <el-dialog
:close-on-click-modal="false" top="5vh" width="70%"> :destroy-on-close="true"
:title="fileContent.dialogTitle"
v-model="fileContent.contentVisible"
:close-on-click-modal="false"
top="5vh"
width="70%"
>
<div> <div>
<monaco-editor :can-change-mode="true" v-model="fileContent.content" :language="fileContent.type" /> <monaco-editor :can-change-mode="true" v-model="fileContent.content" :language="fileContent.type" />
</div> </div>
@@ -188,7 +203,7 @@ import { machineApi } from './api';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { getSession } from '@/common/utils/storage'; import { getSession } from '@/common/utils/storage';
import enums from './enums'; import { FileTypeEnum } from './enums';
import config from '@/common/config'; import config from '@/common/config';
import { isTrue } from '@/common/assert'; import { isTrue } from '@/common/assert';
@@ -196,15 +211,15 @@ const props = defineProps({
visible: { type: Boolean }, visible: { type: Boolean },
machineId: { type: Number }, machineId: { type: Number },
title: { type: String }, title: { type: String },
}) });
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']) const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
const treeProps = { const treeProps = {
label: 'name', label: 'name',
children: 'zones', children: 'zones',
isLeaf: 'leaf', isLeaf: 'leaf',
} };
const addFile = machineApi.addConf; const addFile = machineApi.addConf;
const delFile = machineApi.delConf; const delFile = machineApi.delConf;
@@ -266,18 +281,7 @@ const state = reactive({
file: null as any, file: null as any,
}); });
const { const { dialogVisible, loading, query, total, fileTable, fileContent, tree, progressNum, uploadProgressShow, createFileDialog } = toRefs(state);
dialogVisible,
loading,
query,
total,
fileTable,
fileContent,
tree,
progressNum,
uploadProgressShow,
createFileDialog,
} = toRefs(state)
watch(props, async (newValue) => { watch(props, async (newValue) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
@@ -468,13 +472,13 @@ const getDirSize = async (data: any) => {
const res = await machineApi.dirSize.request({ const res = await machineApi.dirSize.request({
machineId: props.machineId, machineId: props.machineId,
fileId: state.tree.folder.id, fileId: state.tree.folder.id,
path: data.path path: data.path,
}) });
data.dirSize = res; data.dirSize = res;
} finally { } finally {
data.loadingDirSize = false; data.loadingDirSize = false;
} }
} };
const showFileStat = async (data: any) => { const showFileStat = async (data: any) => {
try { try {
@@ -485,13 +489,13 @@ const showFileStat = async (data: any) => {
const res = await machineApi.fileStat.request({ const res = await machineApi.fileStat.request({
machineId: props.machineId, machineId: props.machineId,
fileId: state.tree.folder.id, fileId: state.tree.folder.id,
path: data.path path: data.path,
}) });
data.stat = res; data.stat = res;
} finally { } finally {
data.loadingStat = false; data.loadingStat = false;
} }
} };
const showCreateFileDialog = (node: any) => { const showCreateFileDialog = (node: any) => {
isTrue(node.expanded, '请先点击展开该节点后再创建'); isTrue(node.expanded, '请先点击展开该节点后再创建');
@@ -547,10 +551,7 @@ const deleteFile = (node: any, data: any) => {
const downloadFile = (node: any, data: any) => { const downloadFile = (node: any, data: any) => {
const a = document.createElement('a'); const a = document.createElement('a');
a.setAttribute( a.setAttribute('href', `${config.baseApiUrl}/machines/${props.machineId}/files/${state.tree.folder.id}/read?type=1&path=${data.path}&token=${token}`);
'href',
`${config.baseApiUrl}/machines/${props.machineId}/files/${state.tree.folder.id}/read?type=1&path=${data.path}&token=${token}`
);
a.click(); a.click();
}; };
@@ -602,24 +603,7 @@ const getFilePath = (data: object, visible: boolean) => {
}; };
const dontOperate = (data: any) => { const dontOperate = (data: any) => {
const path = data.path; const path = data.path;
const ls = [ const ls = ['/', '//', '/usr', '/usr/', '/usr/bin', '/opt', '/run', '/etc', '/proc', '/var', '/mnt', '/boot', '/dev', '/home', '/media', '/root'];
'/',
'//',
'/usr',
'/usr/',
'/usr/bin',
'/opt',
'/run',
'/etc',
'/proc',
'/var',
'/mnt',
'/boot',
'/dev',
'/home',
'/media',
'/root',
];
return ls.indexOf(path) != -1; return ls.indexOf(path) != -1;
}; };

View File

@@ -1,7 +1,6 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" <el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" :before-close="cancel" width="650px">
:before-close="cancel" width="650px">
<el-form :model="form" ref="machineForm" :rules="rules" label-width="auto"> <el-form :model="form" ref="machineForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName"> <el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic"> <el-tab-pane label="基础信息" name="basic">
@@ -13,8 +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 :disabled="form.id" v-model.trim="form.ip" placeholder="主机ip" auto-complete="off"> <el-input :disabled="form.id" v-model.trim="form.ip" placeholder="主机ip" auto-complete="off"> </el-input>
</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">
@@ -23,20 +21,17 @@
</el-form-item> </el-form-item>
<el-form-item prop="username" label="用户名:"> <el-form-item prop="username" label="用户名:">
<el-input v-model.trim="form.username" placeholder="请输授权用户名" autocomplete="new-password"> <el-input v-model.trim="form.username" placeholder="请输授权用户名" autocomplete="new-password"> </el-input>
</el-input>
</el-form-item> </el-form-item>
<el-form-item label="认证方式:"> <el-form-item label="认证方式:">
<el-select @change="changeAuthMethod" style="width: 100%" v-model="state.authType" <el-select @change="changeAuthMethod" style="width: 100%" v-model="state.authType" placeholder="请选认证方式">
placeholder="请选认证方式">
<el-option key="1" label="密码" :value="1"> </el-option> <el-option key="1" label="密码" :value="1"> </el-option>
<el-option key="2" label="授权凭证" :value="2"> </el-option> <el-option key="2" label="授权凭证" :value="2"> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="state.authType == 1" prop="password" label="密码:"> <el-form-item v-if="state.authType == 1" prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" <el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
autocomplete="new-password">
</el-input> </el-input>
</el-form-item> </el-form-item>
@@ -78,7 +73,7 @@ import { machineApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import TagSelect from '../component/TagSelect.vue'; import TagSelect from '../component/TagSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue'; import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import AuthCertSelect from './authcert/AuthCertSelect.vue' import AuthCertSelect from './authcert/AuthCertSelect.vue';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -90,17 +85,19 @@ const props = defineProps({
title: { title: {
type: String, type: String,
}, },
}) });
//定义事件 //定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']) const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const rules = { const rules = {
tagId: [{ tagId: [
{
required: true, required: true,
message: '请选择标签', message: '请选择标签',
trigger: ['blur', 'change'], trigger: ['blur', 'change'],
}], },
],
name: [ name: [
{ {
required: true, required: true,
@@ -136,7 +133,7 @@ const rules = {
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
} };
const machineForm: any = ref(null); const machineForm: any = ref(null);
const authCertSelectRef: any = ref(null); const authCertSelectRef: any = ref(null);
@@ -166,13 +163,7 @@ const state = reactive({
btnLoading: false, btnLoading: false,
}); });
const { const { dialogVisible, tabActiveName, form, testConnBtnLoading, btnLoading } = toRefs(state);
dialogVisible,
tabActiveName,
form,
testConnBtnLoading,
btnLoading,
} = toRefs(state)
watch(props, async (newValue: any) => { watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
@@ -183,7 +174,7 @@ watch(props, async (newValue: any) => {
if (newValue.machine) { if (newValue.machine) {
state.form = { ...newValue.machine }; state.form = { ...newValue.machine };
// 如果凭证类型为公共的,则表示使用授权凭证认证 // 如果凭证类型为公共的,则表示使用授权凭证认证
const authCertId = (state.form as any).authCertId const authCertId = (state.form as any).authCertId;
if (authCertId > 0) { if (authCertId > 0) {
state.authType = 2; state.authType = 2;
} else { } else {
@@ -203,7 +194,7 @@ const changeAuthMethod = (val: any) => {
state.form.password = ''; state.form.password = '';
} }
} }
} };
const testConn = async () => { const testConn = async () => {
machineForm.value.validate(async (valid: boolean) => { machineForm.value.validate(async (valid: boolean) => {
@@ -224,7 +215,7 @@ const testConn = async () => {
return false; return false;
} }
}); });
} };
const btnOk = async () => { const btnOk = async () => {
machineForm.value.validate(async (valid: boolean) => { machineForm.value.validate(async (valid: boolean) => {
@@ -256,10 +247,10 @@ const getReqForm = () => {
reqForm.authCertId = -1; reqForm.authCertId = -1;
} }
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) { if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1 reqForm.sshTunnelMachineId = -1;
}
return reqForm
} }
return reqForm;
};
const cancel = () => { const cancel = () => {
emit('update:visible', false); emit('update:visible', false);

View File

@@ -1,21 +1,27 @@
<template> <template>
<div> <div>
<page-table ref="pageTableRef" :query="queryConfig" v-model:query-form="params" :show-selection="true" <page-table
v-model:selection-data="state.selectionData" :data="data.list" :columns="columns" :total="data.total" ref="pageTableRef"
v-model:page-size="params.pageSize" v-model:page-num="params.pageNum" @pageChange="search()"> :query="queryConfig"
v-model:query-form="params"
:show-selection="true"
v-model:selection-data="state.selectionData"
:data="data.list"
:columns="columns"
:total="data.total"
v-model:page-size="params.pageSize"
v-model:page-num="params.pageNum"
@pageChange="search()"
>
<template #tagPathSelect> <template #tagPathSelect>
<el-select @focus="getTags" v-model="params.tagPath" placeholder="请选择标签" @clear="search" filterable <el-select @focus="getTags" v-model="params.tagPath" placeholder="请选择标签" @clear="search" filterable clearable style="width: 200px">
clearable style="width: 200px">
<el-option v-for="item in tags" :key="item" :label="item" :value="item"> </el-option> <el-option v-for="item in tags" :key="item" :label="item" :value="item"> </el-option>
</el-select> </el-select>
</template> </template>
<template #queryRight> <template #queryRight>
<el-button v-auth="perms.addMachine" type="primary" icon="plus" @click="openFormDialog(false)" plain>添加 <el-button v-auth="perms.addMachine" type="primary" icon="plus" @click="openFormDialog(false)" plain>添加 </el-button>
</el-button> <el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete">删除</el-button>
<el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()"
type="danger" icon="delete">删除</el-button>
</template> </template>
<template #tagPath="{ data }"> <template #tagPath="{ data }">
@@ -32,10 +38,18 @@
</template> </template>
<template #status="{ data }"> <template #status="{ data }">
<el-switch v-auth:disabled="'machine:update'" :width="52" v-model="data.status" :active-value="1" <el-switch
:inactive-value="-1" inline-prompt active-text="启用" inactive-text="停用" v-auth:disabled="'machine:update'"
:width="52"
v-model="data.status"
:active-value="1"
:inactive-value="-1"
inline-prompt
active-text="启用"
inactive-text="停用"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949" style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
@change="changeStatus(data)"></el-switch> @change="changeStatus(data)"
></el-switch>
</template> </template>
<template #action="{ data }"> <template #action="{ data }">
@@ -45,8 +59,7 @@
</span> </span>
<span v-auth="'machine:file'"> <span v-auth="'machine:file'">
<el-button type="success" :disabled="data.status == -1" @click="showFileManage(data)" <el-button type="success" :disabled="data.status == -1" @click="showFileManage(data)" link>文件</el-button>
link>文件</el-button>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
</span> </span>
@@ -62,25 +75,21 @@
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item :command="{ type: 'detail', data }"> <el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
详情
</el-dropdown-item>
<el-dropdown-item :command="{ type: 'edit', data }" v-if="actionBtns[perms.updateMachine]"> <el-dropdown-item :command="{ type: 'edit', data }" v-if="actionBtns[perms.updateMachine]"> 编辑 </el-dropdown-item>
编辑
</el-dropdown-item>
<el-dropdown-item :command="{ type: 'process', data }" :disabled="data.status == -1"> <el-dropdown-item :command="{ type: 'process', data }" :disabled="data.status == -1"> 进程 </el-dropdown-item>
进程
</el-dropdown-item>
<el-dropdown-item :command="{ type: 'terminalRec', data }" <el-dropdown-item :command="{ type: 'terminalRec', data }" v-if="actionBtns[perms.updateMachine] && data.enableRecorder == 1">
v-if="actionBtns[perms.updateMachine] && data.enableRecorder == 1">
终端回放 终端回放
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item :command="{ type: 'closeCli', data }" v-if="actionBtns[perms.closeCli]" <el-dropdown-item
:disabled="!data.hasCli || data.status == -1"> :command="{ type: 'closeCli', data }"
v-if="actionBtns[perms.closeCli]"
:disabled="!data.hasCli || data.status == -1"
>
关闭连接 关闭连接
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
@@ -106,39 +115,33 @@
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item> <el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} <el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
</el-descriptions-item> <el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} <el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item> <el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }}
</el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item> <el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
<machine-edit :title="machineEditDialog.title" v-model:visible="machineEditDialog.visible" <machine-edit
v-model:machine="machineEditDialog.data" @valChange="submitSuccess"></machine-edit> :title="machineEditDialog.title"
v-model:visible="machineEditDialog.visible"
v-model:machine="machineEditDialog.data"
@valChange="submitSuccess"
></machine-edit>
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" /> <process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" <script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
v-model:machineId="serviceDialog.machineId" />
<file-manage :title="fileDialog.title" v-model:visible="fileDialog.visible" <file-manage :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
v-model:machineId="fileDialog.machineId" />
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" <machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title"></machine-stats>
:title="machineStatsDialog.title"></machine-stats>
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" <machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title"></machine-rec>
:title="machineRecDialog.title"></machine-rec>
</div> </div>
</template> </template>
@@ -150,7 +153,7 @@ import { machineApi } from './api';
import { tagApi } from '../tag/api'; import { tagApi } from '../tag/api';
import { dateFormat } from '@/common/utils/date'; import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue'; import TagInfo from '../component/TagInfo.vue';
import PageTable from '@/components/pagetable/PageTable.vue' import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
@@ -163,33 +166,29 @@ const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue')); const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
const router = useRouter(); const router = useRouter();
const pageTableRef: any = ref(null) const pageTableRef: any = ref(null);
const perms = { const perms = {
addMachine: "machine:add", addMachine: 'machine:add',
updateMachine: "machine:update", updateMachine: 'machine:update',
delMachine: "machine:del", delMachine: 'machine:del',
terminal: "machine:terminal", terminal: 'machine:terminal',
closeCli: "machine:close-cli", closeCli: 'machine:close-cli',
} };
const queryConfig = [ const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.text('ip', 'IP'), TableQuery.text('name', '名称')];
TableQuery.slot("tagPath", "标签", "tagPathSelect"),
TableQuery.text("ip", "IP"),
TableQuery.text("name", "名称"),
]
const columns = ref([ const columns = ref([
TableColumn.new("tagPath", "标签路径").isSlot().setAddWidth(20), TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
TableColumn.new("name", "名称"), TableColumn.new('name', '名称'),
TableColumn.new("ipPort", "ip:port").isSlot().setAddWidth(35), TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(35),
TableColumn.new("username", "用户名"), TableColumn.new('username', '用户名'),
TableColumn.new("status", "状态").isSlot().setMinWidth(85), TableColumn.new('status', '状态').isSlot().setMinWidth(85),
TableColumn.new("remark", "备注"), TableColumn.new('remark', '备注'),
TableColumn.new("action", "操作").isSlot().setMinWidth(238).fixedRight().alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
]) ]);
// 该用户拥有的的操作列按钮权限使用v-if进行判断v-auth对el-dropdown-item无效 // 该用户拥有的的操作列按钮权限使用v-if进行判断v-auth对el-dropdown-item无效
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]) const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
const state = reactive({ const state = reactive({
tags: [] as any, tags: [] as any,
@@ -243,19 +242,8 @@ const state = reactive({
}, },
}); });
const { const { tags, params, data, infoDialog, selectionData, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineEditDialog, machineRecDialog } =
tags, toRefs(state);
params,
data,
infoDialog,
selectionData,
serviceDialog,
processDialog,
fileDialog,
machineStatsDialog,
machineEditDialog,
machineRecDialog,
} = toRefs(state)
onMounted(async () => { onMounted(async () => {
search(); search();
@@ -266,28 +254,28 @@ const handleCommand = (commond: any) => {
const type = commond.type; const type = commond.type;
console.log(type); console.log(type);
switch (type) { switch (type) {
case "detail": { case 'detail': {
showInfo(data); showInfo(data);
return; return;
} }
case "edit": { case 'edit': {
openFormDialog(data); openFormDialog(data);
return; return;
} }
case "process": { case 'process': {
showProcess(data); showProcess(data);
return; return;
} }
case "terminalRec": { case 'terminalRec': {
showRec(data); showRec(data);
return; return;
} }
case "closeCli": { case 'closeCli': {
closeCli(data); closeCli(data);
return; return;
} }
} }
} };
const showTerminal = (row: any) => { const showTerminal = (row: any) => {
const { href } = router.resolve({ const { href } = router.resolve({
@@ -331,12 +319,16 @@ const openFormDialog = async (machine: any) => {
const deleteMachine = async () => { const deleteMachine = async () => {
try { try {
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(", ")}】机器信息? 该操作将同时删除脚本及文件配置信息`, '提示', { await ElMessageBox.confirm(
`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】机器信息? 该操作将同时删除脚本及文件配置信息`,
'提示',
{
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}); }
await machineApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(",") }); );
await machineApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('操作成功'); ElMessage.success('操作成功');
search(); search();
} catch (err) {} } catch (err) {}
@@ -390,7 +382,7 @@ const search = async () => {
const showInfo = (info: any) => { const showInfo = (info: any) => {
state.infoDialog.data = info; state.infoDialog.data = info;
state.infoDialog.visible = true; state.infoDialog.visible = true;
} };
const showProcess = (row: any) => { const showProcess = (row: any) => {
state.processDialog.machineId = row.id; state.processDialog.machineId = row.id;

View File

@@ -1,7 +1,6 @@
<template> <template>
<div id="terminalRecDialog"> <div id="terminalRecDialog">
<el-dialog :title="title" v-model="dialogVisible" :before-close="handleClose" :close-on-click-modal="false" <el-dialog :title="title" v-model="dialogVisible" :before-close="handleClose" :close-on-click-modal="false" :destroy-on-close="true" width="70%">
:destroy-on-close="true" width="70%">
<div class="toolbar"> <div class="toolbar">
<el-select @change="getUsers" v-model="operateDate" placeholder="操作日期" filterable> <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-option v-for="item in operateDates" :key="item" :label="item" :value="item"> </el-option>
@@ -30,9 +29,9 @@ const props = defineProps({
visible: { type: Boolean }, visible: { type: Boolean },
machineId: { type: Number }, machineId: { type: Number },
title: { type: String }, title: { type: String },
}) });
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']) const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
const playerRef = ref(null); const playerRef = ref(null);
const state = reactive({ const state = reactive({
@@ -47,16 +46,7 @@ const state = reactive({
rec: '', rec: '',
}); });
const { const { dialogVisible, title, operateDates, operateDate, users, recs, user, rec } = toRefs(state);
dialogVisible,
title,
operateDates,
operateDate,
users,
recs,
user,
rec,
} = toRefs(state)
watch(props, async (newValue: any) => { watch(props, async (newValue: any) => {
const visible = newValue.visible; const visible = newValue.visible;

View File

@@ -1,7 +1,6 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="true" :destroy-on-close="true" <el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="true" :destroy-on-close="true" :before-close="cancel" width="1050px">
:before-close="cancel" width="1050px">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :lg="12" :md="12"> <el-col :lg="12" :md="12">
<el-descriptions size="small" title="基础信息" :column="2" border> <el-descriptions size="small" title="基础信息" :column="2" border>
@@ -20,8 +19,7 @@
<el-descriptions-item label="运行中任务"> <el-descriptions-item label="运行中任务">
{{ stats.RunningProcs }} {{ stats.RunningProcs }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="负载"> {{ stats.Load1 }} {{ stats.Load5 }} {{ stats.Load10 }} <el-descriptions-item label="负载"> {{ stats.Load1 }} {{ stats.Load5 }} {{ stats.Load10 }} </el-descriptions-item>
</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-col> </el-col>
@@ -38,8 +36,7 @@
<el-col :lg="8" :md="8"> <el-col :lg="8" :md="8">
<span style="font-size: 16px; font-weight: 700">磁盘</span> <span style="font-size: 16px; font-weight: 700">磁盘</span>
<el-table :data="stats.FSInfos" stripe max-height="250" style="width: 100%" border> <el-table :data="stats.FSInfos" stripe max-height="250" style="width: 100%" border>
<el-table-column prop="MountPoint" label="挂载点" min-width="100" show-overflow-tooltip> <el-table-column prop="MountPoint" label="挂载点" min-width="100" show-overflow-tooltip> </el-table-column>
</el-table-column>
<el-table-column prop="Used" label="可使用" min-width="70" show-overflow-tooltip> <el-table-column prop="Used" label="可使用" min-width="70" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
{{ formatByteSize(scope.row.Free) }} {{ formatByteSize(scope.row.Free) }}
@@ -57,10 +54,8 @@
<span style="font-size: 16px; font-weight: 700">网卡</span> <span style="font-size: 16px; font-weight: 700">网卡</span>
<el-table :data="netInter" stripe max-height="250" style="width: 100%" border> <el-table :data="netInter" stripe max-height="250" style="width: 100%" border>
<el-table-column prop="name" label="网卡" min-width="120" show-overflow-tooltip></el-table-column> <el-table-column prop="name" label="网卡" min-width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="IPv4" label="IPv4" min-width="130" show-overflow-tooltip> <el-table-column prop="IPv4" label="IPv4" min-width="130" show-overflow-tooltip> </el-table-column>
</el-table-column> <el-table-column prop="IPv6" label="IPv6" min-width="130" show-overflow-tooltip> </el-table-column>
<el-table-column prop="IPv6" label="IPv6" min-width="130" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="Rx" label="接收(rx)" min-width="110" show-overflow-tooltip> <el-table-column prop="Rx" label="接收(rx)" min-width="110" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
{{ formatByteSize(scope.row.Rx) }} {{ formatByteSize(scope.row.Rx) }}
@@ -80,7 +75,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, reactive, watch, ref, nextTick } from 'vue'; import { toRefs, reactive, watch, ref, nextTick } from 'vue';
import useEcharts from '@/common/echarts/useEcharts.ts'; import useEcharts from '@/common/echarts/useEcharts';
import tdTheme from '@/common/echarts/theme.json'; import tdTheme from '@/common/echarts/theme.json';
import { formatByteSize } from '@/common/utils/format'; import { formatByteSize } from '@/common/utils/format';
import { machineApi } from './api'; import { machineApi } from './api';
@@ -98,9 +93,9 @@ const props = defineProps({
title: { title: {
type: String, type: String,
}, },
}) });
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']) const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
const cpuRef: any = ref(); const cpuRef: any = ref();
const memRef: any = ref(); const memRef: any = ref();
@@ -114,13 +109,9 @@ const state = reactive({
netInter: [] as any, netInter: [] as any,
}); });
const { const { dialogVisible, stats, netInter } = toRefs(state);
dialogVisible,
stats,
netInter,
} = toRefs(state)
let charts = [] as any let charts = [] as any;
watch(props, async (newValue: any) => { watch(props, async (newValue: any) => {
const visible = newValue.visible; const visible = newValue.visible;

View File

@@ -1,7 +1,6 @@
<template> <template>
<div class="file-manage"> <div class="file-manage">
<el-dialog title="进程信息" v-model="dialogVisible" :destroy-on-close="true" :show-close="true" <el-dialog title="进程信息" v-model="dialogVisible" :destroy-on-close="true" :show-close="true" :before-close="handleClose" width="65%">
:before-close="handleClose" width="65%">
<div class="toolbar"> <div class="toolbar">
<el-row> <el-row>
<el-col :span="4"> <el-col :span="4">
@@ -22,8 +21,7 @@
</el-select> </el-select>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-button class="ml5" @click="getProcess" type="primary" icon="tickets" size="small" plain>刷新 <el-button class="ml5" @click="getProcess" type="primary" icon="tickets" size="small" plain>刷新 </el-button>
</el-button>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@@ -83,15 +81,13 @@
</el-tooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="command" label="command" :min-width="120" show-overflow-tooltip> <el-table-column prop="command" label="command" :min-width="120" show-overflow-tooltip> </el-table-column>
</el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template #default="scope"> <template #default="scope">
<el-popconfirm title="确定终止该进程?" @confirm="confirmKillProcess(scope.row.pid)"> <el-popconfirm title="确定终止该进程?" @confirm="confirmKillProcess(scope.row.pid)">
<template #reference> <template #reference>
<el-button v-auth="'machine:killprocess'" type="danger" icon="delete" size="small" <el-button v-auth="'machine:killprocess'" type="danger" icon="delete" size="small" plain>终止</el-button>
plain>终止</el-button>
</template> </template>
</el-popconfirm> </el-popconfirm>
<!-- <el-button @click="addFiles(scope.row)" type="danger" icon="delete" size="small" plain>终止</el-button> --> <!-- <el-button @click="addFiles(scope.row)" type="danger" icon="delete" size="small" plain>终止</el-button> -->
@@ -111,9 +107,9 @@ const props = defineProps({
visible: { type: Boolean }, visible: { type: Boolean },
machineId: { type: Number }, machineId: { type: Number },
title: { type: String }, title: { type: String },
}) });
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']) const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
@@ -126,12 +122,7 @@ const state = reactive({
processList: [], processList: [],
}); });
const { const { dialogVisible, params, processList } = toRefs(state);
dialogVisible,
params,
processList,
} = toRefs(state)
watch(props, (newValue) => { watch(props, (newValue) => {
if (props.machineId) { if (props.machineId) {

View File

@@ -1,7 +1,14 @@
<template> <template>
<div class="mock-data-dialog"> <div class="mock-data-dialog">
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :before-close="cancel" <el-dialog
:show-close="true" :destroy-on-close="true" width="900px"> :title="title"
v-model="dialogVisible"
:close-on-click-modal="false"
:before-close="cancel"
:show-close="true"
:destroy-on-close="true"
width="900px"
>
<el-form :model="form" ref="scriptForm" label-width="auto"> <el-form :model="form" ref="scriptForm" label-width="auto">
<el-form-item prop="method" label="名称"> <el-form-item prop="method" label="名称">
<el-input v-model="form.name" placeholder="请输入名称"></el-input> <el-input v-model="form.name" placeholder="请输入名称"></el-input>
@@ -13,8 +20,7 @@
<el-form-item prop="type" label="类型"> <el-form-item prop="type" label="类型">
<el-select v-model="form.type" default-first-option style="width: 100%" placeholder="请选择类型"> <el-select v-model="form.type" default-first-option style="width: 100%" placeholder="请选择类型">
<el-option v-for="item in enums.scriptTypeEnum as any" :key="item.value" :label="item.label" <el-option v-for="item in ScriptResultEnum" :key="item.value" :label="item.label" :value="item.value"></el-option>
:value="item.value"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -59,8 +65,7 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="cancel()" :disabled="submitDisabled"> </el-button> <el-button @click="cancel()" :disabled="submitDisabled"> </el-button>
<el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk" <el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk" :disabled="submitDisabled"> </el-button>
:disabled="submitDisabled"> </el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@@ -71,7 +76,7 @@
import { ref, toRefs, reactive, watch } from 'vue'; import { ref, toRefs, reactive, watch } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { machineApi } from './api'; import { machineApi } from './api';
import enums from './enums'; import { ScriptResultEnum } from './enums';
import { notEmpty } from '@/common/assert'; import { notEmpty } from '@/common/assert';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
@@ -91,9 +96,9 @@ const props = defineProps({
isCommon: { isCommon: {
type: Boolean, type: Boolean,
}, },
}) });
const emit = defineEmits(['update:visible', 'cancel', 'submitSuccess']) const emit = defineEmits(['update:visible', 'cancel', 'submitSuccess']);
const { isCommon, machineId } = toRefs(props); const { isCommon, machineId } = toRefs(props);
const scriptForm: any = ref(null); const scriptForm: any = ref(null);
@@ -114,13 +119,7 @@ const state = reactive({
btnLoading: false, btnLoading: false,
}); });
const { const { dialogVisible, submitDisabled, params, form, btnLoading } = toRefs(state);
dialogVisible,
submitDisabled,
params,
form,
btnLoading,
} = toRefs(state)
watch(props, (newValue: any) => { watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;

View File

@@ -1,52 +1,70 @@
<template> <template>
<div class="file-manage"> <div class="file-manage">
<el-dialog @open="getScripts()" :title="title" v-model="dialogVisible" :destroy-on-close="true" :show-close="true" <el-dialog
:before-close="handleClose" width="55%"> @open="getScripts()"
:title="title"
<page-table ref="pageTableRef" :query="queryConfig" v-model:query-form="query" :data="scriptTable" v-model="dialogVisible"
:columns="columns" :total="total" v-model:page-size="query.pageSize" v-model:page-num="query.pageNum" :destroy-on-close="true"
@pageChange="getScripts()" :show-selection="true" v-model:selection-data="selectionData"> :show-close="true"
:before-close="handleClose"
<template #typeSelect> width="55%"
<el-select v-model="type" placeholder="请选择"> >
<el-option :key="0" label="私有" :value="0"> </el-option> <page-table
<el-option :key="1" label="公共" :value="1"> </el-option> ref="pageTableRef"
</el-select> :query="queryConfig"
</template> v-model:query-form="query"
:data="scriptTable"
<template #type="{ data }"> :columns="columns"
{{ enums.scriptTypeEnum.getLabelByValue(data.type) }} :total="total"
</template> v-model:page-size="query.pageSize"
v-model:page-num="query.pageNum"
@pageChange="getScripts()"
:show-selection="true"
v-model:selection-data="selectionData"
>
<template #queryRight> <template #queryRight>
<el-button v-auth="'machine:script:save'" type="primary" @click="editScript(null)" icon="plus" <el-button v-auth="'machine:script:save'" type="primary" @click="editScript(null)" icon="plus" plain>添加</el-button>
plain>添加</el-button> <el-button
<el-button v-auth="'machine:script:del'" :disabled="selectionData.length < 1" type="danger" v-auth="'machine:script:del'"
@click="deleteRow(selectionData)" icon="delete" plain>删除</el-button> :disabled="selectionData.length < 1"
type="danger"
@click="deleteRow(selectionData)"
icon="delete"
plain
>删除</el-button
>
</template> </template>
<template #action="{ data }"> <template #action="{ data }">
<el-button v-auth="'machine:script:run'" v-if="data.id != null" @click="runScript(data)" type="primary" <el-button v-auth="'machine:script:run'" v-if="data.id != null" @click="runScript(data)" type="primary" icon="video-play" link
icon="video-play" link>执行 >执行
</el-button> </el-button>
<el-button @click="editScript(data)" type="primary" icon="tickets" link>查看 <el-button @click="editScript(data)" type="primary" icon="tickets" link>查看 </el-button>
</el-button>
</template> </template>
</page-table> </page-table>
</el-dialog> </el-dialog>
<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="auto"> <el-form ref="paramsForm" :model="scriptParamsDialog.params" label-width="auto">
<el-form-item v-for="item in scriptParamsDialog.paramsFormItem as any" :key="item.name" :prop="item.model" <el-form-item v-for="item in scriptParamsDialog.paramsFormItem as any" :key="item.name" :prop="item.model" :label="item.name" required>
:label="item.name" required> <el-input
<el-input v-if="!item.options" v-model="scriptParamsDialog.params[item.model]" v-if="!item.options"
:placeholder="item.placeholder" autocomplete="off" clearable></el-input> v-model="scriptParamsDialog.params[item.model]"
<el-select v-else v-model="scriptParamsDialog.params[item.model]" :placeholder="item.placeholder" :placeholder="item.placeholder"
filterable autocomplete="off" clearable style="width: 100%"> autocomplete="off"
<el-option v-for="option in item.options.split(',')" :key="option" :label="option" clearable
:value="option" /> ></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-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -63,13 +81,26 @@
</div> </div>
</el-dialog> </el-dialog>
<el-dialog v-if="terminalDialog.visible" title="终端" v-model="terminalDialog.visible" width="80%" <el-dialog
:close-on-click-modal="false" :modal="false" @close="closeTermnial"> v-if="terminalDialog.visible"
title="终端"
v-model="terminalDialog.visible"
width="80%"
:close-on-click-modal="false"
:modal="false"
@close="closeTermnial"
>
<ssh-terminal ref="terminal" :cmd="terminalDialog.cmd" :machineId="terminalDialog.machineId" height="560px" /> <ssh-terminal ref="terminal" :cmd="terminalDialog.cmd" :machineId="terminalDialog.machineId" height="560px" />
</el-dialog> </el-dialog>
<script-edit v-model:visible="editDialog.visible" v-model:data="editDialog.data" :title="editDialog.title" <script-edit
v-model:machineId="editDialog.machineId" :isCommon="type == 1" @submitSuccess="submitSuccess" /> v-model:visible="editDialog.visible"
v-model:data="editDialog.data"
:title="editDialog.title"
v-model:machineId="editDialog.machineId"
:isCommon="state.query.type == ScriptTypeEnum.Public.value"
@submitSuccess="submitSuccess"
/>
</div> </div>
</template> </template>
@@ -78,37 +109,35 @@ import { ref, toRefs, reactive, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import SshTerminal from './SshTerminal.vue'; import SshTerminal from './SshTerminal.vue';
import { machineApi } from './api'; import { machineApi } from './api';
import enums from './enums'; import { ScriptResultEnum, ScriptTypeEnum } from './enums';
import ScriptEdit from './ScriptEdit.vue'; import ScriptEdit from './ScriptEdit.vue';
import PageTable from '@/components/pagetable/PageTable.vue' import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
const props = defineProps({ const props = defineProps({
visible: { type: Boolean }, visible: { type: Boolean },
machineId: { type: Number }, machineId: { type: Number },
title: { type: String }, title: { type: String },
}) });
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']) const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
const paramsForm: any = ref(null); const paramsForm: any = ref(null);
const pageTableRef: any = ref(null); const pageTableRef: any = ref(null);
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
type: 0,
selectionData: [], selectionData: [],
queryConfig: [ queryConfig: [TableQuery.select('type', '类型').setOptions(Object.values(ScriptTypeEnum))],
TableQuery.slot("type", "类型", "typeSelect"),
],
columns: [ columns: [
TableColumn.new("name", "名称"), TableColumn.new('name', '名称'),
TableColumn.new("description", "描述"), TableColumn.new('description', '描述'),
TableColumn.new("type", "类型").isSlot().setAddWidth(5), TableColumn.new('type', '类型').isEnum(ScriptResultEnum),
TableColumn.new("action", "操作").isSlot().setMinWidth(130).alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(130).alignCenter(),
], ],
query: { query: {
machineId: 0 as any, machineId: 0 as any,
type: ScriptTypeEnum.Private.value,
pageNum: 1, pageNum: 1,
pageSize: 6, pageSize: 6,
}, },
@@ -137,20 +166,8 @@ const state = reactive({
}, },
}); });
const { const { dialogVisible, queryConfig, columns, selectionData, query, editDialog, total, scriptTable, scriptParamsDialog, resultDialog, terminalDialog } =
dialogVisible, toRefs(state);
queryConfig,
columns,
type,
selectionData,
query,
editDialog,
total,
scriptTable,
scriptParamsDialog,
resultDialog,
terminalDialog,
} = toRefs(state)
watch(props, async (newValue) => { watch(props, async (newValue) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
@@ -159,13 +176,13 @@ watch(props, async (newValue) => {
const getScripts = async () => { const getScripts = async () => {
try { try {
// 通过open事件才开获取到pageTableRef值 // 通过open事件才开获取到pageTableRef值
pageTableRef.value.loading(true) pageTableRef.value.loading(true);
state.query.machineId = state.type == 0 ? props.machineId : 9999999; state.query.machineId = state.query.type == ScriptTypeEnum.Private.value ? props.machineId : 9999999;
const res = await machineApi.scripts.request(state.query); const res = await machineApi.scripts.request(state.query);
state.scriptTable = res.list; state.scriptTable = res.list;
state.total = res.total; state.total = res.total;
} finally { } finally {
pageTableRef.value.loading(false) pageTableRef.value.loading(false);
} }
}; };
@@ -202,9 +219,9 @@ const hasParamsRun = async () => {
}; };
const run = async (script: any) => { const run = async (script: any) => {
const noResult = script.type == enums.scriptTypeEnum['NO_RESULT'].value; const noResult = script.type == ScriptResultEnum.NoResult.value;
// 如果脚本类型为有结果类型,则显示结果信息 // 如果脚本类型为有结果类型,则显示结果信息
if (script.type == enums.scriptTypeEnum['RESULT'].value || noResult) { if (script.type == ScriptResultEnum.Result.value || noResult) {
const res = await machineApi.runScript.request({ const res = await machineApi.runScript.request({
machineId: props.machineId, machineId: props.machineId,
scriptId: script.id, scriptId: script.id,
@@ -220,7 +237,7 @@ const run = async (script: any) => {
return; return;
} }
if (script.type == enums.scriptTypeEnum['REAL_TIME'].value) { if (script.type == ScriptResultEnum.RealTime.value) {
script = script.script; script = script.script;
if (state.scriptParamsDialog.params) { if (state.scriptParamsDialog.params) {
script = templateResolve(script, state.scriptParamsDialog.params); script = templateResolve(script, state.scriptParamsDialog.params);
@@ -251,7 +268,6 @@ const closeTermnial = () => {
state.terminalDialog.machineId = 0; state.terminalDialog.machineId = 0;
}; };
const editScript = (data: any) => { const editScript = (data: any) => {
state.editDialog.machineId = props.machineId as any; state.editDialog.machineId = props.machineId as any;
state.editDialog.data = data; state.editDialog.data = data;
@@ -268,7 +284,7 @@ const submitSuccess = () => {
}; };
const deleteRow = (rows: any) => { const deleteRow = (rows: any) => {
ElMessageBox.confirm(`此操作将删除【${rows.map((x: any) => x.name).join(", ")}】脚本信息, 是否继续?`, '提示', { ElMessageBox.confirm(`此操作将删除【${rows.map((x: any) => x.name).join(', ')}】脚本信息, 是否继续?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
@@ -276,7 +292,7 @@ const deleteRow = (rows: any) => {
machineApi.deleteScript machineApi.deleteScript
.request({ .request({
machineId: props.machineId, machineId: props.machineId,
scriptId: rows.map((x: any) => x.id).join(","), scriptId: rows.map((x: any) => x.id).join(','),
}) })
.then(() => { .then(() => {
getScripts(); getScripts();
@@ -292,10 +308,9 @@ const handleClose = () => {
emit('update:visible', false); emit('update:visible', false);
emit('update:machineId', null); emit('update:machineId', null);
emit('cancel'); emit('cancel');
state.type = 0; state.query.type = ScriptTypeEnum.Private.value;
state.scriptTable = []; state.scriptTable = [];
state.scriptParamsDialog.paramsFormItem = []; state.scriptParamsDialog.paramsFormItem = [];
}; };
</script> </script>
<style lang="sass"> <style lang="sass"></style>
</style>

View File

@@ -16,7 +16,7 @@ const props = defineProps({
machineId: { type: Number }, machineId: { type: Number },
cmd: { type: String }, cmd: { type: String },
height: { type: [String, Number] }, height: { type: [String, Number] },
}) });
const { themeConfig } = storeToRefs(useThemeConfig()); const { themeConfig } = storeToRefs(useThemeConfig());
const state = reactive({ const state = reactive({
@@ -42,7 +42,6 @@ onBeforeUnmount(() => {
closeAll(); closeAll();
}); });
function initXterm() { function initXterm() {
const term: any = new Terminal({ const term: any = new Terminal({
fontSize: themeConfig.value.terminalFontSize || 15, fontSize: themeConfig.value.terminalFontSize || 15,
@@ -104,8 +103,7 @@ function initXterm() {
let pingInterval: any; let pingInterval: any;
function initSocket() { function initSocket() {
state.socket = new WebSocket( state.socket = new WebSocket(
`${config.baseWsUrl}/machines/${props.machineId}/terminal?token=${getSession('token')}&cols=${state.term.cols}&rows=${state.term.rows `${config.baseWsUrl}/machines/${props.machineId}/terminal?token=${getSession('token')}&cols=${state.term.cols}&rows=${state.term.rows}`
}`
); );
// 监听socket连接 // 监听socket连接
@@ -175,7 +173,7 @@ function closeAll() {
<style lang="scss"> <style lang="scss">
#xterm { #xterm {
.xterm-viewport { .xterm-viewport {
overflow-y: hidden overflow-y: hidden;
} }
} }
</style> </style>

View File

@@ -15,16 +15,11 @@ const state = reactive({
height: 0, height: 0,
}); });
const { const { machineId, height } = toRefs(state);
machineId,
height,
} = toRefs(state)
onMounted(() => { onMounted(() => {
state.height = window.innerHeight + 5; state.height = window.innerHeight + 5;
state.machineId = Number.parseInt(route.query.id as string); state.machineId = Number.parseInt(route.query.id as string);
}); });
</script> </script>
<style lang="scss"> <style lang="scss"></style>
</style>

View File

@@ -2,47 +2,47 @@ import Api from '@/common/Api';
export const machineApi = { export const machineApi = {
// 获取权限列表 // 获取权限列表
list: Api.newGet("/machines"), list: Api.newGet('/machines'),
getMachinePwd: Api.newGet("/machines/{id}/pwd"), getMachinePwd: Api.newGet('/machines/{id}/pwd'),
info: Api.newGet("/machines/{id}/sysinfo"), info: Api.newGet('/machines/{id}/sysinfo'),
stats: Api.newGet("/machines/{id}/stats"), stats: Api.newGet('/machines/{id}/stats'),
process: Api.newGet("/machines/{id}/process"), process: Api.newGet('/machines/{id}/process'),
// 终止进程 // 终止进程
killProcess: Api.newDelete("/machines/{id}/process"), killProcess: Api.newDelete('/machines/{id}/process'),
closeCli: Api.newDelete("/machines/{id}/close-cli"), closeCli: Api.newDelete('/machines/{id}/close-cli'),
testConn: Api.newPost("/machines/test-conn"), testConn: Api.newPost('/machines/test-conn'),
// 保存按钮 // 保存按钮
saveMachine: Api.newPost("/machines"), saveMachine: Api.newPost('/machines'),
// 调整状态 // 调整状态
changeStatus: Api.newPut("/machines/{id}/{status}"), changeStatus: Api.newPut('/machines/{id}/{status}'),
// 删除机器 // 删除机器
del: Api.newDelete("/machines/{id}"), del: Api.newDelete('/machines/{id}'),
scripts: Api.newGet("/machines/{machineId}/scripts"), scripts: Api.newGet('/machines/{machineId}/scripts'),
runScript: Api.newGet("/machines/{machineId}/scripts/{scriptId}/run"), runScript: Api.newGet('/machines/{machineId}/scripts/{scriptId}/run'),
saveScript: Api.newPost("/machines/{machineId}/scripts"), saveScript: Api.newPost('/machines/{machineId}/scripts'),
deleteScript: Api.newDelete("/machines/{machineId}/scripts/{scriptId}"), deleteScript: Api.newDelete('/machines/{machineId}/scripts/{scriptId}'),
// 获取配置文件列表 // 获取配置文件列表
files: Api.newGet("/machines/{id}/files"), files: Api.newGet('/machines/{id}/files'),
lsFile: Api.newGet("/machines/{machineId}/files/{fileId}/read-dir"), lsFile: Api.newGet('/machines/{machineId}/files/{fileId}/read-dir'),
dirSize: Api.newGet("/machines/{machineId}/files/{fileId}/dir-size"), dirSize: Api.newGet('/machines/{machineId}/files/{fileId}/dir-size'),
fileStat: Api.newGet("/machines/{machineId}/files/{fileId}/file-stat"), fileStat: Api.newGet('/machines/{machineId}/files/{fileId}/file-stat'),
rmFile: Api.newDelete("/machines/{machineId}/files/{fileId}/remove"), rmFile: Api.newDelete('/machines/{machineId}/files/{fileId}/remove'),
uploadFile: Api.newPost("/machines/{machineId}/files/{fileId}/upload?token={token}"), uploadFile: Api.newPost('/machines/{machineId}/files/{fileId}/upload?token={token}'),
fileContent: Api.newGet("/machines/{machineId}/files/{fileId}/read"), fileContent: Api.newGet('/machines/{machineId}/files/{fileId}/read'),
createFile: Api.newPost("/machines/{machineId}/files/{id}/create-file"), createFile: Api.newPost('/machines/{machineId}/files/{id}/create-file'),
// 修改文件内容 // 修改文件内容
updateFileContent: Api.newPost("/machines/{machineId}/files/{id}/write"), updateFileContent: Api.newPost('/machines/{machineId}/files/{id}/write'),
// 添加文件or目录 // 添加文件or目录
addConf: Api.newPost("/machines/{machineId}/files"), addConf: Api.newPost('/machines/{machineId}/files'),
// 删除配置的文件or目录 // 删除配置的文件or目录
delConf: Api.newDelete("/machines/{machineId}/files/{id}"), delConf: Api.newDelete('/machines/{machineId}/files/{id}'),
terminal: Api.newGet("/api/machines/{id}/terminal"), terminal: Api.newGet('/api/machines/{id}/terminal'),
recDirNames: Api.newGet("/machines/rec/names") recDirNames: Api.newGet('/machines/rec/names'),
} };
export const authCertApi = { export const authCertApi = {
baseList : Api.newGet("/sys/authcerts/base"), baseList: Api.newGet('/sys/authcerts/base'),
list: Api.newGet("/sys/authcerts"), list: Api.newGet('/sys/authcerts'),
save: Api.newPost("/sys/authcerts"), save: Api.newPost('/sys/authcerts'),
delete: Api.newDelete("/sys/authcerts/{id}"), delete: Api.newDelete('/sys/authcerts/{id}'),
} };

View File

@@ -1,7 +1,6 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" <el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px" :destroy-on-close="true">
:destroy-on-close="true">
<el-form ref="acForm" :rules="rules" :model="form" label-width="auto"> <el-form ref="acForm" :rules="rules" :model="form" label-width="auto">
<el-form-item prop="name" label="名称:" required> <el-form-item prop="name" label="名称:" required>
<el-input v-model="form.name"></el-input> <el-input v-model="form.name"></el-input>
@@ -13,17 +12,14 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="form.authMethod == 1" prop="password" label="密码:"> <el-form-item v-if="form.authMethod == 1" prop="password" label="密码:">
<el-input type="password" show-password clearable v-model.trim="form.password" placeholder="请输入密码" <el-input type="password" show-password clearable v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
autocomplete="new-password">
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item v-if="form.authMethod == 2" prop="password" label="秘钥:"> <el-form-item v-if="form.authMethod == 2" prop="password" label="秘钥:">
<el-input type="textarea" :rows="5" v-model="form.password" placeholder="请将私钥文件内容拷贝至此"> <el-input type="textarea" :rows="5" v-model="form.password" placeholder="请将私钥文件内容拷贝至此"> </el-input>
</el-input>
</el-form-item> </el-form-item>
<el-form-item v-if="form.authMethod == 2" prop="passphrase" label="秘钥密码:"> <el-form-item v-if="form.authMethod == 2" prop="passphrase" label="秘钥密码:">
<el-input type="password" v-model="form.passphrase"> <el-input type="password" v-model="form.passphrase"> </el-input>
</el-input>
</el-form-item> </el-form-item>
<el-form-item label="备注:"> <el-form-item label="备注:">
@@ -54,10 +50,10 @@ const props = defineProps({
title: { title: {
type: String, type: String,
}, },
}) });
//定义事件 //定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']) const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const acForm: any = ref(null); const acForm: any = ref(null);
@@ -68,8 +64,8 @@ const rules = {
message: '授权凭证名称不能为空', message: '授权凭证名称不能为空',
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
] ],
} };
const state = reactive({ const state = reactive({
dvisible: false, dvisible: false,
@@ -85,11 +81,7 @@ const state = reactive({
btnLoading: false, btnLoading: false,
}); });
const { const { dvisible, form, btnLoading } = toRefs(state);
dvisible,
form,
btnLoading,
} = toRefs(state)
watch(props, (newValue: any) => { watch(props, (newValue: any) => {
state.dvisible = newValue.visible; state.dvisible = newValue.visible;

View File

@@ -1,29 +1,28 @@
<template> <template>
<div> <div>
<page-table :query="state.queryConfig" v-model:query-form="query" :show-selection="true" <page-table
v-model:selection-data="selectionData" :data="authcerts" :columns="state.columns" :total="total" :query="state.queryConfig"
v-model:page-size="query.pageSize" v-model:page-num="query.pageNum" @pageChange="search()"> v-model:query-form="query"
:show-selection="true"
v-model:selection-data="selectionData"
:data="authcerts"
:columns="state.columns"
:total="total"
v-model:page-size="query.pageSize"
v-model:page-num="query.pageNum"
@pageChange="search()"
>
<template #queryRight> <template #queryRight>
<el-button type="primary" icon="plus" @click="edit(false)">添加</el-button> <el-button type="primary" icon="plus" @click="edit(false)">添加</el-button>
<el-button :disabled="selectionData.length < 1" @click="deleteAc(selectionData)" type="danger" <el-button :disabled="selectionData.length < 1" @click="deleteAc(selectionData)" type="danger" icon="delete">删除 </el-button>
icon="delete">删除
</el-button>
</template>
<template #authMethod="{ data }">
<el-tag v-if="data.authMethod == 1" type="success" size="small">密码</el-tag>
<el-tag v-if="data.authMethod == 2" size="small">密钥</el-tag>
</template> </template>
<template #action="{ data }"> <template #action="{ data }">
<el-button @click="edit(data)" type="primary" link>编辑 <el-button @click="edit(data)" type="primary" link>编辑 </el-button>
</el-button>
</template> </template>
</page-table> </page-table>
<auth-cert-edit :title="editor.title" v-model:visible="editor.visible" :data="editor.authcert" <auth-cert-edit :title="editor.title" v-model:visible="editor.visible" :data="editor.authcert" @val-change="editChange" />
@val-change="editChange" />
</div> </div>
</template> </template>
@@ -32,8 +31,9 @@ import { toRefs, reactive, onMounted } from 'vue';
import AuthCertEdit from './AuthCertEdit.vue'; import AuthCertEdit from './AuthCertEdit.vue';
import { authCertApi } from '../api'; import { authCertApi } from '../api';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import PageTable from '@/components/pagetable/PageTable.vue' import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
import { AuthMethodEnum } from '../enums';
const state = reactive({ const state = reactive({
query: { query: {
@@ -41,18 +41,16 @@ const state = reactive({
pageSize: 10, pageSize: 10,
name: null, name: null,
}, },
queryConfig: [ queryConfig: [TableQuery.text('name', '凭证名称')],
TableQuery.text("name", "凭证名称"),
],
columns: [ columns: [
TableColumn.new("name", "名称"), TableColumn.new('name', '名称'),
TableColumn.new("authMethod", "认证方式").isSlot(), TableColumn.new('authMethod', '认证方式').typeTag(AuthMethodEnum),
TableColumn.new("remark", "备注"), TableColumn.new('remark', '备注'),
TableColumn.new("creator", "创建人"), TableColumn.new('creator', '创建人'),
TableColumn.new("createTime", "创建时间").isTime(), TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new("creator", "修改者"), TableColumn.new('creator', '修改者'),
TableColumn.new("createTime", "修改时间").isTime(), TableColumn.new('createTime', '修改时间').isTime(),
TableColumn.new("action", "操作").isSlot().fixedRight().setMinWidth(65).alignCenter(), TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(65).alignCenter(),
], ],
total: 0, total: 0,
authcerts: [], authcerts: [],
@@ -70,13 +68,7 @@ const state = reactive({
}, },
}); });
const { const { query, total, authcerts, selectionData, editor } = toRefs(state);
query,
total,
authcerts,
selectionData,
editor,
} = toRefs(state)
onMounted(() => { onMounted(() => {
search(); search();
@@ -105,16 +97,15 @@ const edit = (data: any) => {
const deleteAc = async (data: any) => { const deleteAc = async (data: any) => {
try { try {
await ElMessageBox.confirm(`确定删除该【${data.map((x: any) => x.name).join(", ")}授权凭证?`, '提示', { await ElMessageBox.confirm(`确定删除该【${data.map((x: any) => x.name).join(', ')}授权凭证?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}); });
await authCertApi.delete.request({ id: data.map((x: any) => x.id).join(",") }); await authCertApi.delete.request({ id: data.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功'); ElMessage.success('删除成功');
search(); search();
} catch (err) {} } catch (err) {}
};
}
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -1,7 +1,6 @@
<template> <template>
<div style="width: 100%"> <div style="width: 100%">
<el-select @change="changeValue" v-model="id" filterable placeholder="请选择授权凭证,可前往[机器管理->授权凭证]添加" <el-select @change="changeValue" v-model="id" filterable placeholder="请选择授权凭证,可前往[机器管理->授权凭证]添加" style="width: 100%">
style="width: 100%">
<el-option v-for="ac in acs" :key="ac.id" :value="ac.id" :label="ac.name"> <el-option v-for="ac in acs" :key="ac.id" :value="ac.id" :label="ac.name">
<el-tag v-if="ac.authMethod == 1" type="success" size="small">密码</el-tag> <el-tag v-if="ac.authMethod == 1" type="success" size="small">密码</el-tag>
<el-tag v-if="ac.authMethod == 2" size="small">密钥</el-tag> <el-tag v-if="ac.authMethod == 2" size="small">密钥</el-tag>
@@ -21,43 +20,38 @@ import { reactive, toRefs, onMounted } from 'vue';
import { authCertApi } from '../api'; import { authCertApi } from '../api';
//定义事件 //定义事件
const emit = defineEmits(['update:modelValue', 'change']) const emit = defineEmits(['update:modelValue', 'change']);
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: [Number], type: [Number],
required: true, required: true,
}, },
}) });
const state = reactive({ const state = reactive({
acs: [] as any, acs: [] as any,
id: null as any, id: null as any,
}) });
const { const { acs, id } = toRefs(state);
acs,
id,
} = toRefs(state)
onMounted(async () => { onMounted(async () => {
await getAcs(); await getAcs();
if (props.modelValue) { if (props.modelValue) {
state.id = props.modelValue; state.id = props.modelValue;
} }
}) });
const changeValue = (val: any) => { const changeValue = (val: any) => {
emit('update:modelValue', val); emit('update:modelValue', val);
emit('change', val); emit('change', val);
} };
const getAcs = async () => { const getAcs = async () => {
const acs = await authCertApi.baseList.request({ pageSize: 100, type: 2 }) const acs = await authCertApi.baseList.request({ pageSize: 100, type: 2 });
state.acs = acs.list; state.acs = acs.list;
} };
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -1,8 +1,26 @@
import { Enum } from '@/common/Enum'; import { EnumValue } from '@/common/Enum';
// 脚本执行结果类型
export const ScriptResultEnum = {
Result: EnumValue.of(1, '有结果'),
NoResult: EnumValue.of(2, '无结果'),
RealTime: EnumValue.of(3, '实时交互'),
};
// 脚本类型
export const ScriptTypeEnum = {
Private: EnumValue.of(1, '私有'),
Public: EnumValue.of(2, '公共'),
};
export default {
// uri请求方法
scriptTypeEnum: new Enum().add('RESULT', '有结果', 1).add('NO_RESULT', '无结果', 2).add('REAL_TIME', '实时交互', 3),
// 文件类型枚举 // 文件类型枚举
FileTypeEnum: new Enum().add('DIRECTORY', '目录', 1).add('FILE', '文件', 2), export const FileTypeEnum = {
} Directory: EnumValue.of(1, '目录'),
File: EnumValue.of(2, '文件'),
};
// 授权凭证认证方式枚举
export const AuthMethodEnum = {
Password: EnumValue.of(1, '密码').tagTypeSuccess(),
PrivateKey: EnumValue.of(2, '秘钥'),
};

View File

@@ -20,16 +20,13 @@
<SvgIcon v-if="data.type == NodeType.Dbs" name="Coin" color="#67c23a" /> <SvgIcon v-if="data.type == NodeType.Dbs" name="Coin" color="#67c23a" />
<SvgIcon v-if="data.type == NodeType.Coll || data.type == NodeType.CollMenu" name="Document" <SvgIcon v-if="data.type == NodeType.Coll || data.type == NodeType.CollMenu" name="Document" class="color-primary" />
class="color-primary" />
</template> </template>
<template #label="{ data }"> <template #label="{ data }">
<span v-if="data.type == NodeType.Dbs"> <span v-if="data.type == NodeType.Dbs">
{{ data.params.dbName }} {{ data.params.dbName }}
<span style="color: #8492a6;font-size: 13px"> <span style="color: #8492a6; font-size: 13px"> [{{ formatByteSize(data.params.size) }}] </span>
[{{ formatByteSize(data.params.size) }}]
</span>
</span> </span>
<span v-else>{{ data.label }}</span> <span v-else>{{ data.label }}</span>
@@ -44,17 +41,17 @@
<el-row> <el-row>
<el-col :span="2"> <el-col :span="2">
<div> <div>
<el-link @click="findCommand(state.activeName)" icon="refresh" :underline="false" <el-link @click="findCommand(state.activeName)" icon="refresh" :underline="false" class=""> </el-link>
class=""> <el-link @click="onEditDoc(null)" class="ml5" type="primary" icon="plus" :underline="false"> </el-link>
</el-link>
<el-link @click="onEditDoc(null)" class="ml5" type="primary" icon="plus"
:underline="false">
</el-link>
</div> </div>
</el-col> </el-col>
<el-col :span="22"> <el-col :span="22">
<el-input ref="findParamInputRef" v-model="dt.findParamStr" placeholder="点击输入相应查询条件" <el-input
@focus="showFindDialog(dt.key)"> ref="findParamInputRef"
v-model="dt.findParamStr"
placeholder="点击输入相应查询条件"
@focus="showFindDialog(dt.key)"
>
<template #prepend>查询参数</template> <template #prepend>查询参数</template>
</el-input> </el-input>
</el-col> </el-col>
@@ -65,8 +62,7 @@
<el-input type="textarea" v-model="item.value" :rows="10" /> <el-input type="textarea" v-model="item.value" :rows="10" />
<div style="padding: 3px; float: right" class="mr5 mongo-doc-btns"> <div style="padding: 3px; float: right" class="mr5 mongo-doc-btns">
<div> <div>
<el-link @click="onEditDoc(item)" :underline="false" type="success" <el-link @click="onEditDoc(item)" :underline="false" type="success" icon="MagicStick"></el-link>
icon="MagicStick"></el-link>
<!-- <el-divider direction="vertical" border-style="dashed" /> --> <!-- <el-divider direction="vertical" border-style="dashed" /> -->
@@ -77,8 +73,7 @@
<el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?"> <el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?">
<template #reference> <template #reference>
<el-link :underline="false" type="danger" icon="DocumentDelete"> <el-link :underline="false" type="danger" icon="DocumentDelete"> </el-link>
</el-link>
</template> </template>
</el-popconfirm> </el-popconfirm>
</div> </div>
@@ -90,18 +85,15 @@
</el-tabs> </el-tabs>
</el-container> </el-container>
</el-col> </el-col>
</el-row> </el-row>
<el-dialog width="600px" title="find参数" v-model="findDialog.visible"> <el-dialog width="600px" title="find参数" v-model="findDialog.visible">
<el-form label-width="auto"> <el-form label-width="auto">
<el-form-item label="filter"> <el-form-item label="filter">
<monaco-editor style="width: 100%;" height="150px" ref="monacoEditorRef" <monaco-editor style="width: 100%" height="150px" ref="monacoEditorRef" v-model="findDialog.findParam.filter" language="json" />
v-model="findDialog.findParam.filter" language="json" />
</el-form-item> </el-form-item>
<el-form-item label="sort"> <el-form-item label="sort">
<el-input v-model="findDialog.findParam.sort" type="textarea" :rows="3" clearable <el-input v-model="findDialog.findParam.sort" type="textarea" :rows="3" clearable auto-complete="off"></el-input>
auto-complete="off"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="limit"> <el-form-item label="limit">
<el-input v-model.number="findDialog.findParam.limit" type="number" auto-complete="off"></el-input> <el-input v-model.number="findDialog.findParam.limit" type="number" auto-complete="off"></el-input>
@@ -118,8 +110,12 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog width="60%" :title="`${state.docEditDialog.isAdd ? '新增' : '修改'}'${state.activeName}'集合文档`" <el-dialog
v-model="docEditDialog.visible" :close-on-click-modal="false"> width="60%"
:title="`${state.docEditDialog.isAdd ? '新增' : '修改'}'${state.activeName}'集合文档`"
v-model="docEditDialog.visible"
:close-on-click-modal="false"
>
<monaco-editor v-model="docEditDialog.doc" language="json" /> <monaco-editor v-model="docEditDialog.doc" language="json" />
<template #footer> <template #footer>
<div> <div>
@@ -149,10 +145,10 @@ const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/Mona
* 树节点类型 * 树节点类型
*/ */
class NodeType { class NodeType {
static Mongo = 1 static Mongo = 1;
static Dbs = 2 static Dbs = 2;
static CollMenu = 3 static CollMenu = 3;
static Coll = 4 static Coll = 4;
} }
const findParamInputRef: any = ref(null); const findParamInputRef: any = ref(null);
@@ -187,11 +183,7 @@ const state = reactive({
}, },
}); });
const { const { dataHeight, findDialog, docEditDialog } = toRefs(state);
dataHeight,
findDialog,
docEditDialog,
} = toRefs(state)
/** /**
* instmap; tagPaht -> mongo info[] * instmap; tagPaht -> mongo info[]
@@ -199,15 +191,15 @@ const {
const instMap: Map<string, any[]> = new Map(); const instMap: Map<string, any[]> = new Map();
const getInsts = async () => { const getInsts = async () => {
const res = await mongoApi.mongoList.request({ pageNum: 1, pageSize: 1000, }); const res = await mongoApi.mongoList.request({ pageNum: 1, pageSize: 1000 });
if (!res.total) return if (!res.total) return;
for (const mongoInfo of res.list) { for (const mongoInfo of res.list) {
const tagPath = mongoInfo.tagPath; const tagPath = mongoInfo.tagPath;
let mongoInsts = instMap.get(tagPath) || []; let mongoInsts = instMap.get(tagPath) || [];
mongoInsts.push(mongoInfo); mongoInsts.push(mongoInfo);
instMap.set(tagPath, mongoInsts); instMap.set(tagPath, mongoInsts);
} }
} };
/** /**
* 加载文件树节点 * 加载文件树节点
@@ -232,7 +224,7 @@ const loadNode = async (node: any) => {
// 点击标签 -> 显示mongo信息列表 // 点击标签 -> 显示mongo信息列表
if (nodeType === TagTreeNode.TagPath) { if (nodeType === TagTreeNode.TagPath) {
const mongoInfos = instMap.get(data.key) const mongoInfos = instMap.get(data.key);
return mongoInfos?.map((x: any) => { return mongoInfos?.map((x: any) => {
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Mongo).withParams(x); return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Mongo).withParams(x);
}); });
@@ -250,7 +242,7 @@ const loadNode = async (node: any) => {
// 点击数据库集合节点 -> 加载集合列表 // 点击数据库集合节点 -> 加载集合列表
if (nodeType === NodeType.CollMenu) { if (nodeType === NodeType.CollMenu) {
return await getCollections(params.id, params.dbName) return await getCollections(params.id, params.dbName);
} }
return []; return [];
@@ -268,9 +260,9 @@ const getDatabases = async (inst: any) => {
id: inst.id, id: inst.id,
dbName, dbName,
size: x.SizeOnDisk, size: x.SizeOnDisk,
}) });
}) });
} };
/** /**
* 获取集合列表信息 * 获取集合列表信息
@@ -285,7 +277,7 @@ const getCollections = async (id: any, database: string) => {
collection: x, collection: x,
}); });
}); });
} };
const nodeClick = (data: any) => { const nodeClick = (data: any) => {
// 点击集合 // 点击集合
@@ -293,7 +285,7 @@ const nodeClick = (data: any) => {
const { id, database, collection } = data.params; const { id, database, collection } = data.params;
changeCollection(id, database, collection); changeCollection(id, database, collection);
} }
} };
const changeCollection = (id: any, schema: string, collection: string) => { const changeCollection = (id: any, schema: string, collection: string) => {
const label = `${id}:\`${schema}\`.${collection}`; const label = `${id}:\`${schema}\`.${collection}`;
@@ -405,7 +397,7 @@ const onEditDoc = async (item: any) => {
state.docEditDialog.isAdd = false; state.docEditDialog.isAdd = false;
state.docEditDialog.doc = item.value; state.docEditDialog.doc = item.value;
state.docEditDialog.visible = true; state.docEditDialog.visible = true;
} };
const onSaveDoc = async () => { const onSaveDoc = async () => {
if (state.docEditDialog.isAdd) { if (state.docEditDialog.isAdd) {
@@ -488,9 +480,8 @@ const removeDataTab = (targetName: string) => {
}; };
const getNowDataTab = () => { const getNowDataTab = () => {
return state.dataTabs[state.activeName] return state.dataTabs[state.activeName];
} };
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -1,7 +1,6 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" width="38%" <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" width="38%" :destroy-on-close="true">
:destroy-on-close="true">
<el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px"> <el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px">
<el-tabs v-model="tabActiveName"> <el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic"> <el-tab-pane label="基础信息" name="basic">
@@ -13,8 +12,13 @@
<el-input v-model.trim="form.name" placeholder="请输入名称" auto-complete="off"></el-input> <el-input v-model.trim="form.name" placeholder="请输入名称" auto-complete="off"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="uri" label="uri" required> <el-form-item prop="uri" label="uri" required>
<el-input type="textarea" :rows="2" v-model.trim="form.uri" <el-input
placeholder="形如 mongodb://username:password@host1:port1" auto-complete="off"></el-input> type="textarea"
:rows="2"
v-model.trim="form.uri"
placeholder="形如 mongodb://username:password@host1:port1"
auto-complete="off"
></el-input>
</el-form-item> </el-form-item>
</el-tab-pane> </el-tab-pane>
@@ -53,10 +57,10 @@ const props = defineProps({
title: { title: {
type: String, type: String,
}, },
}) });
//定义事件 //定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']) const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const rules = { const rules = {
tagId: [ tagId: [
@@ -80,7 +84,7 @@ const rules = {
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
} };
const mongoForm: any = ref(null); const mongoForm: any = ref(null);
const state = reactive({ const state = reactive({
@@ -97,12 +101,7 @@ const state = reactive({
btnLoading: false, btnLoading: false,
}); });
const { const { dialogVisible, tabActiveName, form, btnLoading } = toRefs(state);
dialogVisible,
tabActiveName,
form,
btnLoading,
} = toRefs(state)
watch(props, async (newValue: any) => { watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
@@ -122,7 +121,7 @@ const btnOk = async () => {
if (valid) { if (valid) {
const reqForm = { ...state.form }; const reqForm = { ...state.form };
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) { if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1 reqForm.sshTunnelMachineId = -1;
} }
// reqForm.uri = await RsaEncrypt(reqForm.uri); // reqForm.uri = await RsaEncrypt(reqForm.uri);
mongoApi.saveMongo.request(reqForm).then(() => { mongoApi.saveMongo.request(reqForm).then(() => {

View File

@@ -1,20 +1,27 @@
<template> <template>
<div> <div>
<page-table ref="pageTableRef" :query="queryConfig" v-model:query-form="query" :show-selection="true" <page-table
v-model:selection-data="selectionData" :data="list" :columns="columns" :total="total" ref="pageTableRef"
v-model:page-size="query.pageSize" v-model:page-num="query.pageNum" @pageChange="search()"> :query="queryConfig"
v-model:query-form="query"
:show-selection="true"
v-model:selection-data="selectionData"
:data="list"
:columns="columns"
:total="total"
v-model:page-size="query.pageSize"
v-model:page-num="query.pageNum"
@pageChange="search()"
>
<template #tagPathSelect> <template #tagPathSelect>
<el-select @focus="getTags" v-model="query.tagPath" placeholder="请选择标签" @clear="search" filterable clearable <el-select @focus="getTags" v-model="query.tagPath" placeholder="请选择标签" @clear="search" filterable clearable style="width: 200px">
style="width: 200px">
<el-option v-for="item in tags" :key="item" :label="item" :value="item"> </el-option> <el-option v-for="item in tags" :key="item" :label="item" :value="item"> </el-option>
</el-select> </el-select>
</template> </template>
<template #queryRight> <template #queryRight>
<el-button type="primary" icon="plus" @click="editMongo(true)" plain>添加</el-button> <el-button type="primary" icon="plus" @click="editMongo(true)" plain>添加</el-button>
<el-button type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteMongo" plain>删除 <el-button type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteMongo" plain>删除 </el-button>
</el-button>
</template> </template>
<template #tagPath="{ data }"> <template #tagPath="{ data }">
@@ -43,11 +50,9 @@
<el-table-column min-width="150" label="操作"> <el-table-column min-width="150" label="操作">
<template #default="scope"> <template #default="scope">
<el-link type="success" @click="showDatabaseStats(scope.row.Name)" plain size="small" <el-link type="success" @click="showDatabaseStats(scope.row.Name)" plain size="small" :underline="false">stats</el-link>
:underline="false">stats</el-link>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-link type="primary" @click="showCollections(scope.row.Name)" plain size="small" <el-link type="primary" @click="showCollections(scope.row.Name)" plain size="small" :underline="false">集合</el-link>
:underline="false">集合</el-link>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -101,8 +106,7 @@
<el-table-column prop="name" label="名称" show-overflow-tooltip> </el-table-column> <el-table-column prop="name" label="名称" show-overflow-tooltip> </el-table-column>
<el-table-column min-width="80" label="操作"> <el-table-column min-width="80" label="操作">
<template #default="scope"> <template #default="scope">
<el-link type="success" @click="showCollectionStats(scope.row.name)" plain size="small" <el-link type="success" @click="showCollectionStats(scope.row.name)" plain size="small" :underline="false">stats</el-link>
:underline="false">stats</el-link>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteCollection(scope.row.name)" title="确定删除该集合?"> <el-popconfirm @confirm="onDeleteCollection(scope.row.name)" title="确定删除该集合?">
<template #reference> <template #reference>
@@ -113,8 +117,7 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-dialog width="700px" :title="collectionsDialog.statsDialog.title" <el-dialog width="700px" :title="collectionsDialog.statsDialog.title" v-model="collectionsDialog.statsDialog.visible">
v-model="collectionsDialog.statsDialog.visible">
<el-descriptions title="集合状态信息" :column="3" border size="small"> <el-descriptions title="集合状态信息" :column="3" border size="small">
<el-descriptions-item label="ns" label-align="right" :span="2" align="center"> <el-descriptions-item label="ns" label-align="right" :span="2" align="center">
{{ collectionsDialog.statsDialog.data.ns }} {{ collectionsDialog.statsDialog.data.ns }}
@@ -162,8 +165,12 @@
</template> </template>
</el-dialog> </el-dialog>
<mongo-edit @val-change="valChange" :title="mongoEditDialog.title" v-model:visible="mongoEditDialog.visible" <mongo-edit
v-model:mongo="mongoEditDialog.data"></mongo-edit> @val-change="valChange"
:title="mongoEditDialog.title"
v-model:visible="mongoEditDialog.visible"
v-model:mongo="mongoEditDialog.data"
></mongo-edit>
</div> </div>
</template> </template>
@@ -175,22 +182,20 @@ import { tagApi } from '../tag/api';
import MongoEdit from './MongoEdit.vue'; import MongoEdit from './MongoEdit.vue';
import { formatByteSize } from '@/common/utils/format'; import { formatByteSize } from '@/common/utils/format';
import TagInfo from '../component/TagInfo.vue'; import TagInfo from '../component/TagInfo.vue';
import PageTable from '@/components/pagetable/PageTable.vue' import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
const pageTableRef: any = ref(null) const pageTableRef: any = ref(null);
const queryConfig = [ const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')];
TableQuery.slot("tagPath", "标签", "tagPathSelect"),
]
const columns = ref([ const columns = ref([
TableColumn.new("tagPath", "标签路径").isSlot().setAddWidth(20), TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
TableColumn.new("name", "名称"), TableColumn.new('name', '名称'),
TableColumn.new("uri", "连接uri"), TableColumn.new('uri', '连接uri'),
TableColumn.new("createTime", "创建时间").isTime(), TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new("creator", "创建人"), TableColumn.new('creator', '创建人'),
TableColumn.new("action", "操作").isSlot().setMinWidth(100).fixedRight().alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(100).fixedRight().alignCenter(),
]) ]);
const state = reactive({ const state = reactive({
tags: [], tags: [],
@@ -240,17 +245,7 @@ const state = reactive({
}, },
}); });
const { const { tags, list, total, selectionData, query, mongoEditDialog, databaseDialog, collectionsDialog, createCollectionDialog } = toRefs(state);
tags,
list,
total,
selectionData,
query,
mongoEditDialog,
databaseDialog,
collectionsDialog,
createCollectionDialog,
} = toRefs(state)
onMounted(async () => { onMounted(async () => {
search(); search();
@@ -258,7 +253,7 @@ onMounted(async () => {
const showDatabases = async (id: number) => { const showDatabases = async (id: number) => {
// state.query.tagPath = row.tagPath // state.query.tagPath = row.tagPath
state.dbOps.dbId = id state.dbOps.dbId = id;
state.databaseDialog.data = (await mongoApi.databases.request({ id })).Databases; state.databaseDialog.data = (await mongoApi.databases.request({ id })).Databases;
state.databaseDialog.title = `数据库列表`; state.databaseDialog.title = `数据库列表`;
@@ -345,12 +340,12 @@ const onCreateCollection = async () => {
const deleteMongo = async () => { const deleteMongo = async () => {
try { try {
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(", ")}】mongo信息?`, '提示', { await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】mongo信息?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}); });
await mongoApi.deleteMongo.request({ id: state.selectionData.map((x: any) => x.id).join(",") }); await mongoApi.deleteMongo.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功'); ElMessage.success('删除成功');
search(); search();
} catch (err) {} } catch (err) {}
@@ -385,7 +380,6 @@ const editMongo = async (data: any) => {
const valChange = () => { const valChange = () => {
search(); search();
}; };
</script> </script>
<style></style> <style></style>

View File

@@ -1,14 +1,14 @@
import Api from '@/common/Api'; import Api from '@/common/Api';
export const mongoApi = { export const mongoApi = {
mongoList : Api.newGet("/mongos"), mongoList: Api.newGet('/mongos'),
saveMongo : Api.newPost("/mongos"), saveMongo: Api.newPost('/mongos'),
deleteMongo : Api.newDelete("/mongos/{id}"), deleteMongo: Api.newDelete('/mongos/{id}'),
databases: Api.newGet("/mongos/{id}/databases"), databases: Api.newGet('/mongos/{id}/databases'),
collections: Api.newGet("/mongos/{id}/collections"), collections: Api.newGet('/mongos/{id}/collections'),
runCommand: Api.newPost("/mongos/{id}/run-command"), runCommand: Api.newPost('/mongos/{id}/run-command'),
findCommand: Api.newPost("/mongos/{id}/command/find"), findCommand: Api.newPost('/mongos/{id}/command/find'),
updateByIdCommand: Api.newPost("/mongos/{id}/command/update-by-id"), updateByIdCommand: Api.newPost('/mongos/{id}/command/update-by-id'),
deleteByIdCommand: Api.newPost("/mongos/{id}/command/delete-by-id"), deleteByIdCommand: Api.newPost('/mongos/{id}/command/delete-by-id'),
insertCommand: Api.newPost("/mongos/{id}/command/insert"), insertCommand: Api.newPost('/mongos/{id}/command/insert'),
} };

View File

@@ -16,9 +16,7 @@
<el-form-item label="名称:">{{ data.params.name }}</el-form-item> <el-form-item label="名称:">{{ data.params.name }}</el-form-item>
<el-form-item label="模式:">{{ data.params.mode }}</el-form-item> <el-form-item label="模式:">{{ data.params.mode }}</el-form-item>
<el-form-item label="链接:">{{ data.params.host }}</el-form-item> <el-form-item label="链接:">{{ data.params.host }}</el-form-item>
<el-form-item label="备注:">{{ <el-form-item label="备注:">{{ data.params.remark }}</el-form-item>
data.params.remark
}}</el-form-item>
</el-form> </el-form>
</template> </template>
</el-popover> </el-popover>
@@ -31,35 +29,37 @@
</el-row> </el-row>
</el-col> </el-col>
<el-col :span="20" style="border-left: 1px solid var(--el-card-border-color);"> <el-col :span="20" style="border-left: 1px solid var(--el-card-border-color)">
<div class="mt10 ml5"> <div class="mt10 ml5">
<el-col> <el-col>
<el-form class="search-form" label-position="right" :inline="true" label-width="auto"> <el-form class="search-form" label-position="right" :inline="true" label-width="auto">
<el-form-item label="key" label-width="auto"> <el-form-item label="key" label-width="auto">
<el-input placeholder="match 支持*模糊key" style="width: 250px" v-model="scanParam.match" <el-input placeholder="match 支持*模糊key" style="width: 250px" v-model="scanParam.match" @clear="clear()" clearable></el-input>
@clear="clear()" clearable></el-input>
</el-form-item> </el-form-item>
<el-form-item label="count" label-width="auto"> <el-form-item label="count" label-width="auto">
<el-input placeholder="count" style="width: 70px" v-model.number="scanParam.count"> <el-input placeholder="count" style="width: 70px" v-model.number="scanParam.count"> </el-input>
</el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button :disabled="!scanParam.id || !scanParam.db" @click="searchKey()" type="success" <el-button :disabled="!scanParam.id || !scanParam.db" @click="searchKey()" type="success" icon="search" plain></el-button>
icon="search" plain></el-button> <el-button :disabled="!scanParam.id || !scanParam.db" @click="scan()" icon="bottom" plain>scan</el-button>
<el-button :disabled="!scanParam.id || !scanParam.db" @click="scan()" icon="bottom" <el-button
plain>scan</el-button> :disabled="!scanParam.id || !scanParam.db"
<el-button :disabled="!scanParam.id || !scanParam.db" @click="showNewKeyDialog" @click="showNewKeyDialog"
type="primary" icon="plus" plain v-auth="'redis:data:save'"></el-button> type="primary"
<el-button :disabled="!scanParam.id || !scanParam.db" @click="flushDb" type="danger" plain icon="plus"
v-auth="'redis:data:save'">flush</el-button> plain
v-auth="'redis:data:save'"
></el-button>
<el-button :disabled="!scanParam.id || !scanParam.db" @click="flushDb" type="danger" plain v-auth="'redis:data:save'"
>flush</el-button
>
</el-form-item> </el-form-item>
<div style="float: right"> <div style="float: right">
<span>keys: {{ state.dbsize }}</span> <span>keys: {{ state.dbsize }}</span>
</div> </div>
</el-form> </el-form>
</el-col> </el-col>
<el-table v-loading="state.loading" :data="state.keys" :height="tableHeight" stripe <el-table v-loading="state.loading" :data="state.keys" :height="tableHeight" stripe :highlight-current-row="true" style="cursor: pointer">
:highlight-current-row="true" style="cursor: pointer">
<el-table-column show-overflow-tooltip prop="key" label="key"></el-table-column> <el-table-column show-overflow-tooltip prop="key" label="key"></el-table-column>
<el-table-column prop="type" label="type" width="80"> <el-table-column prop="type" label="type" width="80">
<template #default="scope"> <template #default="scope">
@@ -73,11 +73,9 @@
</el-table-column> </el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template #default="scope"> <template #default="scope">
<el-button @click="showKeyDetail(scope.row)" type="success" icon="search" plain <el-button @click="showKeyDetail(scope.row)" type="success" icon="search" plain size="small">查看 </el-button>
size="small">查看 <el-button v-auth="'redis:data:del'" @click="del(scope.row.key)" type="danger" icon="delete" plain size="small"
</el-button> >删除
<el-button v-auth="'redis:data:del'" @click="del(scope.row.key)" type="danger" icon="delete"
plain size="small">删除
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -88,21 +86,17 @@
<div style="text-align: center; margin-top: 10px"></div> <div style="text-align: center; margin-top: 10px"></div>
<el-dialog title="Key详情" v-model="keyDetailDialog.visible" width="800px" :destroy-on-close="true" <el-dialog title="Key详情" v-model="keyDetailDialog.visible" width="800px" :destroy-on-close="true" :close-on-click-modal="false">
:close-on-click-modal="false"> <key-detail :redisId="scanParam.id" :db="scanParam.db" :key-info="keyDetailDialog.keyInfo" @change-key="searchKey()" />
<key-detail :redisId="scanParam.id" :db="scanParam.db" :key-info="keyDetailDialog.keyInfo"
@change-key="searchKey()" />
</el-dialog> </el-dialog>
<el-dialog title="新增Key" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" <el-dialog title="新增Key" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
:close-on-click-modal="false">
<el-form ref="keyForm" label-width="auto"> <el-form ref="keyForm" label-width="auto">
<el-form-item prop="key" label="键名"> <el-form-item prop="key" label="键名">
<el-input v-model.trim="keyDetailDialog.keyInfo.key" placeholder="请输入键名"></el-input> <el-input v-model.trim="keyDetailDialog.keyInfo.key" placeholder="请输入键名"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="type" label="类型"> <el-form-item prop="type" label="类型">
<el-select v-model="keyDetailDialog.keyInfo.type" default-first-option style="width: 100%" <el-select v-model="keyDetailDialog.keyInfo.type" default-first-option style="width: 100%" placeholder="请选择类型">
placeholder="请选择类型">
<el-option key="string" label="string" value="string"></el-option> <el-option key="string" label="string" value="string"></el-option>
<el-option key="hash" label="hash" value="hash"></el-option> <el-option key="hash" label="hash" value="hash"></el-option>
<el-option key="set" label="set" value="set"></el-option> <el-option key="set" label="set" value="set"></el-option>
@@ -136,8 +130,8 @@ const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
* 树节点类型 * 树节点类型
*/ */
class NodeType { class NodeType {
static Redis = 1 static Redis = 1;
static Db = 2 static Db = 2;
} }
const state = reactive({ const state = reactive({
@@ -172,21 +166,15 @@ const state = reactive({
dbsize: 0, dbsize: 0,
}); });
const { const { tableHeight, scanParam, keyDetailDialog, newKeyDialog } = toRefs(state);
tableHeight,
scanParam,
keyDetailDialog,
newKeyDialog,
} = toRefs(state)
onMounted(async () => { onMounted(async () => {
setHeight(); setHeight();
}) });
const setHeight = () => { const setHeight = () => {
state.tableHeight = window.innerHeight - 159; state.tableHeight = window.innerHeight - 159;
} };
/** /**
* instmap; tagPaht -> redis info[] * instmap; tagPaht -> redis info[]
@@ -195,14 +183,14 @@ const instMap: Map<string, any[]> = new Map();
const getInsts = async () => { const getInsts = async () => {
const res = await redisApi.redisList.request({ pageNum: 1, pageSize: 1000 }); const res = await redisApi.redisList.request({ pageNum: 1, pageSize: 1000 });
if (!res.total) return if (!res.total) return;
for (const redisInfo of res.list) { for (const redisInfo of res.list) {
const tagPath = redisInfo.tagPath; const tagPath = redisInfo.tagPath;
let redisInsts = instMap.get(tagPath) || []; let redisInsts = instMap.get(tagPath) || [];
redisInsts.push(redisInfo); redisInsts.push(redisInfo);
instMap.set(tagPath, redisInsts); instMap.set(tagPath, redisInsts);
} }
} };
/** /**
* 加载文件树节点 * 加载文件树节点
@@ -224,7 +212,7 @@ const loadNode = async (node: any) => {
const data = node.data; const data = node.data;
// 点击tagPath -> 加载数据库信息列表 // 点击tagPath -> 加载数据库信息列表
if (data.type === TagTreeNode.TagPath) { if (data.type === TagTreeNode.TagPath) {
const redisInfos = instMap.get(data.key) const redisInfos = instMap.get(data.key);
return redisInfos?.map((x: any) => { return redisInfos?.map((x: any) => {
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Redis).withParams(x); return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Redis).withParams(x);
}); });
@@ -246,7 +234,7 @@ const nodeClick = (data: any) => {
state.scanParam.db = data.params.db; state.scanParam.db = data.params.db;
scan(); scan();
} }
} };
/** /**
* 获取所有库信息 * 获取所有库信息
@@ -259,27 +247,27 @@ const getDbs = async (redisInfo: any) => {
db: x, db: x,
name: `db${x}`, name: `db${x}`,
keys: 0, keys: 0,
}) });
}) });
if (redisInfo.mode == 'cluster') { if (redisInfo.mode == 'cluster') {
return dbs; return dbs;
} }
const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: "Keyspace" }); const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: 'Keyspace' });
for (let db in res.Keyspace) { for (let db in res.Keyspace) {
for (let d of dbs) { for (let d of dbs) {
if (db == d.params.name) { if (db == d.params.name) {
d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0 d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0;
} }
} }
} }
// 替换label // 替换label
dbs.forEach((e: any) => { dbs.forEach((e: any) => {
e.label = `${e.params.name} [${e.params.keys}]` e.label = `${e.params.name} [${e.params.keys}]`;
}); });
return dbs; return dbs;
} };
const scan = async () => { const scan = async () => {
isTrue(state.scanParam.id != null, '请先选择redis'); isTrue(state.scanParam.id != null, '请先选择redis');
@@ -287,7 +275,7 @@ const scan = async () => {
const match: string = state.scanParam.match || ''; const match: string = state.scanParam.match || '';
if (!match) { if (!match) {
isTrue(state.scanParam.count <= 100, "key搜索条件为空时, count不能大于100") isTrue(state.scanParam.count <= 100, 'key搜索条件为空时, count不能大于100');
} else if (match.indexOf('*') != -1) { } else if (match.indexOf('*') != -1) {
const dbsize = state.dbsize; const dbsize = state.dbsize;
// 如果为模糊搜索并且搜索的key模式大于指定字符数则将count设大点scan // 如果为模糊搜索并且搜索的key模式大于指定字符数则将count设大点scan
@@ -298,10 +286,10 @@ const scan = async () => {
} }
} }
const scanParam = { ...state.scanParam } const scanParam = { ...state.scanParam };
// 集群模式count设小点因为后端会从所有master节点scan一遍然后合并结果,默认假设redis集群有3个master // 集群模式count设小点因为后端会从所有master节点scan一遍然后合并结果,默认假设redis集群有3个master
if (scanParam.mode == 'cluster') { if (scanParam.mode == 'cluster') {
scanParam.count = Math.floor(state.scanParam.count / 3) scanParam.count = Math.floor(state.scanParam.count / 3);
} }
state.loading = true; state.loading = true;
@@ -344,17 +332,18 @@ const showKeyDetail = async (row: any) => {
const showNewKeyDialog = () => { const showNewKeyDialog = () => {
notNull(state.scanParam.id, '请先选择redis'); notNull(state.scanParam.id, '请先选择redis');
notNull(state.scanParam.db, "请选择要操作的库") notNull(state.scanParam.db, '请选择要操作的库');
resetKeyDetailInfo(); resetKeyDetailInfo();
state.newKeyDialog.visible = true; state.newKeyDialog.visible = true;
} };
const flushDb = () => { const flushDb = () => {
ElMessageBox.confirm(`确定清空[${state.scanParam.db}]库的所有key?`, '提示', { ElMessageBox.confirm(`确定清空[${state.scanParam.db}]库的所有key?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}).then(() => { })
.then(() => {
redisApi.flushDb redisApi.flushDb
.request({ .request({
id: state.scanParam.id, id: state.scanParam.id,
@@ -364,19 +353,20 @@ const flushDb = () => {
ElMessage.success('清除成功!'); ElMessage.success('清除成功!');
searchKey(); searchKey();
}); });
}).catch(() => { }); })
} .catch(() => {});
};
const cancelNewKey = () => { const cancelNewKey = () => {
resetKeyDetailInfo(); resetKeyDetailInfo();
state.newKeyDialog.visible = false; state.newKeyDialog.visible = false;
} };
const newKey = async () => { const newKey = async () => {
const keyInfo = state.keyDetailDialog.keyInfo const keyInfo = state.keyDetailDialog.keyInfo;
const keyType = keyInfo.type const keyType = keyInfo.type;
const key = keyInfo.key; const key = keyInfo.key;
notBlank(key, "键名不能为空"); notBlank(key, '键名不能为空');
if (keyType == 'string') { if (keyType == 'string') {
await redisApi.setString.request({ await redisApi.setString.request({
@@ -384,25 +374,26 @@ const newKey = async () => {
db: state.scanParam.db, db: state.scanParam.db,
key: key, key: key,
value: '', value: '',
}) });
} }
state.newKeyDialog.visible = false; state.newKeyDialog.visible = false;
state.keyDetailDialog.visible = true; state.keyDetailDialog.visible = true;
searchKey(); searchKey();
} };
const resetKeyDetailInfo = () => { const resetKeyDetailInfo = () => {
state.keyDetailDialog.keyInfo.key = ''; state.keyDetailDialog.keyInfo.key = '';
state.keyDetailDialog.keyInfo.type = 'string'; state.keyDetailDialog.keyInfo.type = 'string';
state.keyDetailDialog.keyInfo.timed = -1; state.keyDetailDialog.keyInfo.timed = -1;
} };
const del = (key: string) => { const del = (key: string) => {
ElMessageBox.confirm(`确定删除[ ${key} ] 该key?`, '提示', { ElMessageBox.confirm(`确定删除[ ${key} ] 该key?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}).then(() => { })
.then(() => {
redisApi.delKey redisApi.delKey
.request({ .request({
key, key,
@@ -413,7 +404,8 @@ const del = (key: string) => {
ElMessage.success('删除成功!'); ElMessage.success('删除成功!');
searchKey(); searchKey();
}); });
}).catch(() => { }); })
.catch(() => {});
}; };
const ttlConveter = (ttl: any) => { const ttlConveter = (ttl: any) => {

View File

@@ -1,17 +1,15 @@
<template> <template>
<div class="format-viewer-container"> <div class="format-viewer-container">
<div class="mb5 fr"> <div class="mb5 fr">
<el-select v-model="selectedView" class='format-selector' size='mini' placeholder='Text'> <el-select v-model="selectedView" class="format-selector" size="mini" placeholder="Text">
<template #prefix> <template #prefix>
<SvgIcon name="view" /> <SvgIcon name="view" />
</template> </template>
<el-option v-for="item of Object.keys(viewers)" :key="item" :label="item" :value="item"> <el-option v-for="item of Object.keys(viewers)" :key="item" :label="item" :value="item"> </el-option>
</el-option>
</el-select> </el-select>
</div> </div>
<component ref='viewerRef' :is='components[viewerComponent]' :content='state.content' :name="selectedView"> <component ref="viewerRef" :is="components[viewerComponent]" :content="state.content" :name="selectedView"> </component>
</component>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -23,12 +21,13 @@ const props = defineProps({
content: { content: {
type: String, type: String,
}, },
}) });
const components = shallowReactive({ const components = shallowReactive({
ViewerText, ViewerJson ViewerText,
}) ViewerJson,
const viewerRef: any = ref(null) });
const viewerRef: any = ref(null);
const state = reactive({ const state = reactive({
content: '', content: '',
@@ -36,18 +35,16 @@ const state = reactive({
}); });
const viewers = { const viewers = {
"Text": { Text: {
value: 'ViewerText', value: 'ViewerText',
}, },
"Json": { Json: {
value: 'ViewerJson', value: 'ViewerJson',
} },
} };
const { const { selectedView } = toRefs(state);
selectedView,
} = toRefs(state)
const viewerComponent = computed(() => { const viewerComponent = computed(() => {
return viewers[state.selectedView].value; return viewers[state.selectedView].value;
@@ -62,15 +59,15 @@ watch(
onMounted(() => { onMounted(() => {
state.content = props.content as any; state.content = props.content as any;
}) });
const getContent = () => { const getContent = () => {
return viewerRef.value.getContent(); return viewerRef.value.getContent();
} };
defineExpose({ getContent })
defineExpose({ getContent });
</script> </script>
<style lang="scss"> <style lang="scss">
.format-selector { .format-selector {
width: 130px; width: 130px;
@@ -85,7 +82,7 @@ defineExpose({ getContent })
border: 1px solid #dcdfe6; border: 1px solid #dcdfe6;
padding: 5px 10px; padding: 5px 10px;
border-radius: 4px; border-radius: 4px;
clear: both clear: both;
} }
.format-viewer-container .formater-binary-tag { .format-viewer-container .formater-binary-tag {

View File

@@ -1,7 +1,6 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dialogVisible" :show-close="true" width="1000px" @close="close()"> <el-dialog :title="title" v-model="dialogVisible" :show-close="true" width="1000px" @close="close()">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :lg="16" :md="16"> <el-col :lg="16" :md="16">
<el-descriptions class="redis-info info-server" title="Redis服务器信息" :column="3" size="small" border> <el-descriptions class="redis-info info-server" title="Redis服务器信息" :column="3" size="small" border>
@@ -31,10 +30,8 @@
<el-col :lg="12" :md="12"> <el-col :lg="12" :md="12">
<el-descriptions class="redis-info info-client" title="客户端连接" :column="3" size="small" border> <el-descriptions class="redis-info info-client" title="客户端连接" :column="3" size="small" border>
<el-descriptions-item label="已连接客户端数">{{ info.Clients.connected_clients <el-descriptions-item label="已连接客户端数">{{ info.Clients.connected_clients }}</el-descriptions-item>
}}</el-descriptions-item> <el-descriptions-item label="正在等待阻塞命令客户端数">{{ info.Clients.blocked_clients }}</el-descriptions-item>
<el-descriptions-item label="正在等待阻塞命令客户端数">{{ info.Clients.blocked_clients
}}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-col> </el-col>
</el-row> </el-row>
@@ -50,14 +47,10 @@
<el-col :lg="24" :md="24"> <el-col :lg="24" :md="24">
<span style="font-size: 14px; font-weight: 700">键值统计</span> <span style="font-size: 14px; font-weight: 700">键值统计</span>
<el-table :data="Keyspace" stripe max-height="250" style="width: 100%" border> <el-table :data="Keyspace" stripe max-height="250" style="width: 100%" border>
<el-table-column prop="db" label="数据库" min-width="100" show-overflow-tooltip> <el-table-column prop="db" label="数据库" min-width="100" show-overflow-tooltip> </el-table-column>
</el-table-column> <el-table-column prop="keys" label="keys" min-width="70" show-overflow-tooltip> </el-table-column>
<el-table-column prop="keys" label="keys" min-width="70" show-overflow-tooltip> <el-table-column prop="expires" label="expires" min-width="70" show-overflow-tooltip> </el-table-column>
</el-table-column> <el-table-column prop="avg_ttl" label="avg_ttl" min-width="70" show-overflow-tooltip> </el-table-column>
<el-table-column prop="expires" label="expires" min-width="70" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="avg_ttl" label="avg_ttl" min-width="70" show-overflow-tooltip>
</el-table-column>
</el-table> </el-table>
</el-col> </el-col>
</el-row> </el-row>
@@ -71,12 +64,9 @@
</el-descriptions> </el-descriptions>
<el-descriptions class="redis-info info-persistence" title="持久化" :column="3" size="small" border> <el-descriptions class="redis-info info-persistence" title="持久化" :column="3" size="small" border>
<el-descriptions-item label="是否启用aof">{{ info.Persistence?.aof_enabled || false <el-descriptions-item label="是否启用aof">{{ info.Persistence?.aof_enabled || false }}</el-descriptions-item>
}}</el-descriptions-item> <el-descriptions-item label="是否正在载入持久化文件">{{ info.Persistence?.loading || false }}</el-descriptions-item>
<el-descriptions-item label="是否正在载入持久化文件">{{ info.Persistence?.loading || false
}}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
@@ -97,24 +87,20 @@ const props = defineProps({
info: { info: {
type: [Boolean, Object], type: [Boolean, Object],
}, },
}) });
const emit = defineEmits(['update:visible', 'close']) const emit = defineEmits(['update:visible', 'close']);
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
memInfo: {} as any, memInfo: {} as any,
Keyspace: [] as any[], Keyspace: [] as any[],
}); });
let memChart: any = null; let memChart: any = null;
let memRef = ref(null); let memRef = ref(null);
const { const { dialogVisible, Keyspace } = toRefs(state);
dialogVisible,
Keyspace,
} = toRefs(state)
watch( watch(
() => props.visible, () => props.visible,
@@ -132,15 +118,15 @@ watch(
if (info['Keyspace']) { if (info['Keyspace']) {
let arr = []; let arr = [];
for (let k in info['Keyspace']) { for (let k in info['Keyspace']) {
let data = { db: k } let data = { db: k };
let d = info['Keyspace'][k].split(',') let d = info['Keyspace'][k].split(',');
for (let f of d) { for (let f of d) {
let v = f.split('=') let v = f.split('=');
data[v[0]] = v[1] data[v[0]] = v[1];
} }
arr.push(data) arr.push(data);
} }
state.Keyspace = arr state.Keyspace = arr;
} }
} }
); );
@@ -149,10 +135,10 @@ const initCharts = () => {
nextTick(() => { nextTick(() => {
initMemStats(); initMemStats();
}); });
} };
const initMemStats = () => { const initMemStats = () => {
let maxMem = state.memInfo.maxmemory === '0' ? state.memInfo.total_system_memory : state.memInfo.maxmemory let maxMem = state.memInfo.maxmemory === '0' ? state.memInfo.total_system_memory : state.memInfo.maxmemory;
const data = [ const data = [
{ name: '可用内存:', value: maxMem - state.memInfo.used_memory }, { name: '可用内存:', value: maxMem - state.memInfo.used_memory },
{ {
@@ -205,7 +191,7 @@ const initMemStats = () => {
return; return;
} }
memChart = useEcharts(memRef.value, tdTheme, option); memChart = useEcharts(memRef.value, tdTheme, option);
} };
const close = () => { const close = () => {
emit('update:visible', false); emit('update:visible', false);

View File

@@ -2,20 +2,26 @@
<div> <div>
<el-container direction="vertical" class="key-tab-container"> <el-container direction="vertical" class="key-tab-container">
<!-- key info --> <!-- key info -->
<key-header ref="keyHeader" :redis-id="redisId" :db="db" :key-info="keyInfo" @refresh-content="refreshContent" <key-header
@change-key="changeKey" class="key-header-info"> ref="keyHeader"
:redis-id="redisId"
:db="db"
:key-info="keyInfo"
@refresh-content="refreshContent"
@change-key="changeKey"
class="key-header-info"
>
</key-header> </key-header>
<!-- key content --> <!-- key content -->
<component ref="keyValueRef" :is="components[componentName]" :redis-id="redisId" :db="db" :key-info="keyInfo"> <component ref="keyValueRef" :is="components[componentName]" :redis-id="redisId" :db="db" :key-info="keyInfo"> </component>
</component>
</el-container> </el-container>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, ref, shallowReactive, reactive, computed, toRefs } from 'vue'; import { defineAsyncComponent, ref, shallowReactive, reactive, computed, toRefs } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import KeyHeader from './KeyHeader.vue' import KeyHeader from './KeyHeader.vue';
const KeyValueString = defineAsyncComponent(() => import('./KeyValueString.vue')); const KeyValueString = defineAsyncComponent(() => import('./KeyValueString.vue'));
const KeyValueHash = defineAsyncComponent(() => import('./KeyValueHash.vue')); const KeyValueHash = defineAsyncComponent(() => import('./KeyValueHash.vue'));
@@ -24,24 +30,28 @@ const KeyValueList = defineAsyncComponent(() => import('./KeyValueList.vue'));
const KeyValueZset = defineAsyncComponent(() => import('./KeyValueZset.vue')); const KeyValueZset = defineAsyncComponent(() => import('./KeyValueZset.vue'));
const components = shallowReactive({ const components = shallowReactive({
KeyValueString, KeyValueHash, KeyValueSet, KeyValueList, KeyValueZset KeyValueString,
}) KeyValueHash,
KeyValueSet,
KeyValueList,
KeyValueZset,
});
const keyValueRef = ref(null) as any const keyValueRef = ref(null) as any;
const props = defineProps({ const props = defineProps({
redisId: { redisId: {
type: Number type: Number,
}, },
db: { db: {
type: Number type: Number,
}, },
keyInfo: { keyInfo: {
type: [Object], type: [Object],
}, },
}) });
const emit = defineEmits(['update:visible', 'changeKey', 'valChange']) const emit = defineEmits(['update:visible', 'changeKey', 'valChange']);
const state = reactive({ const state = reactive({
redisId: 0, redisId: 0,
@@ -56,25 +66,23 @@ const componentMap = {
}; };
const componentName = computed(() => { const componentName = computed(() => {
const component = componentMap[props.keyInfo?.type] const component = componentMap[props.keyInfo?.type];
if (!component) { if (!component) {
ElMessage.error("暂不支持该类型") ElMessage.error('暂不支持该类型');
return '' return '';
} }
return component; return component;
}); });
const refreshContent = () => { const refreshContent = () => {
keyValueRef.value?.initData(); keyValueRef.value?.initData();
} };
const changeKey = () => { const changeKey = () => {
emit('changeKey'); emit('changeKey');
} };
const { const {} = toRefs(state);
} = toRefs(state)
// watch( // watch(
// () => props.keyInfo, // () => props.keyInfo,

View File

@@ -8,10 +8,8 @@
</template> </template>
<template #suffix> <template #suffix>
<SvgIcon v-auth="'redis:data:save'" @click="renameKey" title="点击重命名" name="check" <SvgIcon v-auth="'redis:data:save'" @click="renameKey" title="点击重命名" name="check" class="cursor-pointer" />
class="cursor-pointer" />
</template> </template>
</el-input> </el-input>
</div> </div>
@@ -30,9 +28,8 @@
</div> </div>
<!-- del & refresh btn --> <!-- del & refresh btn -->
<div class='key-header-item key-header-btn-con'> <div class="key-header-item key-header-btn-con">
<el-button slot="reference" ref='refreshBtn' type="success" @click="refreshKey" icon="refresh" <el-button slot="reference" ref="refreshBtn" type="success" @click="refreshKey" icon="refresh" title="刷新"></el-button>
title="刷新"></el-button>
</div> </div>
</div> </div>
</template> </template>
@@ -43,17 +40,17 @@ import { ElMessage, ElMessageBox } from 'element-plus';
const props = defineProps({ const props = defineProps({
redisId: { redisId: {
type: Number type: Number,
}, },
db: { db: {
type: Number type: Number,
}, },
keyInfo: { keyInfo: {
type: [Object], type: [Object],
}, },
}) });
const emit = defineEmits(['refreshContent', 'changeKey', 'valChange']) const emit = defineEmits(['refreshContent', 'changeKey', 'valChange']);
const state = reactive({ const state = reactive({
redisId: 0, redisId: 0,
@@ -66,19 +63,19 @@ const state = reactive({
}); });
onMounted(() => { onMounted(() => {
state.keyInfo = props.keyInfo state.keyInfo = props.keyInfo;
state.oldKey = props.keyInfo?.key state.oldKey = props.keyInfo?.key;
}) });
const refreshKey = async () => { const refreshKey = async () => {
const ttl = await redisApi.keyTtl.request({ const ttl = await redisApi.keyTtl.request({
id: props.redisId, id: props.redisId,
db: props.db, db: props.db,
key: state.oldKey, key: state.oldKey,
}) });
state.keyInfo.timed = ttl; state.keyInfo.timed = ttl;
emit('refreshContent'); emit('refreshContent');
} };
const renameKey = async () => { const renameKey = async () => {
if (!state.oldKey || state.keyInfo.key == state.oldKey) { if (!state.oldKey || state.keyInfo.key == state.oldKey) {
@@ -88,11 +85,11 @@ const renameKey = async () => {
id: props.redisId, id: props.redisId,
db: props.db, db: props.db,
newKey: state.keyInfo.key, newKey: state.keyInfo.key,
key: state.oldKey key: state.oldKey,
}); });
ElMessage.success("设置成功") ElMessage.success('设置成功');
emit('changeKey'); emit('changeKey');
} };
const ttlKey = async () => { const ttlKey = async () => {
if (!state.oldKey) { if (!state.oldKey) {
@@ -101,32 +98,28 @@ const ttlKey = async () => {
// ttl <= 0则持久化该key // ttl <= 0则持久化该key
if (state.keyInfo.timed <= 0) { if (state.keyInfo.timed <= 0) {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm('确定持久化该key?', 'Warning', {
'确定持久化该key?',
'Warning',
{
confirmButtonText: '确认', confirmButtonText: '确认',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
} });
)
} catch (err) { } catch (err) {
return; return;
} }
await persistKey(); await persistKey();
state.keyInfo.timed = -1; state.keyInfo.timed = -1;
return return;
} }
await redisApi.expireKey.request({ await redisApi.expireKey.request({
id: props.redisId, id: props.redisId,
db: props.db, db: props.db,
key: state.keyInfo.key, key: state.keyInfo.key,
seconds: state.keyInfo.timed seconds: state.keyInfo.timed,
}); });
ElMessage.success("设置成功") ElMessage.success('设置成功');
emit('changeKey'); emit('changeKey');
} };
const persistKey = async () => { const persistKey = async () => {
await redisApi.persistKey.request({ await redisApi.persistKey.request({
@@ -134,14 +127,11 @@ const persistKey = async () => {
db: props.db, db: props.db,
key: state.keyInfo.key, key: state.keyInfo.key,
}); });
ElMessage.success("设置成功") ElMessage.success('设置成功');
emit('changeKey'); emit('changeKey');
} };
const { const { keyInfo, oldKey } = toRefs(state);
keyInfo,
oldKey,
} = toRefs(state)
// watch( // watch(
// () => props.keyInfo, // () => props.keyInfo,

View File

@@ -1,40 +1,37 @@
<template> <template>
<div> <div>
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button> <el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
<el-table size="small" border :data="hashValues" height="450" min-height=300 stripe> <el-table size="small" border :data="hashValues" height="450" min-height="300" stripe>
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> <el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> </el-table-column>
</el-table-column> <el-table-column resizable sortable prop="field" label="field" show-overflow-tooltip min-width="100"> </el-table-column>
<el-table-column resizable sortable prop="field" label="field" show-overflow-tooltip min-width="100"> <el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200"> </el-table-column>
</el-table-column>
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
</el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template #header> <template #header>
<el-input class="key-detail-filter-value" v-model="state.filterValue" @keyup.enter='hscan(true, true)' <el-input
placeholder="输入关键词回车搜索" clearable size="small" /> class="key-detail-filter-value"
v-model="state.filterValue"
@keyup.enter="hscan(true, true)"
placeholder="输入关键词回车搜索"
clearable
size="small"
/>
</template> </template>
<template #default="scope"> <template #default="scope">
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" <el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" plain></el-link>
plain></el-link>
<el-popconfirm title="确定删除?" @confirm="hdel(scope.row.field, scope.$index)"> <el-popconfirm title="确定删除?" @confirm="hdel(scope.row.field, scope.$index)">
<template #reference> <template #reference>
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" <el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" plain class="ml5"></el-link>
plain class="ml5"></el-link>
</template> </template>
</el-popconfirm> </el-popconfirm>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- load more content --> <!-- load more content -->
<div class='content-more-container'> <div class="content-more-container">
<el-button size='small' @click='hscan()' :disabled='loadMoreDisable' class='content-more-btn'> <el-button size="small" @click="hscan()" :disabled="loadMoreDisable" class="content-more-btn"> 加载更多 </el-button>
加载更多
</el-button>
</div> </div>
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true" <el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true" :close-on-click-modal="false">
:close-on-click-modal="false">
<el-form> <el-form>
<el-form-item> <el-form-item>
<el-input v-model="editDialog.field" placeholder="field" /> <el-input v-model="editDialog.field" placeholder="field" />
@@ -64,17 +61,17 @@ const props = defineProps({
redisId: { redisId: {
type: [Number], type: [Number],
require: true, require: true,
default: 0 default: 0,
}, },
db: { db: {
type: [Number], type: [Number],
require: true, require: true,
default: 0 default: 0,
}, },
keyInfo: { keyInfo: {
type: [Object], type: [Object],
}, },
}) });
const formatViewerRef = ref(null) as any; const formatViewerRef = ref(null) as any;
@@ -98,29 +95,23 @@ const state = reactive({
}, },
}); });
const { const { hashValues, total, loadMoreDisable, editDialog } = toRefs(state);
hashValues,
total,
loadMoreDisable,
editDialog,
} = toRefs(state)
onMounted(() => { onMounted(() => {
state.redisId = props.redisId; state.redisId = props.redisId;
state.db = props.db; state.db = props.db;
state.key = props.keyInfo?.key; state.key = props.keyInfo?.key;
initData(); initData();
}) });
const initData = () => { const initData = () => {
state.filterValue = ''; state.filterValue = '';
hscan(true, true); hscan(true, true);
} };
const getScanMatch = () => { const getScanMatch = () => {
return state.filterValue ? `*${state.filterValue}*` : '*'; return state.filterValue ? `*${state.filterValue}*` : '*';
} };
const hscan = async (resetTableData = false, resetCursor = false) => { const hscan = async (resetTableData = false, resetCursor = false) => {
if (resetCursor) { if (resetCursor) {
@@ -130,10 +121,10 @@ const hscan = async (resetTableData = false, resetCursor = false) => {
const scanRes = await redisApi.hscan.request({ const scanRes = await redisApi.hscan.request({
...getBaseReqParam(), ...getBaseReqParam(),
match: getScanMatch(), match: getScanMatch(),
...state.scanParam ...state.scanParam,
}); });
state.scanParam.cursor = scanRes.cursor; state.scanParam.cursor = scanRes.cursor;
state.loadMoreDisable = scanRes.cursor == 0 state.loadMoreDisable = scanRes.cursor == 0;
state.total = scanRes.keySize; state.total = scanRes.keySize;
const keys = scanRes.keys; const keys = scanRes.keys;
@@ -147,7 +138,7 @@ const hscan = async (resetTableData = false, resetCursor = false) => {
if (resetTableData) { if (resetTableData) {
state.hashValues = hashValue; state.hashValues = hashValue;
} else { } else {
state.hashValues.push(...hashValue) state.hashValues.push(...hashValue);
} }
}; };
@@ -158,7 +149,7 @@ const hdel = async (field: any, index: any) => {
}); });
ElMessage.success('删除成功'); ElMessage.success('删除成功');
state.hashValues.splice(index, 1) state.hashValues.splice(index, 1);
state.total--; state.total--;
}; };
@@ -167,16 +158,16 @@ const showEditDialog = (row: any) => {
state.editDialog.field = row ? row.field : ''; state.editDialog.field = row ? row.field : '';
state.editDialog.value = row ? row.value : ''; state.editDialog.value = row ? row.value : '';
state.editDialog.visible = true; state.editDialog.visible = true;
} };
const confirmEditData = async () => { const confirmEditData = async () => {
const param = getBaseReqParam(); const param = getBaseReqParam();
const field = state.editDialog.field; const field = state.editDialog.field;
notBlank(field, "field不能为空"); notBlank(field, 'field不能为空');
// 存在数据行,则说明为修改,则要先删除旧数据后新增 // 存在数据行,则说明为修改,则要先删除旧数据后新增
const dataRow = state.editDialog.dataRow const dataRow = state.editDialog.dataRow;
if (dataRow) { if (dataRow) {
await redisApi.hdel.request({ await redisApi.hdel.request({
...param, ...param,
@@ -185,7 +176,7 @@ const confirmEditData = async () => {
} }
// 获取hash value内容并新增 // 获取hash value内容并新增
const value = formatViewerRef.value.getContent() const value = formatViewerRef.value.getContent();
const res = await redisApi.hset.request({ const res = await redisApi.hset.request({
...param, ...param,
value: [ value: [
@@ -196,7 +187,7 @@ const confirmEditData = async () => {
], ],
}); });
ElMessage.success("保存成功"); ElMessage.success('保存成功');
if (dataRow) { if (dataRow) {
state.editDialog.dataRow.value = value; state.editDialog.dataRow.value = value;
state.editDialog.dataRow.field = field; state.editDialog.dataRow.field = field;
@@ -211,18 +202,17 @@ const confirmEditData = async () => {
} }
state.editDialog.visible = false; state.editDialog.visible = false;
state.editDialog.dataRow = null; state.editDialog.dataRow = null;
} };
const getBaseReqParam = () => { const getBaseReqParam = () => {
return { return {
id: state.redisId, id: state.redisId,
db: state.db, db: state.db,
key: state.key key: state.key,
} };
} };
defineExpose({ initData })
defineExpose({ initData });
</script> </script>
<style lang="scss"> <style lang="scss">
#string-value-text { #string-value-text {

View File

@@ -1,34 +1,26 @@
<template> <template>
<div> <div>
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button> <el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
<el-table size="small" border :data="values" height="450" min-height=300 stripe> <el-table size="small" border :data="values" height="450" min-height="300" stripe>
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> <el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> </el-table-column>
</el-table-column> <el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200"> </el-table-column>
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
</el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template #default="scope"> <template #default="scope">
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" <el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" plain></el-link>
plain></el-link>
<el-popconfirm title="确定删除?" @confirm="lrem(scope.row, scope.$index)"> <el-popconfirm title="确定删除?" @confirm="lrem(scope.row, scope.$index)">
<template #reference> <template #reference>
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" <el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" plain class="ml5"></el-link>
plain class="ml5"></el-link>
</template> </template>
</el-popconfirm> </el-popconfirm>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- load more content --> <!-- load more content -->
<div class='content-more-container'> <div class="content-more-container">
<el-button size='small' @click='getListValue(false)' :disabled='loadMoreDisable' class='content-more-btn'> <el-button size="small" @click="getListValue(false)" :disabled="loadMoreDisable" class="content-more-btn"> 加载更多 </el-button>
加载更多
</el-button>
</div> </div>
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true" <el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true" :close-on-click-modal="false">
:close-on-click-modal="false">
<el-form> <el-form>
<el-form-item> <el-form-item>
<format-viewer class="w100" ref="formatViewerRef" :content="editDialog.content"></format-viewer> <format-viewer class="w100" ref="formatViewerRef" :content="editDialog.content"></format-viewer>
@@ -42,7 +34,6 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -60,12 +51,12 @@ const props = defineProps({
db: { db: {
type: [Number], type: [Number],
require: true, require: true,
default: 0 default: 0,
}, },
keyInfo: { keyInfo: {
type: [Object], type: [Object],
}, },
}) });
const formatViewerRef = ref(null) as any; const formatViewerRef = ref(null) as any;
@@ -82,27 +73,22 @@ const state = reactive({
visible: false, visible: false,
content: '', content: '',
dataRow: null as any, dataRow: null as any,
} },
}); });
const { const { total, values, loadMoreDisable, editDialog } = toRefs(state);
total,
values,
loadMoreDisable,
editDialog,
} = toRefs(state)
onMounted(() => { onMounted(() => {
state.redisId = props.redisId; state.redisId = props.redisId;
state.db = props.db; state.db = props.db;
state.key = props.keyInfo?.key; state.key = props.keyInfo?.key;
initData(); initData();
}) });
const initData = () => { const initData = () => {
state.pageNum = 1; state.pageNum = 1;
getListValue(true); getListValue(true);
} };
const getListValue = async (resetTableData = false) => { const getListValue = async (resetTableData = false) => {
const pageNum = state.pageNum; const pageNum = state.pageNum;
@@ -122,10 +108,10 @@ const getListValue = async (resetTableData = false) => {
if (resetTableData) { if (resetTableData) {
state.values = datas; state.values = datas;
} else { } else {
state.values.push(...datas) state.values.push(...datas);
} }
state.pageNum++; state.pageNum++;
state.loadMoreDisable = state.values.length === state.total state.loadMoreDisable = state.values.length === state.total;
}; };
const lset = async (row: any, rowIndex: number) => { const lset = async (row: any, rowIndex: number) => {
@@ -141,29 +127,29 @@ const showEditDialog = (row: any) => {
state.editDialog.dataRow = row; state.editDialog.dataRow = row;
state.editDialog.content = row ? row.value : ''; state.editDialog.content = row ? row.value : '';
state.editDialog.visible = true; state.editDialog.visible = true;
} };
const confirmEditData = async () => { const confirmEditData = async () => {
const param = getBaseReqParam(); const param = getBaseReqParam();
// 存在数据行,则说明为修改,则要先删除旧数据后新增 // 存在数据行,则说明为修改,则要先删除旧数据后新增
const dataRow = state.editDialog.dataRow const dataRow = state.editDialog.dataRow;
if (dataRow) { if (dataRow) {
await redisApi.lrem.request({ await redisApi.lrem.request({
member: state.editDialog.dataRow.value, member: state.editDialog.dataRow.value,
count: 1, count: 1,
...param ...param,
}); });
} }
// 获取list member内容并新增 // 获取list member内容并新增
const member = formatViewerRef.value.getContent() const member = formatViewerRef.value.getContent();
await redisApi.saveListValue.request({ await redisApi.saveListValue.request({
value: [member], value: [member],
...param ...param,
}); });
ElMessage.success("保存成功"); ElMessage.success('保存成功');
if (dataRow) { if (dataRow) {
state.editDialog.dataRow.value = member; state.editDialog.dataRow.value = member;
} else { } else {
@@ -172,28 +158,27 @@ const confirmEditData = async () => {
} }
state.editDialog.visible = false; state.editDialog.visible = false;
state.editDialog.dataRow = null; state.editDialog.dataRow = null;
} };
const lrem = async (row: any, index: any) => { const lrem = async (row: any, index: any) => {
await redisApi.lrem.request({ await redisApi.lrem.request({
...getBaseReqParam(), ...getBaseReqParam(),
member: row.value, member: row.value,
count: 1, count: 1,
}) });
ElMessage.success("删除成功"); ElMessage.success('删除成功');
state.values.splice(index, 1) state.values.splice(index, 1);
state.total--; state.total--;
} };
const getBaseReqParam = () => { const getBaseReqParam = () => {
return { return {
id: state.redisId, id: state.redisId,
db: state.db, db: state.db,
key: state.key key: state.key,
} };
} };
defineExpose({ initData })
defineExpose({ initData });
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -1,38 +1,36 @@
<template> <template>
<div> <div>
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button> <el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
<el-table size="small" border :data="setDatas" height="450" min-height=300 stripe> <el-table size="small" border :data="setDatas" height="450" min-height="300" stripe>
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> <el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> </el-table-column>
</el-table-column> <el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200"> </el-table-column>
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
</el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template #header> <template #header>
<el-input class="key-detail-filter-value" v-model="state.filterValue" <el-input
@keyup.enter='sscanData(true, true)' placeholder="输入关键词回车搜索" clearable size="small" /> class="key-detail-filter-value"
v-model="state.filterValue"
@keyup.enter="sscanData(true, true)"
placeholder="输入关键词回车搜索"
clearable
size="small"
/>
</template> </template>
<template #default="scope"> <template #default="scope">
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" <el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" plain></el-link>
plain></el-link>
<el-popconfirm title="确定删除?" @confirm="srem(scope.row, scope.$index)"> <el-popconfirm title="确定删除?" @confirm="srem(scope.row, scope.$index)">
<template #reference> <template #reference>
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" <el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" plain class="ml5"></el-link>
plain class="ml5"></el-link>
</template> </template>
</el-popconfirm> </el-popconfirm>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- load more content --> <!-- load more content -->
<div class='content-more-container'> <div class="content-more-container">
<el-button size='small' @click='sscanData(false)' :disabled='loadMoreDisable' class='content-more-btn'> <el-button size="small" @click="sscanData(false)" :disabled="loadMoreDisable" class="content-more-btn"> 加载更多 </el-button>
加载更多
</el-button>
</div> </div>
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true" <el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true" :close-on-click-modal="false">
:close-on-click-modal="false">
<el-form> <el-form>
<el-form-item> <el-form-item>
<format-viewer class="w100" ref="formatViewerRef" :content="editDialog.content"></format-viewer> <format-viewer class="w100" ref="formatViewerRef" :content="editDialog.content"></format-viewer>
@@ -46,7 +44,6 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -64,12 +61,12 @@ const props = defineProps({
db: { db: {
type: [Number], type: [Number],
require: true, require: true,
default: 0 default: 0,
}, },
keyInfo: { keyInfo: {
type: [Object], type: [Object],
}, },
}) });
const formatViewerRef = ref(null) as any; const formatViewerRef = ref(null) as any;
@@ -91,32 +88,27 @@ const state = reactive({
visible: false, visible: false,
content: '', content: '',
dataRow: null as any, dataRow: null as any,
} },
}); });
const { const { total, setDatas, loadMoreDisable, editDialog } = toRefs(state);
total,
setDatas,
loadMoreDisable,
editDialog,
} = toRefs(state)
onMounted(() => { onMounted(() => {
state.redisId = props.redisId; state.redisId = props.redisId;
state.db = props.db; state.db = props.db;
state.key = props.keyInfo?.key; state.key = props.keyInfo?.key;
initData(); initData();
}) });
const initData = () => { const initData = () => {
state.filterValue = ''; state.filterValue = '';
sscanData(true, true); sscanData(true, true);
getTotal(); getTotal();
} };
const getScanMatch = () => { const getScanMatch = () => {
return state.filterValue ? `*${state.filterValue}*` : '*'; return state.filterValue ? `*${state.filterValue}*` : '*';
} };
const sscanData = async (resetDatas = true, resetCursor = false) => { const sscanData = async (resetDatas = true, resetCursor = false) => {
if (resetCursor) { if (resetCursor) {
@@ -125,7 +117,7 @@ const sscanData = async (resetDatas = true, resetCursor = false) => {
const res = await redisApi.sscan.request({ const res = await redisApi.sscan.request({
...getBaseReqParam(), ...getBaseReqParam(),
match: getScanMatch(), match: getScanMatch(),
...state.scanParam ...state.scanParam,
}); });
if (resetDatas) { if (resetDatas) {
@@ -134,44 +126,44 @@ const sscanData = async (resetDatas = true, resetCursor = false) => {
res.keys.forEach((x: any) => { res.keys.forEach((x: any) => {
state.setDatas.push({ state.setDatas.push({
value: x, value: x,
}) });
}) });
state.scanParam.cursor = res.cursor; state.scanParam.cursor = res.cursor;
state.loadMoreDisable = res.cursor == 0 state.loadMoreDisable = res.cursor == 0;
}; };
const getTotal = () => { const getTotal = () => {
redisApi.scard.request(getBaseReqParam()).then((res) => { redisApi.scard.request(getBaseReqParam()).then((res) => {
state.total = res; state.total = res;
}); });
} };
const showEditDialog = (row: any) => { const showEditDialog = (row: any) => {
state.editDialog.dataRow = row; state.editDialog.dataRow = row;
state.editDialog.content = row ? row.value : ''; state.editDialog.content = row ? row.value : '';
state.editDialog.visible = true; state.editDialog.visible = true;
} };
const confirmEditData = async () => { const confirmEditData = async () => {
const param = getBaseReqParam(); const param = getBaseReqParam();
// 存在数据行,则说明为修改,则要先删除旧数据后新增 // 存在数据行,则说明为修改,则要先删除旧数据后新增
const dataRow = state.editDialog.dataRow const dataRow = state.editDialog.dataRow;
if (dataRow) { if (dataRow) {
await redisApi.srem.request({ await redisApi.srem.request({
member: state.editDialog.dataRow.value, member: state.editDialog.dataRow.value,
...param ...param,
}); });
} }
// 获取set member内容并新增 // 获取set member内容并新增
const member = formatViewerRef.value.getContent() const member = formatViewerRef.value.getContent();
await redisApi.sadd.request({ await redisApi.sadd.request({
member, member,
...param ...param,
}); });
ElMessage.success("保存成功"); ElMessage.success('保存成功');
if (dataRow) { if (dataRow) {
state.editDialog.dataRow.value = member; state.editDialog.dataRow.value = member;
} else { } else {
@@ -180,26 +172,26 @@ const confirmEditData = async () => {
} }
state.editDialog.visible = false; state.editDialog.visible = false;
state.editDialog.dataRow = null; state.editDialog.dataRow = null;
} };
const srem = async (row: any, index: any) => { const srem = async (row: any, index: any) => {
await redisApi.srem.request({ await redisApi.srem.request({
...getBaseReqParam(), ...getBaseReqParam(),
member: row.value, member: row.value,
}) });
ElMessage.success("删除成功"); ElMessage.success('删除成功');
state.setDatas.splice(index, 1) state.setDatas.splice(index, 1);
state.total--; state.total--;
} };
const getBaseReqParam = () => { const getBaseReqParam = () => {
return { return {
id: state.redisId, id: state.redisId,
db: state.db, db: state.db,
key: state.key key: state.key,
} };
} };
defineExpose({ initData }) defineExpose({ initData });
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<el-form class='key-content-string' label-width="auto"> <el-form class="key-content-string" label-width="auto">
<div> <div>
<format-viewer ref="formatViewerRef" :content="string.value"></format-viewer> <format-viewer ref="formatViewerRef" :content="string.value"></format-viewer>
</div> </div>
@@ -31,9 +31,9 @@ const props = defineProps({
keyInfo: { keyInfo: {
type: [Object], type: [Object],
}, },
}) });
const formatViewerRef = ref(null) as any const formatViewerRef = ref(null) as any;
const state = reactive({ const state = reactive({
redisId: 0, redisId: 0,
@@ -50,20 +50,18 @@ const state = reactive({
}, },
}); });
const { const { string } = toRefs(state);
string,
} = toRefs(state)
onMounted(() => { onMounted(() => {
state.redisId = props.redisId state.redisId = props.redisId;
state.db = props.db state.db = props.db;
state.key = props.keyInfo?.key; state.key = props.keyInfo?.key;
initData(); initData();
}) });
const initData = () => { const initData = () => {
getStringValue(); getStringValue();
} };
const getStringValue = async () => { const getStringValue = async () => {
if (state.key) { if (state.key) {
@@ -86,12 +84,11 @@ const getBaseReqParam = () => {
return { return {
id: state.redisId, id: state.redisId,
db: state.db, db: state.db,
key: state.key key: state.key,
} };
} };
defineExpose({ initData })
defineExpose({ initData });
</script> </script>
<style lang="scss"> <style lang="scss">
.key-content-string .format-viewer-container { .key-content-string .format-viewer-container {

View File

@@ -1,40 +1,37 @@
<template> <template>
<div> <div>
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button> <el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
<el-table size="small" border :data="values" height="450" min-height=300 stripe> <el-table size="small" border :data="values" height="450" min-height="300" stripe>
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> <el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> </el-table-column>
</el-table-column> <el-table-column resizable sortable prop="score" label="score" show-overflow-tooltip min-width="100"> </el-table-column>
<el-table-column resizable sortable prop="score" label="score" show-overflow-tooltip min-width="100"> <el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200"> </el-table-column>
</el-table-column>
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200">
</el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template #header> <template #header>
<el-input class="key-detail-filter-value" v-model="state.filterValue" @keyup.enter='zscanData(true)' <el-input
placeholder="输入关键词回车搜索" clearable size="small" /> class="key-detail-filter-value"
v-model="state.filterValue"
@keyup.enter="zscanData(true)"
placeholder="输入关键词回车搜索"
clearable
size="small"
/>
</template> </template>
<template #default="scope"> <template #default="scope">
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" <el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" plain></el-link>
plain></el-link>
<el-popconfirm title="确定删除?" @confirm="zrem(scope.row, scope.$index)"> <el-popconfirm title="确定删除?" @confirm="zrem(scope.row, scope.$index)">
<template #reference> <template #reference>
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" <el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" plain class="ml5"></el-link>
plain class="ml5"></el-link>
</template> </template>
</el-popconfirm> </el-popconfirm>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- load more content --> <!-- load more content -->
<div class='content-more-container'> <div class="content-more-container">
<el-button size='small' @click='loadDatas()' :disabled='loadMoreDisable' class='content-more-btn'> <el-button size="small" @click="loadDatas()" :disabled="loadMoreDisable" class="content-more-btn"> 加载更多 </el-button>
加载更多
</el-button>
</div> </div>
<el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true" <el-dialog title="添加新行" v-model="editDialog.visible" width="600px" :destroy-on-close="true" :close-on-click-modal="false">
:close-on-click-modal="false">
<el-form> <el-form>
<el-form-item> <el-form-item>
<el-input type="number" v-model.number="editDialog.score" placeholder="score" /> <el-input type="number" v-model.number="editDialog.score" placeholder="score" />
@@ -51,7 +48,6 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -69,12 +65,12 @@ const props = defineProps({
db: { db: {
type: [Number], type: [Number],
require: true, require: true,
default: 0 default: 0,
}, },
keyInfo: { keyInfo: {
type: [Object], type: [Object],
}, },
}) });
const formatViewerRef = ref(null) as any; const formatViewerRef = ref(null) as any;
@@ -94,29 +90,24 @@ const state = reactive({
score: 0, score: 0,
content: '', content: '',
dataRow: null as any, dataRow: null as any,
} },
}); });
const { const { total, values, loadMoreDisable, editDialog } = toRefs(state);
total,
values,
loadMoreDisable,
editDialog,
} = toRefs(state)
onMounted(() => { onMounted(() => {
state.redisId = props.redisId; state.redisId = props.redisId;
state.db = props.db; state.db = props.db;
state.key = props.keyInfo?.key; state.key = props.keyInfo?.key;
initData(); initData();
}) });
const initData = async () => { const initData = async () => {
state.pageNum = 1; state.pageNum = 1;
state.filterValue = ''; state.filterValue = '';
await getTotal(); await getTotal();
await zrevrange(true); await zrevrange(true);
} };
const loadDatas = (resetTableData = false) => { const loadDatas = (resetTableData = false) => {
if (state.filterValue) { if (state.filterValue) {
@@ -124,7 +115,7 @@ const loadDatas = (resetTableData = false) => {
return; return;
} }
zrevrange(resetTableData); zrevrange(resetTableData);
} };
const zrevrange = async (resetTableData = false) => { const zrevrange = async (resetTableData = false) => {
const pageNum = state.pageNum; const pageNum = state.pageNum;
@@ -133,14 +124,14 @@ const zrevrange = async (resetTableData = false) => {
...getBaseReqParam(), ...getBaseReqParam(),
start: (pageNum - 1) * pageSize, start: (pageNum - 1) * pageSize,
stop: pageNum * pageSize - 1, stop: pageNum * pageSize - 1,
}) });
const vs = []; const vs = [];
for (let member of res) { for (let member of res) {
vs.push({ vs.push({
score: member.Score, score: member.Score,
value: member.Member value: member.Member,
}) });
} }
if (resetTableData) { if (resetTableData) {
state.values = vs; state.values = vs;
@@ -148,12 +139,12 @@ const zrevrange = async (resetTableData = false) => {
state.values.push(...vs); state.values.push(...vs);
} }
state.pageNum++; state.pageNum++;
state.loadMoreDisable = state.total <= state.values.length state.loadMoreDisable = state.total <= state.values.length;
} };
const getScanMatch = () => { const getScanMatch = () => {
return state.filterValue ? `*${state.filterValue}*` : '*'; return state.filterValue ? `*${state.filterValue}*` : '*';
} };
const zscanData = async (resetTableData = true, resetCursor = false) => { const zscanData = async (resetTableData = true, resetCursor = false) => {
if (resetCursor) { if (resetCursor) {
@@ -163,7 +154,7 @@ const zscanData = async (resetTableData = true, resetCursor = false) => {
...getBaseReqParam(), ...getBaseReqParam(),
match: getScanMatch(), match: getScanMatch(),
cursor: state.scanCursor, cursor: state.scanCursor,
count: state.pageSize count: state.pageSize,
}); });
const keys = res.keys; const keys = res.keys;
@@ -181,48 +172,50 @@ const zscanData = async (resetTableData = true, resetCursor = false) => {
} }
state.scanCursor = res.cursor; state.scanCursor = res.cursor;
state.loadMoreDisable = res.cursor == 0 state.loadMoreDisable = res.cursor == 0;
}; };
const getTotal = () => { const getTotal = () => {
redisApi.zcard.request({ redisApi.zcard
.request({
id: state.redisId, id: state.redisId,
db: state.db, db: state.db,
key: state.key key: state.key,
}).then((res) => { })
.then((res) => {
state.total = res; state.total = res;
}); });
} };
const showEditDialog = (row: any) => { const showEditDialog = (row: any) => {
state.editDialog.dataRow = row; state.editDialog.dataRow = row;
state.editDialog.content = row ? row.value : ''; state.editDialog.content = row ? row.value : '';
state.editDialog.score = row ? row.score : null; state.editDialog.score = row ? row.score : null;
state.editDialog.visible = true; state.editDialog.visible = true;
} };
const confirmEditData = async () => { const confirmEditData = async () => {
const param = getBaseReqParam(); const param = getBaseReqParam();
// 存在数据行,则说明为修改,则要先删除旧数据后新增 // 存在数据行,则说明为修改,则要先删除旧数据后新增
const dataRow = state.editDialog.dataRow const dataRow = state.editDialog.dataRow;
if (dataRow) { if (dataRow) {
await redisApi.zrem.request({ await redisApi.zrem.request({
member: state.editDialog.dataRow.value, member: state.editDialog.dataRow.value,
...param ...param,
}); });
} }
const score = state.editDialog.score const score = state.editDialog.score;
// 获取zset member内容并新增 // 获取zset member内容并新增
const member = formatViewerRef.value.getContent() const member = formatViewerRef.value.getContent();
await redisApi.zadd.request({ await redisApi.zadd.request({
score, score,
member, member,
...param ...param,
}); });
ElMessage.success("保存成功"); ElMessage.success('保存成功');
if (dataRow) { if (dataRow) {
state.editDialog.dataRow.value = member; state.editDialog.dataRow.value = member;
state.editDialog.dataRow.score = score; state.editDialog.dataRow.score = score;
@@ -232,26 +225,26 @@ const confirmEditData = async () => {
} }
state.editDialog.visible = false; state.editDialog.visible = false;
state.editDialog.dataRow = null; state.editDialog.dataRow = null;
} };
const zrem = async (row: any, index: any) => { const zrem = async (row: any, index: any) => {
await redisApi.zrem.request({ await redisApi.zrem.request({
...getBaseReqParam(), ...getBaseReqParam(),
member: row.value, member: row.value,
}) });
ElMessage.success("删除成功"); ElMessage.success('删除成功');
state.values.splice(index, 1) state.values.splice(index, 1);
state.total--; state.total--;
} };
const getBaseReqParam = () => { const getBaseReqParam = () => {
return { return {
id: state.redisId, id: state.redisId,
db: state.db, db: state.db,
key: state.key key: state.key,
} };
} };
defineExpose({ initData }) defineExpose({ initData });
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -1,7 +1,6 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
:destroy-on-close="true" width="38%">
<el-form :model="form" ref="redisForm" :rules="rules" label-width="auto"> <el-form :model="form" ref="redisForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName"> <el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic"> <el-tab-pane label="基础信息" name="basic">
@@ -19,28 +18,41 @@
</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 v-model.trim="form.host" <el-input
v-model.trim="form.host"
placeholder="请输入host:portsentinel模式为: mastername=sentinelhost:port若集群或哨兵需设多个节点可使用','分割" placeholder="请输入host:portsentinel模式为: mastername=sentinelhost:port若集群或哨兵需设多个节点可使用','分割"
auto-complete="off" type="textarea"></el-input> auto-complete="off"
type="textarea"
></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password" label="密码:"> <el-form-item prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password" <el-input
placeholder="请输入密码, 修改操作可不填" autocomplete="new-password"><template type="password"
v-if="form.id && form.id != 0" #suffix> show-password
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" v-model.trim="form.password"
:content="pwd"> placeholder="请输入密码, 修改操作可不填"
autocomplete="new-password"
><template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
<template #reference> <template #reference>
<el-link @click="getPwd" :underline="false" type="primary" <el-link @click="getPwd" :underline="false" type="primary" class="mr5">原密码</el-link>
class="mr5">原密码</el-link>
</template> </template>
</el-popover> </el-popover>
</template></el-input> </template></el-input
>
</el-form-item> </el-form-item>
<el-form-item prop="db" label="库号:" required> <el-form-item prop="db" label="库号:" required>
<el-select @change="changeDb" :disabled="form.mode == 'cluster'" v-model="dbList" multiple <el-select
allow-create filterable placeholder="请选择可操作库号" style="width: 100%"> @change="changeDb"
<el-option v-for="db in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]" :key="db" :disabled="form.mode == 'cluster'"
:label="db" :value="db" /> v-model="dbList"
multiple
allow-create
filterable
placeholder="请选择可操作库号"
style="width: 100%"
>
<el-option v-for="db in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]" :key="db" :label="db" :value="db" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item prop="remark" label="备注:"> <el-form-item prop="remark" label="备注:">
@@ -84,16 +96,18 @@ const props = defineProps({
title: { title: {
type: String, type: String,
}, },
}) });
const emit = defineEmits(['update:visible', 'val-change', 'cancel']) const emit = defineEmits(['update:visible', 'val-change', 'cancel']);
const rules = { const rules = {
tagId: [{ tagId: [
{
required: true, required: true,
message: '请选择标签', message: '请选择标签',
trigger: ['blur', 'change'], trigger: ['blur', 'change'],
}], },
],
name: [ name: [
{ {
required: true, required: true,
@@ -122,7 +136,7 @@ const rules = {
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
} };
const redisForm: any = ref(null); const redisForm: any = ref(null);
const state = reactive({ const state = reactive({
@@ -143,17 +157,9 @@ const state = reactive({
dbList: [0], dbList: [0],
pwd: '', pwd: '',
btnLoading: false, btnLoading: false,
}); });
const { const { dialogVisible, tabActiveName, form, dbList, pwd, btnLoading } = toRefs(state);
dialogVisible,
tabActiveName,
form,
dbList,
pwd,
btnLoading,
} = toRefs(state)
watch(props, async (newValue: any) => { watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
@@ -194,7 +200,7 @@ const btnOk = async () => {
return; return;
} }
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) { if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1 reqForm.sshTunnelMachineId = -1;
} }
reqForm.password = await RsaEncrypt(reqForm.password); reqForm.password = await RsaEncrypt(reqForm.password);
redisApi.saveRedis.request(reqForm).then(() => { redisApi.saveRedis.request(reqForm).then(() => {

View File

@@ -1,20 +1,27 @@
<template> <template>
<div> <div>
<page-table ref="pageTableRef" :query="queryConfig" v-model:query-form="query" :show-selection="true" <page-table
v-model:selection-data="selectionData" :data="redisTable" :columns="columns" :total="total" ref="pageTableRef"
v-model:page-size="query.pageSize" v-model:page-num="query.pageNum" @pageChange="search()"> :query="queryConfig"
v-model:query-form="query"
:show-selection="true"
v-model:selection-data="selectionData"
:data="redisTable"
:columns="columns"
:total="total"
v-model:page-size="query.pageSize"
v-model:page-num="query.pageNum"
@pageChange="search()"
>
<template #tagPathSelect> <template #tagPathSelect>
<el-select @focus="getTags" v-model="query.tagPath" placeholder="请选择标签" @clear="search" filterable clearable <el-select @focus="getTags" v-model="query.tagPath" placeholder="请选择标签" @clear="search" filterable clearable style="width: 200px">
style="width: 200px">
<el-option v-for="item in tags" :key="item" :label="item" :value="item"> </el-option> <el-option v-for="item in tags" :key="item" :label="item" :value="item"> </el-option>
</el-select> </el-select>
</template> </template>
<template #queryRight> <template #queryRight>
<el-button type="primary" icon="plus" @click="editRedis(true)" plain>添加</el-button> <el-button type="primary" icon="plus" @click="editRedis(true)" plain>添加</el-button>
<el-button type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteRedis" plain>删除 <el-button type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteRedis" plain>删除 </el-button>
</el-button>
</template> </template>
<template #tagPath="{ data }"> <template #tagPath="{ data }">
@@ -27,10 +34,8 @@
<template #more="{ data }"> <template #more="{ data }">
<el-button @click="showDetail(data)" link>详情</el-button> <el-button @click="showDetail(data)" link>详情</el-button>
<el-button v-if="data.mode === 'standalone' || data.mode === 'sentinel'" type="primary" <el-button v-if="data.mode === 'standalone' || data.mode === 'sentinel'" type="primary" @click="showInfoDialog(data)" link>单机信息</el-button>
@click="showInfoDialog(data)" link>单机信息</el-button> <el-button @click="onShowClusterInfo(data)" v-if="data.mode === 'cluster'" type="primary" link>集群信息</el-button>
<el-button @click="onShowClusterInfo(data)" v-if="data.mode === 'cluster'" type="primary"
link>集群信息</el-button>
</template> </template>
<template #action="{ data }"> <template #action="{ data }">
@@ -41,8 +46,7 @@
<info v-model:visible="infoDialog.visible" :title="infoDialog.title" :info="infoDialog.info"></info> <info v-model:visible="infoDialog.visible" :title="infoDialog.title" :info="infoDialog.info"></info>
<el-dialog width="1000px" title="集群信息" v-model="clusterInfoDialog.visible"> <el-dialog width="1000px" title="集群信息" v-model="clusterInfoDialog.visible">
<el-input type="textarea" :autosize="{ minRows: 12, maxRows: 12 }" v-model="clusterInfoDialog.info"> <el-input type="textarea" :autosize="{ minRows: 12, maxRows: 12 }" v-model="clusterInfoDialog.info"> </el-input>
</el-input>
<el-divider content-position="left">节点信息</el-divider> <el-divider content-position="left">节点信息</el-divider>
<el-table :data="clusterInfoDialog.nodes" stripe size="small" border> <el-table :data="clusterInfoDialog.nodes" stripe size="small" border>
@@ -59,16 +63,25 @@
<el-table-column prop="ip" label="ip" min-width="180"> <el-table-column prop="ip" label="ip" min-width="180">
<template #header> <template #header>
ip ip
<el-tooltip class="box-item" effect="dark" <el-tooltip
content="ip:port1@port2port1指redis服务器与客户端通信的端口port2则是集群内部节点间通信的端口" placement="top"> class="box-item"
effect="dark"
content="ip:port1@port2port1指redis服务器与客户端通信的端口port2则是集群内部节点间通信的端口"
placement="top"
>
<el-icon> <el-icon>
<question-filled /> <question-filled />
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
</template> </template>
<template #default="scope"> <template #default="scope">
<el-tag @click="showInfoDialog({ id: clusterInfoDialog.redisId, ip: scope.row.ip })" effect="plain" <el-tag
type="success" size="small" style="cursor: pointer">{{ scope.row.ip }} @click="showInfoDialog({ id: clusterInfoDialog.redisId, ip: scope.row.ip })"
effect="plain"
type="success"
size="small"
style="cursor: pointer"
>{{ scope.row.ip }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -76,8 +89,12 @@
<el-table-column prop="masterSlaveRelation" label="masterSlaveRelation" min-width="300"> <el-table-column prop="masterSlaveRelation" label="masterSlaveRelation" min-width="300">
<template #header> <template #header>
masterSlaveRelation masterSlaveRelation
<el-tooltip class="box-item" effect="dark" content="如果节点是slave并且已知master节点则为master节点ID否则为符号'-'" <el-tooltip
placement="top"> class="box-item"
effect="dark"
content="如果节点是slave并且已知master节点则为master节点ID否则为符号'-'"
placement="top"
>
<el-icon> <el-icon>
<question-filled /> <question-filled />
</el-icon> </el-icon>
@@ -97,8 +114,12 @@
<el-table-column prop="configEpoch" label="configEpoch" min-width="130"> <el-table-column prop="configEpoch" label="configEpoch" min-width="130">
<template #header> <template #header>
configEpoch configEpoch
<el-tooltip class="box-item" effect="dark" <el-tooltip
content="节点的epoch值如果该节点是从节点则为其主节点的epoch值。每当节点发生失败切换时都会创建一个新的独特的递增的epoch。" placement="top"> class="box-item"
effect="dark"
content="节点的epoch值如果该节点是从节点则为其主节点的epoch值。每当节点发生失败切换时都会创建一个新的独特的递增的epoch。"
placement="top"
>
<el-icon> <el-icon>
<question-filled /> <question-filled />
</el-icon> </el-icon>
@@ -122,21 +143,23 @@
<el-descriptions-item :span="3" label="库">{{ detailDialog.data.db }}</el-descriptions-item> <el-descriptions-item :span="3" label="库">{{ detailDialog.data.db }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ detailDialog.data.remark }}</el-descriptions-item> <el-descriptions-item :span="3" label="备注">{{ detailDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="3" label="SSH隧道">{{ detailDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} <el-descriptions-item :span="3" label="SSH隧道">{{ detailDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(detailDialog.data.createTime) }} <el-descriptions-item :span="2" label="创建时间">{{ dateFormat(detailDialog.data.createTime) }} </el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ detailDialog.data.creator }}</el-descriptions-item> <el-descriptions-item :span="1" label="创建者">{{ detailDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(detailDialog.data.updateTime) }} <el-descriptions-item :span="2" label="更新时间">{{ dateFormat(detailDialog.data.updateTime) }} </el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ detailDialog.data.modifier }}</el-descriptions-item> <el-descriptions-item :span="1" label="修改者">{{ detailDialog.data.modifier }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
<redis-edit @val-change="valChange" :tags="tags" :title="redisEditDialog.title" <redis-edit
v-model:visible="redisEditDialog.visible" v-model:redis="redisEditDialog.data"></redis-edit> @val-change="valChange"
:tags="tags"
:title="redisEditDialog.title"
v-model:visible="redisEditDialog.visible"
v-model:redis="redisEditDialog.data"
></redis-edit>
</div> </div>
</template> </template>
@@ -149,23 +172,21 @@ import { tagApi } from '../tag/api';
import RedisEdit from './RedisEdit.vue'; import RedisEdit from './RedisEdit.vue';
import { dateFormat } from '@/common/utils/date'; import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue'; import TagInfo from '../component/TagInfo.vue';
import PageTable from '@/components/pagetable/PageTable.vue' import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
const pageTableRef: any = ref(null) const pageTableRef: any = ref(null);
const queryConfig = [ const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')];
TableQuery.slot("tagPath", "标签", "tagPathSelect"),
]
const columns = ref([ const columns = ref([
TableColumn.new("tagPath", "标签路径").isSlot().setAddWidth(20), TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
TableColumn.new("name", "名称"), TableColumn.new('name', '名称'),
TableColumn.new("host", "host:port"), TableColumn.new('host', 'host:port'),
TableColumn.new("mode", "mode"), TableColumn.new('mode', 'mode'),
TableColumn.new("remark", "备注"), TableColumn.new('remark', '备注'),
TableColumn.new("more", "更多").isSlot().setMinWidth(155).fixedRight(), TableColumn.new('more', '更多').isSlot().setMinWidth(155).fixedRight(),
TableColumn.new("action", "操作").isSlot().setMinWidth(65).fixedRight().alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(65).fixedRight().alignCenter(),
]) ]);
const state = reactive({ const state = reactive({
tags: [], tags: [],
@@ -205,36 +226,25 @@ const state = reactive({
}, },
}); });
const { const { tags, redisTable, total, selectionData, query, detailDialog, clusterInfoDialog, infoDialog, redisEditDialog } = toRefs(state);
tags,
redisTable,
total,
selectionData,
query,
detailDialog,
clusterInfoDialog,
infoDialog,
redisEditDialog,
} = toRefs(state)
onMounted(async () => { onMounted(async () => {
search(); search();
}); });
const showDetail = (detail: any) => { const showDetail = (detail: any) => {
state.detailDialog.data = detail; state.detailDialog.data = detail;
state.detailDialog.visible = true; state.detailDialog.visible = true;
} };
const deleteRedis = async () => { const deleteRedis = async () => {
try { try {
await ElMessageBox.confirm(`确定删除该【${state.selectionData.map((x: any) => x.name).join(", ")}】redis信息?`, '提示', { await ElMessageBox.confirm(`确定删除该【${state.selectionData.map((x: any) => x.name).join(', ')}】redis信息?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}); });
await redisApi.delRedis.request({ id: state.selectionData.map((x: any) => x.id).join(",") }); await redisApi.delRedis.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功'); ElMessage.success('删除成功');
search(); search();
} catch (err) {} } catch (err) {}

View File

@@ -12,9 +12,9 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
}) });
const monacoEditorRef = ref(null) as any const monacoEditorRef = ref(null) as any;
const state = reactive({ const state = reactive({
modelValue: '', modelValue: '',
@@ -30,15 +30,15 @@ const state = reactive({
// ); // );
onMounted(() => { onMounted(() => {
setContent(props.content) setContent(props.content);
}) });
const setContent = (val: any) => { const setContent = (val: any) => {
state.modelValue = val; state.modelValue = val;
setTimeout(() => { setTimeout(() => {
monacoEditorRef.value.format(); monacoEditorRef.value.format();
}, 200) }, 200);
} };
const getContent = () => { const getContent = () => {
// 尝试压缩json // 尝试压缩json
@@ -46,11 +46,10 @@ const getContent = () => {
state.content = JSON.stringify(JSON.parse(state.modelValue)); state.content = JSON.stringify(JSON.parse(state.modelValue));
return state.content; return state.content;
} catch (e) { } catch (e) {
return state.modelValue return state.modelValue;
}
} }
};
defineExpose({ getContent }) defineExpose({ getContent });
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -10,15 +10,13 @@ const props = defineProps({
content: { content: {
type: String, type: String,
}, },
}) });
const state = reactive({ const state = reactive({
modelValue: '', modelValue: '',
}); });
const { const { modelValue } = toRefs(state);
modelValue,
} = toRefs(state)
watch( watch(
() => props.content, () => props.content,
@@ -29,13 +27,12 @@ watch(
onMounted(() => { onMounted(() => {
state.modelValue = props.content as any; state.modelValue = props.content as any;
}) });
const getContent = () => { const getContent = () => {
return state.modelValue return state.modelValue;
} };
defineExpose({ getContent })
defineExpose({ getContent });
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -1,48 +1,48 @@
import Api from '@/common/Api'; import Api from '@/common/Api';
export const redisApi = { export const redisApi = {
redisList: Api.newGet("/redis"), redisList: Api.newGet('/redis'),
getRedisPwd: Api.newGet("/redis/{id}/pwd"), getRedisPwd: Api.newGet('/redis/{id}/pwd'),
redisInfo: Api.newGet("/redis/{id}/info"), redisInfo: Api.newGet('/redis/{id}/info'),
clusterInfo: Api.newGet("/redis/{id}/cluster-info"), clusterInfo: Api.newGet('/redis/{id}/cluster-info'),
saveRedis: Api.newPost("/redis"), saveRedis: Api.newPost('/redis'),
delRedis: Api.newDelete("/redis/{id}"), delRedis: Api.newDelete('/redis/{id}'),
keyTtl: Api.newGet("/redis/{id}/{db}/key-ttl"), keyTtl: Api.newGet('/redis/{id}/{db}/key-ttl'),
renameKey: Api.newPost("/redis/{id}/{db}/rename-key"), renameKey: Api.newPost('/redis/{id}/{db}/rename-key'),
expireKey: Api.newPost("/redis/{id}/{db}/expire-key"), expireKey: Api.newPost('/redis/{id}/{db}/expire-key'),
persistKey: Api.newDelete("/redis/{id}/{db}/persist-key"), persistKey: Api.newDelete('/redis/{id}/{db}/persist-key'),
// 获取权限列表 // 获取权限列表
scan: Api.newPost("/redis/{id}/{db}/scan"), scan: Api.newPost('/redis/{id}/{db}/scan'),
getString: Api.newGet("/redis/{id}/{db}/string-value"), getString: Api.newGet('/redis/{id}/{db}/string-value'),
setString: Api.newPost("/redis/{id}/{db}/string-value"), setString: Api.newPost('/redis/{id}/{db}/string-value'),
getHashValue: Api.newGet("/redis/{id}/{db}/hash-value"), getHashValue: Api.newGet('/redis/{id}/{db}/hash-value'),
hscan: Api.newGet("/redis/{id}/{db}/hscan"), hscan: Api.newGet('/redis/{id}/{db}/hscan'),
hget: Api.newGet("/redis/{id}/{db}/hget"), hget: Api.newGet('/redis/{id}/{db}/hget'),
hset: Api.newPost("/redis/{id}/{db}/hset"), hset: Api.newPost('/redis/{id}/{db}/hset'),
hdel: Api.newDelete("/redis/{id}/{db}/hdel"), hdel: Api.newDelete('/redis/{id}/{db}/hdel'),
saveHashValue: Api.newPost("/redis/{id}/{db}/hash-value"), saveHashValue: Api.newPost('/redis/{id}/{db}/hash-value'),
getSetValue: Api.newGet("/redis/{id}/{db}/set-value"), getSetValue: Api.newGet('/redis/{id}/{db}/set-value'),
scard: Api.newGet("/redis/{id}/{db}/scard"), scard: Api.newGet('/redis/{id}/{db}/scard'),
sscan: Api.newPost("/redis/{id}/{db}/sscan"), sscan: Api.newPost('/redis/{id}/{db}/sscan'),
sadd: Api.newPost("/redis/{id}/{db}/sadd"), sadd: Api.newPost('/redis/{id}/{db}/sadd'),
srem: Api.newPost("/redis/{id}/{db}/srem"), srem: Api.newPost('/redis/{id}/{db}/srem'),
saveSetValue: Api.newPost("/redis/{id}/{db}/set-value"), saveSetValue: Api.newPost('/redis/{id}/{db}/set-value'),
del: Api.newDelete("/redis/{id}/{db}/scan/{cursor}/{count}"), del: Api.newDelete('/redis/{id}/{db}/scan/{cursor}/{count}'),
delKey: Api.newDelete("/redis/{id}/{db}/key"), delKey: Api.newDelete('/redis/{id}/{db}/key'),
flushDb: Api.newDelete("/redis/{id}/{db}/flushdb"), flushDb: Api.newDelete('/redis/{id}/{db}/flushdb'),
lrem: Api.newPost("/redis/{id}/{db}/lrem"), lrem: Api.newPost('/redis/{id}/{db}/lrem'),
getListValue: Api.newGet("/redis/{id}/{db}/list-value"), getListValue: Api.newGet('/redis/{id}/{db}/list-value'),
saveListValue: Api.newPost("/redis/{id}/{db}/list-value"), saveListValue: Api.newPost('/redis/{id}/{db}/list-value'),
setListValue: Api.newPost("/redis/{id}/{db}/list-value/lset"), setListValue: Api.newPost('/redis/{id}/{db}/list-value/lset'),
zcard: Api.newGet("/redis/{id}/{db}/zcard"), zcard: Api.newGet('/redis/{id}/{db}/zcard'),
zscan: Api.newGet("/redis/{id}/{db}/zscan"), zscan: Api.newGet('/redis/{id}/{db}/zscan'),
zrevrange: Api.newGet("/redis/{id}/{db}/zrevrange"), zrevrange: Api.newGet('/redis/{id}/{db}/zrevrange'),
zadd: Api.newPost("/redis/{id}/{db}/zadd"), zadd: Api.newPost('/redis/{id}/{db}/zadd'),
zrem: Api.newPost("/redis/{id}/{db}/zrem"), zrem: Api.newPost('/redis/{id}/{db}/zrem'),
} };

View File

@@ -9,16 +9,27 @@
1. 用于将资产进行归类 1. 用于将资产进行归类
<br />2. 可在团队管理中进行分配用于资源隔离 <br />3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源 <br />2. 可在团队管理中进行分配用于资源隔离 <br />3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源
</template> </template>
<span>标签作用<el-icon> <span
>标签作用<el-icon>
<question-filled /> <question-filled />
</el-icon> </el-icon>
</span> </span>
</el-tooltip> </el-tooltip>
</div> </div>
</div> </div>
<el-tree ref="tagTreeRef" class="none-select" :indent="38" node-key="id" :props="props" :data="data" <el-tree
@node-expand="handleNodeExpand" @node-collapse="handleNodeCollapse" ref="tagTreeRef"
:default-expanded-keys="defaultExpandedKeys" :expand-on-click-node="false" :filter-node-method="filterNode"> class="none-select"
:indent="38"
node-key="id"
:props="props"
:data="data"
@node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"
:default-expanded-keys="defaultExpandedKeys"
:expand-on-click-node="false"
:filter-node-method="filterNode"
>
<template #default="{ data }"> <template #default="{ data }">
<span class="custom-tree-node"> <span class="custom-tree-node">
<span style="font-size: 13px"> <span style="font-size: 13px">
@@ -29,14 +40,11 @@
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }}</el-tag> <el-tag v-if="data.children !== null" size="small">{{ data.children.length }}</el-tag>
</span> </span>
<el-link @click.prevent="info(data)" style="margin-left: 25px" icon="view" type="info" <el-link @click.prevent="info(data)" style="margin-left: 25px" icon="view" type="info" :underline="false" />
:underline="false" />
<el-link v-auth="'tag:save'" @click.prevent="showEditTagDialog(data)" class="ml5" type="primary" <el-link v-auth="'tag:save'" @click.prevent="showEditTagDialog(data)" class="ml5" type="primary" icon="edit" :underline="false" />
icon="edit" :underline="false" />
<el-link v-auth="'tag:save'" @click.prevent="showSaveTabDialog(data)" icon="circle-plus" <el-link v-auth="'tag:save'" @click.prevent="showSaveTabDialog(data)" icon="circle-plus" :underline="false" type="success" class="ml5" />
:underline="false" type="success" class="ml5" />
<!-- <el-link <!-- <el-link
v-auth="'resource:changeStatus'" v-auth="'resource:changeStatus'"
@@ -59,18 +67,24 @@
class="ml5" class="ml5"
/> --> /> -->
<el-link v-auth="'tag:del'" @click.prevent="deleteTag(data)" v-if="data.children == null" <el-link
type="danger" icon="delete" :underline="false" plain class="ml5" /> v-auth="'tag:del'"
@click.prevent="deleteTag(data)"
v-if="data.children == null"
type="danger"
icon="delete"
:underline="false"
plain
class="ml5"
/>
</span> </span>
</template> </template>
</el-tree> </el-tree>
<el-dialog width="500px" :title="saveTabDialog.title" :before-close="cancelSaveTag" <el-dialog width="500px" :title="saveTabDialog.title" :before-close="cancelSaveTag" v-model="saveTabDialog.visible">
v-model="saveTabDialog.visible">
<el-form ref="tagForm" :rules="rules" :model="saveTabDialog.form" label-width="auto"> <el-form ref="tagForm" :rules="rules" :model="saveTabDialog.form" label-width="auto">
<el-form-item prop="code" label="标识:" required> <el-form-item prop="code" label="标识:" required>
<el-input :disabled="saveTabDialog.form.id ? true : false" v-model="saveTabDialog.form.code" <el-input :disabled="saveTabDialog.form.id ? true : false" v-model="saveTabDialog.form.code" auto-complete="off"></el-input>
auto-complete="off"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="name" label="名称:" required> <el-form-item prop="name" label="名称:" required>
<el-input v-model="saveTabDialog.form.name" auto-complete="off"></el-input> <el-input v-model="saveTabDialog.form.name" auto-complete="off"></el-input>
@@ -134,15 +148,10 @@ const state = reactive({
data: null as any, data: null as any,
}, },
// 展开的节点 // 展开的节点
defaultExpandedKeys: [] as any defaultExpandedKeys: [] as any,
}); });
const { const { data, saveTabDialog, infoDialog, defaultExpandedKeys } = toRefs(state);
data,
saveTabDialog,
infoDialog,
defaultExpandedKeys,
} = toRefs(state)
const props = { const props = {
label: 'name', label: 'name',

View File

@@ -1,14 +1,20 @@
<template> <template>
<div> <div>
<page-table :query="state.queryConfig" v-model:query-form="query" :show-selection="true" <page-table
v-model:selection-data="selectionData" :data="data" :columns="state.columns" :total="total" :query="state.queryConfig"
v-model:page-size="query.pageSize" v-model:page-num="query.pageNum" @pageChange="search()"> v-model:query-form="query"
:show-selection="true"
v-model:selection-data="selectionData"
:data="data"
:columns="state.columns"
:total="total"
v-model:page-size="query.pageSize"
v-model:page-num="query.pageNum"
@pageChange="search()"
>
<template #queryRight> <template #queryRight>
<el-button v-auth="'team:save'" type="primary" icon="plus" @click="showSaveTeamDialog(false)">添加</el-button> <el-button v-auth="'team:save'" type="primary" icon="plus" @click="showSaveTeamDialog(false)">添加</el-button>
<el-button v-auth="'team:del'" :disabled="selectionData.length < 1" @click="deleteTeam()" type="danger" <el-button v-auth="'team:del'" :disabled="selectionData.length < 1" @click="deleteTeam()" type="danger" icon="delete">删除</el-button>
icon="delete">删除</el-button>
</template> </template>
<template #tagPath="{ data }"> <template #tagPath="{ data }">
@@ -44,14 +50,23 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog width="500px" :title="showTagDialog.title" :before-close="closeTagDialog" <el-dialog width="500px" :title="showTagDialog.title" :before-close="closeTagDialog" v-model="showTagDialog.visible">
v-model="showTagDialog.visible">
<el-form label-width="auto"> <el-form label-width="auto">
<el-form-item prop="tag" label="标签:"> <el-form-item prop="tag" label="标签:">
<el-tree-select ref="tagTreeRef" style="width: 100%" v-model="showTagDialog.tagTreeTeams" <el-tree-select
:data="showTagDialog.tags" :default-expanded-keys="showTagDialog.tagTreeTeams" multiple ref="tagTreeRef"
:render-after-expand="true" show-checkbox check-strictly node-key="id" :props="showTagDialog.props" style="width: 100%"
@check="tagTreeNodeCheck"> v-model="showTagDialog.tagTreeTeams"
:data="showTagDialog.tags"
:default-expanded-keys="showTagDialog.tagTreeTeams"
multiple
:render-after-expand="true"
show-checkbox
check-strictly
node-key="id"
:props="showTagDialog.props"
@check="tagTreeNodeCheck"
>
<template #default="{ data }"> <template #default="{ data }">
<span class="custom-tree-node"> <span class="custom-tree-node">
<span style="font-size: 13px"> <span style="font-size: 13px">
@@ -59,8 +74,7 @@
<span style="color: #3c8dbc"></span> <span style="color: #3c8dbc"></span>
{{ data.name }} {{ data.name }}
<span style="color: #3c8dbc"></span> <span style="color: #3c8dbc"></span>
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }} <el-tag v-if="data.children !== null" size="small">{{ data.children.length }} </el-tag>
</el-tag>
</span> </span>
</span> </span>
</template> </template>
@@ -77,13 +91,20 @@
<el-dialog width="700px" :title="showMemDialog.title" v-model="showMemDialog.visible"> <el-dialog width="700px" :title="showMemDialog.title" v-model="showMemDialog.visible">
<div class="toolbar"> <div class="toolbar">
<el-button v-auth="'team:member:save'" @click="showAddMemberDialog()" type="primary" icon="plus" <el-button v-auth="'team:member:save'" @click="showAddMemberDialog()" type="primary" icon="plus" size="small">添加</el-button>
size="small">添加</el-button> <el-button v-auth="'team:member:del'" @click="deleteMember" :disabled="showMemDialog.chooseId == null" type="danger" icon="delete" size="small"
<el-button v-auth="'team:member:del'" @click="deleteMember" :disabled="showMemDialog.chooseId == null" >移除</el-button
type="danger" icon="delete" size="small">移除</el-button> >
<div style="float: right"> <div style="float: right">
<el-input placeholder="请输入用户名" class="mr2" style="width: 150px" v-model="showMemDialog.query.username" <el-input
size="small" @clear="search" clearable></el-input> placeholder="请输入用户名"
class="mr2"
style="width: 150px"
v-model="showMemDialog.query.username"
size="small"
@clear="search"
clearable
></el-input>
<el-button @click="setMemebers" type="success" icon="search" size="small"></el-button> <el-button @click="setMemebers" type="success" icon="search" size="small"></el-button>
</div> </div>
</div> </div>
@@ -104,17 +125,30 @@
</el-table-column> </el-table-column>
<el-table-column property="creator" label="分配者" width="135"></el-table-column> <el-table-column property="creator" label="分配者" width="135"></el-table-column>
</el-table> </el-table>
<el-pagination size="small" @current-change="setMemebers" style="text-align: center" background <el-pagination
layout="prev, pager, next, total, jumper" :total="showMemDialog.members.total" size="small"
v-model:current-page="showMemDialog.query.pageNum" :page-size="showMemDialog.query.pageSize" /> @current-change="setMemebers"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="showMemDialog.members.total"
v-model:current-page="showMemDialog.query.pageNum"
:page-size="showMemDialog.query.pageSize"
/>
<el-dialog width="400px" title="添加成员" :before-close="cancelAddMember" v-model="showMemDialog.addVisible"> <el-dialog width="400px" title="添加成员" :before-close="cancelAddMember" v-model="showMemDialog.addVisible">
<el-form :model="showMemDialog.memForm" label-width="auto"> <el-form :model="showMemDialog.memForm" label-width="auto">
<el-form-item label="账号:"> <el-form-item label="账号:">
<el-select style="width: 100%" remote :remote-method="getAccount" <el-select
v-model="showMemDialog.memForm.accountIds" filterable multiple placeholder="请输入账号模糊搜索并选择"> style="width: 100%"
<el-option v-for="item in showMemDialog.accounts" :key="item.id" remote
:label="`${item.username} [${item.name}]`" :value="item.id"> :remote-method="getAccount"
v-model="showMemDialog.memForm.accountIds"
filterable
multiple
placeholder="请输入账号模糊搜索并选择"
>
<el-option v-for="item in showMemDialog.accounts" :key="item.id" :label="`${item.username} [${item.name}]`" :value="item.id">
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -137,7 +171,7 @@ import { accountApi } from '../../system/api';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { dateFormat } from '@/common/utils/date'; import { dateFormat } from '@/common/utils/date';
import { notBlank } from '@/common/assert'; import { notBlank } from '@/common/assert';
import PageTable from '@/components/pagetable/PageTable.vue' import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
const teamForm: any = ref(null); const teamForm: any = ref(null);
@@ -154,15 +188,13 @@ const state = reactive({
pageSize: 10, pageSize: 10,
name: null, name: null,
}, },
queryConfig: [ queryConfig: [TableQuery.text('name', '团队名称')],
TableQuery.text("name", "团队名称"),
],
columns: [ columns: [
TableColumn.new("name", "团队名称"), TableColumn.new('name', '团队名称'),
TableColumn.new("remark", "备注"), TableColumn.new('remark', '备注'),
TableColumn.new("createTime", "创建时间").isTime(), TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new("creator", "创建人"), TableColumn.new('creator', '创建人'),
TableColumn.new("action", "操作").isSlot().setMinWidth(100).fixedRight().alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(100).fixedRight().alignCenter(),
], ],
total: 0, total: 0,
data: [], data: [],
@@ -203,15 +235,7 @@ const state = reactive({
}, },
}); });
const { const { query, addTeamDialog, total, data, selectionData, showMemDialog, showTagDialog } = toRefs(state);
query,
addTeamDialog,
total,
data,
selectionData,
showMemDialog,
showTagDialog,
} = toRefs(state)
onMounted(() => { onMounted(() => {
search(); search();
@@ -252,12 +276,12 @@ const cancelSaveTeam = () => {
}; };
const deleteTeam = () => { const deleteTeam = () => {
ElMessageBox.confirm(`此操作将删除【${state.selectionData.map((x: any) => x.name).join(", ")}】团队信息, 是否继续?`, '提示', { ElMessageBox.confirm(`此操作将删除【${state.selectionData.map((x: any) => x.name).join(', ')}】团队信息, 是否继续?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}).then(async () => { }).then(async () => {
await tagApi.delTeam.request({ id: state.selectionData.map((x: any) => x.id).join(",") }); await tagApi.delTeam.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功!'); ElMessage.success('删除成功!');
search(); search();
}); });

View File

@@ -1,20 +1,20 @@
import Api from '@/common/Api'; import Api from '@/common/Api';
export const tagApi = { export const tagApi = {
getAccountTags: Api.newGet("/tag-trees/account-has"), getAccountTags: Api.newGet('/tag-trees/account-has'),
listByQuery: Api.newGet("/tag-trees/query"), listByQuery: Api.newGet('/tag-trees/query'),
getTagTrees: Api.newGet("/tag-trees"), getTagTrees: Api.newGet('/tag-trees'),
saveTagTree: Api.newPost("/tag-trees"), saveTagTree: Api.newPost('/tag-trees'),
delTagTree: Api.newDelete("/tag-trees/{id}"), delTagTree: Api.newDelete('/tag-trees/{id}'),
getTeams: Api.newGet("/teams"), getTeams: Api.newGet('/teams'),
saveTeam: Api.newPost("/teams"), saveTeam: Api.newPost('/teams'),
delTeam: Api.newDelete("/teams/{id}"), delTeam: Api.newDelete('/teams/{id}'),
getTeamMem: Api.newGet("/teams/{teamId}/members"), getTeamMem: Api.newGet('/teams/{teamId}/members'),
saveTeamMem: Api.newPost("/teams/{teamId}/members"), saveTeamMem: Api.newPost('/teams/{teamId}/members'),
delTeamMem: Api.newDelete("/teams/{teamId}/members/{accountId}"), delTeamMem: Api.newDelete('/teams/{teamId}/members/{accountId}'),
getTeamTagIds: Api.newGet("/teams/{teamId}/tags"), getTeamTagIds: Api.newGet('/teams/{teamId}/tags'),
saveTeamTags: Api.newPost("/teams/{teamId}/tags"), saveTeamTags: Api.newPost('/teams/{teamId}/tags'),
} };

View File

@@ -1,8 +1,7 @@
import Api from '@/common/Api'; import Api from '@/common/Api';
export const personApi = { export const personApi = {
accountInfo: Api.newGet("/sys/accounts/self"), accountInfo: Api.newGet('/sys/accounts/self'),
updateAccount: Api.newPut("/sys/accounts/self"), updateAccount: Api.newPut('/sys/accounts/self'),
getMsgs: Api.newGet("/msgs/self"), getMsgs: Api.newGet('/msgs/self'),
} };

View File

@@ -12,9 +12,8 @@
</div> </div>
<div class="personal-user-right"> <div class="personal-user-right">
<el-row> <el-row>
<el-col :span="24" class="personal-title mb18">{{ currentTime }}{{ <el-col :span="24" class="personal-title mb18"
userInfo.name >{{ currentTime }}{{ userInfo.name }}生活变的再糟糕也不妨碍我变得更好
}}生活变的再糟糕也不妨碍我变得更好
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-row> <el-row>
@@ -36,9 +35,7 @@
</el-col> </el-col>
<el-col :xs="24" :sm="16" class="personal-item mb6"> <el-col :xs="24" :sm="16" class="personal-item mb6">
<div class="personal-item-label">上次登录时间</div> <div class="personal-item-label">上次登录时间</div>
<div class="personal-item-value">{{ <div class="personal-item-value">{{ dateFormat(userInfo.lastLoginTime) }}</div>
dateFormat(userInfo.lastLoginTime)
}}</div>
</el-col> </el-col>
</el-row> </el-row>
</el-col> </el-col>
@@ -80,9 +77,16 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-row type="flex" class="mt5" justify="center"> <el-row type="flex" class="mt5" justify="center">
<el-pagination small @current-change="getMsgs" style="text-align: center" background <el-pagination
layout="prev, pager, next, total, jumper" :total="msgDialog.msgs.total" small
v-model:current-page="msgDialog.query.pageNum" :page-size="msgDialog.query.pageSize" /> @current-change="getMsgs"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="msgDialog.msgs.total"
v-model:current-page="msgDialog.query.pageNum"
:page-size="msgDialog.query.pageSize"
/>
</el-row> </el-row>
</el-dialog> </el-dialog>
@@ -111,8 +115,7 @@
<el-row :gutter="35"> <el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20"> <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="密码"> <el-form-item label="密码">
<el-input type="password" show-password v-model="accountForm.password" <el-input type="password" show-password v-model="accountForm.password" placeholder="请输入新密码" clearable></el-input>
placeholder="请输入新密码" clearable></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- --> <!-- -->
@@ -207,10 +210,7 @@ const state = reactive({
}, },
}); });
const { const { msgDialog, accountForm } = toRefs(state);
msgDialog,
accountForm,
} = toRefs(state)
// 当前时间提示语 // 当前时间提示语
const currentTime = computed(() => { const currentTime = computed(() => {

View File

@@ -1,18 +1,15 @@
<template> <template>
<div class="account-dialog"> <div class="account-dialog">
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :show-close="false" width="35%" <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :show-close="false" width="35%" :destroy-on-close="true">
:destroy-on-close="true">
<el-form :model="form" ref="accountForm" :rules="rules" label-width="auto"> <el-form :model="form" ref="accountForm" :rules="rules" label-width="auto">
<el-form-item prop="name" label="姓名:" required> <el-form-item prop="name" label="姓名:" required>
<el-input v-model.trim="form.name" placeholder="请输入姓名" auto-complete="off"></el-input> <el-input v-model.trim="form.name" placeholder="请输入姓名" auto-complete="off"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="username" label="用户名:" required> <el-form-item prop="username" label="用户名:" required>
<el-input :disabled="edit" v-model.trim="form.username" placeholder="请输入账号用户名,密码默认与账号名一致" <el-input :disabled="edit" v-model.trim="form.username" placeholder="请输入账号用户名,密码默认与账号名一致" auto-complete="off"></el-input>
auto-complete="off"></el-input>
</el-form-item> </el-form-item>
<el-form-item v-if="edit" prop="password" label="密码:"> <el-form-item v-if="edit" prop="password" label="密码:">
<el-input type="password" v-model.trim="form.password" placeholder="输入密码可修改用户密码" <el-input type="password" v-model.trim="form.password" placeholder="输入密码可修改用户密码" autocomplete="new-password"></el-input>
autocomplete="new-password"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -41,10 +38,10 @@ const props = defineProps({
title: { title: {
type: String, type: String,
}, },
}) });
//定义事件 //定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change']) const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
const accountForm: any = ref(null); const accountForm: any = ref(null);
@@ -63,7 +60,7 @@ const rules = {
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
} };
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
@@ -75,15 +72,10 @@ const state = reactive({
password: null, password: null,
repassword: null, repassword: null,
}, },
btnLoading: false btnLoading: false,
}); });
const { const { dialogVisible, edit, form, btnLoading } = toRefs(state);
dialogVisible,
edit,
form,
btnLoading,
} = toRefs(state)
watch(props, (newValue: any) => { watch(props, (newValue: any) => {
if (newValue.account) { if (newValue.account) {
@@ -122,6 +114,4 @@ const cancel = () => {
emit('cancel'); emit('cancel');
}; };
</script> </script>
<style lang="scss"> <style lang="scss"></style>
</style>

View File

@@ -1,18 +1,23 @@
<template> <template>
<div> <div>
<page-table ref="pageTableRef" :query="queryConfig" v-model:query-form="query" :show-selection="true" <page-table
v-model:selection-data="selectionData" :data="datas" :columns="columns" :total="total" ref="pageTableRef"
v-model:page-size="query.pageSize" v-model:page-num="query.pageNum" @pageChange="search()"> :query="queryConfig"
v-model:query-form="query"
:show-selection="true"
v-model:selection-data="selectionData"
:data="datas"
:columns="columns"
:total="total"
v-model:page-size="query.pageSize"
v-model:page-num="query.pageNum"
@pageChange="search()"
>
<template #queryRight> <template #queryRight>
<el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editAccount(false)">添加</el-button> <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editAccount(false)">添加</el-button>
<el-button v-auth="perms.delAccount" :disabled="state.selectionData.length < 1" @click="deleteAccount()" <el-button v-auth="perms.delAccount" :disabled="state.selectionData.length < 1" @click="deleteAccount()" type="danger" icon="delete"
type="danger" icon="delete">删除</el-button> >删除</el-button
</template> >
<template #status="{ data }">
<el-tag v-if="data.status == 1" type="success">正常</el-tag>
<el-tag v-if="data.status == -1" type="danger">禁用</el-tag>
</template> </template>
<template #showmore="{ data }"> <template #showmore="{ data }">
@@ -24,17 +29,20 @@
<template #action="{ data }"> <template #action="{ data }">
<el-button link v-if="actionBtns[perms.addAccount]" @click="editAccount(data)" type="primary">编辑</el-button> <el-button link v-if="actionBtns[perms.addAccount]" @click="editAccount(data)" type="primary">编辑</el-button>
<el-button link v-if="actionBtns[perms.saveAccountRole]" @click="showRoleEdit(data)" <el-button link v-if="actionBtns[perms.saveAccountRole]" @click="showRoleEdit(data)" type="success">角色分配</el-button>
type="success">角色分配</el-button>
<el-button link v-if="actionBtns[perms.changeAccountStatus] && data.status == 1" @click="changeStatus(data)" <el-button link v-if="actionBtns[perms.changeAccountStatus] && data.status == 1" @click="changeStatus(data)" type="danger">禁用</el-button>
type="danger">禁用</el-button>
<el-button link v-if="actionBtns[perms.changeAccountStatus] && data.status == -1" type="success" <el-button link v-if="actionBtns[perms.changeAccountStatus] && data.status == -1" type="success" @click="changeStatus(data)">启用</el-button>
@click="changeStatus(data)">启用</el-button>
<el-button link v-if="actionBtns[perms.addAccount]" :disabled="!data.otpSecret || data.otpSecret == '-'" <el-button
@click="resetOtpSecret(data)" type="warning">重置OTP</el-button> link
v-if="actionBtns[perms.addAccount]"
:disabled="!data.otpSecret || data.otpSecret == '-'"
@click="resetOtpSecret(data)"
type="warning"
>重置OTP
</el-button>
</template> </template>
</page-table> </page-table>
@@ -51,64 +59,64 @@
</el-dialog> </el-dialog>
<el-dialog :title="showResourceDialog.title" v-model="showResourceDialog.visible" width="400px"> <el-dialog :title="showResourceDialog.title" v-model="showResourceDialog.visible" width="400px">
<el-tree style="height: 50vh; overflow: auto" :data="showResourceDialog.resources" node-key="id" <el-tree
:props="showResourceDialog.defaultProps" :expand-on-click-node="true"> style="height: 50vh; overflow: auto"
:data="showResourceDialog.resources"
node-key="id"
:props="showResourceDialog.defaultProps"
:expand-on-click-node="true"
>
<template #default="{ node, data }"> <template #default="{ node, data }">
<span class="custom-tree-node"> <span class="custom-tree-node">
<span v-if="data.type == enums.ResourceTypeEnum['MENU'].value">{{ node.label }}</span> <span v-if="data.type == ResourceTypeEnum.Menu.value">{{ node.label }}</span>
<span v-if="data.type == enums.ResourceTypeEnum['PERMISSION'].value" style="color: #67c23a">{{ <span v-if="data.type == ResourceTypeEnum.Permission.value" style="color: #67c23a">{{ node.label }}</span>
node.label
}}</span>
</span> </span>
</template> </template>
</el-tree> </el-tree>
</el-dialog> </el-dialog>
<role-edit v-model:visible="roleDialog.visible" :account="roleDialog.account" @cancel="cancel()" /> <role-edit v-model:visible="roleDialog.visible" :account="roleDialog.account" @cancel="cancel()" />
<account-edit v-model:visible="accountDialog.visible" v-model:account="accountDialog.data" <account-edit v-model:visible="accountDialog.visible" v-model:account="accountDialog.data" @val-change="valChange()" />
@val-change="valChange()" />
</div> </div>
</template> </template>
<script lang='ts' setup> <script lang="ts" setup>
import { ref, toRefs, reactive, onMounted } from 'vue'; import { ref, toRefs, reactive, onMounted } from 'vue';
import RoleEdit from './RoleEdit.vue'; import RoleEdit from './RoleEdit.vue';
import AccountEdit from './AccountEdit.vue'; import AccountEdit from './AccountEdit.vue';
import enums from '../enums'; import { AccountStatusEnum, ResourceTypeEnum } from '../enums';
import { accountApi } from '../api'; import { accountApi } from '../api';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { dateFormat } from '@/common/utils/date'; import { dateFormat } from '@/common/utils/date';
import PageTable from '@/components/pagetable/PageTable.vue' import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
const pageTableRef: any = ref(null) const pageTableRef: any = ref(null);
const perms = { const perms = {
addAccount: "account:add", addAccount: 'account:add',
delAccount: "account:del", delAccount: 'account:del',
saveAccountRole: "account:saveRoles", saveAccountRole: 'account:saveRoles',
changeAccountStatus: "account:changeStatus", changeAccountStatus: 'account:changeStatus',
} };
const queryConfig = [ const queryConfig = [TableQuery.text('username', '用户名')];
TableQuery.text("username", "用户名"),
]
const columns = ref([ const columns = ref([
TableColumn.new("name", "姓名"), TableColumn.new('name', '姓名'),
TableColumn.new("username", "用户名"), TableColumn.new('username', '用户名'),
TableColumn.new("status", "状态").isSlot(), TableColumn.new('status', '状态').typeTag(AccountStatusEnum),
TableColumn.new("lastLoginTime", "最后登录时间").isTime(), TableColumn.new('lastLoginTime', '最后登录时间').isTime(),
TableColumn.new("showmore", "查看更多").isSlot().setMinWidth(150), TableColumn.new('showmore', '查看更多').isSlot().setMinWidth(150),
TableColumn.new("creator", "创建账号"), TableColumn.new('creator', '创建账号'),
TableColumn.new("createTime", "创建时间").isTime(), TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new("modifier", "更新账号"), TableColumn.new('modifier', '更新账号'),
TableColumn.new("updateTime", "更新时间").isTime(), TableColumn.new('updateTime', '更新时间').isTime(),
]) ]);
// 该用户拥有的的操作列按钮权限 // 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms([perms.addAccount, perms.saveAccountRole, perms.changeAccountStatus]) const actionBtns = hasPerms([perms.addAccount, perms.saveAccountRole, perms.changeAccountStatus]);
const actionColumn = TableColumn.new("action", "操作").isSlot().fixedRight().setMinWidth(260).noShowOverflowTooltip().alignCenter() const actionColumn = TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(260).noShowOverflowTooltip().alignCenter();
const state = reactive({ const state = reactive({
/** /**
@@ -150,16 +158,7 @@ const state = reactive({
}, },
}); });
const { const { selectionData, query, datas, total, showRoleDialog, showResourceDialog, roleDialog, accountDialog } = toRefs(state);
selectionData,
query,
datas,
total,
showRoleDialog,
showResourceDialog,
roleDialog,
accountDialog,
} = toRefs(state)
onMounted(() => { onMounted(() => {
if (Object.keys(actionBtns).length > 0) { if (Object.keys(actionBtns).length > 0) {
@@ -215,7 +214,7 @@ const resetOtpSecret = async (row: any) => {
id, id,
}); });
ElMessage.success('操作成功'); ElMessage.success('操作成功');
row.otpSecret = "-"; row.otpSecret = '-';
}; };
const showRoleEdit = (data: any) => { const showRoleEdit = (data: any) => {
@@ -245,12 +244,12 @@ const valChange = () => {
const deleteAccount = async () => { const deleteAccount = async () => {
try { try {
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(", ")}】的账号?`, '提示', { await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】的账号?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}); });
await accountApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(",") }); await accountApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功'); ElMessage.success('删除成功');
search(); search();
} catch (err) {} } catch (err) {}

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