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">
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<!-- ── 租户平台 ───────────────────────────────────────────── -->
|
2026-04-30 11:47:01 +08:00
|
|
|
|
<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" />
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<el-option label="密钥管理" value="APP_SECRET" />
|
2026-04-30 11:47:01 +08:00
|
|
|
|
<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>
|
2026-05-27 12:27:43 +08:00
|
|
|
|
<el-table-column prop="createdAt" label="时间" width="170" sortable>
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<span class="time-text">{{ formatTime(row.createdAt) }}</span>
|
|
|
|
|
|
</template>
|
2026-04-30 11:47:01 +08:00
|
|
|
|
</el-table-column>
|
2026-05-27 12:27:43 +08:00
|
|
|
|
<el-table-column prop="moduleType" label="模块" width="110">
|
2026-04-30 11:47:01 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-05-27 12:27:43 +08:00
|
|
|
|
<el-tag size="small" :type="moduleTagType(row.moduleType)" effect="plain">
|
2026-05-27 12:52:29 +08:00
|
|
|
|
{{ moduleLabel(row.moduleType) }}
|
2026-05-27 12:27:43 +08:00
|
|
|
|
</el-tag>
|
2026-04-30 11:47:01 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<el-table-column label="操作摘要" min-width="360">
|
2026-04-30 11:47:01 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-05-27 12:27:43 +08:00
|
|
|
|
<div class="summary-cell">
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<el-tag size="small" :type="actionTagType(row.action)" effect="dark" class="action-tag">
|
2026-05-27 12:27:43 +08:00
|
|
|
|
{{ actionLabel(row.action) }}
|
|
|
|
|
|
</el-tag>
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<span class="summary-text">{{ row.summary || generateTenantSummary(row) }}</span>
|
2026-05-27 12:27:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="operator" label="操作人" width="120" />
|
|
|
|
|
|
<el-table-column prop="ipAddress" label="IP 地址" width="140">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<span class="ip-text">{{ row.ipAddress || '-' }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="详细数据" width="80" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<el-popover v-if="hasDetail(row.detailJson)" placement="left" :width="380" trigger="click">
|
2026-05-27 12:27:43 +08:00
|
|
|
|
<template #reference>
|
|
|
|
|
|
<el-button link type="primary" size="small">查看</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div class="detail-popover">
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<div v-for="(item, idx) in parseDetail(row.detailJson)" :key="idx" class="detail-row">
|
2026-05-27 12:27:43 +08:00
|
|
|
|
<span class="detail-key">{{ item.key }}</span>
|
|
|
|
|
|
<span class="detail-value">{{ item.value }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-popover>
|
|
|
|
|
|
<span v-else class="no-detail">-</span>
|
2026-04-30 11:47:01 +08:00
|
|
|
|
</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>
|
|
|
|
|
|
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<!-- ── 即时通讯 ───────────────────────────────────────────── -->
|
|
|
|
|
|
<el-tab-pane label="即时通讯" name="IM">
|
|
|
|
|
|
<div class="toolbar responsive-toolbar">
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="imAppKey"
|
|
|
|
|
|
placeholder="选择应用"
|
|
|
|
|
|
style="width: 320px"
|
|
|
|
|
|
filterable
|
|
|
|
|
|
@change="handleImAppChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="app in apps"
|
|
|
|
|
|
:key="app.appKey"
|
|
|
|
|
|
:label="`${app.name} · ${app.packageName}`"
|
|
|
|
|
|
:value="app.appKey"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-button :loading="imLoading" @click="loadImLogs">刷新</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="table-wrap">
|
|
|
|
|
|
<el-table :data="imLogs" v-loading="imLoading" border stripe>
|
|
|
|
|
|
<el-table-column prop="createdAt" label="时间" width="170">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<span class="time-text">{{ formatTime(row.createdAt) }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="160">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag size="small" :type="actionTagType(row.action)" effect="dark">
|
|
|
|
|
|
{{ actionLabel(row.action) }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="资源类型" width="120">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag size="small" effect="plain">{{ resourceTypeLabel(row.resourceType) }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作详情" min-width="360">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<span class="summary-text">{{ row.detail || generateImSummary(row) }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="operatorId" label="操作人" width="120" />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
style="margin-top: 16px"
|
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
|
:total="imTotal"
|
|
|
|
|
|
:page-size="imPageSize"
|
|
|
|
|
|
:current-page="imPage + 1"
|
|
|
|
|
|
@current-change="handleImPageChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ── 版本管理 ───────────────────────────────────────────── -->
|
2026-04-30 11:47:01 +08:00
|
|
|
|
<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>
|
2026-05-27 12:27:43 +08:00
|
|
|
|
<el-table-column prop="createdAt" label="时间" width="170">
|
2026-04-30 11:47:01 +08:00
|
|
|
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
|
|
|
|
</el-table-column>
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<el-table-column label="操作摘要" min-width="360">
|
2026-04-30 11:47:01 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<div class="summary-cell">
|
|
|
|
|
|
<el-tag size="small" :type="actionTagType(row.action)" effect="dark" class="action-tag">
|
|
|
|
|
|
{{ actionLabel(row.action) }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<span class="summary-text">{{ updateSummaryText(row) }}</span>
|
|
|
|
|
|
</div>
|
2026-04-30 11:47:01 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<el-table-column label="资源类型" width="140">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag size="small" effect="plain">{{ resourceTypeLabel(row.resourceType) }}</el-tag>
|
|
|
|
|
|
</template>
|
2026-04-30 11:47:01 +08:00
|
|
|
|
</el-table-column>
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<el-table-column prop="operator" label="操作人" width="140" />
|
|
|
|
|
|
<el-table-column label="详情" width="100" align="center">
|
2026-04-30 11:47:01 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-05-27 12:52:29 +08:00
|
|
|
|
<el-popover v-if="row.reason || hasDetail(row.detailJson)" placement="left" :width="380" trigger="click">
|
|
|
|
|
|
<template #reference>
|
|
|
|
|
|
<el-button link type="primary" size="small">查看</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div class="detail-popover">
|
|
|
|
|
|
<div v-if="row.reason" class="detail-row">
|
|
|
|
|
|
<span class="detail-key">原因</span>
|
|
|
|
|
|
<span class="detail-value">{{ row.reason }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-for="(item, idx) in parseDetail(row.detailJson)" :key="idx" class="detail-row">
|
|
|
|
|
|
<span class="detail-key">{{ item.key }}</span>
|
|
|
|
|
|
<span class="detail-value">{{ item.value }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-popover>
|
|
|
|
|
|
<span v-else class="no-detail">-</span>
|
2026-04-30 11:47:01 +08:00
|
|
|
|
</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-27 12:52:29 +08:00
|
|
|
|
import { imAdminApi, type OperationLog as ImOperationLog } from '@/api/im'
|
2026-05-21 16:09:55 +08:00
|
|
|
|
import { formatTime } from '@/utils/date'
|
2026-04-30 09:49:05 +08:00
|
|
|
|
|
2026-05-27 12:52:29 +08:00
|
|
|
|
const activeSource = ref<'TENANT' | 'IM' | 'UPDATE'>('TENANT')
|
|
|
|
|
|
|
|
|
|
|
|
// ── 租户平台 state ───────────────────────────────────────────────────────────
|
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)
|
2026-05-27 12:52:29 +08:00
|
|
|
|
const tenantFilters = reactive({ moduleType: '' })
|
|
|
|
|
|
|
|
|
|
|
|
// ── IM state ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const imLoading = ref(false)
|
|
|
|
|
|
const imLogs = ref<ImOperationLog[]>([])
|
|
|
|
|
|
const imTotal = ref(0)
|
|
|
|
|
|
const imPage = ref(0)
|
|
|
|
|
|
const imPageSize = ref(20)
|
|
|
|
|
|
const imAppKey = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
// ── 版本管理 state ──────────────────────────────────────────────────────────
|
2026-04-30 09:49:05 +08:00
|
|
|
|
|
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
|
|
|
|
|
2026-05-27 12:52:29 +08:00
|
|
|
|
// ── 初始化 ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
|
onMounted(async () => {
|
2026-05-27 12:52:29 +08:00
|
|
|
|
await Promise.all([loadApps(), loadTenantLogs()])
|
2026-04-30 11:47:01 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(activeSource, async (value) => {
|
2026-05-27 12:52:29 +08:00
|
|
|
|
if ((value === 'UPDATE' || value === 'IM') && !apps.value.length) {
|
2026-04-30 11:47:01 +08:00
|
|
|
|
await loadApps()
|
|
|
|
|
|
}
|
2026-05-27 12:52:29 +08:00
|
|
|
|
if (value === 'UPDATE') await loadUpdateLogs()
|
|
|
|
|
|
if (value === 'IM' && imAppKey.value) await loadImLogs()
|
2026-04-30 09:49:05 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-27 12:52:29 +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-27 12:52:29 +08:00
|
|
|
|
if (!updateAppKey.value && apps.value.length) updateAppKey.value = apps.value[0].appKey
|
|
|
|
|
|
if (!imAppKey.value && apps.value.length) imAppKey.value = apps.value[0].appKey
|
|
|
|
|
|
} catch { /* ignore */ }
|
2026-04-30 11:47:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-05-27 12:52:29 +08:00
|
|
|
|
async function loadImLogs() {
|
|
|
|
|
|
if (!imAppKey.value) { imLogs.value = []; return }
|
|
|
|
|
|
imLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await imAdminApi.getOperationLogs(imAppKey.value, imPage.value, imPageSize.value)
|
|
|
|
|
|
const data = res.data.data
|
|
|
|
|
|
imLogs.value = data.content ?? []
|
|
|
|
|
|
imTotal.value = data.totalElements ?? 0
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
imLoading.value = false
|
2026-04-30 11:47:01 +08:00
|
|
|
|
}
|
2026-05-27 12:52:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadUpdateLogs() {
|
|
|
|
|
|
if (!updateAppKey.value) { updateLogs.value = []; return }
|
2026-04-30 11:47:01 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 12:52:29 +08:00
|
|
|
|
// ── 事件处理 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
|
function handleTenantPageChange(nextPage: number) {
|
|
|
|
|
|
tenantPage.value = nextPage - 1
|
|
|
|
|
|
loadTenantLogs()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleTenantModuleChange() {
|
|
|
|
|
|
tenantPage.value = 0
|
|
|
|
|
|
loadTenantLogs()
|
2026-04-30 09:49:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 12:52:29 +08:00
|
|
|
|
function handleImAppChange() {
|
|
|
|
|
|
imPage.value = 0
|
|
|
|
|
|
loadImLogs()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleImPageChange(nextPage: number) {
|
|
|
|
|
|
imPage.value = nextPage - 1
|
|
|
|
|
|
loadImLogs()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 11:47:01 +08:00
|
|
|
|
function handleUpdateAppChange() {
|
|
|
|
|
|
loadUpdateLogs()
|
2026-04-30 09:49:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 12:52:29 +08:00
|
|
|
|
// ── 通用标签映射 ────────────────────────────────────────────────────────────
|
2026-05-27 12:27:43 +08:00
|
|
|
|
|
2026-05-27 12:52:29 +08:00
|
|
|
|
function moduleLabel(moduleType: string): 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: '应用管理',
|
2026-05-27 12:27:43 +08:00
|
|
|
|
SUB_ACCOUNT: '子账号',
|
2026-04-30 09:49:05 +08:00
|
|
|
|
SERVICE: '服务管理',
|
2026-05-27 12:27:43 +08:00
|
|
|
|
APP_SECRET: '密钥管理',
|
2026-04-30 09:49:05 +08:00
|
|
|
|
EMAIL_VERIFY: '邮箱验证',
|
|
|
|
|
|
}[moduleType] ?? moduleType
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 12:27:43 +08:00
|
|
|
|
function moduleTagType(moduleType: string): '' | 'success' | 'warning' | 'info' | 'danger' {
|
|
|
|
|
|
return {
|
|
|
|
|
|
CONSOLE: 'info',
|
|
|
|
|
|
APP: '',
|
|
|
|
|
|
SUB_ACCOUNT: 'warning',
|
|
|
|
|
|
SERVICE: 'success',
|
|
|
|
|
|
APP_SECRET: 'danger',
|
|
|
|
|
|
EMAIL_VERIFY: 'info',
|
|
|
|
|
|
}[moduleType] as any ?? ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 12:52:29 +08:00
|
|
|
|
/** 操作类型 → 中文(三个 tab 共用) */
|
2026-05-27 12:27:43 +08:00
|
|
|
|
function actionLabel(action: string): string {
|
2026-05-27 12:52:29 +08:00
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
|
// ── tenant-service: 应用管理 ──
|
|
|
|
|
|
CREATE_APP: '创建应用',
|
|
|
|
|
|
UPDATE_APP: '编辑应用',
|
|
|
|
|
|
DELETE_APP: '删除应用',
|
2026-05-27 12:27:43 +08:00
|
|
|
|
RESET_APP_SECRET: '重置密钥',
|
2026-05-27 12:52:29 +08:00
|
|
|
|
REQUEST_SECRET_VERIFY: '申请密钥验证',
|
2026-05-27 12:27:43 +08:00
|
|
|
|
REVEAL_APP_SECRET: '查看密钥',
|
2026-05-27 12:52:29 +08:00
|
|
|
|
// ── tenant-service: 子账号 ──
|
|
|
|
|
|
CREATE_SUB_ACCOUNT: '创建子账号',
|
|
|
|
|
|
DISABLE_SUB_ACCOUNT: '禁用子账号',
|
2026-05-27 12:27:43 +08:00
|
|
|
|
SEND_VERIFY_CODE: '发送验证码',
|
|
|
|
|
|
VERIFY_EMAIL: '验证邮箱',
|
2026-05-27 12:52:29 +08:00
|
|
|
|
// ── tenant-service: 服务管理 ──
|
|
|
|
|
|
DISABLE_SERVICE: '停用服务',
|
|
|
|
|
|
UPDATE_SERVICE_CONFIG: '更新服务配置',
|
|
|
|
|
|
REQUEST_SERVICE_ACTIVATION: '申请开通服务',
|
2026-05-27 12:27:43 +08:00
|
|
|
|
REGENERATE_KEY: '重新生成密钥',
|
2026-05-27 12:52:29 +08:00
|
|
|
|
// ── tenant-service: 控制台 ──
|
|
|
|
|
|
VIEW_DASHBOARD: '查看控制台',
|
|
|
|
|
|
// ── im-service: 用户管理 ──
|
|
|
|
|
|
UPDATE_USER_STATUS: '变更用户状态',
|
|
|
|
|
|
UPDATE_USER: '编辑用户信息',
|
|
|
|
|
|
REGISTER_USER: '注册用户',
|
|
|
|
|
|
GENERATE_USERSIG: '生成用户签名',
|
|
|
|
|
|
VERIFY_USERSIG: '验证用户签名',
|
|
|
|
|
|
SEARCH_USERS: '搜索用户',
|
|
|
|
|
|
KICK_USERS: '踢用户下线',
|
|
|
|
|
|
// ── im-service: 群组管理 ──
|
|
|
|
|
|
CREATE_GROUP: '创建群组',
|
|
|
|
|
|
UPDATE_GROUP: '编辑群组',
|
|
|
|
|
|
SEARCH_GROUPS: '搜索群组',
|
|
|
|
|
|
ADMIN_DISMISS_GROUP: '解散群组',
|
|
|
|
|
|
ADMIN_ADD_GROUP_MEMBER: '添加群成员',
|
|
|
|
|
|
ADMIN_REMOVE_GROUP_MEMBER: '移除群成员',
|
|
|
|
|
|
ADMIN_SET_GROUP_ROLE: '设置群角色',
|
|
|
|
|
|
ADMIN_TRANSFER_GROUP_OWNER: '转让群主',
|
|
|
|
|
|
ADMIN_UPDATE_GROUP_ATTRIBUTES: '更新群属性',
|
|
|
|
|
|
ADMIN_REMOVE_GROUP_ATTRIBUTES: '删除群属性',
|
|
|
|
|
|
ADMIN_MUTE_GROUP_MEMBER: '禁言群成员',
|
|
|
|
|
|
QUERY_GROUP_READ_RECEIPTS: '查询群已读回执',
|
|
|
|
|
|
ADMIN_ACCEPT_GROUP_JOIN_REQUEST: '同意入群申请',
|
|
|
|
|
|
ADMIN_REJECT_GROUP_JOIN_REQUEST: '拒绝入群申请',
|
|
|
|
|
|
// ── im-service: 消息管理 ──
|
|
|
|
|
|
SEARCH_MESSAGES: '搜索消息',
|
|
|
|
|
|
VIEW_STATS: '查看数据统计',
|
|
|
|
|
|
VIEW_HISTORY: '查看聊天记录',
|
|
|
|
|
|
ADMIN_REVOKE_MESSAGE: '撤回消息',
|
|
|
|
|
|
BATCH_SEND_MESSAGE: '群发消息',
|
|
|
|
|
|
ADMIN_SET_MSG_READ: '标记消息已读',
|
|
|
|
|
|
IMPORT_MESSAGES: '导入消息',
|
|
|
|
|
|
// ── im-service: 社交关系 ──
|
|
|
|
|
|
CREATE_FRIEND_REQUEST: '发起好友申请',
|
|
|
|
|
|
ACCEPT_FRIEND_REQUEST: '接受好友申请',
|
|
|
|
|
|
REJECT_FRIEND_REQUEST: '拒绝好友申请',
|
|
|
|
|
|
ADD_BLACKLIST: '加入黑名单',
|
|
|
|
|
|
REMOVE_BLACKLIST: '移出黑名单',
|
|
|
|
|
|
// ── im-service: Webhook ──
|
|
|
|
|
|
CREATE_WEBHOOK: '创建事件回调',
|
|
|
|
|
|
UPDATE_WEBHOOK: '编辑事件回调',
|
|
|
|
|
|
DELETE_WEBHOOK: '删除事件回调',
|
|
|
|
|
|
ACK_WEBHOOK_ALERT: '确认回调告警',
|
|
|
|
|
|
// ── im-service: 内容审核 ──
|
|
|
|
|
|
CREATE_KEYWORD_FILTER: '创建关键词过滤',
|
|
|
|
|
|
UPDATE_KEYWORD_FILTER: '编辑关键词过滤',
|
|
|
|
|
|
DELETE_KEYWORD_FILTER: '删除关键词过滤',
|
|
|
|
|
|
SET_GLOBAL_MUTE: '设置全局禁言',
|
|
|
|
|
|
// ── update-service: 版本管理 ──
|
|
|
|
|
|
UPLOAD: '上传版本',
|
|
|
|
|
|
PUBLISH: '发布版本',
|
|
|
|
|
|
REPUBLISH: '重新发布',
|
|
|
|
|
|
SCHEDULE_PUBLISH: '定时发布',
|
|
|
|
|
|
SAVE_DRAFT: '存为草稿',
|
|
|
|
|
|
UPDATE_FORCE: '修改强更',
|
|
|
|
|
|
UNPUBLISH: '下架版本',
|
|
|
|
|
|
DELETE: '删除版本',
|
|
|
|
|
|
GRAY_UPDATE: '调整灰度',
|
|
|
|
|
|
CHANGELOG_UPDATE: '更新日志',
|
|
|
|
|
|
// ── update-service: 市场配置 ──
|
|
|
|
|
|
STORE_SUBMIT: '提交应用市场',
|
|
|
|
|
|
CREATE_STORE_CONFIG: '创建市场配置',
|
|
|
|
|
|
UPDATE_STORE_CONFIG: '更新市场配置',
|
|
|
|
|
|
DELETE_STORE_CONFIG: '删除市场配置',
|
|
|
|
|
|
// ── update-service: 自动流程 ──
|
|
|
|
|
|
AUTO_PUBLISH: '自动发布',
|
|
|
|
|
|
AUTO_SCHEDULE: '自动定时发布',
|
|
|
|
|
|
STORE_SUBMIT_REQUEST: '提交市场审核',
|
|
|
|
|
|
STORE_REVIEW: '市场审核回调',
|
|
|
|
|
|
STORE_LIVE_DETECTED: '检测到已上架',
|
|
|
|
|
|
STORE_SUBMIT_STAGE: '市场提交进度',
|
|
|
|
|
|
// ── update-service: 批量提交 ──
|
|
|
|
|
|
STORE_SUBMIT_BATCH_SKIPPED: '批量提交跳过',
|
|
|
|
|
|
STORE_SUBMIT_BATCH_START: '批量提交开始',
|
|
|
|
|
|
STORE_SUBMIT_BATCH_FAILED: '批量提交失败',
|
|
|
|
|
|
STORE_SUBMIT_BATCH_END: '批量提交完成',
|
|
|
|
|
|
STORE_SUBMIT_STORE_START: '单市场提交开始',
|
|
|
|
|
|
STORE_SUBMIT_STORE_SKIPPED: '单市场提交跳过',
|
|
|
|
|
|
STORE_SUBMIT_STORE_FAILED: '单市场提交失败',
|
|
|
|
|
|
STORE_SUBMIT_STORE_SUCCESS: '单市场提交成功',
|
|
|
|
|
|
STORE_SUBMIT_STORE_STAGE: '单市场提交进度',
|
|
|
|
|
|
STORE_SUBMIT_ALREADY_LIVE: '市场已上架',
|
|
|
|
|
|
STORE_WITHDRAW_BEFORE_RESUBMIT: '撤回后重新提交',
|
|
|
|
|
|
}
|
|
|
|
|
|
return map[action] ?? action
|
2026-05-27 12:27:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function actionTagType(action: string): '' | 'success' | 'warning' | 'info' | 'danger' {
|
2026-05-27 12:52:29 +08:00
|
|
|
|
if (action.startsWith('CREATE') || action.startsWith('REGISTER') || action.startsWith('ACCEPT') || action.startsWith('UPLOAD')) return 'success'
|
|
|
|
|
|
if (action.startsWith('DELETE') || action.startsWith('DISABLE') || action.startsWith('DISMISS') || action.startsWith('REMOVE') || action.startsWith('REJECT') || action.startsWith('KICK') || action.startsWith('REVOKE') || action.startsWith('UNPUBLISH')) return 'danger'
|
|
|
|
|
|
if (action.startsWith('RESET') || action.startsWith('REGENERATE') || action.startsWith('MUTE') || action.startsWith('SET_GLOBAL') || action.startsWith('WITHDRAW')) return 'warning'
|
|
|
|
|
|
if (action.startsWith('UPDATE') || action.startsWith('TRANSFER') || action.startsWith('IMPORT') || action.startsWith('BATCH') || action.startsWith('GRAY') || action.startsWith('CHANGELOG')) return ''
|
|
|
|
|
|
if (action.startsWith('VIEW') || action.startsWith('SEARCH') || action.startsWith('QUERY') || action.startsWith('VERIFY') || action.startsWith('GENERATE') || action.startsWith('ACK') || action.startsWith('STORE_LIVE') || action.startsWith('STORE_SUBMIT_STORE_S')) return 'info'
|
|
|
|
|
|
if (action.startsWith('SEND') || action.startsWith('REQUEST') || action.startsWith('PUBLISH') || action.startsWith('REPUBLISH') || action.startsWith('SCHEDULE') || action.startsWith('AUTO')) return 'info'
|
2026-05-27 12:27:43 +08:00
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 12:52:29 +08:00
|
|
|
|
/** 资源类型 → 中文(三个 tab 共用) */
|
|
|
|
|
|
function resourceTypeLabel(resourceType: string): string {
|
|
|
|
|
|
return {
|
|
|
|
|
|
// tenant-service
|
|
|
|
|
|
DASHBOARD: '控制台概览',
|
|
|
|
|
|
APP: '应用',
|
|
|
|
|
|
APP_SECRET: '应用密钥',
|
|
|
|
|
|
SUB_ACCOUNT: '子账号',
|
|
|
|
|
|
FEATURE_SERVICE: '功能服务',
|
|
|
|
|
|
SERVICE_ACTIVATION: '服务开通',
|
|
|
|
|
|
EMAIL_VERIFY: '邮箱验证',
|
|
|
|
|
|
// im-service
|
|
|
|
|
|
ACCOUNT: '用户账号',
|
|
|
|
|
|
GROUP: '群组',
|
|
|
|
|
|
MESSAGE: '消息',
|
|
|
|
|
|
STATS: '数据统计',
|
|
|
|
|
|
FRIEND_REQUEST: '好友请求',
|
|
|
|
|
|
BLACKLIST: '黑名单',
|
|
|
|
|
|
WEBHOOK: '事件回调',
|
|
|
|
|
|
WEBHOOK_ALERT: '回调告警',
|
|
|
|
|
|
KEYWORD_FILTER: '关键词过滤',
|
|
|
|
|
|
GLOBAL_MUTE: '全局禁言',
|
|
|
|
|
|
GROUP_JOIN_REQUEST: '入群申请',
|
|
|
|
|
|
// update-service
|
|
|
|
|
|
APP_VERSION: '应用版本',
|
|
|
|
|
|
RN_BUNDLE: 'RN 包',
|
|
|
|
|
|
STORE_CONFIG: '市场配置',
|
|
|
|
|
|
PUBLISH_CONFIG: '发布配置',
|
|
|
|
|
|
GRAY_MEMBER: '灰度成员',
|
|
|
|
|
|
STORE_SUBMIT: '市场提交',
|
|
|
|
|
|
SERVICE: '服务配置',
|
|
|
|
|
|
}[resourceType] ?? resourceType
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 租户平台摘要生成 ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function generateTenantSummary(row: TenantOperationLog): string {
|
|
|
|
|
|
const act = actionLabel(row.action)
|
|
|
|
|
|
const res = resourceTypeLabel(row.resourceType)
|
|
|
|
|
|
return `${act}(${res})`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── IM 摘要生成 ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function generateImSummary(row: ImOperationLog): string {
|
|
|
|
|
|
const act = actionLabel(row.action)
|
|
|
|
|
|
const res = resourceTypeLabel(row.resourceType)
|
|
|
|
|
|
if (row.resourceId) return `${act} — ${res} ${row.resourceId}`
|
|
|
|
|
|
return `${act}(${res})`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 版本管理摘要 ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function updateSummaryText(row: UpdateOperationLog): string {
|
|
|
|
|
|
const res = resourceTypeLabel(row.resourceType)
|
|
|
|
|
|
if (row.reason) return `${res} — ${row.reason}`
|
|
|
|
|
|
return res
|
2026-05-27 12:27:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 12:52:29 +08:00
|
|
|
|
// ── 详情解析 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-05-27 12:27:43 +08:00
|
|
|
|
function hasDetail(detailJson?: string): boolean {
|
|
|
|
|
|
if (!detailJson) return false
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(detailJson)
|
|
|
|
|
|
return parsed && typeof parsed === 'object' && Object.keys(parsed).length > 0
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const DETAIL_KEY_LABELS: Record<string, string> = {
|
|
|
|
|
|
name: '应用名称',
|
|
|
|
|
|
packageName: '包名',
|
|
|
|
|
|
appKey: 'AppKey',
|
|
|
|
|
|
username: '用户名',
|
|
|
|
|
|
nickname: '昵称',
|
|
|
|
|
|
email: '邮箱',
|
|
|
|
|
|
platform: '平台',
|
|
|
|
|
|
serviceType: '服务类型',
|
|
|
|
|
|
purpose: '用途',
|
|
|
|
|
|
applyReason: '申请原因',
|
|
|
|
|
|
before: '变更前',
|
|
|
|
|
|
after: '变更后',
|
|
|
|
|
|
appCount: '应用数',
|
|
|
|
|
|
serviceCount: '服务数',
|
|
|
|
|
|
subAccountCount: '子账号数',
|
2026-05-27 12:52:29 +08:00
|
|
|
|
userId: '用户ID',
|
|
|
|
|
|
groupId: '群组ID',
|
|
|
|
|
|
messageId: '消息ID',
|
|
|
|
|
|
status: '状态',
|
|
|
|
|
|
reason: '原因',
|
|
|
|
|
|
target: '目标',
|
2026-05-27 12:27:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseDetail(detailJson?: string): { key: string; value: string }[] {
|
|
|
|
|
|
if (!detailJson) return []
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(detailJson)
|
|
|
|
|
|
if (!parsed || typeof parsed !== 'object') return []
|
|
|
|
|
|
return Object.entries(parsed).map(([k, v]) => ({
|
|
|
|
|
|
key: DETAIL_KEY_LABELS[k] ?? k,
|
|
|
|
|
|
value: typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v ?? ''),
|
|
|
|
|
|
}))
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return [{ key: '原始数据', value: detailJson }]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-30 09:49:05 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.responsive-toolbar {
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.table-wrap {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 12:27:43 +08:00
|
|
|
|
.time-text {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.summary-cell {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-tag {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.summary-text {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ip-text {
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-popover {
|
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.detail-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.detail-row:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.detail-key {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
width: 80px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
.detail-value {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.no-detail {
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 09:49:05 +08:00
|
|
|
|
@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>
|