物业代码生成
This commit is contained in:
7
apps/web-antd/src/views/monitor/admin/index.vue
Normal file
7
apps/web-antd/src/views/monitor/admin/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- 建议通过菜单配置成外链/内嵌 达到相同的效果且灵活性更高 -->
|
||||
<template>
|
||||
<iframe
|
||||
class="size-full"
|
||||
src="http://localhost:9090/admin/applications"
|
||||
></iframe>
|
||||
</template>
|
63
apps/web-antd/src/views/monitor/cache/components/command-chart.vue
vendored
Normal file
63
apps/web-antd/src/views/monitor/cache/components/command-chart.vue
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onActivated, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
interface Props {
|
||||
data?: { name: string; value: string }[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: () => [],
|
||||
});
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts, resize } = useEcharts(chartRef);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!chartRef.value) return;
|
||||
setEchartsOption(props.data);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
setEchartsOption(props.data);
|
||||
});
|
||||
/**
|
||||
* 从其他页面切换回来会有一个奇怪的动画效果 需要调用resize
|
||||
* 该饼图组件需要关闭animation
|
||||
*/
|
||||
onActivated(() => resize(false));
|
||||
|
||||
type EChartsOption = Parameters<typeof renderEcharts>['0'];
|
||||
function setEchartsOption(data: any[]) {
|
||||
const option: EChartsOption = {
|
||||
series: [
|
||||
{
|
||||
animationDuration: 1000,
|
||||
animationEasing: 'cubicInOut',
|
||||
center: ['50%', '50%'],
|
||||
data,
|
||||
name: '命令',
|
||||
radius: [15, 95],
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)',
|
||||
trigger: 'item',
|
||||
},
|
||||
};
|
||||
renderEcharts(option);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" height="400px" width="100%" />
|
||||
</template>
|
3
apps/web-antd/src/views/monitor/cache/components/index.ts
vendored
Normal file
3
apps/web-antd/src/views/monitor/cache/components/index.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as CommandChart } from './command-chart.vue';
|
||||
export { default as MemoryChart } from './memory-chart.vue';
|
||||
export { default as RedisDescription } from './redis-description.vue';
|
89
apps/web-antd/src/views/monitor/cache/components/memory-chart.vue
vendored
Normal file
89
apps/web-antd/src/views/monitor/cache/components/memory-chart.vue
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onActivated, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
interface Props {
|
||||
data?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: '0',
|
||||
});
|
||||
|
||||
const memoryHtmlRef = ref<EchartsUIType>();
|
||||
const { renderEcharts, resize } = useEcharts(memoryHtmlRef);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!memoryHtmlRef.value) return;
|
||||
setEchartsOption(props.data);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
setEchartsOption(props.data);
|
||||
});
|
||||
// 从其他页面切换回来会有一个奇怪的动画效果 需要调用resize
|
||||
onActivated(resize);
|
||||
|
||||
/**
|
||||
* 获取最近的十的幂次
|
||||
* 该函数用于寻找大于给定数字num的最近的10的幂次
|
||||
* 主要解决的问题是确定一个数附近较大的十的幂次,这在某些算法中很有用
|
||||
*
|
||||
* @param num {number} 输入的数字,用于寻找最近的十的幂次
|
||||
*/
|
||||
function getNearestPowerOfTen(num: number) {
|
||||
let power = 10;
|
||||
while (power <= num) {
|
||||
power *= 10;
|
||||
}
|
||||
return power;
|
||||
}
|
||||
|
||||
type EChartsOption = Parameters<typeof renderEcharts>['0'];
|
||||
function setEchartsOption(value: string) {
|
||||
// x10
|
||||
const formattedValue = Math.floor(Number.parseFloat(value));
|
||||
// 最大值 10以内取10 100以内取100 以此类推
|
||||
const max = getNearestPowerOfTen(formattedValue);
|
||||
const options: EChartsOption = {
|
||||
series: [
|
||||
{
|
||||
animation: true,
|
||||
animationDuration: 1000,
|
||||
data: [
|
||||
{
|
||||
name: '内存消耗',
|
||||
value: Number.parseFloat(value),
|
||||
},
|
||||
],
|
||||
detail: {
|
||||
formatter: `${value}M`,
|
||||
valueAnimation: true,
|
||||
},
|
||||
max,
|
||||
min: 0,
|
||||
name: '峰值',
|
||||
progress: {
|
||||
show: true,
|
||||
},
|
||||
type: 'gauge',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
formatter: `{b} <br/>{a} : ${value}M`,
|
||||
},
|
||||
};
|
||||
renderEcharts(options);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="memoryHtmlRef" height="400px" width="100%" />
|
||||
</template>
|
58
apps/web-antd/src/views/monitor/cache/components/redis-description.vue
vendored
Normal file
58
apps/web-antd/src/views/monitor/cache/components/redis-description.vue
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import type { RedisInfo } from '#/api/monitor/cache';
|
||||
|
||||
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||
|
||||
interface IRedisInfo extends RedisInfo {
|
||||
dbSize: string;
|
||||
}
|
||||
|
||||
defineProps<{ data: IRedisInfo }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Descriptions
|
||||
bordered
|
||||
:column="{ lg: 4, md: 3, sm: 1, xl: 4, xs: 1 }"
|
||||
size="small"
|
||||
>
|
||||
<DescriptionsItem label="redis版本">
|
||||
{{ data.redis_version }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="redis模式">
|
||||
{{ data.redis_mode === 'standalone' ? '单机模式' : '集群模式' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="tcp端口">
|
||||
{{ data.tcp_port }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="客户端数">
|
||||
{{ data.connected_clients }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="运行时间">
|
||||
{{ data.uptime_in_days }} 天
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="使用内存">
|
||||
{{ data.used_memory_human }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="使用CPU">
|
||||
{{ Number.parseFloat(data?.used_cpu_user_children ?? '0').toFixed(2) }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="内存配置">
|
||||
{{ data.maxmemory_human }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="AOF是否开启">
|
||||
{{ data.aof_enabled === '0' ? '否' : '是' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="RDB是否成功">
|
||||
{{ data.rdb_last_bgsave_status }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="key数量">
|
||||
{{ data.dbSize }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="网络入口/出口">
|
||||
{{
|
||||
`${data.instantaneous_input_kbps}kps/${data.instantaneous_output_kbps}kps`
|
||||
}}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</template>
|
107
apps/web-antd/src/views/monitor/cache/index.vue
vendored
Normal file
107
apps/web-antd/src/views/monitor/cache/index.vue
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import type { RedisInfo } from '#/api/monitor/cache';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { CommandLineIcon, MemoryIcon, RedisIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Card, Col, Row } from 'ant-design-vue';
|
||||
|
||||
import { redisCacheInfo } from '#/api/monitor/cache';
|
||||
|
||||
import { CommandChart, MemoryChart, RedisDescription } from './components';
|
||||
|
||||
const baseSpan = { lg: 12, md: 24, sm: 24, xl: 12, xs: 24 };
|
||||
|
||||
const chartData = reactive<{
|
||||
command: { name: string; value: string }[];
|
||||
memory: string;
|
||||
}>({
|
||||
command: [],
|
||||
memory: '0',
|
||||
});
|
||||
|
||||
interface IRedisInfo extends RedisInfo {
|
||||
dbSize: string;
|
||||
}
|
||||
const redisInfo = ref<IRedisInfo>();
|
||||
|
||||
onMounted(async () => {
|
||||
await loadInfo();
|
||||
});
|
||||
|
||||
async function loadInfo() {
|
||||
try {
|
||||
const ret = await redisCacheInfo();
|
||||
|
||||
// 单位MB 保留两位小数
|
||||
const usedMemory = (
|
||||
Number.parseInt(ret.info.used_memory!) /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2);
|
||||
chartData.memory = usedMemory;
|
||||
// 命令统计
|
||||
chartData.command = ret.commandStats;
|
||||
console.log(chartData.command);
|
||||
// redis信息
|
||||
redisInfo.value = { ...ret.info, dbSize: String(ret.dbSize) };
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Row :gutter="[15, 15]">
|
||||
<Col :span="24">
|
||||
<Card size="small">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-start gap-[6px]">
|
||||
<RedisIcon class="size-[16px]" />
|
||||
<span>redis信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<Button size="small" @click="loadInfo">
|
||||
<div class="flex">
|
||||
<span class="icon-[charm--refresh]"></span>
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
<RedisDescription v-if="redisInfo" :data="redisInfo" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col v-bind="baseSpan">
|
||||
<Card size="small">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-[6px]">
|
||||
<CommandLineIcon class="size-[16px]" />
|
||||
<span>命令统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<CommandChart
|
||||
v-if="chartData.command.length > 0"
|
||||
:data="chartData.command"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col v-bind="baseSpan">
|
||||
<Card size="small">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-start gap-[6px]">
|
||||
<MemoryIcon class="size-[16px]" />
|
||||
<span>内存占用</span>
|
||||
</div>
|
||||
</template>
|
||||
<MemoryChart
|
||||
v-if="chartData.memory !== '0'"
|
||||
:data="chartData.memory"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Page>
|
||||
</template>
|
108
apps/web-antd/src/views/monitor/logininfor/data.tsx
Normal file
108
apps/web-antd/src/views/monitor/logininfor/data.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { VNode } from 'vue';
|
||||
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { getDictOptions } from '#/utils/dict';
|
||||
import { renderBrowserIcon, renderDict, renderOsIcon } from '#/utils/render';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'ipaddr',
|
||||
label: 'IP地址',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'userName',
|
||||
label: '用户账号',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DictEnum.SYS_COMMON_STATUS),
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '登录状态',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'dateTime',
|
||||
label: '登录日期',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
title: '用户账号',
|
||||
field: 'userName',
|
||||
},
|
||||
{
|
||||
title: '登录平台',
|
||||
field: 'clientKey',
|
||||
},
|
||||
{
|
||||
title: 'IP地址',
|
||||
field: 'ipaddr',
|
||||
},
|
||||
{
|
||||
title: 'IP地点',
|
||||
field: 'loginLocation',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '浏览器',
|
||||
field: 'browser',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderBrowserIcon(row.browser, true) as VNode;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '系统',
|
||||
field: 'os',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
/**
|
||||
* Windows 10 or Windows Server 2016 太长了 分割一下 详情依旧能看到详细的
|
||||
*/
|
||||
let value = row.os;
|
||||
if (value) {
|
||||
const split = value.split(' or ');
|
||||
if (split.length === 2) {
|
||||
value = split[0];
|
||||
}
|
||||
}
|
||||
return renderOsIcon(value, true) as VNode;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录结果',
|
||||
field: 'status',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.status, DictEnum.SYS_COMMON_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '信息',
|
||||
field: 'msg',
|
||||
},
|
||||
{
|
||||
title: '日期',
|
||||
field: 'loginTime',
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
width: 150,
|
||||
},
|
||||
];
|
210
apps/web-antd/src/views/monitor/logininfor/index.vue
Normal file
210
apps/web-antd/src/views/monitor/logininfor/index.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { LoginLog } from '#/api/monitor/logininfo/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
loginInfoClean,
|
||||
loginInfoExport,
|
||||
loginInfoList,
|
||||
loginInfoRemove,
|
||||
userUnlock,
|
||||
} from '#/api/monitor/logininfo';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
import { confirmDeleteModal } from '#/utils/modal';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import loginInfoModal from './login-info-modal.vue';
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
// 日期选择格式化
|
||||
fieldMappingTime: [
|
||||
[
|
||||
'dateTime',
|
||||
['params[beginTime]', 'params[endTime]'],
|
||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
trigger: 'row',
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
return await loginInfoList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'infoId',
|
||||
},
|
||||
id: 'monitor-logininfo-index',
|
||||
};
|
||||
|
||||
const canUnlock = ref(false);
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
checkboxChange: (e) => {
|
||||
const records = e.$grid.getCheckboxRecords();
|
||||
canUnlock.value = records.length === 1 && records[0]!.status === '1';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [LoginInfoModal, modalApi] = useVbenModal({
|
||||
connectedComponent: loginInfoModal,
|
||||
});
|
||||
|
||||
function handlePreview(record: LoginLog) {
|
||||
modalApi.setData(record);
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
confirmDeleteModal({
|
||||
onValidated: async () => {
|
||||
await loginInfoClean();
|
||||
await tableApi.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDelete(row: LoginLog) {
|
||||
await loginInfoRemove([row.infoId]);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function handleMultiDelete() {
|
||||
const rows = tableApi.grid.getCheckboxRecords();
|
||||
const ids = rows.map((row: LoginLog) => row.infoId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条记录吗?`,
|
||||
onOk: async () => {
|
||||
await loginInfoRemove(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleUnlock() {
|
||||
const records = tableApi.grid.getCheckboxRecords();
|
||||
if (records.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const { userName } = records[0];
|
||||
await userUnlock(userName);
|
||||
await tableApi.query();
|
||||
canUnlock.value = false;
|
||||
tableApi.grid.clearCheckboxRow();
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(
|
||||
loginInfoExport,
|
||||
'登录日志',
|
||||
tableApi.formApi.form.values,
|
||||
{
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
},
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<BasicTable table-title="登录日志列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['monitor:logininfor:remove']"
|
||||
@click="handleClear"
|
||||
>
|
||||
{{ $t('pages.common.clear') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['monitor:logininfor:export']"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['monitor:logininfor:remove']"
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:disabled="!canUnlock"
|
||||
type="primary"
|
||||
v-access:code="['monitor:logininfor:unlock']"
|
||||
@click="handleUnlock"
|
||||
>
|
||||
{{ $t('pages.common.unlock') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<ghost-button @click.stop="handlePreview(row)">
|
||||
{{ $t('pages.common.info') }}
|
||||
</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="() => handleDelete(row)"
|
||||
>
|
||||
<ghost-button
|
||||
danger
|
||||
v-access:code="['monitor:logininfor:remove']"
|
||||
@click.stop=""
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<LoginInfoModal />
|
||||
</Page>
|
||||
</template>
|
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import type { LoginLog } from '#/api/monitor/logininfo/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||
|
||||
import { renderBrowserIcon, renderDict, renderOsIcon } from '#/utils/render';
|
||||
|
||||
const loginInfo = ref<LoginLog>();
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
onOpenChange: (isOpen) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
const record = modalApi.getData() as LoginLog;
|
||||
loginInfo.value = record;
|
||||
},
|
||||
onClosed() {
|
||||
loginInfo.value = undefined;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal
|
||||
:footer="false"
|
||||
:fullscreen-button="false"
|
||||
class="w-[550px]"
|
||||
title="登录日志"
|
||||
>
|
||||
<Descriptions v-if="loginInfo" size="small" :column="1" bordered>
|
||||
<DescriptionsItem label="登录状态">
|
||||
<component
|
||||
:is="renderDict(loginInfo.status, DictEnum.SYS_COMMON_STATUS)"
|
||||
/>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="登录平台">
|
||||
{{ loginInfo.clientKey.toLowerCase() }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="账号信息">
|
||||
{{
|
||||
`账号: ${loginInfo.userName} / ${loginInfo.ipaddr} / ${loginInfo.loginLocation}`
|
||||
}}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="登录时间">
|
||||
{{ loginInfo.loginTime }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="登录信息">
|
||||
<span
|
||||
class="font-semibold"
|
||||
:class="{ 'text-red-500': loginInfo.status !== '0' }"
|
||||
>
|
||||
{{ loginInfo.msg }}
|
||||
</span>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="登录设备">
|
||||
<component :is="renderOsIcon(loginInfo.os)" />
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="浏览器">
|
||||
<component :is="renderBrowserIcon(loginInfo.browser)" />
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</BasicModal>
|
||||
</template>
|
85
apps/web-antd/src/views/monitor/online/data.ts
Normal file
85
apps/web-antd/src/views/monitor/online/data.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { VNode } from 'vue';
|
||||
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { renderBrowserIcon, renderOsIcon } from '#/utils/render';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'ipaddr',
|
||||
label: 'IP地址',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'userName',
|
||||
label: '用户账号',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{
|
||||
title: '登录平台',
|
||||
field: 'deviceType',
|
||||
},
|
||||
{
|
||||
title: '登录账号',
|
||||
field: 'userName',
|
||||
},
|
||||
{
|
||||
title: '部门名称',
|
||||
field: 'deptName',
|
||||
},
|
||||
{
|
||||
title: 'IP地址',
|
||||
field: 'ipaddr',
|
||||
},
|
||||
{
|
||||
title: '登录地址',
|
||||
field: 'loginLocation',
|
||||
},
|
||||
{
|
||||
title: '浏览器',
|
||||
field: 'browser',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderBrowserIcon(row.browser, true) as VNode;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '系统',
|
||||
field: 'os',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
// Windows 10 or Windows Server 2016 太长了 分割一下 详情依旧能看到详细的
|
||||
let value = row.os;
|
||||
if (value) {
|
||||
const split = value.split(' or ');
|
||||
if (split.length === 2) {
|
||||
value = split[0];
|
||||
}
|
||||
}
|
||||
return renderOsIcon(value, true) as VNode;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录时间',
|
||||
field: 'loginTime',
|
||||
formatter: ({ cellValue }) => {
|
||||
return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
resizable: false,
|
||||
width: 'auto',
|
||||
},
|
||||
];
|
91
apps/web-antd/src/views/monitor/online/index.vue
Normal file
91
apps/web-antd/src/views/monitor/online/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { OnlineUser } from '#/api/monitor/online/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Popconfirm } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { forceLogout, onlineList } from '#/api/monitor/online';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
};
|
||||
|
||||
const onlineCount = ref(0);
|
||||
const gridOptions: VxeGridProps = {
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_, formValues = {}) => {
|
||||
const resp = await onlineList({
|
||||
...formValues,
|
||||
});
|
||||
onlineCount.value = resp.total;
|
||||
return resp;
|
||||
},
|
||||
},
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'tokenId',
|
||||
},
|
||||
id: 'monitor-online-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||
|
||||
async function handleForceOffline(row: OnlineUser) {
|
||||
await forceLogout(row.tokenId);
|
||||
await tableApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable>
|
||||
<template #toolbar-actions>
|
||||
<div class="mr-1 pl-1 text-[1rem]">
|
||||
<div>
|
||||
在线用户列表 (共
|
||||
<span class="text-primary font-bold">{{ onlineCount }}</span>
|
||||
人在线)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
:title="`确认强制下线[${row.userName}]?`"
|
||||
placement="left"
|
||||
@confirm="handleForceOffline(row)"
|
||||
>
|
||||
<ghost-button danger>强制下线</ghost-button>
|
||||
</Popconfirm>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</Page>
|
||||
</template>
|
92
apps/web-antd/src/views/monitor/operlog/data.tsx
Normal file
92
apps/web-antd/src/views/monitor/operlog/data.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { getDictOptions } from '#/utils/dict';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'title',
|
||||
label: '系统模块',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'operName',
|
||||
label: '操作人员',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DictEnum.SYS_OPER_TYPE),
|
||||
},
|
||||
fieldName: 'businessType',
|
||||
label: '操作类型',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'operIp',
|
||||
label: '操作IP',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DictEnum.SYS_COMMON_STATUS),
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'createTime',
|
||||
label: '操作时间',
|
||||
componentProps: {
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'title', title: '系统模块' },
|
||||
{
|
||||
title: '操作类型',
|
||||
field: 'businessType',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.businessType, DictEnum.SYS_OPER_TYPE);
|
||||
},
|
||||
},
|
||||
},
|
||||
{ field: 'operName', title: '操作人员' },
|
||||
{ field: 'operIp', title: 'IP地址' },
|
||||
{ field: 'operLocation', title: 'IP信息' },
|
||||
{
|
||||
field: 'status',
|
||||
title: '操作状态',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.status, DictEnum.SYS_COMMON_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{ field: 'operTime', title: '操作日期', sortable: true },
|
||||
{
|
||||
field: 'costTime',
|
||||
title: '操作耗时',
|
||||
sortable: true,
|
||||
formatter({ cellValue }) {
|
||||
return `${cellValue} ms`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
resizable: false,
|
||||
width: 'auto',
|
||||
},
|
||||
];
|
184
apps/web-antd/src/views/monitor/operlog/index.vue
Normal file
184
apps/web-antd/src/views/monitor/operlog/index.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { PageQuery } from '#/api/common';
|
||||
import type { OperationLog } from '#/api/monitor/operlog/model';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Modal, Space } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
addSortParams,
|
||||
useVbenVxeGrid,
|
||||
vxeCheckboxChecked,
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
operLogClean,
|
||||
operLogDelete,
|
||||
operLogExport,
|
||||
operLogList,
|
||||
} from '#/api/monitor/operlog';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
import { confirmDeleteModal } from '#/utils/modal';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import operationPreviewDrawer from './operation-preview-drawer.vue';
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
// 日期选择格式化
|
||||
fieldMappingTime: [
|
||||
[
|
||||
'createTime',
|
||||
['params[beginTime]', 'params[endTime]'],
|
||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps<OperationLog> = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
trigger: 'row',
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page, sorts }, formValues = {}) => {
|
||||
const params: PageQuery = {
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
// 添加排序参数
|
||||
addSortParams(params, sorts);
|
||||
return await operLogList(params);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'operId',
|
||||
},
|
||||
sortConfig: {
|
||||
// 远程排序
|
||||
remote: true,
|
||||
// 支持多字段排序 默认关闭
|
||||
multiple: true,
|
||||
},
|
||||
id: 'monitor-operlog-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
// 排序 重新请求接口
|
||||
sortChange: () => tableApi.query(),
|
||||
},
|
||||
});
|
||||
|
||||
const [OperationPreviewDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: operationPreviewDrawer,
|
||||
});
|
||||
|
||||
/**
|
||||
* 预览
|
||||
* @param record 操作日志记录
|
||||
*/
|
||||
function handlePreview(record: OperationLog) {
|
||||
drawerApi.setData({ record });
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空全部日志
|
||||
*/
|
||||
function handleClear() {
|
||||
confirmDeleteModal({
|
||||
onValidated: async () => {
|
||||
await operLogClean();
|
||||
await tableApi.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 删除日志
|
||||
*/
|
||||
async function handleDelete() {
|
||||
const rows = tableApi.grid.getCheckboxRecords();
|
||||
const ids = rows.map((row: OperationLog) => row.operId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条操作日志吗?`,
|
||||
onOk: async () => {
|
||||
await operLogDelete(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(operLogExport, '操作日志', tableApi.formApi.form.values, {
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable table-title="操作日志列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['monitor:operlog:remove']"
|
||||
@click="handleClear"
|
||||
>
|
||||
{{ $t('pages.common.clear') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['monitor:operlog:export']"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['monitor:operlog:remove']"
|
||||
@click="handleDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<ghost-button
|
||||
v-access:code="['monitor:operlog:list']"
|
||||
@click.stop="handlePreview(row)"
|
||||
>
|
||||
{{ $t('pages.common.preview') }}
|
||||
</ghost-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<OperationPreviewDrawer />
|
||||
</Page>
|
||||
</template>
|
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import type { OperationLog } from '#/api/monitor/operlog/model';
|
||||
|
||||
import { computed, shallowRef } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { Descriptions, DescriptionsItem, Tag } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
renderDict,
|
||||
renderHttpMethodTag,
|
||||
renderJsonPreview,
|
||||
} from '#/utils/render';
|
||||
|
||||
const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
onOpenChange: handleOpenChange,
|
||||
onClosed() {
|
||||
currentLog.value = null;
|
||||
},
|
||||
});
|
||||
|
||||
const currentLog = shallowRef<null | OperationLog>(null);
|
||||
function handleOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
const { record } = drawerApi.getData() as { record: OperationLog };
|
||||
currentLog.value = record;
|
||||
}
|
||||
|
||||
const actionInfo = computed(() => {
|
||||
if (!currentLog.value) {
|
||||
return '-';
|
||||
}
|
||||
const data = currentLog.value;
|
||||
return `账号: ${data.operName} / ${data.deptName} / ${data.operIp} / ${data.operLocation}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicDrawer :footer="false" class="w-[600px]" title="查看日志">
|
||||
<Descriptions v-if="currentLog" size="small" bordered :column="1">
|
||||
<DescriptionsItem label="日志编号" :label-style="{ minWidth: '120px' }">
|
||||
{{ currentLog.operId }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="操作结果">
|
||||
<component
|
||||
:is="renderDict(currentLog.status, DictEnum.SYS_COMMON_STATUS)"
|
||||
/>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="操作模块">
|
||||
<div class="flex items-center">
|
||||
<Tag>{{ currentLog.title }}</Tag>
|
||||
<component
|
||||
:is="renderDict(currentLog.businessType, DictEnum.SYS_OPER_TYPE)"
|
||||
/>
|
||||
</div>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="操作信息">
|
||||
{{ actionInfo }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="请求信息">
|
||||
<component :is="renderHttpMethodTag(currentLog.requestMethod)" />
|
||||
{{ currentLog.operUrl }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem v-if="currentLog.errorMsg" label="异常信息">
|
||||
<span class="font-semibold text-red-600">
|
||||
{{ currentLog.errorMsg }}
|
||||
</span>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="方法">
|
||||
{{ currentLog.method }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="请求参数">
|
||||
<div class="max-h-[300px] overflow-y-auto">
|
||||
<component :is="renderJsonPreview(currentLog.operParam)" />
|
||||
</div>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem v-if="currentLog.jsonResult" label="响应参数">
|
||||
<div class="max-h-[300px] overflow-y-auto">
|
||||
<component :is="renderJsonPreview(currentLog.jsonResult)" />
|
||||
</div>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="请求耗时">
|
||||
{{ `${currentLog.costTime} ms` }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="操作时间">
|
||||
{{ `${currentLog.operTime}` }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</BasicDrawer>
|
||||
</template>
|
4
apps/web-antd/src/views/monitor/snailjob/index.vue
Normal file
4
apps/web-antd/src/views/monitor/snailjob/index.vue
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- 建议通过菜单配置成外链/内嵌 达到相同的效果且灵活性更高 -->
|
||||
<template>
|
||||
<iframe class="size-full" src="http://localhost:8800/snail-job"></iframe>
|
||||
</template>
|
Reference in New Issue
Block a user