2026-04-30 09:49:05 +08:00
|
|
|
<template>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 style="margin-bottom: 24px">操作日志</h2>
|
|
|
|
|
|
|
|
|
|
<el-card shadow="never">
|
2026-04-30 11:47:01 +08:00
|
|
|
<el-tabs v-model="activeSource">
|
|
|
|
|
<el-tab-pane label="租户平台" name="TENANT">
|
|
|
|
|
<div class="toolbar responsive-toolbar">
|
|
|
|
|
<el-select
|
|
|
|
|
v-model="tenantFilters.moduleType"
|
|
|
|
|
placeholder="模块"
|
|
|
|
|
style="width: 180px"
|
|
|
|
|
clearable
|
|
|
|
|
@change="handleTenantModuleChange"
|
|
|
|
|
>
|
|
|
|
|
<el-option label="全部模块" value="" />
|
|
|
|
|
<el-option label="控制台" value="CONSOLE" />
|
|
|
|
|
<el-option label="应用管理" value="APP" />
|
|
|
|
|
<el-option label="子账号管理" value="SUB_ACCOUNT" />
|
|
|
|
|
<el-option label="服务管理" value="SERVICE" />
|
|
|
|
|
<el-option label="密钥验证" value="APP_SECRET" />
|
|
|
|
|
<el-option label="邮箱验证" value="EMAIL_VERIFY" />
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-button :loading="tenantLoading" @click="loadTenantLogs">刷新</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="table-wrap">
|
|
|
|
|
<el-table :data="tenantLogs" v-loading="tenantLoading" border stripe>
|
|
|
|
|
<el-table-column prop="createdAt" label="时间" width="180">
|
|
|
|
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="moduleType" label="模块" width="140">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag size="small" effect="plain">{{ tenantModuleLabel(row.moduleType) }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="action" label="动作" width="180" />
|
|
|
|
|
<el-table-column prop="resourceType" label="资源类型" width="140" />
|
|
|
|
|
<el-table-column prop="resourceId" label="资源ID" min-width="220" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="operator" label="操作人" width="180" />
|
|
|
|
|
<el-table-column label="详情" min-width="280">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span class="detail">{{ formatDetail(row.detailJson) }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
style="margin-top: 16px"
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
:total="tenantTotal"
|
|
|
|
|
:page-size="tenantPageSize"
|
|
|
|
|
:current-page="tenantPage + 1"
|
|
|
|
|
@current-change="handleTenantPageChange"
|
|
|
|
|
/>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
<el-tab-pane label="版本管理" name="UPDATE">
|
|
|
|
|
<div class="toolbar responsive-toolbar">
|
|
|
|
|
<el-select
|
2026-05-08 10:09:22 +08:00
|
|
|
v-model="updateAppKey"
|
2026-04-30 11:47:01 +08:00
|
|
|
placeholder="选择应用"
|
|
|
|
|
style="width: 320px"
|
|
|
|
|
filterable
|
|
|
|
|
@change="handleUpdateAppChange"
|
|
|
|
|
>
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="app in apps"
|
2026-05-08 10:09:22 +08:00
|
|
|
:key="app.appKey"
|
2026-04-30 11:47:01 +08:00
|
|
|
:label="`${app.name} · ${app.packageName}`"
|
2026-05-08 10:09:22 +08:00
|
|
|
:value="app.appKey"
|
2026-04-30 11:47:01 +08:00
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-input-number v-model="updateLimit" :min="20" :max="200" :step="10" controls-position="right" />
|
|
|
|
|
<el-button :loading="updateLoading" @click="loadUpdateLogs">刷新</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="table-wrap">
|
|
|
|
|
<el-table :data="updateLogs" v-loading="updateLoading" border stripe>
|
|
|
|
|
<el-table-column prop="createdAt" label="时间" width="180">
|
|
|
|
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="resourceType" label="资源类型" width="140">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag size="small" effect="plain">{{ updateResourceLabel(row.resourceType) }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="action" label="动作" width="160">
|
|
|
|
|
<template #default="{ row }">{{ updateActionLabel(row.action) }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="resourceId" label="资源ID" min-width="220" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="operator" label="操作人" width="180" />
|
|
|
|
|
<el-table-column label="原因 / 详情" min-width="320">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span class="detail">{{ row.reason || formatDetail(row.detailJson) }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
</el-tabs>
|
2026-04-30 09:49:05 +08:00
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-04-30 11:47:01 +08:00
|
|
|
import { onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
import { appApi, type App } from '@/api/app'
|
|
|
|
|
import { updateAdminApi, type OperationLog as UpdateOperationLog } from '@/api/update'
|
2026-04-30 09:49:05 +08:00
|
|
|
import { operationLogApi, type TenantOperationLog } from '@/api/operationLog'
|
2026-05-21 16:09:55 +08:00
|
|
|
import { formatTime } from '@/utils/date'
|
2026-04-30 09:49:05 +08:00
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
const activeSource = ref<'TENANT' | 'UPDATE'>('TENANT')
|
2026-04-30 09:49:05 +08:00
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
const tenantLoading = ref(false)
|
|
|
|
|
const tenantLogs = ref<TenantOperationLog[]>([])
|
|
|
|
|
const tenantTotal = ref(0)
|
|
|
|
|
const tenantPage = ref(0)
|
|
|
|
|
const tenantPageSize = ref(20)
|
|
|
|
|
const tenantFilters = reactive({
|
2026-04-30 09:49:05 +08:00
|
|
|
moduleType: '',
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
const updateLoading = ref(false)
|
|
|
|
|
const updateLogs = ref<UpdateOperationLog[]>([])
|
|
|
|
|
const updateLimit = ref(100)
|
|
|
|
|
const apps = ref<App[]>([])
|
2026-05-08 10:09:22 +08:00
|
|
|
const updateAppKey = ref('')
|
2026-04-30 11:47:01 +08:00
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
loadApps(),
|
|
|
|
|
loadTenantLogs(),
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch(activeSource, async (value) => {
|
|
|
|
|
if (value === 'UPDATE' && !apps.value.length) {
|
|
|
|
|
await loadApps()
|
|
|
|
|
}
|
|
|
|
|
if (value === 'UPDATE') {
|
|
|
|
|
await loadUpdateLogs()
|
|
|
|
|
}
|
2026-04-30 09:49:05 +08:00
|
|
|
})
|
|
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
async function loadApps() {
|
|
|
|
|
if (apps.value.length) return
|
|
|
|
|
try {
|
|
|
|
|
const res = await appApi.list()
|
|
|
|
|
apps.value = res.data.data
|
2026-05-08 10:09:22 +08:00
|
|
|
if (!updateAppKey.value && apps.value.length) {
|
|
|
|
|
updateAppKey.value = apps.value[0].appKey
|
2026-04-30 11:47:01 +08:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore; empty state will be shown in the selector
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadTenantLogs() {
|
|
|
|
|
tenantLoading.value = true
|
2026-04-30 09:49:05 +08:00
|
|
|
try {
|
|
|
|
|
const res = await operationLogApi.list({
|
2026-04-30 11:47:01 +08:00
|
|
|
moduleType: tenantFilters.moduleType || undefined,
|
|
|
|
|
page: tenantPage.value,
|
|
|
|
|
size: tenantPageSize.value,
|
2026-04-30 09:49:05 +08:00
|
|
|
})
|
|
|
|
|
const data = res.data.data
|
2026-04-30 11:47:01 +08:00
|
|
|
tenantLogs.value = data.content ?? []
|
|
|
|
|
tenantTotal.value = data.totalElements ?? 0
|
2026-04-30 09:49:05 +08:00
|
|
|
} finally {
|
2026-04-30 11:47:01 +08:00
|
|
|
tenantLoading.value = false
|
2026-04-30 09:49:05 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
async function loadUpdateLogs() {
|
2026-05-08 10:09:22 +08:00
|
|
|
if (!updateAppKey.value) {
|
2026-04-30 11:47:01 +08:00
|
|
|
updateLogs.value = []
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
updateLoading.value = true
|
|
|
|
|
try {
|
2026-05-08 10:09:22 +08:00
|
|
|
const res = await updateAdminApi.listOperationLogs(updateAppKey.value, updateLimit.value)
|
2026-04-30 11:47:01 +08:00
|
|
|
updateLogs.value = res.data.data ?? []
|
|
|
|
|
} finally {
|
|
|
|
|
updateLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleTenantPageChange(nextPage: number) {
|
|
|
|
|
tenantPage.value = nextPage - 1
|
|
|
|
|
loadTenantLogs()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleTenantModuleChange() {
|
|
|
|
|
tenantPage.value = 0
|
|
|
|
|
loadTenantLogs()
|
2026-04-30 09:49:05 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
function handleUpdateAppChange() {
|
|
|
|
|
loadUpdateLogs()
|
2026-04-30 09:49:05 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
function tenantModuleLabel(moduleType: string) {
|
2026-04-30 09:49:05 +08:00
|
|
|
return {
|
2026-04-30 11:47:01 +08:00
|
|
|
CONSOLE: '控制台',
|
2026-04-30 09:49:05 +08:00
|
|
|
APP: '应用管理',
|
|
|
|
|
SUB_ACCOUNT: '子账号管理',
|
|
|
|
|
SERVICE: '服务管理',
|
|
|
|
|
APP_SECRET: '应用密钥',
|
|
|
|
|
EMAIL_VERIFY: '邮箱验证',
|
|
|
|
|
}[moduleType] ?? moduleType
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
function updateResourceLabel(resourceType: string) {
|
|
|
|
|
return {
|
|
|
|
|
APP_VERSION: '应用版本',
|
|
|
|
|
RN_BUNDLE: 'RN 包',
|
|
|
|
|
STORE_CONFIG: '应用市场',
|
|
|
|
|
PUBLISH_CONFIG: '发布配置',
|
|
|
|
|
GRAY_MEMBER: '灰度成员',
|
|
|
|
|
STORE_SUBMIT: '市场提交流程',
|
|
|
|
|
SERVICE: '服务配置',
|
|
|
|
|
}[resourceType] ?? resourceType
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateActionLabel(action: string) {
|
|
|
|
|
return {
|
|
|
|
|
UPLOAD: '上传',
|
|
|
|
|
PUBLISH: '发布',
|
|
|
|
|
REPUBLISH: '重新发布',
|
|
|
|
|
SCHEDULE_PUBLISH: '定时发布',
|
|
|
|
|
UPDATE_FORCE: '修改强更',
|
|
|
|
|
UNPUBLISH: '下架',
|
|
|
|
|
GRAY_UPDATE: '灰度调整',
|
|
|
|
|
STORE_SUBMIT: '提交市场',
|
|
|
|
|
CREATE_STORE_CONFIG: '创建配置',
|
|
|
|
|
UPDATE_STORE_CONFIG: '更新配置',
|
|
|
|
|
DELETE_STORE_CONFIG: '删除配置',
|
|
|
|
|
AUTO_PUBLISH: '自动发布',
|
|
|
|
|
REQUEST_SERVICE_ACTIVATION: '申请开通',
|
|
|
|
|
UPDATE_SERVICE_CONFIG: '更新服务配置',
|
|
|
|
|
DISABLE_SERVICE: '停用服务',
|
|
|
|
|
VIEW_DASHBOARD: '查看控制台',
|
|
|
|
|
}[action] ?? action
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
function formatDetail(detailJson?: string) {
|
|
|
|
|
if (!detailJson) return '-'
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(detailJson)
|
2026-04-30 11:47:01 +08:00
|
|
|
if (parsed === null || parsed === undefined) return '-'
|
|
|
|
|
return typeof parsed === 'string' ? parsed : JSON.stringify(parsed)
|
2026-04-30 09:49:05 +08:00
|
|
|
} catch {
|
|
|
|
|
return detailJson
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.responsive-toolbar {
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-wrap {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 767px) {
|
|
|
|
|
.responsive-toolbar {
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.responsive-toolbar :deep(.el-select),
|
2026-04-30 11:47:01 +08:00
|
|
|
.responsive-toolbar :deep(.el-button),
|
|
|
|
|
.responsive-toolbar :deep(.el-input-number) {
|
2026-04-30 09:49:05 +08:00
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-wrap :deep(.el-table) {
|
|
|
|
|
min-width: 900px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|