@@ -16,12 +16,12 @@ VITE_PWA = false | |||||
# 开发环境接口地址 | # 开发环境接口地址 | ||||
# VITE_API_URL = http://192.168.10.186:5566 | # 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_HTTP_PROXY = true | ||||
# 开发环境跨域代理,支持配置多个 | # 开发环境跨域代理,支持配置多个 | ||||
# VITE_PROXY = [["/api","http://192.168.10.186:5566","/Files"]] | # 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 "./visLog"; | ||||
export * from "./opLog"; | 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-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-button v-for="(item, index) in visLogTypeOptions" :key="index" :label="item.value">{{ item.label }}</el-radio-button> | ||||
</el-radio-group> | </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> | <template #append> | ||||
<el-button :icon="Search" class="el-input-button" /> | <el-button :icon="Search" class="el-input-button" /> | ||||
</template> | </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-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-button v-for="(item, index) in visLogTypeOptions" :key="index" :label="item.value">{{ item.label }}</el-radio-button> | ||||
</el-radio-group> | </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> | <template #append> | ||||
<el-button :icon="Search" class="el-input-button" /> | <el-button :icon="Search" class="el-input-button" /> | ||||
</template> | </template> | ||||