feat: AI虚拟用户新闻互动系统 v1.3.0 初始提交
- 虚拟用户管理(昵称/头像/性别/简介/邮箱同步到目标平台) - AI互动调度(点赞/收藏/评论/转发) - 日志时间改为北京时间 - 评论达上限后继续执行点赞收藏转发 - 一键登出全部功能 - 浅色主题UI
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<span class="page-title">日志管理</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="log-tabs">
|
||||
<!-- Login logs -->
|
||||
<el-tab-pane label="登录日志" name="login">
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="loginFilter.action" placeholder="操作类型" clearable @change="loadLoginLogs" style="width:130px">
|
||||
<el-option label="登录" value="login"/>
|
||||
<el-option label="登出" value="logout"/>
|
||||
<el-option label="刷新" value="refresh"/>
|
||||
<el-option label="失败" value="fail"/>
|
||||
</el-select>
|
||||
<el-button @click="loadLoginLogs"><el-icon><Refresh /></el-icon></el-button>
|
||||
</div>
|
||||
<el-table :data="loginLogs" v-loading="loginLoading" stripe class="data-table">
|
||||
<el-table-column label="用户账号" prop="user_account" width="180" />
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="actionType[row.action] || 'info'" size="small">{{ actionLabel[row.action] || row.action }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="会话ID" prop="session_id" show-overflow-tooltip />
|
||||
<el-table-column label="失败原因" prop="error_msg" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span style="color:var(--color-accent-red);font-size:12px">{{ row.error_msg || '--' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间" width="160">
|
||||
<template #default="{ row }">
|
||||
<span style="font-size:12px;color:var(--color-text-muted)">{{ row.created_at?.slice(0,16) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="loginPage" :page-size="50" :total="loginTotal"
|
||||
layout="total, prev, pager, next" @current-change="loadLoginLogs" style="margin-top:12px;display:flex;justify-content:flex-end" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Log files -->
|
||||
<el-tab-pane label="日志文件" name="files">
|
||||
<div style="display:flex;gap:16px;height:600px">
|
||||
<!-- File list -->
|
||||
<div class="file-list">
|
||||
<div class="file-list-header">日志文件</div>
|
||||
<div
|
||||
v-for="f in logFiles" :key="f.name"
|
||||
class="file-item" :class="{ active: selectedFile === f.name }"
|
||||
@click="openFile(f.name)"
|
||||
>
|
||||
<el-icon style="flex-shrink:0"><Document /></el-icon>
|
||||
<div class="file-info">
|
||||
<div class="file-name">{{ f.name }}</div>
|
||||
<div class="file-size">{{ f.size_kb }} KB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!logFiles.length" style="padding:20px;color:var(--color-text-muted);font-size:13px;text-align:center">暂无日志文件</div>
|
||||
</div>
|
||||
|
||||
<!-- File content -->
|
||||
<div class="file-content" v-if="selectedFile">
|
||||
<div class="file-toolbar">
|
||||
<span style="font-size:13px;font-weight:600">{{ selectedFile }}</span>
|
||||
<div style="display:flex;gap:8px">
|
||||
<el-select v-model="tailLines" style="width:100px" @change="loadFileContent">
|
||||
<el-option label="最后100行" :value="100"/>
|
||||
<el-option label="最后500行" :value="500"/>
|
||||
<el-option label="最后1000行" :value="1000"/>
|
||||
</el-select>
|
||||
<el-button size="small" @click="loadFileContent"><el-icon><Refresh /></el-icon></el-button>
|
||||
<el-button size="small" type="primary" @click="downloadFile">下载</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-content" ref="logContentRef">
|
||||
<div v-for="(line, i) in fileLines" :key="i" class="log-line" :class="getLineClass(line)">{{ line }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="file-empty">
|
||||
<el-empty description="选择左侧日志文件查看内容" />
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, nextTick, onMounted } from 'vue'
|
||||
import { getLoginLogs, getLogFiles, tailLogFile } from '@/api'
|
||||
|
||||
const activeTab = ref('login')
|
||||
const loginLogs = ref([])
|
||||
const loginLoading = ref(false)
|
||||
const loginPage = ref(1)
|
||||
const loginTotal = ref(0)
|
||||
const loginFilter = reactive({ action: '' })
|
||||
|
||||
const logFiles = ref([])
|
||||
const selectedFile = ref('')
|
||||
const fileLines = ref([])
|
||||
const tailLines = ref(100)
|
||||
const logContentRef = ref()
|
||||
|
||||
const actionLabel = { login: '登录', logout: '登出', refresh: '刷新', fail: '失败' }
|
||||
const actionType = { login: 'success', logout: 'info', refresh: 'warning', fail: 'danger' }
|
||||
|
||||
function getLineClass(line) {
|
||||
if (line.includes('ERROR') || line.includes('error')) return 'line-error'
|
||||
if (line.includes('WARNING') || line.includes('WARN')) return 'line-warn'
|
||||
if (line.includes('INFO')) return 'line-info'
|
||||
return ''
|
||||
}
|
||||
|
||||
async function loadLoginLogs() {
|
||||
loginLoading.value = true
|
||||
try {
|
||||
const res = await getLoginLogs({ page: loginPage.value, page_size: 50, action: loginFilter.action || undefined })
|
||||
loginLogs.value = res.data?.items || []
|
||||
loginTotal.value = res.data?.total || 0
|
||||
} finally { loginLoading.value = false }
|
||||
}
|
||||
|
||||
async function loadLogFiles() {
|
||||
const res = await getLogFiles()
|
||||
logFiles.value = res.data || []
|
||||
}
|
||||
|
||||
async function openFile(name) {
|
||||
selectedFile.value = name
|
||||
await loadFileContent()
|
||||
}
|
||||
|
||||
async function loadFileContent() {
|
||||
if (!selectedFile.value) return
|
||||
const res = await tailLogFile(selectedFile.value, tailLines.value)
|
||||
fileLines.value = (res.data?.lines || []).map(l => l.replace(/\n$/, ''))
|
||||
await nextTick()
|
||||
if (logContentRef.value) logContentRef.value.scrollTop = logContentRef.value.scrollHeight
|
||||
}
|
||||
|
||||
function downloadFile() {
|
||||
window.open(`/api/logs/files/${selectedFile.value}/download`, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => { loadLoginLogs(); loadLogFiles() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-bar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
.data-table { border-radius: 8px; }
|
||||
:deep(.log-tabs .el-tabs__header) { margin-bottom: 16px; }
|
||||
:deep(.el-tabs__item) { color: var(--color-text-muted) !important; }
|
||||
:deep(.el-tabs__item.is-active) { color: var(--color-accent) !important; }
|
||||
|
||||
.file-list {
|
||||
width: 220px; flex-shrink: 0;
|
||||
background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.file-list-header { padding: 12px 14px; font-size: 12px; font-weight: 600; color: var(--color-text-muted); border-bottom: 1px solid var(--color-border); }
|
||||
.file-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 10px 14px; cursor: pointer; transition: background 0.15s;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
.file-item:hover { background: rgba(88,166,255,0.06); }
|
||||
.file-item.active { background: rgba(88,166,255,0.12); color: var(--color-accent); }
|
||||
.file-name { font-size: 12px; font-weight: 500; }
|
||||
.file-size { font-size: 11px; color: var(--color-text-muted); }
|
||||
|
||||
.file-content {
|
||||
flex: 1; display: flex; flex-direction: column;
|
||||
background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px; overflow: hidden;
|
||||
}
|
||||
.file-toolbar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 14px; border-bottom: 1px solid var(--color-border); flex-shrink: 0;
|
||||
}
|
||||
.log-content { flex: 1; overflow-y: auto; padding: 10px 14px; font-family: 'Consolas','Monaco',monospace; font-size: 12px; line-height: 1.6; }
|
||||
.log-line { padding: 1px 0; color: var(--color-text-muted); white-space: pre-wrap; word-break: break-all; }
|
||||
.line-error { color: var(--color-accent-red); }
|
||||
.line-warn { color: var(--color-accent-orange); }
|
||||
.line-info { color: var(--color-text); }
|
||||
.file-empty { flex: 1; display: flex; align-items: center; justify-content: center; background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: 8px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user