index.vue 7.88 KB
<script setup lang="ts">
import { OperateLogApi, type OperateLogPageReqVO, type OperateLogVO } from '@/api/system/log'
import { formatDateTime } from '@/utils'

/**
 * 系统管理 - 操作日志
 *
 * 接口:/admin-api/system/operate-log(权限点 system:operate-log:query / export)
 *
 * 操作日志由后端 `OperateLogAspect` 在请求切面里自动落库,前端**只读**:
 * 没有新增 / 编辑 / 删除入口(也就不需要 system:operate-log:create 之类的权限点)。
 *
 * 两个约定:
 *  - `type` 存的是**中文**(查询 / 新增 / 修改 / 删除)。这里用 DictTag 渲染,
 *    字典 `system_operate_type` 的 label 与 value 都是中文,等价于原样显示;
 *    留成字典是为了将来加英文或换配色时不用改库里的数据。
 *  - 日期范围必须 `value-format="YYYY-MM-DD"`:后端用
 *    `@DateTimeFormat(pattern="yyyy-MM-dd") List<LocalDate>` 接收,
 *    发 ISO 串(带 T00:00:00.000Z)会绑定失败。
 */
defineOptions({ name: 'SystemLog' })

const loading = ref(false)
const list = ref<OperateLogVO[]>([])
const total = ref(0)
/** 操作时间范围([开始日期, 结束日期]),由日期选择器单独维护,请求时并入查询条件 */
const createTimeRange = ref<[string, string]>()

const queryParams = reactive<OperateLogPageReqVO>({
  pageNo: 1,
  pageSize: 10,
  userNickname: '',
  module: '',
  name: ''
})

async function loadList() {
  loading.value = true
  try {
    const page = await OperateLogApi.getOperateLogPage({ ...queryParams, createTime: createTimeRange.value })
    list.value = page.list || []
    total.value = page.total || 0
  } finally {
    loading.value = false
  }
}

function handleSearch() {
  queryParams.pageNo = 1
  loadList()
}

function handleReset() {
  queryParams.userNickname = ''
  queryParams.module = ''
  queryParams.name = ''
  createTimeRange.value = undefined
  queryParams.pageNo = 1
  loadList()
}

async function handleExport() {
  await OperateLogApi.exportOperateLog({ ...queryParams, createTime: createTimeRange.value })
}

// ---------------------------------------------------------------------------
// 详情
// ---------------------------------------------------------------------------

const detailVisible = ref(false)
const detailLoading = ref(false)
const detail = ref<OperateLogVO>({} as OperateLogVO)

async function handleDetail(row: OperateLogVO) {
  detailVisible.value = true
  detailLoading.value = true
  detail.value = { ...row }
  try {
    // 列表接口已经带全字段,这里仍然查一次详情:
    // 一是让 /get 这条契约在页面上有真实调用方(否则回归时容易被忽略),
    // 二是以后列表要是瘦身成不含 requestUrl 之类的字段,详情页仍然正确
    detail.value = (await OperateLogApi.getOperateLog(row.id)) || detail.value
  } finally {
    detailLoading.value = false
  }
}

onMounted(loadList)
</script>

<template>
  <div class="app-container">
    <el-card shadow="never" class="mb-3">
      <el-form :model="queryParams" inline class="search-form">
        <el-form-item label="操作人">
          <el-input v-model="queryParams.userNickname" placeholder="请输入操作人" clearable style="width: 160px" />
        </el-form-item>
        <el-form-item label="模块">
          <el-input v-model="queryParams.module" placeholder="请输入模块" clearable style="width: 160px" />
        </el-form-item>
        <el-form-item label="操作名">
          <el-input v-model="queryParams.name" placeholder="请输入操作名" clearable style="width: 160px" />
        </el-form-item>
        <el-form-item label="操作时间">
          <el-date-picker
            v-model="createTimeRange"
            type="daterange"
            value-format="YYYY-MM-DD"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            style="width: 260px"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <el-card shadow="never">
      <div class="mb-3">
        <el-button v-hasPermi="'system:operate-log:export'" plain @click="handleExport">导出</el-button>
        <el-button plain @click="loadList">刷新</el-button>
      </div>

      <el-table v-loading="loading" :data="list" border stripe size="small">
        <el-table-column type="index" label="#" width="50" align="center" />
        <el-table-column prop="userNickname" label="操作人" min-width="110" />
        <el-table-column prop="module" label="模块" min-width="120" />
        <el-table-column prop="name" label="操作名" min-width="140" />
        <el-table-column label="操作类型" width="100" align="center">
          <template #default="{ row }">
            <DictTag dict-type="system_operate_type" :value="row.type" />
          </template>
        </el-table-column>
        <el-table-column prop="requestMethod" label="请求方法" width="100" align="center" />
        <el-table-column prop="requestUrl" label="请求地址" min-width="220" show-overflow-tooltip />
        <el-table-column prop="ip" label="IP" min-width="130" />
        <el-table-column prop="duration" label="耗时(ms)" width="110" align="center" />
        <el-table-column label="结果" width="90" align="center">
          <template #default="{ row }">
            <el-tooltip :content="`结果码:${row.resultCode}`" placement="top">
              <el-tag :type="row.resultCode === 0 ? 'success' : 'danger'" size="small">
                {{ row.resultCode === 0 ? '成功' : '失败' }}
              </el-tag>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column label="操作时间" width="170">
          <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
        </el-table-column>
        <el-table-column label="操作" width="80" fixed="right" align="center">
          <template #default="{ row }">
            <el-button v-hasPermi="'system:operate-log:query'" link type="primary" @click="handleDetail(row)">
              详情
            </el-button>
          </template>
        </el-table-column>
      </el-table>

      <el-pagination
        v-model:current-page="queryParams.pageNo"
        v-model:page-size="queryParams.pageSize"
        :total="total"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next, jumper"
        class="mt-3 justify-end"
        @current-change="loadList"
        @size-change="handleSearch"
      />
    </el-card>

    <el-dialog v-model="detailVisible" title="操作日志详情" width="640px">
      <el-descriptions v-loading="detailLoading" :column="1" border size="small">
        <el-descriptions-item label="操作人">{{ detail.userNickname || '-' }}</el-descriptions-item>
        <el-descriptions-item label="模块">{{ detail.module || '-' }}</el-descriptions-item>
        <el-descriptions-item label="操作名">{{ detail.name || '-' }}</el-descriptions-item>
        <el-descriptions-item label="操作类型">
          <DictTag dict-type="system_operate_type" :value="detail.type" />
        </el-descriptions-item>
        <el-descriptions-item label="请求方法">{{ detail.requestMethod || '-' }}</el-descriptions-item>
        <el-descriptions-item label="请求地址">{{ detail.requestUrl || '-' }}</el-descriptions-item>
        <el-descriptions-item label="操作 IP">{{ detail.ip || '-' }}</el-descriptions-item>
        <el-descriptions-item label="耗时">{{ detail.duration }} ms</el-descriptions-item>
        <el-descriptions-item label="结果码">{{ detail.resultCode }}</el-descriptions-item>
        <el-descriptions-item label="操作时间">{{ formatDateTime(detail.createTime) }}</el-descriptions-item>
      </el-descriptions>
    </el-dialog>
  </div>
</template>