first commit

This commit is contained in:
ye1023
2026-04-09 15:09:34 +08:00
commit d8e3a63df1
2246 changed files with 352109 additions and 0 deletions
@@ -0,0 +1,220 @@
<!--用户选择框-->
<template>
<div>
<BasicModal v-bind="$attrs" @register="register" title="数据对比" width="50%" destroyOnClose :showOkBtn="false">
<a-row :gutter="6" v-if="dataVersionList" style="margin-left: 2px">
<span style="margin-top: 5px; margin-right: 3px; margin-left: 4px">版本对比:</span>
<a-select placeholder="版本号" @change="handleChange1" v-model:value="params.dataId1">
<a-select-option v-for="(log, logindex) in dataVersionList" :key="log.value" :value="log.value">
{{ log.text }}
</a-select-option>
</a-select>
<a-select placeholder="版本号" @change="handleChange2" style="padding-left: 10px" v-model:value="params.dataId2">
<a-select-option v-for="(log, logindex) in dataVersionList" :key="log.value" :value="log.value">
{{ log.text }}
</a-select-option>
</a-select>
</a-row>
<BasicTable
:columns="columns"
v-bind="getBindValue"
:rowClassName="setDataCss"
:striped="false"
:showIndexColumn="false"
:pagination="false"
:canResize="false"
:bordered="true"
:dataSource="dataSource"
:searchInfo="searchInfo"
v-if="isUpdate"
>
<template #dataVersionTitle1="{ record }"> <Icon icon="icon-park-outline:grinning-face" /> 版本:{{ dataVersion1Num }} </template>
<template #dataVersionTitle2="{ record }"> <Icon icon="icon-park-outline:grinning-face" /> 版本:{{ dataVersion2Num }} </template>
<template #avatarslot="{ record }">
<div class="anty-img-wrap" v-if="record.dataVersion1 != record.dataVersion2">
<Icon icon="mdi:arrow-right-bold" style="color: red"></Icon>
</div>
</template>
</BasicTable>
</BasicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, unref, ref, reactive, watch } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { queryCompareList, queryDataVerList } from './datalog.api';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
import { useMessage } from '/@/hooks/web/useMessage';
export default defineComponent({
name: 'DataLogCompareModal',
components: {
//此处需要异步加载BasicTable
BasicModal,
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), { loading: true }),
},
props: {
...selectProps,
},
emits: ['register', 'btnOk'],
setup(props, { emit, refs }) {
const { createMessage } = useMessage();
const attrs = useAttrs();
const getBindValue = Object.assign({}, unref(props), unref(attrs));
const dataSource = ref([]);
const dataVersion1Num = ref('');
const dataVersion2Num = ref('');
const isUpdate = ref(true);
const searchInfo = {};
const dataId1 = ref('');
const dataId2 = ref('');
const dataId = ref('');
const dataTable1 = ref('');
const dataID3 = ref('');
const dataTable = ref('');
const confirmLoading = ref(false);
const dataVersionList = ref([]);
let params = reactive({ dataId1: '', dataId2: '' });
let dataLog = reactive({});
const [register, { setModalProps, closeModal }] = useModalInner(async (data) => {
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
let checkedRows = data.selectedRows;
dataTable.value = checkedRows[0].dataTable;
dataId.value = checkedRows[0].dataId;
dataId1.value = checkedRows[0].id;
dataId2.value = checkedRows[1].id;
params.dataId1 = dataId1.value;
params.dataId2 = dataId2.value;
await initDataVersionList();
await initTableData();
}
});
//定义表格列
const columns = [
{
title: '字段名',
dataIndex: 'code',
width: 20,
align: 'left',
},
{
dataIndex: 'dataVersion1',
align: 'left',
width: 60,
slots: { title: 'dataVersionTitle1' },
},
{
title: '',
dataIndex: 'imgshow',
align: 'center',
slots: { customRender: 'avatarslot' },
width: 5,
},
{
align: 'left',
dataIndex: 'dataVersion2',
width: 60,
filters: [],
filterMultiple: false,
slots: { title: 'dataVersionTitle2' },
},
];
async function initTableData() {
console.info('params', params);
queryCompareList(unref(params)).then((res) => {
console.info('test', res);
dataVersion1Num.value = res[0].dataVersion;
dataVersion2Num.value = res[1].dataVersion;
let json1 = JSON.parse(res[0].dataContent);
let json2 = JSON.parse(res[1].dataContent);
let data = [];
for (var item1 in json1) {
for (var item2 in json2) {
if (item1 == item2) {
data.push({
code: item1,
imgshow: '',
dataVersion1: json1[item1],
dataVersion2: json2[item2],
});
}
}
}
dataSource.value = data;
});
}
function handleChange1(value) {
if (params.dataId2 == value) {
createMessage.warning('相同版本号不能比较');
return;
}
params.dataId1 = value;
initTableData();
}
function handleChange2(value) {
if (params.dataId1 == value) {
createMessage.warning('相同版本号不能比较');
return;
}
params.dataId2 = value;
initTableData();
}
function setDataCss(record) {
let className = 'trcolor';
const dataVersion1 = record.dataVersion1;
const dataVersion2 = record.dataVersion2;
if (dataVersion1 != dataVersion2) {
return className;
}
}
async function initDataVersionList() {
queryDataVerList({ dataTable: dataTable.value, dataId: dataId.value }).then((res) => {
dataVersionList.value = res.map((value, key, arr) => {
let item = {};
item['text'] = value['dataVersion'];
item['value'] = value['id'];
return item;
});
});
}
return {
//config,
searchInfo,
dataSource,
setDataCss,
isUpdate,
dataVersionList,
dataVersion1Num,
dataVersion2Num,
queryCompareList,
initDataVersionList,
register,
handleChange1,
handleChange2,
params,
getBindValue,
columns,
};
},
});
</script>
<style scoped>
.anty-img-wrap {
height: 25px;
position: relative;
}
.anty-img-wrap > img {
max-height: 100%;
}
.marginCss {
margin-top: 20px;
}
</style>
+111
View File
@@ -0,0 +1,111 @@
<template>
<div>
<BasicModal v-bind="$attrs" @register="registerModal" title="数据对比窗口" :minHeight="300" width="800px" @ok="handleSubmit">
<a-spin :spinning="confirmLoading">
<a-form @submit="handleSubmit" :form="form" class="form">
<a-row class="form-row" :gutter="24">
<a-col :md="12" :sm="8">
<a-form-item label="数据库表名" :label-col="{ span: 6 }" :wrapper-col="{ span: 15 }" name="dataTable">
<a-input placeholder="请输入数据库表名" v-model:value="dataTable" disabled />
</a-form-item>
</a-col>
<a-col :md="12" :sm="8">
<a-form-item label="数据ID" :label-col="{ span: 5 }" :wrapper-col="{ span: 15 }">
<a-input placeholder="请输入数据ID" v-model:value="dataId" disabled />
</a-form-item>
</a-col>
</a-row>
<a-row class="form-row" :gutter="24">
<a-col :md="12" :sm="8">
<a-form-item label="版本号1" :label-col="{ span: 6 }" :wrapper-col="{ span: 15 }">
<a-select placeholder="请选择版本号" @change="handleChange1" v-model:value="dataVersion1">
<a-select-option v-for="(log, logindex) in dataVersionList" :key="logindex.toString()" :value="log.id">
{{ log.dataVersion }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="12" :sm="8">
<a-form-item label="版本号2" :label-col="{ span: 5 }" :wrapper-col="{ span: 15 }">
<a-select placeholder="请选择版本号" @change="handleChange2" v-model:value="dataVersion2">
<a-select-option v-for="(log, logindex) in dataVersionList" :key="logindex.toString()" :value="log.id">
{{ log.dataVersion }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-spin>
</BasicModal>
<DataLogCompareModal @register="registerDataLogCompareModal"></DataLogCompareModal>
</div>
</template>
<script lang="ts" setup>
import { BasicModal, useModal, useModalInner } from '/@/components/Modal';
import { queryDataVerList } from './datalog.api';
import { reactive, ref, unref } from 'vue';
import DataLogCompareModal from './DataLogCompareModal.vue';
const dataId1 = ref('');
const dataId2 = ref('');
const dataId = ref('');
const dataTable1 = ref('');
const dataID3 = ref('');
const dataTable = ref('');
const confirmLoading = ref(false);
const isUpdate = ref(true);
const dataVersionList = ref([]);
let dataLog = reactive({});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
let checkedRows = data.selectedRows;
dataTable.value = checkedRows[0].dataTable;
dataId.value = checkedRows[0].dataId;
dataId1.value = checkedRows[0].id;
dataId2.value = checkedRows[1].id;
initDataVersionList();
}
});
const [registerDataLogCompareModal, { openModal }] = useModal();
function handleChange1(value) {
dataId1.value = value;
}
function handleChange2(value) {
dataId2.value = value;
}
function handleSubmit() {
let result = { dataId1: dataId1.value, dataId2: dataId2.value };
openModal(true, {
result,
isUpdate: true,
});
closeModal();
}
function initDataVersionList() {
queryDataVerList({ dataTable: dataTable.value, dataId: dataId.value }).then((res) => {
dataVersionList.value = res.map((value, key, arr) => {
arr['label'] = value;
return arr;
});
console.info(dataVersionList.value);
});
}
</script>
<style scoped lang="less">
.detail-iframe {
border: 0;
width: 100%;
height: 100%;
min-height: 600px;
}
</style>
+31
View File
@@ -0,0 +1,31 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
list = '/sys/dataLog/list',
queryDataVerList = '/sys/dataLog/queryDataVerList',
queryCompareList = '/sys/dataLog/queryCompareList',
}
/**
* 查询数据日志列表
* @param params
*/
export const getDataLogList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 查询数据日志列表
* @param params
*/
export const queryDataVerList = (params) => {
return defHttp.get({ url: Api.queryDataVerList, params });
};
/**
* 查询对比数据
* @param params
*/
export const queryCompareList = (params) => {
return defHttp.get({ url: Api.queryCompareList, params });
};
+45
View File
@@ -0,0 +1,45 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: '表名',
dataIndex: 'dataTable',
width: 150,
align: 'left',
},
{
title: '数据ID',
dataIndex: 'dataId',
width: 350,
},
{
title: '版本号',
dataIndex: 'dataVersion',
width: 100,
},
{
title: '数据内容',
dataIndex: 'dataContent',
},
{
title: '创建人',
dataIndex: 'createBy',
sorter: true,
width: 200,
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'dataTable',
label: '表名',
component: 'Input',
colProps: { span: 8 },
},
{
field: 'dataId',
label: '数据ID',
component: 'Input',
colProps: { span: 8 },
},
];
+57
View File
@@ -0,0 +1,57 @@
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="handleCompare" style="margin-right: 5px">数据比较</a-button>
</template>
</BasicTable>
<DataLogCompareModal @register="registerModal" @success="reload" />
</div>
</template>
<script lang="ts" name="monitor-datalog" setup>
import { ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import DataLogCompareModal from './DataLogCompareModal.vue';
const [registerModal, { openModal }] = useModal();
import { getDataLogList } from './datalog.api';
import { columns, searchFormSchema } from './datalog.data';
import { useMessage } from '/@/hooks/web/useMessage';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
const { createMessage } = useMessage();
const checkedRows = ref<Array<object | number>>([]);
// 列表页面公共参数、方法
const { prefixCls, tableContext } = useListPage({
designScope: 'datalog-template',
tableProps: {
title: '数据日志列表',
api: getDataLogList,
columns: columns,
formConfig: {
labelWidth: 120,
schemas: searchFormSchema,
},
actionColumn: false,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
function handleCompare() {
let obj = selectedRows.value;
console.info('sfsfsf', obj);
if (!obj || obj.length != 2) {
createMessage.warning('请选择两条数据!');
return false;
}
if (obj[0].dataId != obj[1].dataId) {
createMessage.warning('请选择相同的数据库表和数据ID进行比较!');
return false;
}
openModal(true, {
selectedRows,
isUpdate: true,
});
}
</script>
@@ -0,0 +1,87 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit" width="40%">
<BasicForm @register="registerForm">
<template #pwd="{ model, field }">
<a-row :gutter="8">
<a-col :sm="15" :md="16" :lg="17" :xl="19">
<a-input-password v-model:value="model[field]" placeholder="请输入密码" />
</a-col>
<a-col :sm="9" :md="7" :lg="7" :xl="5">
<a-button type="primary" style="width: 100%" @click="handleTest">测试</a-button>
</a-col>
</a-row>
</template>
</BasicForm>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from './datasource.data';
import { saveOrUpdateDataSource, getDataSourceById, testConnection } from './datasource.api';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
// Emits声明
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
//表单配置
const [registerForm, { getFieldsValue, resetFields, validateFields, setFieldsValue, validate }] = useForm({
// labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
//获取详情
data.record = await getDataSourceById({ id: data.record.id });
//表单赋值
await setFieldsValue({
...data.record,
});
}
});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增数据源' : '编辑数据源'));
async function handleTest() {
let keys = ['dbType', 'dbDriver', 'dbUrl', 'dbName', 'dbUsername', 'dbPassword'];
// 获取以上字段的值,并清除校验状态
let fieldsValues = getFieldsValue(keys);
let setFields = {};
keys.forEach((key) => (setFields[key] = { value: fieldsValues[key], errors: null }));
await validateFields(keys).then((values) => {
let loading = createMessage.loading('连接中....', 0);
testConnection(values)
.then((data) => {
if (data.success) {
createMessage.success('连接成功');
}
})
.catch((error) => {})
.finally(() => loading());
});
}
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdateDataSource(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,83 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/dataSource/list',
save = '/sys/dataSource/add',
edit = '/sys/dataSource/edit',
get = '/sys/dataSource/queryById',
delete = '/sys/dataSource/delete',
testConnection = '/online/cgreport/api/testConnection',
deleteBatch = '/sys/dataSource/deleteBatch',
exportXlsUrl = 'sys/dataSource/exportXls',
importExcelUrl = 'sys/dataSource/importExcel',
}
/**
* 导出api
*/
export const getExportUrl = Api.exportXlsUrl;
/**
* 导入api
*/
export const getImportUrl = Api.importExcelUrl;
/**
* 查询数据源列表
* @param params
*/
export const getDataSourceList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 保存或者更新数据源
* @param params
*/
export const saveOrUpdateDataSource = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
* 查询数据源详情
* @param params
*/
export const getDataSourceById = (params) => {
return defHttp.get({ url: Api.get, params });
};
/**
* 删除数据源
* @param params
*/
export const deleteDataSource = (params, handleSuccess) => {
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 测试连接
* @param params
*/
export const testConnection = (params) => {
return defHttp.post({ url: Api.testConnection, params });
};
/**
* 批量删除数据源
* @param params
*/
export const batchDeleteDataSource = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
@@ -0,0 +1,185 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
const dbDriverMap = {
// MySQL 数据库
'1': { dbDriver: 'com.mysql.jdbc.Driver' },
//MySQL5.7+ 数据库
'4': { dbDriver: 'com.mysql.cj.jdbc.Driver' },
// Oracle
'2': { dbDriver: 'oracle.jdbc.OracleDriver' },
// SQLServer 数据库
'3': { dbDriver: 'com.microsoft.sqlserver.jdbc.SQLServerDriver' },
// marialDB 数据库
'5': { dbDriver: 'org.mariadb.jdbc.Driver' },
// postgresql 数据库
'6': { dbDriver: 'org.postgresql.Driver' },
// 达梦 数据库
'7': { dbDriver: 'dm.jdbc.driver.DmDriver' },
// 人大金仓 数据库
'8': { dbDriver: 'com.kingbase8.Driver' },
// 神通 数据库
'9': { dbDriver: 'com.oscar.Driver' },
// SQLite 数据库
'10': { dbDriver: 'org.sqlite.JDBC' },
// DB2 数据库
'11': { dbDriver: 'com.ibm.db2.jcc.DB2Driver' },
// Hsqldb 数据库
'12': { dbDriver: 'org.hsqldb.jdbc.JDBCDriver' },
// Derby 数据库
'13': { dbDriver: 'org.apache.derby.jdbc.ClientDriver' },
// H2 数据库
'14': { dbDriver: 'org.h2.Driver' },
// 其他数据库
'15': { dbDriver: '' },
};
const dbUrlMap = {
// MySQL 数据库
'1': { dbUrl: 'jdbc:mysql://127.0.0.1:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false' },
//MySQL5.7+ 数据库
'4': {
dbUrl:
'jdbc:mysql://127.0.0.1:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai',
},
// Oracle
'2': { dbUrl: 'jdbc:oracle:thin:@127.0.0.1:1521:ORCL' },
// SQLServer 数据库
'3': { dbUrl: 'jdbc:sqlserver://127.0.0.1:1433;SelectMethod=cursor;DatabaseName=jeecgboot' },
// Mariadb 数据库
'5': { dbUrl: 'jdbc:mariadb://127.0.0.1:3306/jeecg-boot?characterEncoding=UTF-8&useSSL=false' },
// Postgresql 数据库
'6': { dbUrl: 'jdbc:postgresql://127.0.0.1:5432/jeecg-boot' },
// 达梦 数据库
'7': { dbUrl: 'jdbc:dm://127.0.0.1:5236/?jeecg-boot&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8' },
// 人大金仓 数据库
'8': { dbUrl: 'jdbc:kingbase8://127.0.0.1:54321/jeecg-boot' },
// 神通 数据库
'9': { dbUrl: 'jdbc:oscar://192.168.1.125:2003/jeecg-boot' },
// SQLite 数据库
'10': { dbUrl: 'jdbc:sqlite://opt/test.db' },
// DB2 数据库
'11': { dbUrl: 'jdbc:db2://127.0.0.1:50000/jeecg-boot' },
// Hsqldb 数据库
'12': { dbUrl: 'jdbc:hsqldb:hsql://127.0.0.1/jeecg-boot' },
// Derby 数据库
'13': { dbUrl: 'jdbc:derby://127.0.0.1:1527/jeecg-boot' },
// H2 数据库
'14': { dbUrl: 'jdbc:h2:tcp://127.0.0.1:8082/jeecg-boot' },
// 其他数据库
'15': { dbUrl: '' },
};
export const columns: BasicColumn[] = [
{
title: '数据源名称',
dataIndex: 'name',
width: 200,
align: 'left',
},
{
title: '数据库类型',
dataIndex: 'dbType_dictText',
width: 200,
},
{
title: '驱动类',
dataIndex: 'dbDriver',
width: 200,
},
{
title: '数据源地址',
dataIndex: 'dbUrl',
},
{
title: '用户名',
dataIndex: 'dbUsername',
width: 200,
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'name',
label: '数据源名称',
component: 'Input',
colProps: { span: 8 },
},
{
field: 'dbType',
label: '数据库类型',
component: 'JDictSelectTag',
colProps: { span: 8 },
componentProps: () => {
return {
dictCode: 'database_type',
};
},
},
];
export const formSchema: FormSchema[] = [
{
field: 'id',
label: 'id',
component: 'Input',
show: false,
},
{
field: 'code',
label: '数据源编码',
component: 'Input',
required: true,
dynamicDisabled: ({ values }) => {
return !!values.id;
},
},
{
field: 'name',
label: '数据源名称',
component: 'Input',
required: true,
},
{
field: 'dbType',
label: '数据库类型',
component: 'JDictSelectTag',
required: true,
componentProps: ({ formModel }) => {
return {
dictCode: 'database_type',
onChange: (e: any) => {
formModel = Object.assign(formModel, dbDriverMap[e], dbUrlMap[e]);
},
};
},
},
{
field: 'dbDriver',
label: '驱动类',
required: true,
component: 'Input',
},
{
field: 'dbUrl',
label: '数据源地址',
required: true,
component: 'Input',
},
{
field: 'dbUsername',
label: '用户名',
required: true,
component: 'Input',
},
{
field: 'dbPassword',
label: '密码',
required: true,
component: 'InputPassword',
slot: 'pwd',
},
{
field: 'remark',
label: '备注',
component: 'InputTextArea',
},
];
+118
View File
@@ -0,0 +1,118 @@
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd" style="margin-right: 5px">新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" />
</template>
</BasicTable>
<DataSourceModal @register="registerModal" @success="reload" />
</div>
</template>
<script lang="ts" name="monitor-datasource" setup>
import { ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { getDataSourceList, deleteDataSource, batchDeleteDataSource, getExportUrl, getImportUrl } from './datasource.api';
import { columns, searchFormSchema } from './datasource.data';
import DataSourceModal from './DataSourceModal.vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useListPage } from '/@/hooks/system/useListPage';
const { createMessage } = useMessage();
const [registerModal, { openModal }] = useModal();
// 列表页面公共参数、方法
const { prefixCls, tableContext, onImportXls, onExportXls } = useListPage({
designScope: 'quartz-template',
tableProps: {
title: '任务列表',
api: getDataSourceList,
columns: columns,
formConfig: {
labelWidth: 120,
schemas: searchFormSchema,
fieldMapToTime: [['fieldTime', ['beginDate', 'endDate'], 'YYYY-MM-DD HH:mm:ss']],
},
},
exportConfig: {
name: '数据源列表',
url: getExportUrl,
},
importConifg: {
url: getImportUrl,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
/**
* 操作列定义
* @param record
*/
function getActions(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑事件
*/
function handleEdit(record) {
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteDataSource({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteDataSource({ ids: selectedRowKeys.value }, reload);
}
</script>
+37
View File
@@ -0,0 +1,37 @@
<template>
<Skeleton v-if="spinning" active />
<div v-else>
<a-row>
<template v-if="diskInfo && diskInfo.length > 0">
<a-col :span="6" v-for="(item, index) in diskInfo" :key="'diskInfo' + index">
<gauge :data="item"></gauge>
</a-col>
</template>
</a-row>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Skeleton } from 'ant-design-vue';
import { queryDiskInfo } from './disk.api';
import gauge from './gauge.vue';
const diskInfo = ref([]);
const spinning = ref(true);
function loadRedisInfo() {
queryDiskInfo()
.then((res) => {
for (let i = 0; i < res.length; i++) {
// 当前算法算的是磁盘的已使用空间
res[i].restPPT = 100 - parseInt(String((res[i].rest / res[i].max) * 100));
}
diskInfo.value = res;
})
.finally(() => (spinning.value = false));
}
onMounted(() => {
loadRedisInfo();
});
</script>
+12
View File
@@ -0,0 +1,12 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
queryDiskInfo = '/sys/actuator/redis/queryDiskInfo',
}
/**
* 详细信息
*/
export const queryDiskInfo = () => {
return defHttp.get({ url: Api.queryDiskInfo }, { successMessageMode: 'none' });
};
+82
View File
@@ -0,0 +1,82 @@
<template>
<div>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, reactive, Ref } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useECharts } from '/@/hooks/web/useECharts';
import { GaugeChart } from 'echarts/charts';
const props = defineProps({ data: {} });
const dataSource = ref([]);
const chartRef = ref<HTMLDivElement | null>(null);
const { setOptions, echarts } = useECharts(chartRef as Ref<HTMLDivElement>);
const loading = ref(false);
const { createMessage } = useMessage();
const option = reactive({
series: [
{
type: 'gauge',
progress: {
show: true,
width: 18,
},
axisLine: {
lineStyle: {
width: 18,
},
},
axisTick: {
show: true,
},
splitLine: {
length: 15,
lineStyle: {
width: 2,
color: '#999',
},
},
axisLabel: {
distance: 25,
color: '#999',
fontSize: 15,
},
anchor: {
show: true,
showAbove: true,
size: 25,
itemStyle: {
borderWidth: 10,
},
},
title: {},
detail: {
valueAnimation: true,
fontSize: 50,
formatter: '{value}%',
offsetCenter: [0, '80%'],
},
data: [
{
value: 70,
name: '本地磁盘',
},
],
},
],
});
function initCharts() {
option.series[0].data[0].name = props.data.name;
option.series[0].data[0].value = props.data.restPPT;
setOptions(option);
}
onMounted(() => {
console.info(props.data);
echarts.use(GaugeChart);
initCharts();
});
</script>
+98
View File
@@ -0,0 +1,98 @@
<template>
<BasicTable :ellipsis="true" @register="registerTable" :searchInfo="searchInfo" :columns="logColumns" :expand-column-width="16">
<template #tableTitle>
<a-tabs defaultActiveKey="4" @change="tabChange" size="small">
<a-tab-pane tab="异常日志" key="4"></a-tab-pane>
<a-tab-pane tab="登录日志" key="1"></a-tab-pane>
<a-tab-pane tab="操作日志" key="2"></a-tab-pane>
</a-tabs>
</template>
<template #expandedRowRender="{ record }">
<div v-if="searchInfo.logType == 2">
<div style="margin-bottom: 5px">
<a-badge status="success" style="vertical-align: middle" />
<span style="vertical-align: middle">请求方法:{{ record.method }}</span></div
>
<div>
<a-badge status="processing" style="vertical-align: middle" />
<span style="vertical-align: middle">请求参数:{{ record.requestParam }}</span></div
>
</div>
<div v-if="searchInfo.logType == 4">
<div style="margin-bottom: 5px">
<a-badge status="success" style="vertical-align: middle" />
<span class="error-box" style="vertical-align: middle">异常堆栈:{{ record.requestParam }}</span>
</div>
</div>
</template>
</BasicTable>
</template>
<script lang="ts" name="monitor-log" setup>
import { ref } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { getLogList } from './log.api';
import {
columns,
searchFormSchema,
operationLogColumn,
operationSearchFormSchema,
exceptionColumns
} from './log.data';
import { useMessage } from '/@/hooks/web/useMessage';
import { useListPage } from '/@/hooks/system/useListPage';
const { createMessage } = useMessage();
const checkedKeys = ref<Array<string | number>>([]);
const logColumns = ref<any>(exceptionColumns);
const searchSchema = ref<any>(searchFormSchema);
const searchInfo = { logType: '4' };
// 列表页面公共参数、方法
const { prefixCls, tableContext } = useListPage({
designScope: 'user-list',
tableProps: {
title: '日志列表',
api: getLogList,
expandRowByClick: true,
showActionColumn: false,
rowSelection: {
columnWidth: 20,
},
formConfig: {
schemas: searchSchema,
fieldMapToTime: [['fieldTime', ['createTime_begin', 'createTime_end'], 'YYYY-MM-DD']],
},
},
});
const [registerTable, { reload }] = tableContext;
// 日志类型
function tabChange(key) {
searchInfo.logType = key;
//update-begin---author:wangshuai ---date:20220506 for[VUEN-943]vue3日志管理列表翻译不对------------
if (key == '2') {
logColumns.value = operationLogColumn;
searchSchema.value = operationSearchFormSchema;
}else if(key == '4'){
searchSchema.value = searchFormSchema;
logColumns.value = exceptionColumns;
} else {
searchSchema.value = searchFormSchema;
logColumns.value = columns;
}
//update-end---author:wangshuai ---date:20220506 for[VUEN-943]vue3日志管理列表翻译不对--------------
reload();
}
/**
* 选择事件
*/
function onSelectChange(selectedRowKeys: (string | number)[]) {
checkedKeys.value = selectedRowKeys;
}
</script>
<style lang="less" scoped>
.error-box {
white-space: break-spaces;
}
</style>
+13
View File
@@ -0,0 +1,13 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
list = '/sys/log/list',
}
/**
* 查询日志列表
* @param params
*/
export const getLogList = (params) => {
return defHttp.get({ url: Api.list, params });
};
+134
View File
@@ -0,0 +1,134 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: '日志内容',
dataIndex: 'logContent',
width: 100,
align: 'left',
},
{
title: '操作人ID',
dataIndex: 'userid',
width: 80,
},
{
title: '操作人',
dataIndex: 'username',
width: 80,
},
{
title: 'IP',
dataIndex: 'ip',
width: 80,
},
{
title: '耗时(毫秒)',
dataIndex: 'costTime',
width: 80,
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
width: 80,
},
{
title: '客户端类型',
dataIndex: 'clientType_dictText',
width: 60,
},
];
/**
* 操作日志需要操作类型
*/
export const operationLogColumn: BasicColumn[] = [
...columns,
{
title: '操作类型',
dataIndex: 'operateType_dictText',
width: 40,
},
];
export const exceptionColumns: BasicColumn[] = [
{
title: '异常标题',
dataIndex: 'logContent',
width: 100,
align: 'left',
},
{
title: '请求地址',
dataIndex: 'requestUrl',
width: 100,
},
{
title: '请求参数',
dataIndex: 'method',
width: 60,
},
{
title: '操作人',
dataIndex: 'username',
width: 60,
customRender: ({ record }) => {
let pname = record.username;
let pid = record.userid;
if(!pname && !pid){
return "";
}
return pname + " (账号: "+ pid + " )";
},
},
{
title: 'IP',
dataIndex: 'ip',
width: 60,
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
width: 60,
},
{
title: '客户端类型',
dataIndex: 'clientType_dictText',
width: 60,
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'keyWord',
label: '搜索日志',
component: 'Input',
colProps: { span: 8 },
},
{
field: 'fieldTime',
component: 'RangePicker',
label: '创建时间',
componentProps: {
valueType: 'Date',
},
colProps: {
span: 6,
},
},
];
export const operationSearchFormSchema: FormSchema[] = [
...searchFormSchema,
{
field: 'operateType',
label: '操作类型',
component: 'JDictSelectTag',
colProps: { span: 4 },
componentProps: {
dictCode: 'operate_type',
},
},
];
+92
View File
@@ -0,0 +1,92 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="查看详情" :minHeight="600" :showCancelBtn="false" :showOkBtn="false" :height="88" :destroyOnClose="true">
<a-card class="daily-article">
<a-card-meta :title="content.titile" :description="'发布人:' + content.sender + ' 发布时间: ' + content.sendTime"> </a-card-meta>
<a-divider />
<div v-html="content.msgContent" class="article-content"></div>
<div>
<a-button v-if="hasHref" @click="jumpToHandlePage">前往办理<ArrowRightOutlined /></a-button>
</div>
</a-card>
</BasicModal>
</template>
<script lang="ts" setup>
import { BasicModal, useModalInner } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { ArrowRightOutlined } from '@ant-design/icons-vue';
import { useRouter } from 'vue-router'
import xss from 'xss'
import { options } from './XssWhiteList'
const router = useRouter()
import { ref, unref } from 'vue';
const isUpdate = ref(true);
const content = ref({});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
//data.record.msgContent = '<p>2323</p><input onmouseover=alert(1)>xss test';
//update-begin-author:taoyan date:2022-7-14 for: VUEN-1702 【禁止问题】sql注入漏洞
if(data.record.msgContent){
//update-begin---author:wangshuai---date:2023-11-15---for:【QQYUN-7049】3.6.0版本 通知公告中发布的富文本消息,在我的消息中查看没有样式---
data.record.msgContent = xss(data.record.msgContent,options);
//update-end---author:wangshuai---date:2023-11-15---for:【QQYUN-7049】3.6.0版本 通知公告中发布的富文本消息,在我的消息中查看没有样式---
}
//update-end-author:taoyan date:2022-7-14 for: VUEN-1702 【禁止问题】sql注入漏洞
content.value = data.record;
showHrefButton();
}
});
const hasHref = ref(false)
//查看消息详情可以跳转
function showHrefButton(){
if(content.value.busId){
hasHref.value = true;
}
}
//跳转至办理页面
function jumpToHandlePage(){
let temp:any = content.value
if(temp.busId){
//这个busId是 任务ID
let jsonStr = temp.msgAbstract;
let query = {};
try {
if(jsonStr){
let temp = JSON.parse(jsonStr)
if(temp){
Object.keys(temp).map(k=>{
query[k] = temp[k]
});
}
}
}catch(e){
console.log('参数解析异常', e)
}
console.log('query', query, jsonStr)
console.log('busId', temp.busId)
if(Object.keys(query).length>0){
// taskId taskDefKey procInsId
router.push({ path: '/task/handle/' + temp.busId, query: query })
}else{
router.push({ path: '/task/handle/' + temp.busId })
}
}
closeModal();
}
</script>
<style scoped lang="less">
.detail-iframe {
border: 0;
width: 100%;
height: 100%;
min-height: 600px;
}
</style>
@@ -0,0 +1,35 @@
<template>
<component :is="currentModal" :formData="formData" v-model:visible="modalVisible"></component>
</template>
<script setup lang="ts" name="dynamic-notice">
import { ref, shallowRef, ComponentOptions, nextTick, defineAsyncComponent } from 'vue';
const props = defineProps({
path: { type: String, default: '' },
formData: { type: Object, default: {} },
});
const modalVisible = ref<Boolean>(false);
const currentModal = shallowRef<Nullable<ComponentOptions>>(null);
const formData = ref<any>(props.formData);
const componentType = {
};
/**
* 跟换组件和传值事件
*/
function detail() {
setTimeout(() => {
if (props.path) {
nextTick(() => {
currentModal.value = componentType[props.path];
formData.value = props.formData;
modalVisible.value = true;
});
}
}, 200);
}
defineExpose({
detail,
});
</script>
+41
View File
@@ -0,0 +1,41 @@
//xss攻击白名单列表
export const options = {
whiteList: {
h1: ['style'],
h2: ['style'],
h3: ['style'],
h4: ['style'],
h5: ['style'],
h6: ['style'],
hr: ['style'],
span: ['style'],
strong: ['style'],
b: ['style'],
i: ['style'],
br: [],
p: ['style'],
pre: ['style'],
code: ['style'],
a: ['style', 'target', 'href', 'title', 'rel'],
img: ['style', 'src', 'title','width','height'],
div: ['style'],
table: ['style', 'width', 'border', 'height'],
tr: ['style'],
td: ['style', 'width', 'colspan'],
th: ['style', 'width', 'colspan'],
tbody: ['style'],
ul: ['style'],
li: ['style'],
ol: ['style'],
dl: ['style'],
dt: ['style'],
em: ['style'],
cite: ['style'],
section: ['style'],
header: ['style'],
footer: ['style'],
blockquote: ['style'],
audio: ['autoplay', 'controls', 'loop', 'preload', 'src'],
video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width'],
},
};
+133
View File
@@ -0,0 +1,133 @@
<template>
<div>
<BasicTable @register="registerTable" :searchInfo="searchInfo">
<template #tableTitle>
<a-button type="primary" @click="handlerReadAllMsg">全部标注已读</a-button>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" />
</template>
</BasicTable>
<DetailModal @register="register" />
</div>
</template>
<script lang="ts" name="monitor-mynews" setup>
import { ref, onMounted } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import DetailModal from './DetailModal.vue';
import { getMyNewsList, editCementSend, syncNotic, readAllMsg, getOne } from './mynews.api';
import { columns, searchFormSchema } from './mynews.data';
import { useMessage } from '/@/hooks/web/useMessage';
import { getToken } from '/@/utils/auth';
import { useModal } from '/@/components/Modal';
import { useGlobSetting } from '/@/hooks/setting';
const glob = useGlobSetting();
const { createMessage } = useMessage();
const checkedKeys = ref<Array<string | number>>([]);
const content = ref({});
const searchInfo = { logType: '1' };
const [register, { openModal: openDetail }] = useModal();
import { useListPage } from '/@/hooks/system/useListPage';
import { getLogList } from '/@/views/monitor/log/log.api';
import { useRouter } from 'vue-router';
import { useAppStore } from '/@/store/modules/app';
import { useMessageHref } from '/@/views/system/message/components/useSysMessage';
const appStore = useAppStore();
const {goPage} = useMessageHref()
const { prefixCls, tableContext } = useListPage({
designScope: 'mynews-list',
tableProps: {
title: '我的消息',
api: getMyNewsList,
columns: columns,
formConfig: {
schemas: searchFormSchema,
//update-begin---author:wangshuai---date:2024-06-11---for:【TV360X-545】我的消息列表不能通过时间范围查询---
fieldMapToTime: [['sendTime', ['sendTimeBegin', 'sendTimeEnd'], 'YYYY-MM-DD']],
//update-end---author:wangshuai---date:2024-06-11---for:【TV360X-545】我的消息列表不能通过时间范围查询---
},
},
});
const [registerTable, { reload }] = tableContext;
/**
* 操作列定义
* @param record
*/
function getActions(record) {
return [
{
label: '查看',
onClick: handleDetail.bind(null, record),
},
];
}
/**
* 查看
*/
function handleDetail(record) {
let anntId = record.anntId;
editCementSend({ anntId: anntId }).then((res) => {
reload();
syncNotic({ anntId: anntId });
});
const openModalFun = ()=>{
openDetail(true, {
record,
isUpdate: true,
});
}
goPage(record, openModalFun);
}
// 日志类型
function callback(key) {
searchInfo.logType = key;
reload();
}
//全部标记已读
function handlerReadAllMsg() {
readAllMsg({}, reload);
}
/**
* 选择事件
*/
function onSelectChange(selectedRowKeys: (string | number)[]) {
checkedKeys.value = selectedRowKeys;
}
//update-begin-author:taoyan date:2022-8-23 for: 消息跳转,打开详情表单
onMounted(()=>{
initHrefModal();
});
function initHrefModal(){
let params = appStore.getMessageHrefParams;
if(params){
let anntId = params.id;
if(anntId){
editCementSend({ anntId: anntId }).then(() => {
reload();
syncNotic({ anntId: anntId });
});
}
let detailId = params.detailId;
if(detailId){
getOne(detailId).then(data=>{
console.log('getOne', data)
openDetail(true, {
record: data,
isUpdate: true,
});
appStore.setMessageHrefParams('')
})
}
}
}
//update-end-author:taoyan date:2022-8-23 for: 消息跳转,打开详情表单
</script>
+61
View File
@@ -0,0 +1,61 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/sysAnnouncementSend/getMyAnnouncementSend',
editCementSend = '/sys/sysAnnouncementSend/editByAnntIdAndUserId',
readAllMsg = '/sys/sysAnnouncementSend/readAll',
syncNotic = '/sys/annountCement/syncNotic',
getOne = '/sys/sysAnnouncementSend/getOne',
}
/**
* 查询消息列表
* @param params
*/
export const getMyNewsList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 更新用户系统消息阅读状态
* @param params
*/
export const editCementSend = (params) => {
return defHttp.put({ url: Api.editCementSend, params });
};
/**
* 一键已读
* @param params
*/
export const readAllMsg = (params, handleSuccess) => {
Modal.confirm({
title: '确认操作',
content: '是否全部标注已读?',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.put({ url: Api.readAllMsg, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 同步消息
* @param params
*/
export const syncNotic = (params) => {
return defHttp.get({ url: Api.syncNotic, params });
};
/**
* 根据消息发送记录ID获取消息内容
* @param sendId
*/
export const getOne = (sendId) => {
return defHttp.get({ url: Api.getOne, params:{sendId} });
};
+84
View File
@@ -0,0 +1,84 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { render } from '/@/utils/common/renderUtils';
export const columns: BasicColumn[] = [
{
title: '标题',
dataIndex: 'titile',
width: 100,
align: 'left',
},
{
title: '消息类型',
dataIndex: 'msgCategory',
width: 80,
customRender: ({ text }) => {
return render.renderDictNative(
text,
[
{ label: '通知公告', value: '1', color: 'blue' },
{ label: '系统消息', value: '2' },
],
true
);
},
},
{
title: '发布人',
dataIndex: 'sender',
width: 80,
},
{
title: '发布时间',
dataIndex: 'sendTime',
width: 80,
},
{
title: '优先级',
dataIndex: 'priority',
width: 80,
customRender: ({ text }) => {
const color = text == 'L' ? 'blue' : text == 'M' ? 'yellow' : 'red';
return render.renderTag(render.renderDict(text, 'priority'), color);
},
},
{
title: '阅读状态',
dataIndex: 'readFlag',
width: 80,
customRender: ({ text }) => {
return render.renderDictNative(
text,
[
{ label: '未读', value: '0', color: 'red' },
{ label: '已读', value: '1' },
],
true
);
},
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'titile',
label: '标题',
component: 'Input',
colProps: { span: 6 },
},
{
field: 'sender',
label: '发布人',
component: 'Input',
colProps: { span: 6 },
},
{
field: 'sendTime',
label: '发布时间',
component: 'RangeDate',
componentProps: {
valueType: 'Date',
},
colProps: { span: 6 },
},
];
+62
View File
@@ -0,0 +1,62 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit" width="40%">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from './quartz.data';
import { saveOrUpdateQuartz, getQuartzById } from './quartz.api';
import { isJsonObjectString } from '/@/utils/is';
// Emits声明
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
// labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
// update-begin--author:liaozhiyang---date:20231017---for:【issues/790】弹窗内文本框不居中问题
labelWidth: 100,
// update-end--author:liaozhiyang---date:20231017---for:【issues/790】弹窗内文本框不居中问题
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
//获取详情
//data.record = await getQuartzById({id: data.record.id});
try {
data.record.paramterType = isJsonObjectString(data?.record?.parameter) ? 'json' : 'string';
} catch (e) {
console.log(e);
}
//表单赋值
await setFieldsValue({
...data.record,
});
}
});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增任务' : '编辑任务'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdateQuartz(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
+183
View File
@@ -0,0 +1,183 @@
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="handleAdd" style="margin-right: 5px">新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<QuartzModal @register="registerModal" @success="reload" />
</div>
</template>
<script lang="ts" name="monitor-quartz" setup>
import { ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import { getQuartzList, deleteQuartz, batchDeleteQuartz, executeImmediately, resumeJob, pauseJob, getExportUrl, getImportUrl } from './quartz.api';
import { columns, searchFormSchema } from './quartz.data';
import QuartzModal from './QuartzModal.vue';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
const [registerModal, { openModal }] = useModal();
// 列表页面公共参数、方法
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
designScope: 'quartz-template',
tableProps: {
title: '任务列表',
api: getQuartzList,
columns: columns,
actionColumn: {
width: 180,
},
formConfig: {
labelWidth: 120,
schemas: searchFormSchema,
fieldMapToTime: [['fieldTime', ['beginDate', 'endDate'], 'YYYY-MM-DD HH:mm:ss']],
},
},
exportConfig: {
name: '定时任务列表',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
/**
* 操作列定义
* @param record
*/
function getActions(record) {
return [
{
label: '启动',
popConfirm: {
title: '是否启动选中任务?',
confirm: handlerResume.bind(null, record),
},
ifShow: (_action) => {
return record.status == -1;
},
},
{
label: '停止',
popConfirm: {
title: '是否暂停选中任务?',
confirm: handlerPause.bind(null, record),
},
ifShow: (_action) => {
return record.status == 0;
},
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '立即执行',
popConfirm: {
title: '是否立即执行任务?',
confirm: handlerExecute.bind(null, record),
},
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑事件
*/
function handleEdit(record) {
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteQuartz({ id: record.id }, reload);
}
/**
* 立即执行
*/
async function handlerExecute(record) {
await executeImmediately({ id: record.id }, reload);
}
/**
* 暂停
*/
async function handlerPause(record) {
await pauseJob({ id: record.id }, reload);
}
/**
* 启动
*/
async function handlerResume(record) {
await resumeJob({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteQuartz({ ids: selectedRowKeys.value }, () => {
// -update-begin--author:liaozhiyang---date:20240702---for:【TV360X-1662】菜单管理、定时任务批量删除清空选中
reload();
selectedRows.value = [];
selectedRowKeys.value = [];
// -update-end--author:liaozhiyang---date:20240702---for:【TV360X-1662】菜单管理、定时任务批量删除清空选中
});
}
</script>
+107
View File
@@ -0,0 +1,107 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/quartzJob/list',
save = '/sys/quartzJob/add',
edit = '/sys/quartzJob/edit',
get = '/sys/quartzJob/queryById',
pause = '/sys/quartzJob/pause',
resume = '/sys/quartzJob/resume',
delete = '/sys/quartzJob/delete',
exportXlsUrl = '/sys/quartzJob/exportXls',
importExcelUrl = '/sys/quartzJob/importExcel',
execute = '/sys/quartzJob/execute',
deleteBatch = '/sys/quartzJob/deleteBatch',
}
/**
* 导出api
*/
export const getExportUrl = Api.exportXlsUrl;
/**
* 导入api
*/
export const getImportUrl = Api.importExcelUrl;
/**
* 查询任务列表
* @param params
*/
export const getQuartzList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 保存或者更新任务
* @param params
*/
export const saveOrUpdateQuartz = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
* 查询任务详情
* @param params
*/
export const getQuartzById = (params) => {
return defHttp.get({ url: Api.get, params });
};
/**
* 删除任务
* @param params
*/
export const deleteQuartz = (params, handleSuccess) => {
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 启动
* @param params
*/
export const resumeJob = (params, handleSuccess) => {
return defHttp.get({ url: Api.resume, params }).then(() => {
handleSuccess();
});
};
/**
* 暂停
* @param params
*/
export const pauseJob = (params, handleSuccess) => {
return defHttp.get({ url: Api.pause, params }).then(() => {
handleSuccess();
});
};
/**
* 立即执行
* @param params
*/
export const executeImmediately = (params, handleSuccess) => {
return defHttp.get({ url: Api.execute, params }).then(() => {
handleSuccess();
});
};
/**
* 批量删除任务
* @param params
*/
export const batchDeleteQuartz = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
+124
View File
@@ -0,0 +1,124 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { render } from '/@/utils/common/renderUtils';
import { JCronValidator } from '/@/components/Form';
export const columns: BasicColumn[] = [
{
title: '任务类名',
dataIndex: 'jobClassName',
width: 200,
align: 'left',
},
{
title: 'Cron表达式',
dataIndex: 'cronExpression',
width: 200,
},
{
title: '参数',
dataIndex: 'parameter',
width: 200,
},
{
title: '描述',
dataIndex: 'description',
width: 200,
},
{
title: '状态',
dataIndex: 'status',
width: 100,
customRender: ({ text }) => {
const color = text == '0' ? 'green' : text == '-1' ? 'red' : 'gray';
return render.renderTag(render.renderDict(text, 'quartz_status'), color);
},
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'jobClassName',
label: '任务类名',
component: 'Input',
colProps: { span: 8 },
},
{
field: 'status',
label: '任务状态',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'quartz_status',
stringToNumber: true,
},
colProps: { span: 8 },
},
];
export const formSchema: FormSchema[] = [
{
field: 'id',
label: 'id',
component: 'Input',
show: false,
},
{
field: 'jobClassName',
label: '任务类名',
component: 'Input',
required: true,
},
{
field: 'cronExpression',
label: 'Cron表达式',
component: 'JEasyCron',
defaultValue: '* * * * * ? *',
rules: [{ required: true, message: '请输入Cron表达式' }, { validator: JCronValidator }],
},
{
field: 'paramterType',
label: '参数类型',
component: 'Select',
defaultValue: 'string',
componentProps: {
options: [
{ label: '字符串', value: 'string' },
{ label: 'JSON对象', value: 'json' },
],
},
},
{
field: 'parameter',
label: '参数',
component: 'InputTextArea',
ifShow: ({ values }) => {
return values.paramterType == 'string';
},
},
{
field: 'parameter',
label: '参数',
component: 'JAddInput',
helpMessage: '键值对形式填写',
ifShow: ({ values }) => {
return values.paramterType == 'json';
},
},
{
field: 'status',
label: '状态',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'quartz_status',
type: 'radioButton',
stringToNumber: true,
dropdownStyle: {
maxHeight: '6vh',
},
},
},
{
field: 'description',
label: '描述',
component: 'InputTextArea',
},
];
+211
View File
@@ -0,0 +1,211 @@
<template>
<div class="p-4">
<a-card>
<!-- Redis 信息实时监控 -->
<a-row :gutter="8">
<a-col :sm="24" :xl="12">
<div ref="chartRef" style="width: 100%; height: 300px"></div>
</a-col>
<a-col :sm="24" :xl="12">
<div ref="chartRef2" style="width: 100%; height: 300px"></div>
</a-col>
</a-row>
</a-card>
<BasicTable @register="registerTable" :api="getInfo"></BasicTable>
</div>
</template>
<script lang="ts" name="monitor-redis" setup>
import { onMounted, ref, reactive, Ref, onUnmounted } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { getInfo, getRedisInfo, getMetricsHistory } from './redis.api';
import dayjs from 'dayjs';
import { columns } from './redis.data';
import { useMessage } from '/@/hooks/web/useMessage';
import { useECharts } from '/@/hooks/web/useECharts';
const dataSource = ref([]);
const chartRef = ref<HTMLDivElement | null>(null);
const chartRef2 = ref<HTMLDivElement | null>(null);
const { setOptions, echarts } = useECharts(chartRef as Ref<HTMLDivElement>);
const { setOptions: setOptions2, echarts: echarts2 } = useECharts(chartRef2 as Ref<HTMLDivElement>);
const loading = ref(false);
let timer = null;
const { createMessage } = useMessage();
const key = reactive({
title: {
text: 'Redis Key 实时数量(个)',
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [],
},
yAxis: {
type: 'value',
},
series: [
{
data: [],
type: 'line',
areaStyle: {
color: '#ff6987',
},
lineStyle: {
color: '#dc143c',
width: 10,
type: 'solid',
},
},
],
});
const memory = reactive({
title: {
text: 'Redis 内存实时占用情况(KB',
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [],
},
yAxis: {
type: 'value',
},
series: [
{
data: [],
type: 'line',
areaStyle: {
color: '#74bcff',
},
lineStyle: {
color: '#1890ff',
width: 10,
type: 'solid',
},
},
],
});
const [registerTable, { reload }] = useTable({
columns,
showIndexColumn: false,
pagination: false,
bordered: true,
});
// 获取一组数据中最大和最小的值
function getMaxAndMin(dataSource, field) {
let maxValue = null,
minValue = null;
dataSource.forEach((item) => {
let value = Number.parseInt(item[field]);
// max
if (maxValue == null) {
maxValue = value;
} else if (value > maxValue) {
maxValue = value;
}
// min
if (minValue == null) {
minValue = value;
} else if (value < minValue) {
minValue = value;
}
});
return [maxValue, minValue];
}
function loadRedisInfo() {
getInfo().then((res) => {
dataSource.value = res.result;
});
}
function initCharts() {
setOptions(memory);
setOptions2(key);
}
/** 开启定时器 */
function openTimer() {
loadHistoryData();
closeTimer();
timer = setInterval(() => {
loadData();
}, 15000);
}
/** 关闭定时器 */
function closeTimer() {
if (timer) clearInterval(timer);
}
/**
* 加载历史监控数据
*/
function loadHistoryData() {
getMetricsHistory().then((res) => {
let dbSizes = res.dbSize;
let memories = res.memory;
dbSizes.forEach((dbSize) => {
key.xAxis.data.push(dayjs(dbSize.create_time).format('hh:mm:ss'));
key.series[0].data.push(dbSize.dbSize);
});
memories.forEach((memoryData) => {
memory.xAxis.data.push(dayjs(memoryData.create_time).format('hh:mm:ss'));
memory.series[0].data.push(memoryData.used_memory / 1000);
});
setOptions(memory, false);
setOptions2(key, false);
});
}
function loadData() {
getRedisInfo()
.then((res) => {
let time = dayjs().format('hh:mm:ss');
let [{ dbSize: currentSize }, memoryInfo] = res;
let currentMemory = memoryInfo.used_memory / 1000;
// push 数据
key.xAxis.data.push(time);
key.series[0].data.push(currentSize);
memory.xAxis.data.push(time);
memory.series[0].data.push(currentMemory);
// 最大长度为80
if (key.series[0].data.length > 80) {
key.xAxis.data.splice(0, 1);
key.series[0].data.splice(0, 1);
memory.xAxis.data.splice(0, 1);
memory.series[0].data.splice(0, 1);
}
setOptions(memory, false);
setOptions2(key, false);
// 计算 Key 最大最小值
//let keyPole = getMaxAndMin(key.dataSource, 'y');
//key.max = Math.floor(keyPole[0]) + 10;
//key.min = Math.floor(keyPole[1]) - 10;
//if (key.min < 0) this.key.min = 0;
// 计算 Memory 最大最小值
//let memoryPole = getMaxAndMin(memory.dataSource, 'y');
//memory.max = Math.floor(memoryPole[0]) + 100;
//memory.min = Math.floor(memoryPole[1]) - 100;
//if (memory.min < 0) memory.min = 0;
})
.catch((e) => {
//closeTimer()
});
}
onMounted(() => {
initCharts();
openTimer();
});
// update-begin--author:liaozhiyang---date:220230719---for:【issues-615】系统监控中的REDIS监控页面打开,再关闭后,没有关闭计时器
onUnmounted(() => {
closeTimer();
});
// update-end--author:liaozhiyang---date:220230719---for:【issues-615】系统监控中的REDIS监控页面打开,再关闭后,没有关闭计时器
</script>
+40
View File
@@ -0,0 +1,40 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
keysSize = '/sys/actuator/redis/keysSize',
memoryInfo = '/sys/actuator/redis/memoryInfo',
info = '/sys/actuator/redis/info',
metricsHistory = '/sys/actuator/redis/metrics/history',
}
/**
* key个数
*/
export const getKeysSize = () => {
return defHttp.get({ url: Api.keysSize }, { isTransformResponse: false });
};
/**
* 内存信息
*/
export const getMemoryInfo = () => {
return defHttp.get({ url: Api.memoryInfo }, { isTransformResponse: false });
};
/**
* 详细信息
*/
export const getInfo = () => {
return defHttp.get({ url: Api.info });
};
/**
* 历史监控记录
*/
export const getMetricsHistory = () => {
return defHttp.get({ url: Api.metricsHistory });
};
export const getRedisInfo = () => {
return Promise.all([getKeysSize(), getMemoryInfo()]);
};
+19
View File
@@ -0,0 +1,19 @@
import { BasicColumn } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: 'Key',
dataIndex: 'key',
width: 100,
},
{
title: 'Description',
dataIndex: 'description',
width: 80,
},
{
title: 'Value',
dataIndex: 'value',
width: 80,
},
];
+424
View File
@@ -0,0 +1,424 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" :title="getTitle" width="30%" @ok="handleSubmit" destroyOnClose showFooter>
<a-form ref="formRef" :label-col="labelCol" :wrapper-col="wrapperCol" :model="router" :rules="validatorRules">
<a-form-item label="路由ID" name="routerId">
<a-input v-model:value="router.routerId" placeholder="路由唯一ID" />
</a-form-item>
<a-form-item label="路由名称" name="name">
<a-input v-model:value="router.name" placeholder="路由名称" />
</a-form-item>
<a-form-item label="路由URI" name="uri">
<a-input v-model:value="router.uri" placeholder="路由URL" />
</a-form-item>
<a-form-item label="路由状态" name="status">
<a-switch default-checked :checked-value="1" :un-checked-value="0" v-model:checked="router.status" />
</a-form-item>
<a-form-item name="predicates" label="路由条件">
<div v-for="(item, index) in router.predicates">
<!--当name在noKeyRouter时不需要指定key-->
<template v-if="noKeyRouter.includes(item.name)">
<a-divider
>{{ item.name }}
<DeleteOutlined size="22" @click="removePredicate(router, index)" />
</a-divider>
<div>
<template v-for="(tag, tagIndx) in item.args">
<a-input
ref="inputRef2"
v-if="tagIndx == currentTagIndex && index == currentNameIndex"
type="text"
size="small"
:style="{ width: '190px' }"
v-model:value="state.inputValue"
@change="handleInputChange"
@blur="handleInputEditConfirm(item, tag, tagIndx)"
@keyup.enter="handleInputEditConfirm(item, tag, tagIndx)"
/>
<a-tag
v-else
:key="tag"
style="margin-bottom: 2px"
:closable="true"
@close="() => removeTag(item, tag)"
@click="editTag(item, tag, tagIndx, index)"
>
{{ tag }}
</a-tag>
</template>
<a-input
ref="inputRef"
v-if="state.inputVisible && index == currentNameIndex"
type="text"
size="small"
:style="{ width: '100px' }"
v-model:value="state.inputValue"
@change="handleInputChange"
@blur="handleInputConfirm(item)"
@keyup.enter="handleInputConfirm(item)"
/>
<a-tag v-else style="background: #fff; borderstyle: dashed; margin-bottom: 2px" @click="showInput(item, index)">
<PlusOutlined size="22" />
新建{{ item.name }}
</a-tag>
</div>
</template>
<!--当name不在noKeyRouter时需要指定key-->
<template v-if="!noKeyRouter.includes(item.name)">
<a-divider
>{{ item.name }}
<DeleteOutlined size="22" @click="removePredicate(router, index)" />
</a-divider>
<div>
<template v-for="(value, key) in item.args">
<a-row>
<a-col :span="5" style="margin-top: 8px">
<span v-if="key == 'header'">Header名称</span>
<span v-if="key == 'regexp'">参数值</span>
<span v-if="key == 'param'">参数名</span>
<span v-if="key == 'name'">参数名</span>
</a-col>
<a-col :span="18">
<a-input
:defaultValue="value"
placeholder="参数值"
style="width: 70%; margin-right: 8px; margin-top: 3px"
@change="(e) => valueChange(e, item.args, key)"
/>
</a-col>
</a-row>
</template>
</div>
</template>
</div>
<p class="btn" style="padding-top: 10px">
<a-dropdown trigger="click">
<template #overlay>
<a-menu>
<a-menu-item :key="item.name" v-for="item in tagArray" @click="predicatesHandleMenuClick(item)">{{ item.name }}</a-menu-item>
</a-menu>
</template>
<a-button type="dashed" style="margin-left: 8px; width: 100%">
添加路由条件
<DownOutlined :size="22" />
</a-button>
</a-dropdown>
</p>
</a-form-item>
<a-form-item name="predicates" label="过滤器">
<div v-for="(item, index) in router.filters">
<a-divider
>{{ item.name }}
<DeleteOutlined size="22" @click="removeFilter(router, index)" />
</a-divider>
<div v-for="(tag, index) in item.args" :key="tag.key">
<!-- update-begin---author:wangshuai ---date: 20230829 forvue3.0后自定义表单重复组件要用a-form-item-rest,否则会警告提醒------------ -->
<a-form-item-rest>
<a-input v-model:value="tag.key" placeholder="参数键" style="width: 45%; margin-right: 8px" />
<a-input v-model:value="tag.value" placeholder="参数值" style="width: 40%; margin-right: 8px; margin-top: 3px" />
</a-form-item-rest>
<!-- update-end---author:wangshuai ---date: 20230829 forvue3.0后自定义表单重复组件要用a-form-item-rest,否则会警告提醒------------ -->
<CloseOutlined :size="22" @click="removeFilterParams(item, index)" />
</div>
<a-button type="dashed" style="margin-left: 28%; width: 37%; margin-top: 5px" size="small" @click="addFilterParams(item)">
<DownOutlined :size="22" />
添加参数
</a-button>
</div>
<p class="btn" style="padding-top: 10px">
<a-dropdown trigger="click">
<template #overlay>
<a-menu @click="filterHandleMenuClick">
<a-menu-item :key="item.key" :name="item.name" v-for="item in filterArray">{{ item.name }}</a-menu-item>
</a-menu>
</template>
<a-button type="dashed" style="margin-left: 8px; width: 100%">
添加过滤器
<DownOutlined />
</a-button>
</a-dropdown>
</p>
</a-form-item>
</a-form>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, computed, unref, useAttrs, reactive, nextTick } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { saveOrUpdateRoute } from './route.api';
import { DeleteOutlined } from '@ant-design/icons-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { CloseOutlined } from '@ant-design/icons-vue';
import { DownOutlined } from '@ant-design/icons-vue';
// 声明Emits
const emit = defineEmits(['register', 'success']);
const labelCol = reactive({
xs: { span: 24 },
sm: { span: 5 },
});
const wrapperCol = reactive({
xs: { span: 24 },
sm: { span: 16 },
});
const attrs = useAttrs();
const isUpdate = ref(true);
const inputRef = ref();
const inputRef2 = ref();
let state = reactive({
inputVisible: false,
inputValue: '',
});
const currentNameIndex = ref(0);
const currentTagIndex = ref(-1);
const validatorRules = {
routerId: [{ required: true, message: 'routerId不能为空', trigger: 'blur' }],
name: [{ required: true, message: '路由名称不能为空', trigger: 'blur' }],
uri: [{ required: true, message: 'uri不能为空', trigger: 'blur' }],
};
const noKeyRouter = ['Path', 'Host', 'Method', 'After', 'Before', 'Between', 'RemoteAddr'];
const filterArray = [/*{ key: 0, name: '熔断器' },*/ { key: 1, name: '限流过滤器' }];
const tagArray = ref([
{
name: 'Path',
args: [],
},
{
name: 'Header',
args: {
header: '',
regexp: '',
},
},
{
name: 'Query',
args: {
param: '',
regexp: '',
},
},
{
name: 'Method',
args: [],
},
{
name: 'Host',
args: [],
},
{
name: 'Cookie',
args: {
name: '',
regexp: '',
},
},
{
name: 'After',
args: [],
},
{
name: 'Before',
args: [],
},
{
name: 'Between',
args: [],
},
{
name: 'RemoteAddr',
args: [],
},
]);
const formRef = ref();
let router = reactive({});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
isUpdate.value = !!data?.isUpdate;
setDrawerProps({ confirmLoading: false });
initRouter();
if (unref(isUpdate)) {
router = Object.assign(router, data.record);
}
});
/**
* 标题
*/
const getTitle = computed(() => (!unref(isUpdate) ? '新增路由' : '编辑路由'));
//删除路由条件配置项
function removeTag(item, removedTag) {
let tags = item.args.filter((tag) => tag !== removedTag);
item.args = tags;
}
//初始化参数
function initRouter() {
router = Object.assign(router, {
id: '',
routerId: '',
name: '',
uri: '',
status: 1,
predicates: [],
filters: [],
});
}
//添加路由选项
function predicatesHandleMenuClick(e) {
router.predicates.push({
args: e.args,
name: e.name,
});
}
/**
* 值修改事件
* @param e
* @param item
* @param key
*/
function valueChange(e, item, key) {
item[key] = e.target.value;
}
function editTag(item, tag, tagIndex, index) {
currentNameIndex.value = index;
currentTagIndex.value = tagIndex;
state.inputValue = tag;
nextTick(() => {
inputRef2.value[0].focus();
});
}
//显示输入框
function showInput(item, index) {
state.inputValue = '';
state.inputVisible = true;
currentNameIndex.value = index;
nextTick(() => {
inputRef.value[0].focus();
});
}
//路由选项输入框改变事件
function handleInputChange(e) {
console.info('change', e);
console.info('change', e.target.value);
//state.value = e.target.value;
//state.tag=true;
}
//删除路由条件
function removePredicate(item, index) {
item.predicates.splice(index, 1);
}
//删除过滤器参数
function removeFilterParams(item, index) {
item.args.splice(index, 1);
}
//删除过滤器
function removeFilter(item, index) {
item.filters.splice(index, 1);
}
//添加过滤器参数
function addFilterParams(item) {
item.args.push({
key: 'key' + item.args.length + 1,
value: '',
});
}
//过滤器添加事件
function filterHandleMenuClick(e) {
if (e.key == 0) {
router.filters.push({
args: [
{
key: 'name',
value: 'default',
},
{
key: 'fallbackUri',
value: 'forward:/fallback',
},
],
name: 'Hystrix',
title: filterArray[0].name,
});
}
console.info('test', router);
if (e.key == 1) {
router.filters.push({
args: [
{
key: 'key-resolver',
value: '#{@ipKeyResolver}',
},
{
key: 'redis-rate-limiter.replenishRate',
value: 20,
},
{
key: 'redis-rate-limiter.burstCapacity',
value: 20,
},
],
name: 'RequestRateLimiter',
title: filterArray[0].name,
});
}
}
//输入框确认
function handleInputConfirm(item) {
let tags = item.args;
const inputValue = state.inputValue;
if (inputValue && tags.indexOf(inputValue) === -1) {
item.args = [...tags, state.inputValue];
}
state.inputVisible = false;
state.inputValue = '';
currentTagIndex.value = -1;
currentNameIndex.value = -1;
}
//输入框确认
function handleInputEditConfirm(item, tag, index) {
const inputValue = state.inputValue;
if (inputValue) {
item.args[index] = state.inputValue;
}
currentTagIndex.value = -1;
currentNameIndex.value = -1;
}
//关闭弹窗
function handleCancel() {}
/**
* 提交
*/
async function handleSubmit() {
await formRef.value.validate().then(() => {
try {
setDrawerProps({ confirmLoading: true });
//重新构造表单提交对象,切记不可修改router对象,数组修改为字符串容易造成界面混乱
let params = Object.assign({}, router, {
predicates: JSON.stringify(router.predicates),
filters: JSON.stringify(router.filters),
});
//提交表单
saveOrUpdateRoute({ router: params }).then(() => {
closeDrawer();
//刷新列表
emit('success');
});
} finally {
setDrawerProps({ confirmLoading: false });
}
});
}
</script>
@@ -0,0 +1,84 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="路由回收站" :showOkBtn="false" width="1000px" destroyOnClose>
<BasicTable @register="registerTable">
<template #status="{ record, text }">
<a-tag color="pink" v-if="text == 0">禁用</a-tag>
<a-tag color="#87d068" v-if="text == 1">正常</a-tag>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { columns } from '../route.data';
import { deleteRouteList, putRecycleBin, deleteRecycleBin } from '../route.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
const checkedKeys = ref<Array<string | number>>([]);
const [registerModal] = useModalInner(() => {
checkedKeys.value = [];
});
//注册table数据
const [registerTable, { reload }] = useTable({
rowKey: 'id',
api: deleteRouteList,
columns: columns,
striped: true,
useSearchForm: false,
bordered: true,
showIndexColumn: false,
pagination: false,
tableSetting: { fullScreen: true },
canResize: false,
actionColumn: {
width: 150,
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: 'right',
},
});
/**
* 还原事件
*/
async function handleRevert(record) {
await putRecycleBin({ ids: record.id }, reload);
emit('success');
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteRecycleBin({ ids: record.id }, reload);
}
//获取操作栏事件
function getTableAction(record) {
return [
{
label: '取回',
icon: 'ant-design:redo-outlined',
popConfirm: {
title: '是否确认取回',
confirm: handleRevert.bind(null, record),
},
},
{
label: '彻底删除',
icon: 'ant-design:scissor-outlined',
color: 'error',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
</script>
+124
View File
@@ -0,0 +1,124 @@
<template>
<div class="p-4">
<BasicTable @register="registerTable" :indexColumnProps="indexColumnProps">
<template #tableTitle>
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="handleAdd" style="margin-right: 5px">新增</a-button>
<a-button type="primary" @click="openRecycleModal(true)" preIcon="ant-design:hdd-outlined"> 回收站</a-button>
</template>
<template #status="{ record, text }">
<a-tag color="pink" v-if="text == 0">禁用</a-tag>
<a-tag color="#87d068" v-if="text == 1">正常</a-tag>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" />
</template>
</BasicTable>
<RouteModal @register="registerDrawer" @success="reload" />
<!--回收站弹窗-->
<RouteRecycleBinModal @register="registerRecycleModal" @success="reload" />
</div>
</template>
<script lang="ts" name="monitor-route" setup>
import { ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { getRouteList, deleteRoute, copyRoute } from './route.api';
import { columns } from './route.data';
import RouteModal from './RouteModal.vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useDrawer } from '/@/components/Drawer';
import { useListPage } from '/@/hooks/system/useListPage';
import RouteRecycleBinModal from './components/RouteRecycleBinModal.vue';
const { createMessage } = useMessage();
const [registerDrawer, { openDrawer }] = useDrawer();
const checkedKeys = ref<Array<string | number>>([]);
//回收站model
const [registerRecycleModal, { openModal: openRecycleModal }] = useModal();
// 列表页面公共参数、方法
const { prefixCls, tableContext } = useListPage({
designScope: 'router-template',
tableProps: {
title: '路由列表',
api: getRouteList,
useSearchForm: false,
columns: columns,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
/**
* 序号列配置
*/
const indexColumnProps = {
dataIndex: 'index',
width: '15px',
};
/**
* 操作列定义
* @param record
*/
function getActions(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '复制',
popConfirm: {
title: '是否确认复制',
confirm: handleCopy.bind(null, record),
},
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
/**
* 选择事件
*/
function onSelectChange(selectedRowKeys: (string | number)[]) {
checkedKeys.value = selectedRowKeys;
}
/**
* 新增事件
*/
function handleAdd() {
openDrawer(true, {
isUpdate: false,
});
}
/**
* 编辑事件
*/
function handleEdit(record) {
openDrawer(true, {
record,
isUpdate: true,
});
}
/**
* 复制
*/
async function handleCopy(record) {
await copyRoute({ id: record.id }, reload);
createMessage.success('复制成功');
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteRoute({ id: record.id }, reload);
}
</script>
+73
View File
@@ -0,0 +1,73 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
list = '/sys/gatewayRoute/list',
deleteList = '/sys/gatewayRoute/deleteList',
save = '/sys/gatewayRoute/add',
edit = '/sys/gatewayRoute/updateAll',
delete = '/sys/gatewayRoute/delete',
copyRoute = '/sys/gatewayRoute/copyRoute',
batchPutRecycleBin = '/sys/gatewayRoute/putRecycleBin',
batchDeleteRecycleBin = '/sys/gatewayRoute/deleteRecycleBin',
}
/**
* 查询路由列表
* @param params
*/
export const getRouteList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 查询逻辑删除的路由列表
* @param params
*/
export const deleteRouteList = (params) => {
return defHttp.get({ url: Api.deleteList, params });
};
/**
* 保存或者更新路由
* @param params
*/
export const saveOrUpdateRoute = (params) => {
return defHttp.post({ url: Api.edit, params });
};
/**
* 删除路由
* @param params
*/
export const deleteRoute = (params, handleSuccess) => {
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 回收站还原
* @param params
*/
export const putRecycleBin = (params, handleSuccess) => {
return defHttp.put({ url: Api.batchPutRecycleBin, params }).then(() => {
handleSuccess();
});
};
/**
* 回收站删除
* @param params
*/
export const deleteRecycleBin = (params, handleSuccess) => {
return defHttp.delete({ url: `${Api.batchDeleteRecycleBin}?ids=${params.ids}` }).then(() => {
handleSuccess();
});
};
/**
* 复制
*/
export const copyRoute = (params, handleSuccess) => {
return defHttp.get({ url: Api.copyRoute, params }).then(() => {
handleSuccess();
});
};
+52
View File
@@ -0,0 +1,52 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: '路由ID',
dataIndex: 'routerId',
width: 200,
align: 'left',
},
{
title: '路由名称',
dataIndex: 'name',
width: 200,
},
{
title: '路由URI',
dataIndex: 'uri',
width: 200,
},
{
title: '状态',
dataIndex: 'status',
slots: { customRender: 'status' },
width: 150,
},
];
export const formSchema: FormSchema[] = [
{
field: 'name',
label: '路由ID',
component: 'Input',
required: true,
},
{
field: 'name',
label: '路由名称',
component: 'InputNumber',
required: true,
},
{
field: 'uri',
label: '路由URI',
component: 'Input',
},
{
field: 'predicates',
label: '路由条件',
slot: 'predicates',
component: 'Input',
},
];
+119
View File
@@ -0,0 +1,119 @@
<template>
<div class="p-4">
<a-card :bordered="false" style="height: 100%">
<a-tabs v-model:activeKey="activeKey" @change="tabChange">
<a-tab-pane key="1" tab="服务器信息"></a-tab-pane>
<a-tab-pane key="2" tab="JVM信息" force-render></a-tab-pane>
<!-- <a-tab-pane key="3" tab="Tomcat信息"></a-tab-pane> -->
<a-tab-pane key="6" tab="Undertow信息"></a-tab-pane>
<a-tab-pane key="4" tab="磁盘监控">
<DiskInfo v-if="activeKey == 4" style="height: 100%"></DiskInfo>
</a-tab-pane>
<a-tab-pane key="5" tab="内存信息" />
</a-tabs>
<!-- update-begin---author:wangshuai ---date: 20230829 for性能监控切换到磁盘监控再切回来报错列为空不能用if判断------------>
<BasicTable @register="registerTable" :searchInfo="searchInfo" :dataSource="dataSource" v-show="activeKey != 4">
<!-- update-end---author:wangshuai ---date: 20230829 for性能监控切换到磁盘监控再切回来报错列为空不能用if判断------------>
<template #tableTitle>
<div slot="message"
>上次更新时间{{ lastUpdateTime }}
<a-divider type="vertical" />
<a @click="handleUpdate">立即更新</a></div
>
</template>
<template #param="{ record, text }">
<a-tag :color="textInfo[record.param].color">{{ text }}</a-tag>
</template>
<template #text="{ record }">
{{ textInfo[record.param].text }}
</template>
<template #value="{ record, text }"> {{ text }} {{ textInfo[record.param].unit }} </template>
</BasicTable>
</a-card>
</div>
</template>
<script lang="ts" name="monitor-server" setup>
import { onMounted, ref, unref } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import DiskInfo from '../disk/DiskInfo.vue';
import { getServerInfo, getTextInfo, getMoreInfo } from './server.api';
import { columns } from './server.data';
import { useMessage } from '/@/hooks/web/useMessage';
import dayjs from 'dayjs';
const dataSource = ref([]);
const activeKey = ref('1');
const moreInfo = ref({});
const lastUpdateTime = ref({});
let textInfo = ref({});
const { createMessage } = useMessage();
const checkedKeys = ref<Array<string | number>>([]);
const searchInfo = { logType: '1' };
const [registerTable, { reload }] = useTable({
columns,
showIndexColumn: false,
bordered: true,
pagination: false,
canResize: false,
tableSetting: { fullScreen: true },
rowKey: 'id',
});
//tab切换
function tabChange(key) {
if (key != 4) {
getInfoList(key);
}
}
//加载信息
function getInfoList(infoType) {
lastUpdateTime.value = dayjs().format('YYYY年MM月DD日 HH时mm分ss秒');
getServerInfo(infoType).then((res) => {
textInfo.value = getTextInfo(infoType);
moreInfo.value = getMoreInfo(infoType);
let info = [];
if (infoType === '5') {
for (let param in res[0].result) {
let data = res[0].result[param];
let val = convert(data, unref(textInfo)[param].valueType);
info.push({ id: param, param, text: 'false value', value: val });
}
} else {
res.forEach((value, id) => {
let more = unref(moreInfo)[value.name];
if (!(more instanceof Array)) {
more = [''];
}
more.forEach((item, idx) => {
let param = value.name + item;
let val = convert(value.measurements[idx].value, unref(textInfo)[param].valueType);
info.push({ id: param + id, param, text: 'false value', value: val });
});
});
}
dataSource.value = info;
});
}
function handleUpdate() {
getInfoList(activeKey.value);
}
//单位转换
function convert(value, type) {
if (type === 'Number') {
return Number(value * 100).toFixed(2);
} else if (type === 'Date') {
return dayjs(value * 1000).format('YYYY-MM-DD HH:mm:ss');
} else if (type === 'RAM') {
return Number(value / 1048576).toFixed(3);
}
return value;
}
onMounted(() => {
getInfoList(activeKey.value);
});
</script>
+392
View File
@@ -0,0 +1,392 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
cpuCount = '/actuator/metrics/system.cpu.count',
cpuUsage = '/actuator/metrics/system.cpu.usage',
processStartTime = '/actuator/metrics/process.start.time',
processUptime = '/actuator/metrics/process.uptime',
processCpuUsage = '/actuator/metrics/process.cpu.usage',
jvmMemoryMax = '/actuator/metrics/jvm.memory.max',
jvmMemoryCommitted = '/actuator/metrics/jvm.memory.committed',
jvmMemoryUsed = '/actuator/metrics/jvm.memory.used',
jvmBufferMemoryUsed = '/actuator/metrics/jvm.buffer.memory.used',
jvmBufferCount = '/actuator/metrics/jvm.buffer.count',
jvmThreadsDaemon = '/actuator/metrics/jvm.threads.daemon',
jvmThreadsLive = '/actuator/metrics/jvm.threads.live',
jvmThreadsPeak = '/actuator/metrics/jvm.threads.peak',
jvmClassesLoaded = '/actuator/metrics/jvm.classes.loaded',
jvmClassesUnloaded = '/actuator/metrics/jvm.classes.unloaded',
jvmGcMemoryAllocated = '/actuator/metrics/jvm.gc.memory.allocated',
jvmGcMemoryPromoted = '/actuator/metrics/jvm.gc.memory.promoted',
jvmGcMaxDataSize = '/actuator/metrics/jvm.gc.max.data.size',
jvmGcLiveDataSize = '/actuator/metrics/jvm.gc.live.data.size',
jvmGcPause = '/actuator/metrics/jvm.gc.pause',
tomcatSessionsCreated = '/actuator/metrics/tomcat.sessions.created',
tomcatSessionsExpired = '/actuator/metrics/tomcat.sessions.expired',
tomcatSessionsActiveCurrent = '/actuator/metrics/tomcat.sessions.active.current',
tomcatSessionsActiveMax = '/actuator/metrics/tomcat.sessions.active.max',
tomcatSessionsRejected = '/actuator/metrics/tomcat.sessions.rejected',
memoryInfo = '/sys/actuator/memory/info',
// undertow 监控
undertowSessionsCreated = '/actuator/metrics/undertow.sessions.created',
undertowSessionsExpired = '/actuator/metrics/undertow.sessions.expired',
undertowSessionsActiveCurrent = '/actuator/metrics/undertow.sessions.active.current',
undertowSessionsActiveMax = '/actuator/metrics/undertow.sessions.active.max',
}
/**
* 查询cpu数量
*/
export const getCpuCount = () => {
return defHttp.get({ url: Api.cpuCount }, { isTransformResponse: false });
};
/**
* 查询系统 CPU 使用率
*/
export const getCpuUsage = () => {
return defHttp.get({ url: Api.cpuUsage }, { isTransformResponse: false });
};
/**
* 查询应用启动时间点
*/
export const getProcessStartTime = () => {
return defHttp.get({ url: Api.processStartTime }, { isTransformResponse: false });
};
/**
* 查询应用已运行时间
*/
export const getProcessUptime = () => {
return defHttp.get({ url: Api.processUptime }, { isTransformResponse: false });
};
/**
* 查询当前应用 CPU 使用率
*/
export const getProcessCpuUsage = () => {
return defHttp.get({ url: Api.processCpuUsage }, { isTransformResponse: false });
};
/**
* 查询JVM 最大内存
*/
export const getJvmMemoryMax = () => {
return defHttp.get({ url: Api.jvmMemoryMax }, { isTransformResponse: false });
};
/**
* JVM 可用内存
*/
export const getJvmMemoryCommitted = () => {
return defHttp.get({ url: Api.jvmMemoryCommitted }, { isTransformResponse: false });
};
/**
* JVM 已用内存
*/
export const getJvmMemoryUsed = () => {
return defHttp.get({ url: Api.jvmMemoryUsed }, { isTransformResponse: false });
};
/**
* JVM 缓冲区已用内存
*/
export const getJvmBufferMemoryUsed = () => {
return defHttp.get({ url: Api.jvmBufferMemoryUsed }, { isTransformResponse: false });
};
/**
*JVM 当前缓冲区数量
*/
export const getJvmBufferCount = () => {
return defHttp.get({ url: Api.jvmBufferCount }, { isTransformResponse: false });
};
/**
**JVM 守护线程数量
*/
export const getJvmThreadsDaemon = () => {
return defHttp.get({ url: Api.jvmThreadsDaemon }, { isTransformResponse: false });
};
/**
*JVM 当前活跃线程数量
*/
export const getJvmThreadsLive = () => {
return defHttp.get({ url: Api.jvmThreadsLive }, { isTransformResponse: false });
};
/**
*JVM 峰值线程数量
*/
export const getJvmThreadsPeak = () => {
return defHttp.get({ url: Api.jvmThreadsPeak }, { isTransformResponse: false });
};
/**
*JVM 已加载 Class 数量
*/
export const getJvmClassesLoaded = () => {
return defHttp.get({ url: Api.jvmClassesLoaded }, { isTransformResponse: false });
};
/**
*JVM 未加载 Class 数量
*/
export const getJvmClassesUnloaded = () => {
return defHttp.get({ url: Api.jvmClassesUnloaded }, { isTransformResponse: false });
};
/**
**GC 时, 年轻代分配的内存空间
*/
export const getJvmGcMemoryAllocated = () => {
return defHttp.get({ url: Api.jvmGcMemoryAllocated }, { isTransformResponse: false });
};
/**
*GC 时, 老年代分配的内存空间
*/
export const getJvmGcMemoryPromoted = () => {
return defHttp.get({ url: Api.jvmGcMemoryPromoted }, { isTransformResponse: false });
};
/**
*GC 时, 老年代的最大内存空间
*/
export const getJvmGcMaxDataSize = () => {
return defHttp.get({ url: Api.jvmGcMaxDataSize }, { isTransformResponse: false });
};
/**
*FullGC 时, 老年代的内存空间
*/
export const getJvmGcLiveDataSize = () => {
return defHttp.get({ url: Api.jvmGcLiveDataSize }, { isTransformResponse: false });
};
/**
*系统启动以来GC 次数
*/
export const getJvmGcPause = () => {
return defHttp.get({ url: Api.jvmGcPause }, { isTransformResponse: false });
};
/**
*tomcat 已创建 session 数
*/
export const getTomcatSessionsCreated = () => {
return defHttp.get({ url: Api.tomcatSessionsCreated }, { isTransformResponse: false });
};
/**
*tomcat 已过期 session 数
*/
export const getTomcatSessionsExpired = () => {
return defHttp.get({ url: Api.tomcatSessionsExpired }, { isTransformResponse: false });
};
/**
*tomcat 当前活跃 session 数
*/
export const getTomcatSessionsActiveCurrent = () => {
return defHttp.get({ url: Api.tomcatSessionsActiveCurrent }, { isTransformResponse: false });
};
/**
*tomcat 活跃 session 数峰值
*/
export const getTomcatSessionsActiveMax = () => {
return defHttp.get({ url: Api.tomcatSessionsActiveMax }, { isTransformResponse: false });
};
/**
*超过session 最大配置后,拒绝的 session 个数
*/
export const getTomcatSessionsRejected = () => {
return defHttp.get({ url: Api.tomcatSessionsRejected }, { isTransformResponse: false });
};
/**
*undertow 已创建 session 数
*/
export const getUndertowSessionsCreated = () => {
return defHttp.get({ url: Api.undertowSessionsCreated }, { isTransformResponse: false });
};
/**
*undertow 已过期 session 数
*/
export const getUndertowSessionsExpired = () => {
return defHttp.get({ url: Api.undertowSessionsExpired }, { isTransformResponse: false });
};
/**
*undertow 当前活跃 session 数
*/
export const getUndertowSessionsActiveCurrent = () => {
return defHttp.get({ url: Api.undertowSessionsActiveCurrent }, { isTransformResponse: false });
};
/**
*undertow 活跃 session 数峰值
*/
export const getUndertowSessionsActiveMax = () => {
return defHttp.get({ url: Api.undertowSessionsActiveMax }, { isTransformResponse: false });
};
/**
* 内存信息
*/
export const getMemoryInfo = () => {
return defHttp.get({ url: Api.memoryInfo }, { isTransformResponse: false });
};
export const getMoreInfo = (infoType) => {
if (infoType == '1') {
return {};
}
if (infoType == '2') {
return { 'jvm.gc.pause': ['.count', '.totalTime'] };
}
if (infoType == '3') {
return {
'tomcat.global.request': ['.count', '.totalTime'],
'tomcat.servlet.request': ['.count', '.totalTime'],
};
}
if (infoType == '5') {
return {};
}
if (infoType == '6') {
return {};
}
};
export const getTextInfo = (infoType) => {
if (infoType == '1') {
return {
'system.cpu.count': { color: 'green', text: 'CPU 数量', unit: '核' },
'system.cpu.usage': { color: 'green', text: '系统 CPU 使用率', unit: '%', valueType: 'Number' },
'process.start.time': { color: 'purple', text: '应用启动时间点', unit: '', valueType: 'Date' },
'process.uptime': { color: 'purple', text: '应用已运行时间', unit: '秒' },
'process.cpu.usage': { color: 'purple', text: '当前应用 CPU 使用率', unit: '%', valueType: 'Number' },
};
}
if (infoType == '2') {
return {
'jvm.memory.max': { color: 'purple', text: 'JVM 最大内存', unit: 'MB', valueType: 'RAM' },
'jvm.memory.committed': { color: 'purple', text: 'JVM 可用内存', unit: 'MB', valueType: 'RAM' },
'jvm.memory.used': { color: 'purple', text: 'JVM 已用内存', unit: 'MB', valueType: 'RAM' },
'jvm.buffer.memory.used': { color: 'cyan', text: 'JVM 缓冲区已用内存', unit: 'MB', valueType: 'RAM' },
'jvm.buffer.count': { color: 'cyan', text: '当前缓冲区数量', unit: '个' },
'jvm.threads.daemon': { color: 'green', text: 'JVM 守护线程数量', unit: '个' },
'jvm.threads.live': { color: 'green', text: 'JVM 当前活跃线程数量', unit: '个' },
'jvm.threads.peak': { color: 'green', text: 'JVM 峰值线程数量', unit: '个' },
'jvm.classes.loaded': { color: 'orange', text: 'JVM 已加载 Class 数量', unit: '个' },
'jvm.classes.unloaded': { color: 'orange', text: 'JVM 未加载 Class 数量', unit: '个' },
'jvm.gc.memory.allocated': { color: 'pink', text: 'GC 时, 年轻代分配的内存空间', unit: 'MB', valueType: 'RAM' },
'jvm.gc.memory.promoted': { color: 'pink', text: 'GC 时, 老年代分配的内存空间', unit: 'MB', valueType: 'RAM' },
'jvm.gc.max.data.size': { color: 'pink', text: 'GC 时, 老年代的最大内存空间', unit: 'MB', valueType: 'RAM' },
'jvm.gc.live.data.size': { color: 'pink', text: 'FullGC 时, 老年代的内存空间', unit: 'MB', valueType: 'RAM' },
'jvm.gc.pause.count': { color: 'blue', text: '系统启动以来GC 次数', unit: '次' },
'jvm.gc.pause.totalTime': { color: 'blue', text: '系统启动以来GC 总耗时', unit: '秒' },
};
}
if (infoType == '3') {
return {
'tomcat.sessions.created': { color: 'green', text: 'tomcat 已创建 session 数', unit: '个' },
'tomcat.sessions.expired': { color: 'green', text: 'tomcat 已过期 session 数', unit: '个' },
'tomcat.sessions.active.current': { color: 'green', text: 'tomcat 当前活跃 session 数', unit: '个' },
'tomcat.sessions.active.max': { color: 'green', text: 'tomcat 活跃 session 数峰值', unit: '个' },
'tomcat.sessions.rejected': { color: 'green', text: '超过session 最大配置后,拒绝的 session 个数', unit: '个' },
'tomcat.global.sent': { color: 'purple', text: '发送的字节数', unit: 'bytes' },
'tomcat.global.request.max': { color: 'purple', text: 'request 请求最长耗时', unit: '秒' },
'tomcat.global.request.count': { color: 'purple', text: '全局 request 请求次数', unit: '次' },
'tomcat.global.request.totalTime': { color: 'purple', text: '全局 request 请求总耗时', unit: '秒' },
'tomcat.servlet.request.max': { color: 'cyan', text: 'servlet 请求最长耗时', unit: '秒' },
'tomcat.servlet.request.count': { color: 'cyan', text: 'servlet 总请求次数', unit: '次' },
'tomcat.servlet.request.totalTime': { color: 'cyan', text: 'servlet 请求总耗时', unit: '秒' },
'tomcat.threads.current': { color: 'pink', text: 'tomcat 当前线程数(包括守护线程)', unit: '个' },
'tomcat.threads.config.max': { color: 'pink', text: 'tomcat 配置的线程最大数', unit: '个' },
};
}
if (infoType == '5') {
return {
'memory.physical.total': { color: 'green', text: '总物理内存', unit: 'MB', valueType: 'RAM' },
'memory.physical.used': { color: 'green', text: '已使用物理内存', unit: 'MB', valueType: 'RAM' },
'memory.physical.free': { color: 'green', text: '可用物理内存', unit: 'MB', valueType: 'RAM' },
'memory.physical.usage': { color: 'green', text: '物理内存使用率', unit: '%', valueType: 'Number' },
'memory.runtime.total': { color: 'purple', text: 'JVM总内存', unit: 'MB', valueType: 'RAM' },
'memory.runtime.used': { color: 'purple', text: 'JVM已使用内存', unit: 'MB', valueType: 'RAM' },
'memory.runtime.max': { color: 'purple', text: 'JVM最大内存', unit: 'MB', valueType: 'RAM' },
'memory.runtime.free': { color: 'purple', text: 'JVM可用内存', unit: 'MB', valueType: 'RAM' },
'memory.runtime.usage': { color: 'purple', text: 'JVM内存使用率', unit: '%', valueType: 'Number' },
};
}
if (infoType == '6') {
// undertow 监控
return {
'undertow.sessions.created': { color: 'green', text: 'undertow 已创建 session 数', unit: '个' },
'undertow.sessions.expired': { color: 'green', text: 'undertow 已过期 session 数', unit: '个' },
'undertow.sessions.active.current': { color: 'green', text: 'undertow 当前活跃 session 数', unit: '个' },
'undertow.sessions.active.max': { color: 'green', text: 'undertow 活跃 session 数峰值', unit: '个' },
'undertow.sessions.rejected': { color: 'green', text: '超过session 最大配置后,拒绝的 session 个数', unit: '个' },
};
}
};
/**
* 查询cpu数量
* @param params
*/
export const getServerInfo = (infoType) => {
if (infoType == '1') {
return Promise.all([getCpuCount(), getCpuUsage(), getProcessStartTime(), getProcessUptime(), getProcessCpuUsage()]);
}
if (infoType == '2') {
return Promise.all([
getJvmMemoryMax(),
getJvmMemoryCommitted(),
getJvmMemoryUsed(),
getJvmBufferCount(),
getJvmBufferMemoryUsed(),
getJvmThreadsDaemon(),
getJvmThreadsLive(),
getJvmThreadsPeak(),
getJvmClassesLoaded(),
getJvmClassesUnloaded(),
getJvmGcLiveDataSize(),
getJvmGcMaxDataSize(),
getJvmGcMemoryAllocated(),
getJvmGcMemoryPromoted(),
getJvmGcPause(),
]);
}
if (infoType == '3') {
return Promise.all([
getTomcatSessionsActiveCurrent(),
getTomcatSessionsActiveMax(),
getTomcatSessionsCreated(),
getTomcatSessionsExpired(),
getTomcatSessionsRejected(),
]);
}
if (infoType == '5') {
return Promise.all([getMemoryInfo()]);
}
// undertow监控
if (infoType == '6') {
return Promise.all([
getUndertowSessionsActiveCurrent(),
getUndertowSessionsActiveMax(),
getUndertowSessionsCreated(),
getUndertowSessionsExpired(),
]);
}
};
+23
View File
@@ -0,0 +1,23 @@
import { BasicColumn } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: '参数',
dataIndex: 'param',
width: 80,
align: 'left',
slots: { customRender: 'param' },
},
{
title: '描述',
dataIndex: 'text',
slots: { customRender: 'text' },
width: 80,
},
{
title: '当前值',
dataIndex: 'value',
slots: { customRender: 'value' },
width: 80,
},
];
+72
View File
@@ -0,0 +1,72 @@
<template>
<div class="p-4">
<BasicTable @register="registerTable" :dataSource="dataSource" @change="handlerTableChange">
<template #tableTitle>
<div slot="message">
共追踪到 {{ dataSource.length }} 条近期HTTP请求记录
<a-divider type="vertical" />
<a @click="loadDate">立即刷新</a>
</div>
</template>
<template #toolbar>
<a-radio-group class="http-status-choose" size="small" v-model:value="query" @change="loadDate">
<a-radio-button value="all">全部</a-radio-button>
<a-radio-button value="success">成功</a-radio-button>
<a-radio-button value="error">错误</a-radio-button>
</a-radio-group>
</template>
</BasicTable>
</div>
</template>
<script lang="ts" name="monitor-trace" setup>
import { onMounted, ref, reactive } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { getActuatorList } from './trace.api';
import { columns } from './trace.data';
import { useMessage } from '/@/hooks/web/useMessage';
const dataSource = ref([]);
const { createMessage } = useMessage();
const query = ref('all');
const order = ref('');
const [registerTable, { reload }] = useTable({
columns,
showIndexColumn: false,
bordered: true,
rowKey: 'id',
});
function loadDate() {
getActuatorList(query.value,order.value).then((res) => {
let filterData = [];
for (let d of res.traces) {
if (d.request.method !== 'OPTIONS' && d.request.uri.indexOf('httptrace') === -1) {
filterData.push(d);
}
}
dataSource.value = filterData;
});
}
const handlerTableChange = (args, arg1, sort, action) => {
if ('sort' == action.action && sort.field) {
order.value = sort.field;
if (sort.order) {
order.value += sort.order == 'ascend' ? '/asc' : '/desc';
} else {
order.value = '';
}
}
loadDate();
};
onMounted(() => {
loadDate();
});
</script>
<style scoped>
:deep(.jeecg-basic-table-header__toolbar) {
width: 150px;
}
</style>
+12
View File
@@ -0,0 +1,12 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
actuatorList = '/actuator/jeecghttptrace/',
}
/**
* 追踪信息
*/
export const getActuatorList = (query: String, order: String) => {
return defHttp.get({ url: Api.actuatorList + query + '/' + order }, { isTransformResponse: false });
};
+85
View File
@@ -0,0 +1,85 @@
import { BasicColumn } from '/@/components/Table';
import dayjs from 'dayjs';
import _get from 'lodash.get';
import { h } from 'vue';
import { Tag } from 'ant-design-vue';
export const columns: BasicColumn[] = [
{
title: '请求时间',
dataIndex: 'timestamp',
width: 50,
customRender({ text }) {
return dayjs(text).format('YYYY-MM-DD HH:mm:ss');
},
},
{
title: '请求方法',
dataIndex: 'request.method',
width: 20,
customRender({ record, column }) {
let value = _get(record, column.dataIndex!);
let color = '';
if (value === 'GET') {
color = '#87d068';
}
if (value === 'POST') {
color = '#2db7f5';
}
if (value === 'PUT') {
color = '#ffba5a';
}
if (value === 'DELETE') {
color = '#ff5500';
}
return h(Tag, { color }, () => value);
},
},
{
title: '请求URL',
dataIndex: 'request.uri',
width: 200,
customRender({ record, column }) {
return _get(record, column.dataIndex!);
},
},
{
title: '响应状态',
dataIndex: 'response.status',
width: 50,
customRender({ record, column }) {
let value = _get(record, column.dataIndex!);
let color = '';
if (value < 200) {
color = 'pink';
} else if (value < 201) {
color = 'green';
} else if (value < 399) {
color = 'cyan';
} else if (value < 403) {
color = 'orange';
} else if (value < 501) {
color = 'red';
}
return h(Tag, { color }, () => value);
},
},
{
title: '请求耗时',
dataIndex: 'timeTaken',
width: 50,
customRender({ record, column }) {
let value = _get(record, column.dataIndex!);
let color = 'red';
if (value < 500) {
color = 'green';
} else if (value < 1000) {
color = 'cyan';
} else if (value < 1500) {
color = 'orange';
}
return h(Tag, { color }, () => `${value} ms`);
},
sorter: true,
},
];