mirror of
https://github.com/tnb-labs/panel.git
synced 2026-10-13 08:59:07 +08:00
feat(log): 操作、数据库、HTTP 日志支持按日期清理
- 选择日期后清理该日期及之前的日志,已归档的删除,正在写入的当前文件只清空 - 清理后在操作日志中记录一条,清空操作日志也能留下痕迹 - 修复日期下拉错位一天:零点轮转的归档时间戳是次日零点,改为按文件内日志的实际日期归类 - 同一天因大小轮转拆成多个文件时合并查看,今天也包含当天已轮转的部分 - 日期下拉默认显示今天,不再显示请选择 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -372,7 +372,7 @@ func initAce() (*app.Ace, func() error, error) {
|
||||
|
||||
wireValue110_0 := data.NewLogRepo(wireValue4_0)
|
||||
|
||||
wireValue71_0 := biz.NewLogUsecase(wireValue110_0)
|
||||
wireValue71_0 := biz.NewLogUsecase(wireValue110_0, wireValue3_0)
|
||||
|
||||
wireValue161_0 := service.NewLogService(wireValue71_0, wireValue1_0)
|
||||
|
||||
|
||||
+18
-2
@@ -1,6 +1,8 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -51,14 +53,17 @@ type LogRepo interface {
|
||||
List(logType string, limit int, date string) ([]LogEntry, error)
|
||||
// ListDates 获取可用的日志日期列表
|
||||
ListDates(logType string) ([]string, error)
|
||||
// Clean 清理指定日期及之前的日志
|
||||
Clean(logType string, date string) error
|
||||
}
|
||||
|
||||
type LogUsecase struct {
|
||||
repo LogRepo
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewLogUsecase(repo LogRepo) *LogUsecase {
|
||||
return &LogUsecase{repo: repo}
|
||||
func NewLogUsecase(repo LogRepo, log *slog.Logger) *LogUsecase {
|
||||
return &LogUsecase{repo: repo, log: log}
|
||||
}
|
||||
|
||||
func (uc *LogUsecase) List(logType string, limit int, date string) ([]LogEntry, error) {
|
||||
@@ -68,3 +73,14 @@ func (uc *LogUsecase) List(logType string, limit int, date string) ([]LogEntry,
|
||||
func (uc *LogUsecase) ListDates(logType string) ([]string, error) {
|
||||
return uc.repo.ListDates(logType)
|
||||
}
|
||||
|
||||
func (uc *LogUsecase) Clean(ctx context.Context, logType, date string) error {
|
||||
if err := uc.repo.Clean(logType, date); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志,操作日志被清空后留下这一条作为痕迹
|
||||
uc.log.Info("logs cleaned", slog.String("type", OperationTypePanel), slog.Uint64("operator_id", operatorID(ctx)), slog.String("log_type", logType), slog.String("date", date))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+113
-101
@@ -17,11 +17,15 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
)
|
||||
|
||||
var (
|
||||
logArchivePatternApp = regexp.MustCompile(`^app-(\d{4}-\d{2}-\d{2})T.*\.log$`)
|
||||
logArchivePatternDB = regexp.MustCompile(`^db-(\d{4}-\d{2}-\d{2})T.*\.log$`)
|
||||
logArchivePatternHTTP = regexp.MustCompile(`^http-(\d{4}-\d{2}-\d{2})T.*\.log$`)
|
||||
)
|
||||
// logArchivePattern 轮转归档的文件名,时间戳是该文件内日志的结束时刻
|
||||
var logArchivePattern = regexp.MustCompile(`^(app|db|http)-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3})(?:\.\d+)?\.log$`)
|
||||
|
||||
// logFile 日志文件及其内日志所属的日期
|
||||
type logFile struct {
|
||||
path string
|
||||
date string
|
||||
current bool
|
||||
}
|
||||
|
||||
type logRepo struct {
|
||||
db *gorm.DB
|
||||
@@ -36,75 +40,27 @@ func NewLogRepo(db *gorm.DB) biz.LogRepo {
|
||||
// List 获取日志列表
|
||||
// date 格式为 YYYY-MM-DD,空字符串表示当天日志
|
||||
func (r *logRepo) List(logType string, limit int, date string) ([]biz.LogEntry, error) {
|
||||
logDir := filepath.Join(app.Root, "panel/storage/logs")
|
||||
|
||||
var logPath string
|
||||
if date == "" {
|
||||
// 无日期参数,读取当前日志文件
|
||||
logPath = filepath.Join(logDir, logType+".log")
|
||||
} else {
|
||||
// 有日期参数,查找对应的归档日志文件
|
||||
pattern := getLogArchivePattern(logType)
|
||||
date = time.Now().Format(time.DateOnly)
|
||||
}
|
||||
files, err := r.files(logType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(logDir)
|
||||
// 一天可能因为大小轮转分成多个文件,从最新的往前读,凑够 limit 行为止
|
||||
var lines []string
|
||||
for i := len(files) - 1; i >= 0 && len(lines) < limit; i-- {
|
||||
if files[i].date != date {
|
||||
continue
|
||||
}
|
||||
fileLines, err := readLines(files[i].path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []biz.LogEntry{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
matches := pattern.FindStringSubmatch(entry.Name())
|
||||
if len(matches) == 2 && matches[1] == date {
|
||||
logPath = filepath.Join(logDir, entry.Name())
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return []biz.LogEntry{}, nil
|
||||
}
|
||||
lines = append(fileLines, lines...)
|
||||
}
|
||||
|
||||
file, err := os.Open(logPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []biz.LogEntry{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
// 读取所有行
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
// 增加缓冲区大小以处理较长的日志行
|
||||
buf := make([]byte, 0, 64*1024)
|
||||
scanner.Buffer(buf, 1024*1024)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.TrimSpace(line) != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 从末尾取指定数量的行
|
||||
start := 0
|
||||
if len(lines) > limit {
|
||||
start = len(lines) - limit
|
||||
}
|
||||
lines = lines[start:]
|
||||
lines = lines[max(0, len(lines)-limit):]
|
||||
|
||||
// 倒序处理,最新的在前面
|
||||
entries := make([]biz.LogEntry, 0, len(lines))
|
||||
@@ -124,30 +80,17 @@ func (r *logRepo) List(logType string, limit int, date string) ([]biz.LogEntry,
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// ListDates 获取可用的日志日期列表
|
||||
// ListDates 获取可用的日志日期列表,当天由 List 的空日期覆盖,不在其中
|
||||
func (r *logRepo) ListDates(logType string) ([]string, error) {
|
||||
logDir := filepath.Join(app.Root, "panel/storage/logs")
|
||||
|
||||
entries, err := os.ReadDir(logDir)
|
||||
files, err := r.files(logType)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pattern := getLogArchivePattern(logType)
|
||||
|
||||
dates := make([]string, 0)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
matches := pattern.FindStringSubmatch(entry.Name())
|
||||
if len(matches) == 2 {
|
||||
dates = append(dates, matches[1])
|
||||
}
|
||||
}
|
||||
today := time.Now().Format(time.DateOnly)
|
||||
dates := lo.Uniq(lo.FilterMap(files, func(file logFile, _ int) (string, bool) {
|
||||
return file.date, file.date != today
|
||||
}))
|
||||
|
||||
// 按日期倒序排列,最新的在前面
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(dates)))
|
||||
@@ -155,6 +98,89 @@ func (r *logRepo) ListDates(logType string) ([]string, error) {
|
||||
return dates, nil
|
||||
}
|
||||
|
||||
// Clean 清理指定日期及之前的日志,当前文件还在写入,只能清空
|
||||
func (r *logRepo) Clean(logType string, date string) error {
|
||||
files, err := r.files(logType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.date > date {
|
||||
continue
|
||||
}
|
||||
if file.current {
|
||||
err = os.Truncate(file.path, 0)
|
||||
} else {
|
||||
err = os.Remove(file.path)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// files 按时间先后列出某类日志的归档与当前文件
|
||||
func (r *logRepo) files(logType string) ([]logFile, error) {
|
||||
dir := filepath.Join(app.Root, "panel/storage/logs")
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files := make([]logFile, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
matches := logArchivePattern.FindStringSubmatch(entry.Name())
|
||||
if matches == nil || matches[1] != logType {
|
||||
continue
|
||||
}
|
||||
end, err := time.ParseInLocation("2006-01-02T15-04-05.000", matches[2], time.Local)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 零点轮转的归档时间戳是次日零点,往前退一点才是日志所属的日期
|
||||
files = append(files, logFile{
|
||||
path: filepath.Join(dir, entry.Name()),
|
||||
date: end.Add(-time.Nanosecond).Format(time.DateOnly),
|
||||
})
|
||||
}
|
||||
|
||||
// 当前文件名里没有时间戳,按最后写入时间算
|
||||
current := filepath.Join(dir, logType+".log")
|
||||
if info, err := os.Stat(current); err == nil {
|
||||
files = append(files, logFile{path: current, date: info.ModTime().Format(time.DateOnly), current: true})
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// readLines 读取文件中的非空行
|
||||
func readLines(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
// 列出后可能刚被轮转清理掉
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
// 增加缓冲区大小以处理较长的日志行
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
if line := scanner.Text(); strings.TrimSpace(line) != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
|
||||
return lines, scanner.Err()
|
||||
}
|
||||
|
||||
// fillOperatorNames 填充操作员用户名
|
||||
func (r *logRepo) fillOperatorNames(entries []biz.LogEntry) {
|
||||
// 收集所有用户ID
|
||||
@@ -236,17 +262,3 @@ func (r *logRepo) parseLine(line string, logType string) (biz.LogEntry, error) {
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// getLogArchivePattern 获取归档日志文件名匹配正则表达式
|
||||
func getLogArchivePattern(logType string) *regexp.Regexp {
|
||||
switch logType {
|
||||
case biz.LogTypeApp:
|
||||
return logArchivePatternApp
|
||||
case biz.LogTypeDB:
|
||||
return logArchivePatternDB
|
||||
case biz.LogTypeHTTP:
|
||||
return logArchivePatternHTTP
|
||||
default:
|
||||
return logArchivePatternApp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ var _ biz.LogRepo = &LogRepo{}
|
||||
//
|
||||
// // make and configure a mocked biz.LogRepo
|
||||
// mockedLogRepo := &LogRepo{
|
||||
// CleanFunc: func(logType string, date string) error {
|
||||
// panic("mock out the Clean method")
|
||||
// },
|
||||
// ListFunc: func(logType string, limit int, date string) ([]biz.LogEntry, error) {
|
||||
// panic("mock out the List method")
|
||||
// },
|
||||
@@ -33,6 +36,9 @@ var _ biz.LogRepo = &LogRepo{}
|
||||
//
|
||||
// }
|
||||
type LogRepo struct {
|
||||
// CleanFunc mocks the Clean method.
|
||||
CleanFunc func(logType string, date string) error
|
||||
|
||||
// ListFunc mocks the List method.
|
||||
ListFunc func(logType string, limit int, date string) ([]biz.LogEntry, error)
|
||||
|
||||
@@ -41,6 +47,13 @@ type LogRepo struct {
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// Clean holds details about calls to the Clean method.
|
||||
Clean []struct {
|
||||
// LogType is the logType argument value.
|
||||
LogType string
|
||||
// Date is the date argument value.
|
||||
Date string
|
||||
}
|
||||
// List holds details about calls to the List method.
|
||||
List []struct {
|
||||
// LogType is the logType argument value.
|
||||
@@ -56,10 +69,47 @@ type LogRepo struct {
|
||||
LogType string
|
||||
}
|
||||
}
|
||||
lockClean sync.RWMutex
|
||||
lockList sync.RWMutex
|
||||
lockListDates sync.RWMutex
|
||||
}
|
||||
|
||||
// Clean calls CleanFunc.
|
||||
func (mock *LogRepo) Clean(logType string, date string) error {
|
||||
if mock.CleanFunc == nil {
|
||||
panic("LogRepo.CleanFunc: method is nil but LogRepo.Clean was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
LogType string
|
||||
Date string
|
||||
}{
|
||||
LogType: logType,
|
||||
Date: date,
|
||||
}
|
||||
mock.lockClean.Lock()
|
||||
mock.calls.Clean = append(mock.calls.Clean, callInfo)
|
||||
mock.lockClean.Unlock()
|
||||
return mock.CleanFunc(logType, date)
|
||||
}
|
||||
|
||||
// CleanCalls gets all the calls that were made to Clean.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedLogRepo.CleanCalls())
|
||||
func (mock *LogRepo) CleanCalls() []struct {
|
||||
LogType string
|
||||
Date string
|
||||
} {
|
||||
var calls []struct {
|
||||
LogType string
|
||||
Date string
|
||||
}
|
||||
mock.lockClean.RLock()
|
||||
calls = mock.calls.Clean
|
||||
mock.lockClean.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// List calls ListFunc.
|
||||
func (mock *LogRepo) List(logType string, limit int, date string) ([]biz.LogEntry, error) {
|
||||
if mock.ListFunc == nil {
|
||||
|
||||
@@ -11,3 +11,9 @@ type LogList struct {
|
||||
type LogDates struct {
|
||||
Type string `json:"type" form:"type" query:"type" validate:"required && in:app,db,http"`
|
||||
}
|
||||
|
||||
// LogClean 日志清理请求
|
||||
type LogClean struct {
|
||||
Type string `json:"type" form:"type" validate:"required && in:app,db,http"`
|
||||
Date string `json:"date" form:"date" validate:"required && datetime:2006-01-02"` // 清理该日期及之前的日志
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ func LogRoutes(logService *service.LogService) Endpoints {
|
||||
return Endpoints{
|
||||
{Method: http.MethodGet, Path: "/api/log/list", Handler: svc.List, Summary: "日志列表", Tags: []string{"日志"}, Document: DescribeReq[request.LogList]()},
|
||||
{Method: http.MethodGet, Path: "/api/log/dates", Handler: svc.Dates, Summary: "日志日期列表", Tags: []string{"日志"}, Document: DescribeReq[request.LogDates]()},
|
||||
{Method: http.MethodPost, Path: "/api/log/clean", Handler: svc.Clean, Summary: "清理日志", Tags: []string{"日志"}, Document: DescribeReq[request.LogClean]()},
|
||||
{Method: http.MethodGet, Path: "/api/log/ssh", Handler: svc.SSH, Summary: "SSH 登录日志", Tags: []string{"日志"}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,22 @@ func (s *LogService) Dates(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, dates)
|
||||
}
|
||||
|
||||
// Clean 清理指定日期及之前的日志
|
||||
func (s *LogService) Clean(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.LogClean](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.logRepo.Clean(r.Context(), req.Type, req.Date); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// SSH 获取 SSH 登录日志
|
||||
func (s *LogService) SSH(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 100
|
||||
|
||||
@@ -6,6 +6,9 @@ export default {
|
||||
http.Get('/log/list', { params: { type, limit, date } }),
|
||||
// 获取日志日期列表
|
||||
dates: (type: 'app' | 'db' | 'http'): any => http.Get('/log/dates', { params: { type } }),
|
||||
// 清理指定日期及之前的日志
|
||||
clean: (type: 'app' | 'db' | 'http', date: string): any =>
|
||||
http.Post('/log/clean', { type, date }),
|
||||
// 获取 SSH 登录日志
|
||||
ssh: (limit: number = 100): any => http.Get('/log/ssh', { params: { limit } }),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import log from '@/api/panel/log'
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'app' | 'db' | 'http'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'cleaned'): void
|
||||
}>()
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const show = ref(false)
|
||||
const date = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const handleClean = () => {
|
||||
loading.value = true
|
||||
useRequest(log.clean(props.type, date.value!))
|
||||
.onSuccess(() => {
|
||||
show.value = false
|
||||
date.value = null
|
||||
window.$message.success($gettext('Cleaned successfully'))
|
||||
emit('cleaned')
|
||||
})
|
||||
.onComplete(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-popover v-model:show="show" trigger="click" placement="bottom-start">
|
||||
<template #trigger>
|
||||
<n-button type="error" ghost>
|
||||
{{ $gettext('Clean') }}
|
||||
</n-button>
|
||||
</template>
|
||||
<n-flex vertical>
|
||||
<n-date-picker
|
||||
v-model:formatted-value="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
type="date"
|
||||
panel
|
||||
:actions="null"
|
||||
:is-date-disabled="(ts: number) => ts > Date.now()"
|
||||
/>
|
||||
<n-text depth="3">
|
||||
{{ $gettext('Delete logs on and before the selected date') }}
|
||||
</n-text>
|
||||
<n-button type="error" block :disabled="!date" :loading="loading" @click="handleClean">
|
||||
{{ $gettext('Clean') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-popover>
|
||||
</template>
|
||||
@@ -8,6 +8,8 @@ import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import log from '@/api/panel/log'
|
||||
|
||||
import CleanPopover from './CleanPopover.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
// 日志条目类型定义
|
||||
@@ -20,10 +22,12 @@ interface LogEntry {
|
||||
|
||||
// 数据加载
|
||||
const limit = ref(200)
|
||||
const selectedDate = ref<string | null>(null)
|
||||
const selectedDate = ref('')
|
||||
|
||||
// 获取可用的日志日期列表
|
||||
const { data: dates } = useRequest(() => log.dates('db'), { initialData: [] })
|
||||
const { data: dates, send: refreshDates } = useRequest(() => log.dates('db'), {
|
||||
initialData: [],
|
||||
})
|
||||
|
||||
// 日期选项
|
||||
const dateOptions = computed(() => {
|
||||
@@ -40,7 +44,7 @@ const {
|
||||
loading,
|
||||
data,
|
||||
send: refresh,
|
||||
} = useRequest(() => log.list('db', limit.value, selectedDate.value || ''), { initialData: [] })
|
||||
} = useRequest(() => log.list('db', limit.value, selectedDate.value), { initialData: [] })
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
@@ -104,6 +108,13 @@ const columns = [
|
||||
const handleRefresh = () => {
|
||||
refresh()
|
||||
}
|
||||
|
||||
// 清理后选中的日期可能已被删掉,回到今天
|
||||
const handleCleaned = () => {
|
||||
selectedDate.value = ''
|
||||
refreshDates()
|
||||
refresh()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -131,6 +142,7 @@ const handleRefresh = () => {
|
||||
<n-button type="primary" @click="handleRefresh">
|
||||
{{ $gettext('Refresh') }}
|
||||
</n-button>
|
||||
<clean-popover type="db" @cleaned="handleCleaned" />
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
class="flex-1 min-h-0"
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import log from '@/api/panel/log'
|
||||
|
||||
import CleanPopover from './CleanPopover.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
// 日志条目类型定义
|
||||
@@ -20,10 +22,12 @@ interface LogEntry {
|
||||
|
||||
// 数据加载
|
||||
const limit = ref(200)
|
||||
const selectedDate = ref<string | null>(null)
|
||||
const selectedDate = ref('')
|
||||
|
||||
// 获取可用的日志日期列表
|
||||
const { data: dates } = useRequest(() => log.dates('http'), { initialData: [] })
|
||||
const { data: dates, send: refreshDates } = useRequest(() => log.dates('http'), {
|
||||
initialData: [],
|
||||
})
|
||||
|
||||
// 日期选项
|
||||
const dateOptions = computed(() => {
|
||||
@@ -40,7 +44,7 @@ const {
|
||||
loading,
|
||||
data,
|
||||
send: refresh,
|
||||
} = useRequest(() => log.list('http', limit.value, selectedDate.value || ''), { initialData: [] })
|
||||
} = useRequest(() => log.list('http', limit.value, selectedDate.value), { initialData: [] })
|
||||
|
||||
// 获取状态码颜色
|
||||
const getStatusType = (code: number): 'success' | 'warning' | 'error' | 'info' => {
|
||||
@@ -133,6 +137,13 @@ const columns = [
|
||||
const handleRefresh = () => {
|
||||
refresh()
|
||||
}
|
||||
|
||||
// 清理后选中的日期可能已被删掉,回到今天
|
||||
const handleCleaned = () => {
|
||||
selectedDate.value = ''
|
||||
refreshDates()
|
||||
refresh()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -160,6 +171,7 @@ const handleRefresh = () => {
|
||||
<n-button type="primary" @click="handleRefresh">
|
||||
{{ $gettext('Refresh') }}
|
||||
</n-button>
|
||||
<clean-popover type="http" @cleaned="handleCleaned" />
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
class="flex-1 min-h-0"
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import log from '@/api/panel/log'
|
||||
|
||||
import CleanPopover from './CleanPopover.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
// 日志条目类型定义
|
||||
@@ -23,10 +25,12 @@ interface LogEntry {
|
||||
|
||||
// 数据加载
|
||||
const limit = ref(200)
|
||||
const selectedDate = ref<string | null>(null)
|
||||
const selectedDate = ref('')
|
||||
|
||||
// 获取可用的日志日期列表
|
||||
const { data: dates } = useRequest(() => log.dates('app'), { initialData: [] })
|
||||
const { data: dates, send: refreshDates } = useRequest(() => log.dates('app'), {
|
||||
initialData: [],
|
||||
})
|
||||
|
||||
// 日期选项
|
||||
const dateOptions = computed(() => {
|
||||
@@ -43,7 +47,7 @@ const {
|
||||
loading,
|
||||
data,
|
||||
send: refresh,
|
||||
} = useRequest(() => log.list('app', limit.value, selectedDate.value || ''), { initialData: [] })
|
||||
} = useRequest(() => log.list('app', limit.value, selectedDate.value), { initialData: [] })
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
@@ -156,6 +160,13 @@ const columns = [
|
||||
const handleRefresh = () => {
|
||||
refresh()
|
||||
}
|
||||
|
||||
// 清理后选中的日期可能已被删掉,回到今天
|
||||
const handleCleaned = () => {
|
||||
selectedDate.value = ''
|
||||
refreshDates()
|
||||
refresh()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -183,6 +194,7 @@ const handleRefresh = () => {
|
||||
<n-button type="primary" @click="handleRefresh">
|
||||
{{ $gettext('Refresh') }}
|
||||
</n-button>
|
||||
<clean-popover type="app" @cleaned="handleCleaned" />
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
class="flex-1 min-h-0"
|
||||
|
||||
Reference in New Issue
Block a user