Files
AUTO-MAS-test/frontend/src/views/History.vue

1004 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="history-header">
<div class="header-title">
<h1>历史记录</h1>
</div>
</div>
<!-- 搜索筛选区域 -->
<div class="search-section">
<a-card size="small" title="筛选条件">
<!-- 快捷时间选择 -->
<div class="quick-time-section">
<a-form-item label="快捷选择" style="margin-bottom: 16px">
<a-space wrap>
<a-button
v-for="preset in timePresets"
:key="preset.key"
:type="currentPreset === preset.key ? 'primary' : 'default'"
size="middle"
@click="handleQuickTimeSelect(preset)"
>
{{ preset.label }}
</a-button>
</a-space>
</a-form-item>
</div>
<!-- 详细筛选条件 -->
<a-row :gutter="16" :align="'middle'">
<a-col :span="6">
<a-form-item label="合并模式" style="margin-bottom: 0">
<a-select v-model:value="searchForm.mode" style="width: 100%">
<a-select-option value="按日合并">按日合并</a-select-option>
<a-select-option value="按周合并">按周合并</a-select-option>
<a-select-option value="按月合并">按月合并</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="开始日期" style="margin-bottom: 0">
<a-date-picker
v-model:value="searchForm.startDate"
style="width: 100%"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleDateChange"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="结束日期" style="margin-bottom: 0">
<a-date-picker
v-model:value="searchForm.endDate"
style="width: 100%"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleDateChange"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label=" " style="margin-bottom: 0" :colon="false">
<a-space>
<a-button type="primary" @click="handleSearch" :loading="searchLoading">
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon>
<ClearOutlined />
</template>
重置
</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-card>
</div>
<!-- 历史记录内容区域 -->
<div class="history-content">
<a-spin :spinning="searchLoading">
<div v-if="historyData.length === 0 && !searchLoading" class="empty-state">
<img src="@/assets/NoData.png" alt="无数据" class="empty-image" />
</div>
<div v-else class="history-layout">
<!-- 左侧日期列表 -->
<div class="date-sidebar">
<!-- 日期折叠列表 -->
<div class="date-list">
<a-collapse v-model:activeKey="activeKeys" ghost accordion>
<a-collapse-panel
v-for="dateGroup in historyData"
:key="dateGroup.date"
class="date-panel"
>
<template #header>
<div class="date-header">
<span class="date-text">{{ dateGroup.date }}</span>
</div>
</template>
<div class="user-list">
<div
v-for="(userData, username) in dateGroup.users"
:key="username"
class="user-item"
:class="{ active: selectedUser === `${dateGroup.date}-${username}` }"
@click="handleSelectUser(dateGroup.date, username, userData)"
>
<div class="user-info">
<span class="username">{{ username }}</span>
</div>
</div>
</div>
</a-collapse-panel>
</a-collapse>
</div>
</div>
<!-- 右侧详情区域 -->
<div class="detail-area">
<div v-if="!selectedUserData" class="no-selection">
<a-empty description="请选择左侧的用户查看详细信息">
<template #image>
<FileSearchOutlined style="font-size: 64px; color: #d9d9d9" />
</template>
</a-empty>
</div>
<div v-else class="detail-content">
<!-- 中间记录条目和统计数据 -->
<div class="records-area">
<!-- 记录条目列表 -->
<div class="records-section">
<a-card size="small" title="记录条目" class="records-card">
<template #extra>
<a-space>
<span class="record-count"
>{{ selectedUserData.index?.length || 0 }} 条记录</span
>
<HistoryOutlined />
</a-space>
</template>
<div class="records-list">
<div
v-for="(record, index) in selectedUserData.index || []"
:key="record.jsonFile"
class="record-item"
:class="{
active: selectedRecordIndex === index,
success: record.status === '完成',
error: record.status === '异常',
}"
@click="handleSelectRecord(index, record)"
>
<div class="record-info">
<div class="record-header">
<span class="record-time">{{ record.date }}</span>
<a-tooltip
v-if="
record.status === '异常' &&
selectedUserData?.error_info &&
selectedUserData.error_info[record.date]
"
:title="selectedUserData.error_info[record.date]"
placement="topLeft"
>
<a-tag color="error" size="small" class="error-tag-with-tooltip">
{{ record.status }}
</a-tag>
</a-tooltip>
<a-tag
v-else
:color="record.status === '完成' ? 'success' : 'error'"
size="small"
>
{{ record.status }}
</a-tag>
</div>
<div class="record-file">{{ record.jsonFile }}</div>
</div>
<div class="record-indicator">
<RightOutlined v-if="selectedRecordIndex === index" />
</div>
</div>
</div>
</a-card>
</div>
<!-- 统计数据 -->
<div class="statistics-section">
<!-- 公招统计 -->
<a-card size="small" class="stat-card">
<template #title>
<span>公招统计</span>
<span v-if="selectedRecordIndex >= 0" class="stat-subtitle">(当前记录)</span>
<span v-else class="stat-subtitle">(用户总计)</span>
</template>
<template #extra>
<UserOutlined />
</template>
<div v-if="currentStatistics.recruit_statistics" class="recruit-stats">
<a-row :gutter="8">
<a-col
v-for="(count, star) in currentStatistics.recruit_statistics"
:key="star"
:span="8"
>
<a-statistic
:title="`${star}星`"
:value="count"
:value-style="{ fontSize: '16px' }"
/>
</a-col>
</a-row>
</div>
<div v-else class="no-data">
<a-empty
description="暂无公招数据"
:image="NodataImage"
:image-style="{
height: '60px',
}"
/>
</div>
</a-card>
<!-- 掉落统计 -->
<a-card size="small" class="stat-card">
<template #title>
<span>掉落统计</span>
<span v-if="selectedRecordIndex >= 0" class="stat-subtitle">(当前记录)</span>
<span v-else class="stat-subtitle">(用户总计)</span>
</template>
<template #extra>
<GiftOutlined />
</template>
<div v-if="currentStatistics.drop_statistics" class="drop-stats">
<a-collapse size="small" ghost>
<a-collapse-panel
v-for="(drops, stage) in currentStatistics.drop_statistics"
:key="stage"
:header="stage"
>
<a-row :gutter="8">
<a-col v-for="(count, item) in drops" :key="item" :span="12">
<a-statistic
:title="item"
:value="count"
:value-style="{ fontSize: '14px' }"
/>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
</div>
<div v-else class="no-data">
<a-empty
description="暂无掉落数据"
:image="NodataImage"
:image-style="{
height: '60px',
}"
/>
</div>
</a-card>
</div>
</div>
<!-- 右侧:详细日志 -->
<div class="log-area">
<a-card size="small" title="详细日志" class="log-card">
<template #extra>
<a-space>
<a-tooltip title="打开日志文件" :getPopupContainer="tooltipContainer">
<a-button
size="small"
type="text"
:disabled="!currentJsonFile"
@click="handleOpenLogFile"
:class="{ 'no-hover-shift': true }"
:style="buttonFixedStyle"
>
<template #icon>
<FileOutlined />
</template>
</a-button>
</a-tooltip>
<a-tooltip title="打开日志文件所在目录" :getPopupContainer="tooltipContainer">
<a-button
size="small"
type="text"
:disabled="!currentJsonFile"
@click="handleOpenLogDirectory"
:class="{ 'no-hover-shift': true }"
:style="buttonFixedStyle"
>
<template #icon>
<FolderOpenOutlined />
</template>
</a-button>
</a-tooltip>
<a-tooltip :getPopupContainer="tooltipContainer">
<a-select
v-model:value="logFontSize"
size="small"
class="log-font-size-select"
style="width: 72px"
:options="logFontSizeOptions.map(v => ({ value: v, label: v + 'px' }))"
/>
</a-tooltip>
</a-space>
</template>
<a-spin :spinning="detailLoading">
<div v-if="currentDetail?.log_content" class="log-content" :style="{ fontSize: logFontSize + 'px' }">
<pre>{{ currentDetail.log_content }}</pre>
</div>
<div v-else class="no-log">
<a-empty
description="未选择日志,请从左边记录条目中选择"
:image="NodataImage"
:image-style="{ height: '60px' }"
/>
</div>
</a-spin>
</a-card>
</div>
</div>
</div>
</div>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { message } from 'ant-design-vue'
import {
SearchOutlined,
ClearOutlined,
HistoryOutlined,
UserOutlined,
GiftOutlined,
FileSearchOutlined,
RightOutlined,
FolderOpenOutlined,
FileOutlined,
} from '@ant-design/icons-vue'
import { Service } from '@/api/services/Service'
import { HistorySearchIn, type HistoryData } from '@/api' // 调整枚举需要值导入
import dayjs from 'dayjs'
import NodataImage from '@/assets/NoData.png'
// 响应式数据
const searchLoading = ref(false)
const detailLoading = ref(false)
const activeKeys = ref<string[]>([])
const currentPreset = ref('week') // 当前选中的快捷选项
// 选中的用户相关数据
const selectedUser = ref('')
const selectedUserData = ref<HistoryData | null>(null)
const selectedRecordIndex = ref(-1)
const currentDetail = ref<HistoryData | null>(null)
const currentJsonFile = ref('')
// 快捷时间选择预设(改用枚举值)
const timePresets = [
{ key: 'today', label: '今天', startDate: () => dayjs().format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.DAILY },
{ key: 'yesterday', label: '昨天', startDate: () => dayjs().subtract(1, 'day').format('YYYY-MM-DD'), endDate: () => dayjs().subtract(1, 'day').format('YYYY-MM-DD'), mode: HistorySearchIn.mode.DAILY },
{ key: 'week', label: '最近一周', startDate: () => dayjs().subtract(7, 'day').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.DAILY },
{ key: 'month', label: '最近一个月', startDate: () => dayjs().subtract(1, 'month').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.WEEKLY },
{ key: 'twoMonths', label: '最近两个月', startDate: () => dayjs().subtract(2, 'month').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.WEEKLY },
{ key: 'threeMonths', label: '最近三个月', startDate: () => dayjs().subtract(3, 'month').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.MONTHLY },
{ key: 'halfYear', label: '最近半年', startDate: () => dayjs().subtract(6, 'month').format('YYYY-MM-DD'), endDate: () => dayjs().format('YYYY-MM-DD'), mode: HistorySearchIn.mode.MONTHLY },
]
// 搜索表单(默认按日合并)
const searchForm = reactive({
mode: HistorySearchIn.mode.DAILY as HistorySearchIn.mode,
startDate: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
endDate: dayjs().format('YYYY-MM-DD'),
})
// 历史记录数据
interface HistoryDateGroup {
date: string
users: Record<string, HistoryData>
}
const historyData = ref<HistoryDateGroup[]>([])
// 当前显示的统计数据(根据是否选中记录条目来决定显示用户总计还是单条记录的数据)
const currentStatistics = computed(() => {
if (selectedRecordIndex.value >= 0 && currentDetail.value) {
// 显示选中记录的统计数据
return {
recruit_statistics: currentDetail.value.recruit_statistics,
drop_statistics: currentDetail.value.drop_statistics,
}
} else if (selectedUserData.value) {
// 显示用户总计统计数据
return {
recruit_statistics: selectedUserData.value.recruit_statistics,
drop_statistics: selectedUserData.value.drop_statistics,
}
} else {
// 没有选中任何数据
return {
recruit_statistics: null,
drop_statistics: null,
}
}
})
// 页面加载时自动搜索
onMounted(() => {
handleSearch()
})
// 搜索历史记录
const handleSearch = async () => {
if (!searchForm.startDate || !searchForm.endDate) {
message.error('请选择开始日期和结束日期')
return
}
try {
searchLoading.value = true
const response = await Service.searchHistoryApiHistorySearchPost({
mode: searchForm.mode,
start_date: searchForm.startDate,
end_date: searchForm.endDate,
})
if (response.code === 200) {
// 转换数据格式
historyData.value = Object.entries(response.data)
.map(([date, users]) => ({
date,
users,
}))
.sort((a, b) => b.date.localeCompare(a.date)) // 按日期倒序排列
message.success('搜索完成')
} else {
message.error(response.message || '搜索失败')
}
} catch (error) {
console.error('搜索历史记录失败:', error)
message.error('搜索历史记录失败')
} finally {
searchLoading.value = false
}
}
// 重置搜索条件
const handleReset = () => {
searchForm.mode = HistorySearchIn.mode.DAILY
searchForm.startDate = dayjs().subtract(7, 'day').format('YYYY-MM-DD')
searchForm.endDate = dayjs().format('YYYY-MM-DD')
historyData.value = []
activeKeys.value = []
}
// 快捷时间选择处理
const handleQuickTimeSelect = (preset: (typeof timePresets)[0]) => {
currentPreset.value = preset.key
searchForm.startDate = preset.startDate()
searchForm.endDate = preset.endDate()
searchForm.mode = preset.mode
// 自动搜索
handleSearch()
}
// 日期变化处理(手动选择日期时清除快捷选择状态)
const handleDateChange = () => {
currentPreset.value = ''
}
// 选择用户处理(修正乱码注释)
const handleSelectUser = async (date: string, username: string, userData: HistoryData) => {
selectedUser.value = `${date}-${username}`
selectedUserData.value = userData
selectedRecordIndex.value = -1
currentDetail.value = null
currentJsonFile.value = ''
}
// 选择记录处理
const handleSelectRecord = async (index: number, record: any) => {
selectedRecordIndex.value = index
currentJsonFile.value = record.jsonFile
await loadUserLog(record.jsonFile)
}
// 加载用户日志
const loadUserLog = async (jsonFile: string) => {
try {
detailLoading.value = true
const response = await Service.getHistoryDataApiHistoryDataPost({
jsonPath: jsonFile,
})
if (response.code === 200) {
currentDetail.value = response.data
} else {
message.error(response.message || '获取详细日志失败')
currentDetail.value = null
}
} catch (error) {
console.error('获取历史记录详情失败:', error)
message.error('获取历史记录详情失败')
currentDetail.value = null
} finally {
detailLoading.value = false
}
}
// 打开日志文件
const handleOpenLogFile = async () => {
if (!currentJsonFile.value) {
message.warning('请先选择一条记录')
return
}
try {
// 将 .json 扩展名替换为 .log
const logFilePath = currentJsonFile.value.replace(/\.json$/, '.log')
console.log('尝试打开日志文件:', logFilePath)
console.log('electronAPI 可用性:', !!window.electronAPI)
console.log(
'openFile 方法可用性:',
!!(window.electronAPI && (window.electronAPI as any).openFile)
)
// 调用系统API打开文件
if (window.electronAPI && (window.electronAPI as any).openFile) {
await (window.electronAPI as any).openFile(logFilePath)
message.success('日志文件已打开')
} else {
const errorMsg = !window.electronAPI
? '当前环境不支持打开文件功能electronAPI 不可用)'
: '当前环境不支持打开文件功能openFile 方法不可用)'
console.error(errorMsg)
message.error(errorMsg)
}
} catch (error) {
console.error('打开日志文件失败:', error)
message.error(`打开日志文件失败: ${error}`)
}
}
// 打开日志文件所在目录
const handleOpenLogDirectory = async () => {
if (!currentJsonFile.value) {
message.warning('请先选择一条记录')
return
}
try {
// 将 .json 扩展名替换为 .log
const logFilePath = currentJsonFile.value.replace(/\.json$/, '.log')
console.log('尝试打开日志文件目录:', logFilePath)
console.log('electronAPI 可用性:', !!window.electronAPI)
console.log(
'showItemInFolder 方法可用性:',
!!(window.electronAPI && (window.electronAPI as any).showItemInFolder)
)
// 调用系统API打开目录并选中文件
if (window.electronAPI && (window.electronAPI as any).showItemInFolder) {
await (window.electronAPI as any).showItemInFolder(logFilePath)
message.success('日志文件目录已打开')
} else {
const errorMsg = !window.electronAPI
? '当前环境不支持打开目录功能electronAPI 不可用)'
: '当前环境不支持打开目录功能showItemInFolder 方法不可用)'
console.error(errorMsg)
message.error(errorMsg)
}
} catch (error) {
console.error('打开日志文件目录失败:', error)
message.error(`<EFBFBD><EFBFBD>开日志文件目录失败: ${error}`)
}
}
// 日志字体大小(恢复)
const logFontSize = ref(14)
const logFontSizeOptions = [12, 13, 14, 16, 18, 20]
// Tooltip 容器:避免挂载到 body 造成全局滚动条闪烁与布局抖动
const tooltipContainer = (triggerNode: HTMLElement) => triggerNode?.parentElement || document.body
// 固定 button 尺寸,避免 hover/tooltip 状态导致宽度高度微调
const buttonFixedStyle = { width: '28px', height: '28px', padding: 0 }
</script>
<style scoped>
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
padding: 0 8px;
}
.header-title {
display: flex;
align-items: center;
gap: 16px;
}
.header-title h1 {
margin: 0;
font-size: 32px;
font-weight: 700;
color: var(--ant-color-text);
background: linear-gradient(135deg, var(--ant-color-primary), var(--ant-color-primary-hover));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.search-section {
margin-bottom: 24px;
}
.history-content { /* 避免 tooltip 在局部弹出时引起外层出现滚动条 */
height: calc(80vh - 200px);
overflow: hidden;
}
.empty-state {
text-align: center;
padding: 60px 0;
}
/* 新的布局样式 */
.history-layout {
display: flex;
gap: 16px;
height: 100%;
}
/* 左侧日期栏 */
.date-sidebar {
width: 200px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.date-list {
flex: 1;
overflow-y: auto;
border: 1px solid var(--ant-color-border);
border-radius: 8px;
background: var(--ant-color-bg-container);
}
.date-panel {
border-bottom: 1px solid var(--ant-color-border-secondary);
}
.date-panel:last-child {
border-bottom: none;
}
.date-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.date-text {
font-weight: 600;
font-size: 14px;
}
.user-list {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 0;
}
.user-item {
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.user-item:hover {
background: rgba(0, 0, 0, 0.04); /* 移除未知 CSS 变量 */
border-color: var(--ant-color-border);
}
.user-item.active {
background: var(--ant-color-primary-bg);
border-color: var(--ant-color-primary);
}
.user-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.username {
font-weight: 500;
font-size: 13px;
}
/* 右侧详情区域 */
.detail-area {
flex: 1;
display: flex;
flex-direction: column;
}
.no-selection {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--ant-color-border);
border-radius: 8px;
background: var(--ant-color-bg-container);
min-height: 400px;
}
.detail-content {
flex: 1;
display: flex;
gap: 16px;
min-height: 0;
min-width: 0; /* 确保子项 flex:1 时可以收缩 */
overflow: hidden; /* 避免被长行撑出 */
}
/* 记录条目区域 */
.records-area {
width: 400px;
flex-shrink: 1; /* 新增: 允许一定程度收缩 */
min-width: 260px; /* 给一个合理下限 */
display: flex;
flex-direction: column;
gap: 16px;
}
.records-section {
flex-shrink: 0;
}
.records-card {
border: 1px solid var(--ant-color-border);
border-radius: 8px;
}
.record-count {
font-size: 12px;
color: var(--ant-color-text-secondary);
}
.records-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--ant-color-border-secondary);
border-radius: 6px;
background: var(--ant-color-bg-layout);
}
.record-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--ant-color-border-secondary);
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.record-item:last-child {
border-bottom: none;
}
.record-item:hover {
background: rgba(0, 0, 0, 0.04); /* 移除未知 CSS 变量 */
}
.record-item.active {
background: var(--ant-color-primary-bg);
border-left: 3px solid var(--ant-color-primary);
}
.record-item.success {
border-left: 3px solid var(--ant-color-success);
}
.record-item.error {
border-left: 3px solid var(--ant-color-error);
}
.record-item.active.success {
border-left: 3px solid var(--ant-color-primary);
}
.record-item.active.error {
border-left: 3px solid var(--ant-color-primary);
}
.record-info {
flex: 1;
min-width: 0;
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.record-time {
font-size: 13px;
font-weight: 500;
color: var(--ant-color-text);
}
.record-file {
font-size: 11px;
color: var(--ant-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.record-indicator {
flex-shrink: 0;
width: 16px;
display: flex;
justify-content: center;
align-items: center;
color: var(--ant-color-primary);
}
.statistics-section {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.stat-card {
border: 1px solid var(--ant-color-border);
border-radius: 8px;
height: fit-content;
}
.recruit-stats,
.drop-stats {
min-height: 120px;
}
.no-data {
display: flex;
align-items: center;
justify-content: center;
min-height: 120px;
}
/* 日志区域 */
.log-area {
flex: 1;
/* 允许在父级 flex 宽度不足时压缩,避免整体被撑出视口 */
min-width: 0; /* 修改: 原来是 300px导致在内容渲染后无法收缩 */
display: flex;
flex-direction: column;
}
.log-card {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid var(--ant-color-border);
border-radius: 8px;
}
.log-card :deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
padding: 12px;
}
.log-content {
flex: 1;
max-height: 500px;
overflow-y: auto;
/* 新增: 防止超长无空格字符串把容器撑宽 */
overflow-x: auto; /* 横向单独滚动,而不是撑出布局 */
word-break: break-all;
overflow-wrap: anywhere;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
line-height: 1.5;
}
.log-content pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
overflow-wrap: anywhere;
max-width: 100%;
font-size: inherit;
line-height: inherit;
}
/* 恢复字体选择器样式 */
.log-font-size-select :deep(.ant-select-selector) {
padding: 0 4px;
text-align: center;
}
/* 按钮样式 */
/* 移除未使用 .title-icon */
/* 移除 unused overview-section / overview-card / overview-stats / user-status / error-section / error-card */
.default {
border-color: var(--ant-color-border);
color: var(--ant-color-text);
}
.default:hover {
border-color: var(--ant-color-primary);
color: var(--ant-color-primary);
}
/* 防止按钮在获得焦点/激活时出现位移(如出现 outline 或行高变化导致的抖动) */
.no-hover-shift {
line-height: 1; /* 固定行高 */
}
.no-hover-shift :deep(.ant-btn-icon) {
display: flex;
align-items: center;
justify-content: center;
}
/* 约束 tooltip 在本容器内时的最大宽度,减少撑开 */
:deep(.ant-tooltip) {
max-width: 260px;
word-break: break-word;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.history-layout {
flex-direction: column;
}
.date-sidebar {
width: 100%;
max-height: 300px;
}
.detail-content {
flex-direction: column;
}
.log-area {
width: 100%;
min-width: 0;
}
}
/* 针对极窄窗口再降级为纵向布局,提前触发布局切换,避免出现水平滚动 */
@media (max-width: 1000px) {
.history-layout {
flex-direction: column;
}
.records-area {
width: 100%;
min-width: 0;
}
.log-area {
width: 100%;
min-width: 0;
}
}
</style>