fix: 优化病毒扫描报告界面 (#5675)

pull/5678/head
ssongliu 5 months ago committed by GitHub
parent a4accc071d
commit f370f7eacf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -163,6 +163,29 @@ func (b *BaseApi) SearchClamRecord(c *gin.Context) {
})
}
// @Tags Clam
// @Summary Load clam record detail
// @Description 获取扫描结果详情
// @Accept json
// @Param request body dto.ClamLogReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/clam/record/log [post]
func (b *BaseApi) LoadClamRecordLog(c *gin.Context) {
var req dto.ClamLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
content, err := clamService.LoadRecordLog(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, content)
}
// @Tags Clam
// @Summary Load clam file
// @Description 获取扫描文件

@ -30,12 +30,18 @@ type ClamLogSearch struct {
EndTime time.Time `json:"endTime"`
}
type ClamLogReq struct {
Tail string `json:"tail"`
ClamName string `json:"clamName"`
RecordName string `json:"recordName"`
}
type ClamLog struct {
Name string `json:"name"`
ScanDate string `json:"scanDate"`
ScanTime string `json:"scanTime"`
InfectedFiles string `json:"infectedFiles"`
Log string `json:"log"`
TotalError string `json:"totalError"`
Status string `json:"status"`
}
@ -58,7 +64,7 @@ type ClamUpdate struct {
}
type ClamDelete struct {
RemoveResult bool `json:"removeResult"`
RemoveRecord bool `json:"removeRecord"`
RemoveInfected bool `json:"removeInfected"`
Ids []uint `json:"ids" validate:"required"`
}

@ -45,6 +45,8 @@ type IClamService interface {
UpdateFile(req dto.UpdateByNameAndFile) error
LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error)
CleanRecord(req dto.OperateByID) error
LoadRecordLog(req dto.ClamLogReq) (string, error)
}
func NewIClamService() IClamService {
@ -160,7 +162,7 @@ func (u *ClamService) Delete(req dto.ClamDelete) error {
if clam.ID == 0 {
continue
}
if req.RemoveResult {
if req.RemoveRecord {
_ = os.RemoveAll(path.Join(global.CONF.System.DataDir, resultDir, clam.Name))
}
if req.RemoveInfected {
@ -255,6 +257,21 @@ func (u *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, er
}
return int64(total), datas, nil
}
func (u *ClamService) LoadRecordLog(req dto.ClamLogReq) (string, error) {
logPath := path.Join(global.CONF.System.DataDir, resultDir, req.ClamName, req.RecordName)
var tail string
if req.Tail != "0" {
tail = req.Tail
} else {
tail = "+1"
}
cmd := exec.Command("tail", "-n", tail, logPath)
stdout, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err)
}
return string(stdout), nil
}
func (u *ClamService) CleanRecord(req dto.OperateByID) error {
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID))
@ -364,7 +381,6 @@ func loadResultFromLog(pathItem string) dto.ClamLog {
if err != nil {
return data
}
data.Log = string(file)
lines := strings.Split(string(file), "\n")
for _, line := range lines {
if strings.Contains(line, "- SCAN SUMMARY -") {
@ -376,6 +392,8 @@ func loadResultFromLog(pathItem string) dto.ClamLog {
switch {
case strings.HasPrefix(line, "Infected files:"):
data.InfectedFiles = strings.TrimPrefix(line, "Infected files:")
case strings.HasPrefix(line, "Total errors:"):
data.TotalError = strings.TrimPrefix(line, "Total errors:")
case strings.HasPrefix(line, "Time:"):
if strings.Contains(line, "(") {
data.ScanTime = strings.ReplaceAll(strings.Split(line, "(")[1], ")", "")

@ -49,6 +49,7 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/clam/search", baseApi.SearchClam)
toolboxRouter.POST("/clam/record/search", baseApi.SearchClamRecord)
toolboxRouter.POST("/clam/record/clean", baseApi.CleanClamRecord)
toolboxRouter.POST("/clam/record/log", baseApi.LoadClamRecordLog)
toolboxRouter.POST("/clam/file/search", baseApi.SearchClamFile)
toolboxRouter.POST("/clam/file/update", baseApi.UpdateFile)
toolboxRouter.POST("/clam", baseApi.CreateClam)

@ -11395,6 +11395,39 @@ const docTemplate = `{
}
}
},
"/toolbox/clam/record/log": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描结果详情",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Load clam record detail",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamLogReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/toolbox/clam/record/search": {
"post": {
"security": [
@ -15495,11 +15528,25 @@ const docTemplate = `{
"removeInfected": {
"type": "boolean"
},
"removeResult": {
"removeRecord": {
"type": "boolean"
}
}
},
"dto.ClamLogReq": {
"type": "object",
"properties": {
"clamName": {
"type": "string"
},
"recordName": {
"type": "string"
},
"tail": {
"type": "string"
}
}
},
"dto.ClamLogSearch": {
"type": "object",
"required": [

@ -11388,6 +11388,39 @@
}
}
},
"/toolbox/clam/record/log": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取扫描结果详情",
"consumes": [
"application/json"
],
"tags": [
"Clam"
],
"summary": "Load clam record detail",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ClamLogReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/toolbox/clam/record/search": {
"post": {
"security": [
@ -15488,11 +15521,25 @@
"removeInfected": {
"type": "boolean"
},
"removeResult": {
"removeRecord": {
"type": "boolean"
}
}
},
"dto.ClamLogReq": {
"type": "object",
"properties": {
"clamName": {
"type": "string"
},
"recordName": {
"type": "string"
},
"tail": {
"type": "string"
}
}
},
"dto.ClamLogSearch": {
"type": "object",
"required": [

@ -246,11 +246,20 @@ definitions:
type: array
removeInfected:
type: boolean
removeResult:
removeRecord:
type: boolean
required:
- ids
type: object
dto.ClamLogReq:
properties:
clamName:
type: string
recordName:
type: string
tail:
type: string
type: object
dto.ClamLogSearch:
properties:
clamID:
@ -12669,6 +12678,26 @@ paths:
formatEN: clean clam record [name]
formatZH: 清空扫描报告 [name]
paramKeys: []
/toolbox/clam/record/log:
post:
consumes:
- application/json
description: 获取扫描结果详情
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ClamLogReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Load clam record detail
tags:
- Clam
/toolbox/clam/record/search:
post:
consumes:

@ -151,13 +151,17 @@ export namespace Toolbox {
startTime: Date;
endTime: Date;
}
export interface ClamRecordReq {
tail: string;
clamName: string;
recordName: string;
}
export interface ClamLog {
name: string;
scanDate: string;
scanTime: string;
scannedFiles: string;
totalError: string;
infectedFiles: string;
log: string;
status: string;
}
}

@ -114,6 +114,9 @@ export const cleanClamRecord = (id: number) => {
export const searchClamRecord = (param: Toolbox.ClamSearchLog) => {
return http.post<ResPage<Toolbox.ClamLog>>(`/toolbox/clam/record/search`, param);
};
export const getClamRecordLog = (param: Toolbox.ClamRecordReq) => {
return http.post<string>(`/toolbox/clam/record/log`, param);
};
export const searchClamFile = (name: string) => {
return http.post<string>(`/toolbox/clam/file/search`, { name: name });
};
@ -135,7 +138,7 @@ export const createClam = (params: Toolbox.ClamCreate) => {
export const updateClam = (params: Toolbox.ClamUpdate) => {
return http.post(`/toolbox/clam/update`, params);
};
export const deleteClam = (params: { ids: number[]; removeResult: boolean; removeInfected: boolean }) => {
export const deleteClam = (params: { ids: number[]; removeRecord: boolean; removeInfected: boolean }) => {
return http.post(`/toolbox/clam/del`, params);
};
export const handleClamScan = (id: number) => {

@ -1081,7 +1081,7 @@ const message = {
removeInfectedHelper:
'Delete virus files detected during the task to ensure server security and normal operation.',
clamCreate: 'Create Scan Rules',
infectedStrategy: 'Virus Strategy',
infectedStrategy: 'Infected Strategy',
remove: 'Delete',
removeHelper: 'Delete virus files, choose carefully!',
move: 'Move',
@ -1093,8 +1093,8 @@ const message = {
scanDir: 'Scan Directory',
infectedDir: 'Infected Directory',
scanDate: 'Scan Date',
scanResult: 'Scan log tail',
scanTime: 'Time Taken',
scannedFiles: 'Scanned Files',
infectedFiles: 'Infected Files',
log: 'Details',
clamConf: 'Scan Configuration',

@ -1022,7 +1022,7 @@ const message = {
removeInfected: '',
removeInfectedHelper: '',
clamCreate: '',
infectedStrategy: '',
infectedStrategy: '',
remove: '',
removeHelper: '',
move: '',
@ -1032,11 +1032,11 @@ const message = {
none: '',
noneHelper: '',
scanDir: '',
infectedDir: '',
infectedDir: '',
scanDate: '',
scanResult: '',
scanTime: '',
scannedFiles: '',
infectedFiles: '',
infectedFiles: '',
log: '',
clamConf: '',
clamLog: '',

@ -1023,7 +1023,7 @@ const message = {
removeInfected: '',
removeInfectedHelper: '',
clamCreate: '',
infectedStrategy: '',
infectedStrategy: '',
remove: '',
removeHelper: '',
move: '',
@ -1033,11 +1033,11 @@ const message = {
none: '',
noneHelper: '',
scanDir: '',
infectedDir: '',
infectedDir: '',
scanDate: '',
scanResult: '',
scanTime: '',
scannedFiles: '',
infectedFiles: '',
infectedFiles: '',
log: '',
clamConf: '',
clamLog: '',

@ -236,7 +236,7 @@
</template>
<script lang="ts" setup>
import { onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
import { nextTick, onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
import { Cronjob } from '@/api/interface/cronjob';
import { searchRecords, handleOnce, updateStatus, cleanRecords, getRecordLog } from '@/api/modules/cronjob';
import { dateFormat } from '@/utils/util';
@ -415,10 +415,12 @@ const loadRecord = async (row: Cronjob.Record) => {
return;
}
currentRecordDetail.value = log;
const state = view.value.state;
view.value.dispatch({
selection: { anchor: state.doc.length, head: state.doc.length },
scrollIntoView: true,
nextTick(() => {
const state = view.value.state;
view.value.dispatch({
selection: { anchor: state.doc.length, head: state.doc.length },
scrollIntoView: true,
});
});
}
};

@ -62,7 +62,13 @@
:min-width="60"
prop="name"
show-overflow-tooltip
/>
>
<template #default="{ row }">
<el-text type="primary" class="cursor-pointer" @click="onOpenRecord(row)">
{{ row.name }}
</el-text>
</template>
</el-table-column>
<el-table-column
:label="$t('toolbox.clam.scanDir')"
:min-width="120"
@ -115,7 +121,7 @@
<template #content>
<el-form class="mt-4 mb-1" ref="deleteForm" label-position="left">
<el-form-item>
<el-checkbox v-model="removeResult" :label="$t('toolbox.clam.removeResult')" />
<el-checkbox v-model="removeRecord" :label="$t('toolbox.clam.removeRecord')" />
<span class="input-help">{{ $t('toolbox.clam.removeResultHelper') }}</span>
</el-form-item>
<el-form-item>
@ -163,7 +169,7 @@ const operateIDs = ref();
const dialogLogRef = ref();
const isRecordShow = ref();
const removeResult = ref();
const removeRecord = ref();
const removeInfected = ref();
const isSettingShow = ref();
@ -223,6 +229,13 @@ const onOpenDialog = async (
};
dialogRef.value!.acceptParams(params);
};
const onOpenRecord = (row: Toolbox.ClamInfo) => {
isRecordShow.value = true;
let params = {
rowData: { ...row },
};
dialogLogRef.value!.acceptParams(params);
};
const onDelete = async (row: Toolbox.ClamInfo | null) => {
let names = [];
@ -251,7 +264,7 @@ const onDelete = async (row: Toolbox.ClamInfo | null) => {
const onSubmitDelete = async () => {
loading.value = true;
await deleteClam({ ids: operateIDs.value, removeResult: removeResult.value, removeInfected: removeInfected.value })
await deleteClam({ ids: operateIDs.value, removeRecord: removeRecord.value, removeInfected: removeInfected.value })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
@ -287,11 +300,7 @@ const buttons = [
{
label: i18n.global.t('cronjob.record'),
click: (row: Toolbox.ClamInfo) => {
isRecordShow.value = true;
let params = {
rowData: { ...row },
};
dialogLogRef.value!.acceptParams(params);
onOpenRecord(row);
},
},
{

@ -25,7 +25,7 @@
effect="dark"
type="success"
>
{{ $t('file.path') }}: {{ dialogData.rowData.path }}
{{ $t('toolbox.clam.scanDir') }}: {{ dialogData.rowData.path }}
</el-tag>
<span class="buttons">
@ -125,22 +125,34 @@
</div>
</el-form-item>
</el-row>
<el-row v-if="currentRecord?.log">
<span>{{ $t('commons.table.records') }}</span>
<el-row>
<el-select
class="descriptionWide"
@change="search"
v-model.number="searchInfo.tail"
>
<template #prefix>{{ $t('toolbox.clam.scanResult') }}</template>
<el-option :value="0" :label="$t('commons.table.all')" />
<el-option :value="10" :label="10" />
<el-option :value="100" :label="100" />
<el-option :value="200" :label="200" />
<el-option :value="500" :label="500" />
<el-option :value="1000" :label="1000" />
</el-select>
<codemirror
ref="mymirror"
:autofocus="true"
:placeholder="$t('cronjob.noLogs')"
:indent-with-tab="true"
:tabSize="4"
style="height: calc(100vh - 488px); width: 100%; margin-top: 5px"
style="height: calc(100vh - 498px); width: 100%; margin-top: 5px"
:lineWrapping="true"
:matchBrackets="true"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
@ready="handleReady"
v-model="currentRecord.log"
v-model="logContent"
:disabled="true"
/>
</el-row>
@ -162,7 +174,7 @@
</template>
<script lang="ts" setup>
import { onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
import { nextTick, onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
import i18n from '@/lang';
import { ElMessageBox } from 'element-plus';
import { Codemirror } from 'vue-codemirror';
@ -171,7 +183,7 @@ import { oneDark } from '@codemirror/theme-one-dark';
import { MsgSuccess } from '@/utils/message';
import { shortcuts } from '@/utils/shortcuts';
import { Toolbox } from '@/api/interface/toolbox';
import { cleanClamRecord, handleClamScan, searchClamRecord } from '@/api/modules/toolbox';
import { cleanClamRecord, getClamRecordLog, handleClamScan, searchClamRecord } from '@/api/modules/toolbox';
import { useRouter } from 'vue-router';
const router = useRouter();
@ -195,6 +207,7 @@ interface DialogProps {
const dialogData = ref();
const records = ref<Array<Toolbox.ClamLog>>([]);
const currentRecord = ref<Toolbox.ClamLog>();
const logContent = ref();
const acceptParams = async (params: DialogProps): Promise<void> => {
let itemSize = Number(localStorage.getItem(searchInfo.cacheSizeKey));
@ -233,8 +246,8 @@ const searchInfo = reactive({
cacheSizeKey: 'clam-record-page-size',
page: 1,
pageSize: 8,
tail: '100',
recordTotal: 0,
cronjobID: 0,
startTime: new Date(),
endTime: new Date(),
});
@ -268,6 +281,7 @@ const search = async () => {
page: searchInfo.page,
pageSize: searchInfo.pageSize,
clamID: dialogData.value.rowData!.id,
tail: searchInfo.tail,
startTime: searchInfo.startTime,
endTime: searchInfo.endTime,
};
@ -281,10 +295,32 @@ const search = async () => {
if (!currentRecord.value) {
currentRecord.value = records.value[0];
}
loadRecordLog();
};
const clickRow = async (row: Toolbox.ClamLog) => {
currentRecord.value = row;
loadRecordLog();
};
const loadRecordLog = async () => {
let param = {
tail: searchInfo.tail + '',
clamName: dialogData.value.rowData?.name,
recordName: currentRecord.value.name,
};
const res = await getClamRecordLog(param);
if (logContent.value === res.data) {
return;
}
logContent.value = res.data;
nextTick(() => {
const state = view.value.state;
view.value.dispatch({
selection: { anchor: state.doc.length, head: state.doc.length },
scrollIntoView: true,
});
});
};
const onClean = async () => {

Loading…
Cancel
Save