jiaxing.zhou

refactor(log):重构日志管理页面组件和样式

- 将 Element Plus 组件替换为原生 HTML 元素
- 重新设计搜索区域布局和样式
- 使用原生表格替换 el-table 组件
- 自定义分页控件实现
- 重构对话框组件样式和交互
- 添加全选和单选功能
- 优化响应式设计和移动端适配
- 更新组件类型声明文件
- 改进状态标签和操作按钮样式
- 实现表格视图切换功能
......@@ -8,6 +8,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
......@@ -25,6 +26,7 @@ declare module 'vue' {
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElText: typeof import('element-plus/es')['ElText']
Header: typeof import('./src/components/layout/Header.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
......
<template>
<div class="log-management">
<!-- 页面标题 -->
<div class="page-header">
<h2>日志管理</h2>
<p>系统操作日志查询与管理</p>
</div>
<!-- 搜索表单 -->
<el-card class="search-card" shadow="never">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="日志类型">
<el-select v-model="searchForm.logType" placeholder="请选择日志类型" clearable style="width: 200px">
<el-option label="操作日志" value="0" />
<el-option label="异常日志" value="1" />
<el-option label="登录日志" value="2" />
</el-select>
</el-form-item>
<el-form-item label="用户名">
<el-input
<!-- 搜索筛选区域 -->
<div class="search-section">
<div class="search-row">
<div class="search-item">
<label>日志类型:</label>
<select v-model="searchForm.logType" class="search-select">
<option value="">全部</option>
<option value="0">操作日志</option>
<option value="1">异常日志</option>
<option value="2">登录日志</option>
</select>
</div>
<div class="search-item">
<label>用户名:</label>
<input
v-model="searchForm.username"
type="text"
placeholder="请输入用户名"
clearable
style="width: 200px"
class="search-input"
/>
</el-form-item>
<el-form-item label="操作模块">
<el-input
</div>
<div class="search-item">
<label>操作模块:</label>
<input
v-model="searchForm.module"
type="text"
placeholder="请输入操作模块"
clearable
style="width: 200px"
class="search-input"
/>
</el-form-item>
<el-form-item label="操作结果">
<el-select v-model="searchForm.status" placeholder="请选择操作结果" clearable style="width: 200px">
<el-option label="成功" :value="1" />
<el-option label="失败" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleDateRangeChange"
style="width: 350px"
</div>
<div class="search-item">
<label>操作结果:</label>
<select v-model="searchForm.status" class="search-select">
<option value="">全部</option>
<option value="1">成功</option>
<option value="0">失败</option>
</select>
</div>
<div class="search-item">
<label>时间范围:</label>
<input
v-model="dateRangeText"
type="text"
placeholder="请选择时间范围"
class="search-input"
@click="showDatePicker = true"
readonly
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :loading="loading">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮 -->
<el-card class="toolbar-card" shadow="never">
<div class="toolbar">
<div class="toolbar-left">
<el-button
type="danger"
:disabled="selectedLogs.length === 0"
</div>
<div class="search-actions">
<button @click="handleSearch" class="search-btn">🔍 搜索</button>
<button @click="handleReset" class="reset-btn">🔄 重置</button>
</div>
</div>
</div>
<!-- 操作按钮区域 -->
<div class="action-section">
<div class="action-buttons">
<button
@click="handleBatchDelete"
:disabled="selectedLogs.length === 0"
class="action-btn danger"
>
<el-icon><Delete /></el-icon>
批量删除
</el-button>
<el-button type="warning" @click="handleCleanExpired">
<el-icon><Delete /></el-icon>
清理过期日志
</el-button>
<el-button type="info" @click="handleExport">
<el-icon><Download /></el-icon>
导出日志
</el-button>
</div>
<div class="toolbar-right">
<el-button @click="handleRefresh" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</el-card>
<!-- 日志列表 -->
<el-card class="table-card" shadow="never">
<el-table
v-loading="loading"
:data="logList"
@selection-change="handleSelectionChange"
stripe
border
style="width: 100%"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column prop="logId" label="日志ID" width="80" align="center" />
<el-table-column prop="logTypeText" label="日志类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getLogTypeTagType(row.logType)" size="small">
{{ row.logTypeText }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" width="100" align="center" />
<el-table-column prop="module" label="操作模块" width="120" align="center" />
<el-table-column prop="operation" label="操作内容" min-width="180" show-overflow-tooltip />
<el-table-column prop="ip" label="IP地址" width="120" align="center" />
<el-table-column prop="logTime" label="操作时间" width="160" align="center">
<template #default="{ row }">
{{ formatDate(row.logTime) }}
</template>
</el-table-column>
<el-table-column prop="statusText" label="操作结果" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
{{ row.statusText }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right" align="center">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleViewDetail(row)"
link
>
详情
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
link
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
🗑️ 批量删除
</button>
<button @click="handleCleanExpired" class="action-btn warning">
🧹 清理过期日志
</button>
<button @click="handleExport" class="action-btn info">
📋 导出日志
</button>
</div>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
<!-- 数据表格 -->
<div class="table-section">
<div class="table-header">
<div class="table-controls">
<button @click="handleTableSearch" class="control-btn" title="搜索">🔍</button>
<button @click="handleTableRefresh" class="control-btn" title="刷新">🔄</button>
<button @click="handleTableExport" class="control-btn" title="导出">📋</button>
<button @click="handleTableViewToggle" class="control-btn" title="视图切换">⊞</button>
</div>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th class="checkbox-col">
<input
type="checkbox"
:checked="selectAll"
@change="handleSelectAll"
class="table-checkbox"
/>
</th>
<th>日志ID</th>
<th>日志类型</th>
<th>用户名</th>
<th>操作模块</th>
<th>操作内容</th>
<th>IP地址</th>
<th>操作时间</th>
<th>操作结果</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="log in logList" :key="log.logId">
<td class="checkbox-col">
<input
type="checkbox"
:checked="selectedLogs.includes(log.logId)"
@change="handleSelectLog(log.logId)"
class="table-checkbox"
/>
</td>
<td>{{ log.logId }}</td>
<td>
<span :class="['status-badge', getLogTypeClass(log.logType)]">
{{ log.logTypeText }}
</span>
</td>
<td>{{ log.username }}</td>
<td>{{ log.module }}</td>
<td class="operation-cell" :title="log.operation">{{ log.operation }}</td>
<td>{{ log.ip }}</td>
<td>{{ formatDate(log.logTime) }}</td>
<td>
<span :class="['status-badge', log.status === 1 ? 'success' : 'error']">
{{ log.statusText }}
</span>
</td>
<td class="action-col">
<button @click="handleViewDetail(log)" class="action-btn primary" title="查看">👁️</button>
<button @click="handleDelete(log)" class="action-btn danger" title="删除">🗑️</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</el-card>
<!-- 日志详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="日志详情"
width="800px"
:close-on-click-modal="false"
<!-- 分页 -->
<div class="pagination-section">
<div class="pagination-info">
共 {{ pagination.total }} 条记录,第 {{ pagination.pageNum }} / {{ Math.ceil(pagination.total / pagination.pageSize) }} 页
</div>
<div class="pagination-controls">
<button
@click="handlePrevPage"
:disabled="pagination.pageNum <= 1"
class="pagination-btn"
>
← 上一页
</button>
<span class="page-info">{{ pagination.pageNum }} / {{ Math.ceil(pagination.total / pagination.pageSize) }}</span>
<button
@click="handleNextPage"
:disabled="pagination.pageNum >= Math.ceil(pagination.total / pagination.pageSize)"
class="pagination-btn"
>
<div v-if="logDetail" class="log-detail">
<el-descriptions :column="2" border>
<el-descriptions-item label="日志ID">
{{ logDetail.logId }}
</el-descriptions-item>
<el-descriptions-item label="日志类型">
<el-tag :type="getLogTypeTagType(logDetail.logType)">
{{ logDetail.logTypeText }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="用户名">
{{ logDetail.username }}
</el-descriptions-item>
<el-descriptions-item label="用户ID">
{{ logDetail.userId }}
</el-descriptions-item>
<el-descriptions-item label="操作模块">
{{ logDetail.module }}
</el-descriptions-item>
<el-descriptions-item label="操作内容">
{{ logDetail.operation }}
</el-descriptions-item>
<el-descriptions-item label="IP地址">
{{ logDetail.ip }}
</el-descriptions-item>
<el-descriptions-item label="操作时间">
{{ formatDate(logDetail.logTime) }}
</el-descriptions-item>
<el-descriptions-item label="操作结果">
<el-tag :type="logDetail.status === 1 ? 'success' : 'danger'">
{{ logDetail.statusText }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="错误信息" v-if="logDetail.errorMsg">
{{ logDetail.errorMsg }}
</el-descriptions-item>
</el-descriptions>
<div v-if="logDetail.requestParam" class="request-param">
下一页 →
</button>
<select v-model="pagination.pageSize" @change="handleSizeChange($event)" class="page-size-select">
<option value="10">10条/页</option>
<option value="20">20条/页</option>
<option value="50">50条/页</option>
<option value="100">100条/页</option>
</select>
</div>
</div>
<!-- 日志详情对话框 -->
<div v-if="detailDialogVisible" class="dialog-overlay" @click="closeDetailDialog">
<div class="dialog-content" @click.stop>
<div class="dialog-header">
<h3>日志详情</h3>
<button @click="closeDetailDialog" class="close-btn">×</button>
</div>
<div class="dialog-body">
<div class="log-detail">
<div class="detail-grid">
<div class="detail-item">
<label>日志ID:</label>
<span>{{ logDetail?.logId }}</span>
</div>
<div class="detail-item">
<label>日志类型:</label>
<span :class="['status-badge', getLogTypeClass(logDetail?.logType || '')]">
{{ logDetail?.logTypeText }}
</span>
</div>
<div class="detail-item">
<label>用户ID:</label>
<span>{{ logDetail?.userId }}</span>
</div>
<div class="detail-item">
<label>用户名:</label>
<span>{{ logDetail?.username }}</span>
</div>
<div class="detail-item">
<label>操作模块:</label>
<span>{{ logDetail?.module }}</span>
</div>
<div class="detail-item">
<label>操作内容:</label>
<span>{{ logDetail?.operation }}</span>
</div>
<div class="detail-item">
<label>IP地址:</label>
<span>{{ logDetail?.ip }}</span>
</div>
<div class="detail-item">
<label>操作时间:</label>
<span>{{ formatDate(logDetail?.logTime || '') }}</span>
</div>
<div class="detail-item">
<label>操作结果:</label>
<span :class="['status-badge', logDetail?.status === 1 ? 'success' : 'error']">
{{ logDetail?.statusText }}
</span>
</div>
<div v-if="logDetail?.errorMsg" class="detail-item full-width">
<label>错误信息:</label>
<span>{{ logDetail.errorMsg }}</span>
</div>
</div>
<div v-if="logDetail?.requestParam" class="request-param">
<h4>请求参数</h4>
<el-input
v-model="logDetail.requestParam"
type="textarea"
:rows="6"
<textarea
:value="logDetail.requestParam"
readonly
placeholder="无请求参数"
/>
class="request-textarea"
rows="6"
></textarea>
</div>
</div>
</div>
<div class="dialog-footer">
<button @click="closeDetailDialog" class="action-btn primary">关闭</button>
</div>
</div>
</div>
<template #footer>
<el-button @click="detailDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 清理过期日志对话框 -->
<el-dialog
v-model="cleanDialogVisible"
title="清理过期日志"
width="400px"
:close-on-click-modal="false"
>
<el-form :model="cleanForm" label-width="100px">
<el-form-item label="过期天数">
<el-input-number
v-model="cleanForm.expireDays"
:min="1"
:max="365"
<div v-if="cleanDialogVisible" class="dialog-overlay" @click="closeCleanDialog">
<div class="dialog-content" @click.stop>
<div class="dialog-header">
<h3>清理过期日志</h3>
<button @click="closeCleanDialog" class="close-btn">×</button>
</div>
<div class="dialog-body">
<form @submit.prevent="handleConfirmClean" class="clean-form">
<div class="form-group">
<label class="required">过期天数:</label>
<input
v-model.number="cleanForm.expireDays"
type="number"
min="1"
max="365"
placeholder="请输入过期天数"
style="width: 100%"
class="form-input"
required
/>
</el-form-item>
<el-form-item>
<el-text type="warning">
将删除指定天数之前的所有日志记录,此操作不可恢复!
</el-text>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cleanDialogVisible = false">取消</el-button>
<el-button type="danger" @click="handleConfirmClean" :loading="cleanLoading">
确认清理
</el-button>
</template>
</el-dialog>
</div>
<div class="warning-text">
⚠️ 将删除指定天数之前的所有日志记录,此操作不可恢复!
</div>
</form>
</div>
<div class="dialog-footer">
<button @click="closeCleanDialog" class="action-btn">取消</button>
<button @click="handleConfirmClean" :disabled="cleanLoading" class="action-btn danger">
{{ cleanLoading ? '清理中...' : '确认清理' }}
</button>
</div>
</div>
</div>
</div>
</template>
......@@ -286,12 +296,15 @@ import { formatDate } from '@/utils/index'
// 响应式数据
const loading = ref(false)
const logList = ref<Log[]>([])
const selectedLogs = ref<Log[]>([])
const selectedLogs = ref<number[]>([])
const detailDialogVisible = ref(false)
const logDetail = ref<LogDetail | null>(null)
const cleanDialogVisible = ref(false)
const cleanLoading = ref(false)
const dateRange = ref<[string, string] | null>(null)
const dateRangeText = ref('')
const showDatePicker = ref(false)
const selectAll = ref(false)
// 搜索表单
const searchForm = reactive<LogSearchParams>({
......@@ -337,6 +350,70 @@ const handleExport = () => {
ElMessage.info('导出功能开发中...')
}
// 全选/取消全选
const handleSelectAll = () => {
selectAll.value = !selectAll.value
if (selectAll.value) {
selectedLogs.value = logList.value.map(log => log.logId)
} else {
selectedLogs.value = []
}
}
// 选择单个日志
const handleSelectLog = (logId: number) => {
const index = selectedLogs.value.indexOf(logId)
if (index > -1) {
selectedLogs.value.splice(index, 1)
} else {
selectedLogs.value.push(logId)
}
selectAll.value = selectedLogs.value.length === logList.value.length
}
// 获取日志类型样式类
const getLogTypeClass = (logType: string) => {
switch (logType) {
case '0': return 'info'
case '1': return 'warning'
case '2': return 'success'
default: return 'default'
}
}
// 表格控制按钮
const handleTableSearch = () => {
handleSearch()
}
const handleTableRefresh = () => {
handleRefresh()
}
const handleTableExport = () => {
handleExport()
}
const handleTableViewToggle = () => {
ElMessage.info('视图切换功能开发中...')
}
// 分页控制
const handlePrevPage = () => {
if (pagination.pageNum > 1) {
pagination.pageNum--
getLogList()
}
}
const handleNextPage = () => {
const totalPages = Math.ceil(pagination.total / pagination.pageSize)
if (pagination.pageNum < totalPages) {
pagination.pageNum++
getLogList()
}
}
// 获取日志列表
const getLogList = async () => {
try {
......@@ -402,8 +479,9 @@ const handleDateRangeChange = (value: [string, string] | null) => {
}
// 分页大小变化
const handleSizeChange = (size: number) => {
pagination.pageSize = size
const handleSizeChange = (event: Event) => {
const target = event.target as HTMLSelectElement
pagination.pageSize = parseInt(target.value)
pagination.pageNum = 1
getLogList()
}
......@@ -416,7 +494,7 @@ const handleCurrentChange = (page: number) => {
// 选择变化
const handleSelectionChange = (selection: Log[]) => {
selectedLogs.value = selection
selectedLogs.value = selection.map(log => log.logId)
}
// 查看详情
......@@ -481,7 +559,7 @@ const handleBatchDelete = async () => {
}
)
const logIds = selectedLogs.value.map(log => log.logId)
const logIds = selectedLogs.value
const response = await logApi.deleteLogs(logIds)
if (response.code === 200) {
ElMessage.success('批量删除成功')
......@@ -502,6 +580,16 @@ const handleCleanExpired = () => {
cleanDialogVisible.value = true
}
// 关闭详情对话框
const closeDetailDialog = () => {
detailDialogVisible.value = false
}
// 关闭清理对话框
const closeCleanDialog = () => {
cleanDialogVisible.value = false
}
// 确认清理
const handleConfirmClean = async () => {
if (!cleanForm.expireDays || cleanForm.expireDays <= 0) {
......@@ -524,7 +612,7 @@ const handleConfirmClean = async () => {
const response = await logApi.cleanExpiredLogs(cleanForm.expireDays)
if (response.code === 200) {
ElMessage.success(response.data)
cleanDialogVisible.value = false
closeCleanDialog()
getLogList()
} else {
ElMessage.error(response.message || '清理失败')
......@@ -545,99 +633,505 @@ onMounted(() => {
})
</script>
<style scoped>
<style scoped lang="scss">
.log-management {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
}
.page-header {
margin-bottom: 20px;
// 搜索区域
.search-section {
background: white;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.page-header h2 {
margin: 0 0 8px 0;
color: #303133;
font-size: 20px;
.search-row {
display: flex;
gap: 16px;
align-items: center;
flex-wrap: wrap;
}
.search-item {
display: flex;
align-items: center;
gap: 8px;
}
.search-item label {
font-size: 12px;
color: #333;
white-space: nowrap;
}
.search-input, .search-select {
padding: 6px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 12px;
width: 120px;
}
.search-input:focus, .search-select:focus {
outline: none;
border-color: #1890ff;
}
.search-actions {
display: flex;
gap: 8px;
}
.search-btn {
background: #1890ff;
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
font-size: 11px;
cursor: pointer;
}
.search-btn:hover {
background: #40a9ff;
}
.reset-btn {
background: #ff7875;
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
font-size: 11px;
cursor: pointer;
height: 24px;
display: flex;
align-items: center;
}
.reset-btn:hover {
background: #ff9c99;
}
// 操作区域
.action-section {
background: white;
padding: 12px 16px;
border-bottom: 1px solid #e0e0e0;
}
.action-buttons {
display: flex;
gap: 6px;
}
.action-btn {
padding: 4px 8px;
border: none;
border-radius: 3px;
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn.primary {
background: #1890ff;
color: white;
}
.action-btn.primary:hover {
background: #40a9ff;
}
.action-btn.danger {
background: #ff4d4f;
color: white;
}
.action-btn.danger:hover {
background: #ff7875;
}
.action-btn.warning {
background: #faad14;
color: white;
}
.action-btn.warning:hover {
background: #ffc53d;
}
.action-btn.info {
background: #52c41a;
color: white;
}
.action-btn.info:hover {
background: #73d13d;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
// 表格区域
.table-section {
background: white;
margin: 16px;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.table-header {
padding: 8px 16px;
background: #fafafa;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
}
.table-controls {
display: flex;
gap: 4px;
}
.control-btn {
background: white;
border: 1px solid #d9d9d9;
border-radius: 3px;
padding: 4px 8px;
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover {
background: #f0f8ff;
border-color: #1890ff;
color: #1890ff;
}
.table-container {
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
}
.data-table th {
background: #fafafa;
font-weight: 600;
color: #333;
white-space: nowrap;
}
.page-header p {
margin: 0;
color: #909399;
font-size: 14px;
.data-table tbody tr:hover {
background: #f5f7fa;
}
.checkbox-col {
width: 50px;
text-align: center;
}
.table-checkbox {
width: 14px;
height: 14px;
cursor: pointer;
}
.operation-cell {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.action-col {
width: 120px;
text-align: center;
}
.action-col .action-btn {
padding: 2px 6px;
margin-right: 4px;
border: none;
border-radius: 3px;
font-size: 10px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 2px;
}
// 状态标签
.status-badge {
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
}
.status-badge.info {
background: #e6f7ff;
color: #1890ff;
}
.status-badge.warning {
background: #fff7e6;
color: #fa8c16;
}
.search-card {
margin-bottom: 16px;
.status-badge.success {
background: #f6ffed;
color: #52c41a;
}
.search-form {
margin-bottom: 0;
.status-badge.error {
background: #fff2f0;
color: #ff4d4f;
}
.toolbar-card {
margin-bottom: 16px;
.status-badge.default {
background: #f5f5f5;
color: #8c8c8c;
}
.toolbar {
// 分页区域
.pagination-section {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #fafafa;
border-top: 1px solid #e0e0e0;
}
.pagination-info {
font-size: 12px;
color: #666;
}
.toolbar-left,
.toolbar-right {
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
}
.table-card {
margin-bottom: 16px;
.pagination-btn {
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 3px;
background: white;
cursor: pointer;
font-size: 11px;
transition: all 0.2s;
}
.pagination-btn:hover:not(:disabled) {
background: #f0f8ff;
border-color: #1890ff;
color: #1890ff;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-container {
.page-info {
font-size: 12px;
color: #666;
padding: 0 8px;
}
.page-size-select {
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 12px;
background: white;
cursor: pointer;
}
// 对话框样式
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
z-index: 1000;
}
.dialog-content {
background: white;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.dialog-header {
padding: 12px 16px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-header h3 {
margin: 0;
font-size: 14px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
color: #999;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #666;
}
.dialog-body {
padding: 16px;
}
.dialog-footer {
padding: 12px 16px;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
gap: 6px;
}
// 详情网格布局
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-item.full-width {
grid-column: 1 / -1;
}
.detail-item label {
font-weight: 600;
color: #333;
font-size: 12px;
}
.detail-item span {
color: #666;
font-size: 12px;
}
.log-detail {
max-height: 600px;
max-height: 400px;
overflow-y: auto;
}
.request-param {
margin-top: 20px;
margin-top: 16px;
}
.request-param h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
margin: 0 0 8px 0;
color: #333;
font-size: 12px;
font-weight: 600;
}
/* 表格优化 */
.el-table {
font-size: 14px;
.request-textarea {
width: 100%;
padding: 6px 8px;
border: 1px solid #d9d9d9;
border-radius: 3px;
font-size: 11px;
font-family: monospace;
resize: vertical;
background: #f9f9f9;
}
.el-table .cell {
padding: 8px 12px;
line-height: 1.5;
// 表单样式
.clean-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.el-table th.el-table__cell {
background: #fafafa;
color: #606266;
.form-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.form-group label {
font-weight: 600;
color: #333;
font-size: 12px;
}
.form-group label.required::after {
content: ' *';
color: #ff4d4f;
}
.form-input {
padding: 6px 8px;
border: 1px solid #d9d9d9;
border-radius: 3px;
font-size: 12px;
transition: border-color 0.2s;
}
.el-table td.el-table__cell {
padding: 12px 0;
.form-input:focus {
outline: none;
border-color: #1890ff;
}
.el-table .el-button + .el-button {
margin-left: 8px;
.warning-text {
padding: 8px;
background: #fff7e6;
border: 1px solid #ffd591;
border-radius: 3px;
color: #fa8c16;
font-size: 11px;
}
</style>
......