@@ -16,12 +16,12 @@ VITE_PWA = false | |||
# 开发环境接口地址 | |||
# VITE_API_URL = http://192.168.10.186:5566 | |||
VITE_API_URL = http://192.168.10.186:8003 | |||
VITE_API_URL = http://192.168.10.51:8003 | |||
# 否开启代理 | |||
VITE_HTTP_PROXY = true | |||
# 开发环境跨域代理,支持配置多个 | |||
# VITE_PROXY = [["/api","http://192.168.10.186:5566","/Files"]] | |||
VITE_PROXY = [["/api","http://192.168.10.186:8003"],["/Files","http://192.168.10.186:8003/Files"]] | |||
VITE_PROXY = [["/api","http://192.168.10.51:8003"],["/Files","http://192.168.10.51:8003/Files"]] | |||
@@ -14,3 +14,4 @@ | |||
*/ | |||
export * from "./visLog"; | |||
export * from "./opLog"; | |||
export * from "./smsLog"; |
@@ -0,0 +1,44 @@ | |||
/** | |||
* @description 操作日志 | |||
* @license Apache License Version 2.0 | |||
* @Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 | |||
* @remarks | |||
* SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: | |||
* 1.请不要删除和修改根目录下的LICENSE文件。 | |||
* 2.请不要删除和修改SimpleAdmin源码头部的版权声明。 | |||
* 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin | |||
* 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。 | |||
* 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为不要删除和修改作者声明。 | |||
* 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关 | |||
* @see https://gitee.com/dotnetmoyu/SimpleAdmin | |||
*/ | |||
import { ResPage, VisLog } from "@/api/interface"; | |||
import { moduleRequest } from "@/api/request"; | |||
const http = moduleRequest("/sys/audit/smsLog/"); | |||
/** | |||
* @Description: 操作日志 | |||
* @Author: huguodong | |||
* @Date: 2024-11-19 15:34:54 | |||
*/ | |||
const smsLogApi = { | |||
/** 获取操作日志分页 */ | |||
page(params: VisLog.Page) { | |||
return http.get<ResPage<VisLog.VisLogInfo>>("page", params); | |||
}, | |||
/** 获取操作日志柱状图数据 */ | |||
lineChart() { | |||
return http.get<VisLog.LineChart[]>("lineChartData", {}, { loading: false }); | |||
}, | |||
/** 获取操作日志饼状图数据 */ | |||
pieChart() { | |||
return http.get<VisLog.PineChart[]>("pieChartData", {}, { loading: false }); | |||
}, | |||
/** 清空操作日志 */ | |||
delete(category: string) { | |||
return http.post("delete", { category }); | |||
} | |||
}; | |||
export { smsLogApi }; |
@@ -34,7 +34,7 @@ | |||
<el-radio-group v-model="initParam.category" class="mb-15px"> | |||
<el-radio-button v-for="(item, index) in visLogTypeOptions" :key="index" :label="item.value">{{ item.label }}</el-radio-button> | |||
</el-radio-group> | |||
<el-input v-model="initParam.searchKey" placeholder="请输入字典名称" class="mb-15px"> | |||
<el-input v-model="initParam.searchKey" placeholder="请输入关键字" class="mb-15px"> | |||
<template #append> | |||
<el-button :icon="Search" class="el-input-button" /> | |||
</template> | |||
@@ -0,0 +1,70 @@ | |||
<!-- | |||
* @Description: 详情 | |||
* @Author: Syy | |||
* @Date: 2024-11-20 15:44:33 | |||
!--> | |||
<template> | |||
<form-container v-model="visible" title="日志详情" form-size="600px"> | |||
<el-descriptions :column="1" border :label-width="170" :label-style="{ 'text-align': 'right' }"> | |||
<el-descriptions-item label="名称" label-align="center">{{ logInfo.name }}</el-descriptions-item> | |||
<el-descriptions-item label="发送状态" label-align="center" label-class-name="my-label"> | |||
<el-tag v-if="logInfo.exeStatus === 'FAIL'" type="danger">失败</el-tag> | |||
<el-tag v-if="logInfo.exeStatus === 'SUCCESS'" type="success">成功</el-tag> | |||
</el-descriptions-item> | |||
<el-descriptions-item label="通知用户" label-align="center">{{ logInfo.opUser }}</el-descriptions-item> | |||
<el-descriptions-item label="发送人" label-align="center">{{ logInfo.opAccount }}</el-descriptions-item> | |||
<el-descriptions-item label="短信内容" label-align="center">{{ logInfo.opAddress }}</el-descriptions-item> | |||
<el-descriptions-item label="发送时间" label-align="center">{{ logInfo.opTime }}</el-descriptions-item> | |||
</el-descriptions> | |||
<template #footer> | |||
<el-button type="primary" @click="onClose"> 确定 </el-button> | |||
</template> | |||
</form-container> | |||
</template> | |||
<script setup lang="ts" name="detail"> | |||
import { VisLog } from "@/api"; | |||
const visible = ref(false); //是否显示表单 | |||
// 表单参数 | |||
const logInfo = ref<VisLog.VisLogInfo>({ | |||
name: "", | |||
opIp: "", | |||
opAddress: "", | |||
opBrowser: "", | |||
opOs: "", | |||
opTime: "", | |||
opUser: "", | |||
opAccount: "", | |||
id: 0, | |||
category: "", | |||
exeStatus: "", | |||
createTime: "" | |||
}); | |||
/** | |||
* 打开表单 | |||
* @param props 表单参数 | |||
*/ | |||
function onOpen(record: VisLog.VisLogInfo) { | |||
logInfo.value = record; | |||
visible.value = true; //显示表单 | |||
} | |||
/** 关闭表单*/ | |||
function onClose() { | |||
visible.value = false; | |||
} | |||
// 暴露给父组件的方法 | |||
defineExpose({ | |||
onOpen | |||
}); | |||
</script> | |||
<style lang="scss" scoped> | |||
:deep(.my-label) { | |||
width: 100px !important; | |||
} | |||
</style> |
@@ -0,0 +1,73 @@ | |||
<!-- | |||
* @Description: 折线图 | |||
* @Author: Syy | |||
* @Date: 2024-11-20 15:44:33 | |||
!--> | |||
<template> | |||
<div id="lineChat" class="h-150px"></div> | |||
</template> | |||
<script setup lang="ts" name="lineChart"> | |||
import { VisLog, smsLogApi } from "@/api"; | |||
import { Line } from "@antv/g2plot"; | |||
const seriesField = "series"; //分组字段 | |||
const yField = "value"; //图形在 y 方向对应的数据字段名,一般是纵向的坐标轴对应的字段 | |||
type alias = "用户登录" | "用户登出"; //别名 | |||
// lineMeta 用于处理数据,将数据中的字段名转换为别名 | |||
interface LineMeta { | |||
[key: string]: { | |||
alias: alias; | |||
}; | |||
} | |||
const lineMeta: LineMeta = { | |||
successCount: { | |||
alias: "发送成功" | |||
}, | |||
failCount: { | |||
alias: "发送失败" | |||
} | |||
}; | |||
onMounted(() => { | |||
smsLogApi.lineChart().then(res => { | |||
createLineChat(res.data); | |||
}); | |||
}); | |||
/** | |||
* 创建折线图,具体文档参考https://g2plot.antv.antgroup.com/api/plots/line | |||
* @param data 数据 | |||
*/ | |||
function createLineChat(data: VisLog.LineChart[]) { | |||
const line = new Line("lineChat", { | |||
data: processData(data, lineMeta), | |||
padding: "auto", | |||
xField: "date", | |||
yField: yField, | |||
seriesField: seriesField, | |||
color: ["#409EFF", "rgb(188, 189, 190)"], | |||
appendPadding: [0, 8, 0, 0] | |||
}); | |||
line.render(); //渲染 | |||
} | |||
/** | |||
* 处理数据,将数据中的字段名转换为别名 | |||
* @param data 数据 | |||
* @param yFields y轴字段 | |||
* @param meta 别名 | |||
*/ | |||
function processData(data: VisLog.LineChart[], meta: LineMeta) { | |||
const result: any = []; | |||
//将数据中的字段名转换为别名 | |||
data.forEach((item: VisLog.LineChart) => { | |||
result.push({ ...item, [seriesField]: meta.successCount?.alias, [yField]: item.successCount }); | |||
result.push({ ...item, [seriesField]: meta.failCount?.alias, [yField]: item.failCount }); | |||
}); | |||
return result; | |||
} | |||
</script> | |||
<style scoped lang="scss"></style> |
@@ -0,0 +1,47 @@ | |||
<!-- | |||
* @Description: 饼状图 | |||
* @Author: Syy | |||
* @Date: 2024-11-20 15:44:33 | |||
!--> | |||
<template> | |||
<div id="pieChat" class="h-150px"></div> | |||
</template> | |||
<script setup lang="ts" name="pieChart"> | |||
import { VisLog, smsLogApi } from "@/api"; | |||
import { Pie } from "@antv/g2plot"; | |||
onMounted(() => { | |||
smsLogApi.pieChart().then(res => { | |||
createPieChat(res.data); | |||
}); | |||
}); | |||
/** | |||
* 创建折线图,具体文档参考https://g2plot.antv.antgroup.com/api/plots/pie | |||
* @param data 数据 | |||
*/ | |||
function createPieChat(data: VisLog.PineChart[]) { | |||
const piePlot = new Pie("pieChat", { | |||
appendPadding: 10, | |||
data, | |||
angleField: "value", | |||
colorField: "type", | |||
radius: 0.9, | |||
color: ["#409EFF", "rgb(188, 189, 190)"], | |||
label: { | |||
type: "inner", | |||
offset: "-30%", | |||
content: ({ percent }) => `${(percent * 100).toFixed(0)}%`, | |||
style: { | |||
fontSize: 14, | |||
textAlign: "center" | |||
} | |||
}, | |||
interactions: [{ type: "element-active" }] | |||
}); | |||
piePlot.render(); //渲染 | |||
} | |||
</script> | |||
<style scoped lang="scss"></style> |
@@ -0,0 +1,124 @@ | |||
<!-- | |||
* @Description: 短信日志 | |||
* @Author: Syy | |||
* @Date: 2024-11-20 15:44:33 | |||
!--> | |||
<template> | |||
<div> | |||
<el-row :gutter="10" class="mb-2"> | |||
<el-col :span="16"> | |||
<el-card header="周统计" shadow="never"> | |||
<line-chat /> | |||
</el-card> | |||
</el-col> | |||
<el-col :span="8"> | |||
<el-card header="总比例" shadow="never"> | |||
<pie-chat /> | |||
</el-card> | |||
</el-col> | |||
</el-row> | |||
<el-row> | |||
<div class="table-box min-h-400px"> | |||
<ProTable ref="proTable" title="系统字典" class="table" :tool-button="false" :columns="columns" :request-api="smsLogApi.page"> | |||
<!-- 表格 header 按钮 --> | |||
<template #tableHeader> | |||
<el-space> | |||
<el-radio-group v-model="initParam.category" class="mb-15px"> | |||
<el-radio-button v-for="(item, index) in visLogTypeOptions" :key="index" :label="item.value">{{ item.label }}</el-radio-button> | |||
</el-radio-group> | |||
<el-input v-model="initParam.searchKey" placeholder="请输入关键字" class="mb-15px"> | |||
<template #append> | |||
<el-button :icon="Search" class="el-input-button" /> | |||
</template> | |||
</el-input> | |||
<s-button type="danger" plain :opt="FormOptEnum.DELETE" @click="onDelete('清空当前日志')">清空</s-button> | |||
</el-space> | |||
</template> | |||
<!-- 操作 --> | |||
<template #operation="scope"> | |||
<s-button link :opt="FormOptEnum.VIEW" @click="onOpen(scope.row)"> 详情 </s-button> | |||
</template> | |||
</ProTable> | |||
<!-- 日志详情 --> | |||
<Detail ref="detailRef" /> | |||
</div> | |||
</el-row> | |||
</div> | |||
</template> | |||
<script setup lang="tsx" name="sysVisLog"> | |||
import LineChat from "./components/lineChat.vue"; | |||
import PieChat from "./components/pieChat.vue"; | |||
import Detail from "./components/detail.vue"; | |||
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface"; | |||
import { Search } from "@element-plus/icons-vue"; | |||
import { VisLog, smsLogApi } from "@/api"; | |||
import { FormOptEnum } from "@/enums"; | |||
import { useHandleData } from "@/hooks/useHandleData"; | |||
// 左侧表格初始化条件 | |||
interface InitParam { | |||
category?: string; //字典分类 | |||
searchKey?: string; //关键字 | |||
} | |||
// 访问日志类型 | |||
let visLogTypeOptions = [ | |||
{ | |||
label: "短信日志", | |||
value: "SUCCESS" | |||
} | |||
]; | |||
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据) | |||
const initParam = reactive<InitParam>({ category: visLogTypeOptions[0].value }); //主表格初始化参数 | |||
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数) | |||
const proTable = ref<ProTableInstance>(); | |||
// 表格配置项 | |||
const columns: ColumnProps<VisLog.VisLogInfo>[] = [ | |||
{ prop: "name", label: "名称" }, | |||
{ | |||
prop: "exeStatus", | |||
label: "发送状态", | |||
render: scope => { | |||
if (scope.row.exeStatus === "FAIL") { | |||
return (<el-tag type="danger">失败</el-tag>); | |||
} | |||
if (scope.row.exeStatus === "SUCCESS") { | |||
return (<el-tag type="success">成功</el-tag>); | |||
} | |||
} | |||
}, | |||
{ prop: "opUser", label: "通知用户" }, | |||
{ prop: "opAccount", label: "发送人" }, | |||
{ prop: "opAddress", label: "短信内容" }, | |||
{ prop: "opTime", label: "发送时间" }, | |||
{ prop: "operation", label: "操作", fixed: "right", width: 100 } | |||
]; | |||
const detailRef = ref<InstanceType<typeof Detail> | null>(null); //详情引用 | |||
/** | |||
* 打开表单 | |||
* @param record 记录 | |||
*/ | |||
function onOpen(record: VisLog.VisLogInfo) { | |||
detailRef.value?.onOpen(record); | |||
} | |||
/** | |||
* 清空日志 | |||
*/ | |||
async function onDelete(msg: string) { | |||
// 二次确认 => 请求api => 刷新表格 | |||
await useHandleData(smsLogApi.delete, initParam.category, msg); | |||
proTable.value?.refresh(); //刷新主表格 | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
:deep(.el-card__header) { | |||
padding: 9px; | |||
} | |||
</style> |
@@ -34,7 +34,7 @@ | |||
<el-radio-group v-model="initParam.category" class="mb-15px"> | |||
<el-radio-button v-for="(item, index) in visLogTypeOptions" :key="index" :label="item.value">{{ item.label }}</el-radio-button> | |||
</el-radio-group> | |||
<el-input v-model="initParam.searchKey" placeholder="请输入字典名称" class="mb-15px"> | |||
<el-input v-model="initParam.searchKey" placeholder="请输入关键字" class="mb-15px"> | |||
<template #append> | |||
<el-button :icon="Search" class="el-input-button" /> | |||
</template> | |||