refactor(History): 重构历史记录页面布局和功能
- 新增左侧日期列表和右侧详情区域的布局结构 - 实现用户和记录选择功能 - 优化统计数据展示 - 添加详细日志刷新功能 - 调整样式和响应式设计
This commit is contained in:
@@ -24,7 +24,7 @@
|
|||||||
v-for="preset in timePresets"
|
v-for="preset in timePresets"
|
||||||
:key="preset.key"
|
:key="preset.key"
|
||||||
:type="currentPreset === preset.key ? 'primary' : 'default'"
|
:type="currentPreset === preset.key ? 'primary' : 'default'"
|
||||||
size="small"
|
size="middle"
|
||||||
@click="handleQuickTimeSelect(preset)"
|
@click="handleQuickTimeSelect(preset)"
|
||||||
>
|
>
|
||||||
{{ preset.label }}
|
{{ preset.label }}
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
|
<a-form-item label=" " style="margin-bottom: 0">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button type="primary" @click="handleSearch" :loading="searchLoading">
|
<a-button type="primary" @click="handleSearch" :loading="searchLoading">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -81,12 +82,13 @@
|
|||||||
重置
|
重置
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 历史记录列表 -->
|
<!-- 历史记录内容区域 -->
|
||||||
<div class="history-content">
|
<div class="history-content">
|
||||||
<a-spin :spinning="searchLoading">
|
<a-spin :spinning="searchLoading">
|
||||||
<div v-if="historyData.length === 0 && !searchLoading" class="empty-state">
|
<div v-if="historyData.length === 0 && !searchLoading" class="empty-state">
|
||||||
@@ -97,151 +99,230 @@
|
|||||||
</a-empty>
|
</a-empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="history-list">
|
<div v-else class="history-layout">
|
||||||
|
<!-- 左侧日期列表 -->
|
||||||
|
<div class="date-sidebar">
|
||||||
|
<!-- <!– 数据总览 –>-->
|
||||||
|
<!-- <div class="overview-section">-->
|
||||||
|
<!-- <a-card size="small" title="数据总览" class="overview-card">-->
|
||||||
|
<!-- <div class="overview-stats">-->
|
||||||
|
<!-- <a-statistic-->
|
||||||
|
<!-- title="总公招数"-->
|
||||||
|
<!-- :value="totalOverview.totalRecruit"-->
|
||||||
|
<!-- :value-style="{ color: '#1890ff', fontSize: '18px' }"-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- <template #prefix>-->
|
||||||
|
<!-- <UserOutlined />-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- </a-statistic>-->
|
||||||
|
<!-- <a-statistic-->
|
||||||
|
<!-- title="总掉落数"-->
|
||||||
|
<!-- :value="totalOverview.totalDrop"-->
|
||||||
|
<!-- :value-style="{ color: '#52c41a', fontSize: '18px' }"-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- <template #prefix>-->
|
||||||
|
<!-- <GiftOutlined />-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- </a-statistic>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </a-card>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
<!-- 日期折叠列表 -->
|
||||||
|
<div class="date-list">
|
||||||
<a-collapse v-model:activeKey="activeKeys" ghost>
|
<a-collapse v-model:activeKey="activeKeys" ghost>
|
||||||
<a-collapse-panel
|
<a-collapse-panel
|
||||||
v-for="dateGroup in historyData"
|
v-for="dateGroup in historyData"
|
||||||
:key="dateGroup.date"
|
:key="dateGroup.date"
|
||||||
:header="dateGroup.date"
|
|
||||||
class="date-panel"
|
class="date-panel"
|
||||||
>
|
>
|
||||||
<template #extra>
|
<template #header>
|
||||||
<a-tag :color="getDateStatusColor(dateGroup.users)">
|
<div class="date-header">
|
||||||
{{ Object.keys(dateGroup.users).length }} 个用户
|
<span class="date-text">{{ dateGroup.date }}</span>
|
||||||
</a-tag>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="user-list">
|
<div class="user-list">
|
||||||
<a-card
|
<div
|
||||||
v-for="(userData, username) in dateGroup.users"
|
v-for="(userData, username) in dateGroup.users"
|
||||||
:key="username"
|
:key="username"
|
||||||
size="small"
|
class="user-item"
|
||||||
class="user-card"
|
:class="{ active: selectedUser === `${dateGroup.date}-${username}` }"
|
||||||
:title="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>
|
<template #extra>
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-tag
|
<span class="record-count">{{ selectedUserData.index?.length || 0 }} 条记录</span>
|
||||||
v-for="item in userData.index || []"
|
<HistoryOutlined />
|
||||||
:key="item.jsonFile"
|
|
||||||
:color="item.status === '完成' ? 'success' : 'error'"
|
|
||||||
>
|
|
||||||
{{ item.status }}
|
|
||||||
</a-tag>
|
|
||||||
<a-button
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
@click="handleViewDetails(userData, username, dateGroup.date)"
|
|
||||||
>
|
|
||||||
查看详情
|
|
||||||
</a-button>
|
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</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-tag
|
||||||
|
: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">
|
<div class="statistics-section">
|
||||||
|
<a-row :gutter="16">
|
||||||
<!-- 公招统计 -->
|
<!-- 公招统计 -->
|
||||||
<div v-if="userData.recruit_statistics" class="stat-item">
|
<a-col :span="12">
|
||||||
<h4>
|
<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 />
|
<UserOutlined />
|
||||||
公招统计
|
</template>
|
||||||
</h4>
|
<div v-if="currentStatistics.recruit_statistics" class="recruit-stats">
|
||||||
<a-row :gutter="8">
|
<a-row :gutter="8">
|
||||||
<a-col
|
<a-col
|
||||||
v-for="(count, star) in userData.recruit_statistics"
|
v-for="(count, star) in currentStatistics.recruit_statistics"
|
||||||
:key="star"
|
:key="star"
|
||||||
:span="4"
|
:span="8"
|
||||||
>
|
>
|
||||||
<a-statistic
|
<a-statistic
|
||||||
:title="`${star}星`"
|
:title="`${star}星`"
|
||||||
:value="count"
|
:value="count"
|
||||||
|
:value-style="{ fontSize: '16px' }"
|
||||||
|
/>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-data">
|
||||||
|
<a-empty description="暂无公招数据" :image="false" />
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
|
||||||
|
<!-- 掉落统计 -->
|
||||||
|
<a-col :span="12">
|
||||||
|
<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' }"
|
:value-style="{ fontSize: '14px' }"
|
||||||
/>
|
/>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 掉落统计 -->
|
|
||||||
<div v-if="userData.drop_statistics" class="stat-item">
|
|
||||||
<h4>
|
|
||||||
<GiftOutlined />
|
|
||||||
掉落统计
|
|
||||||
</h4>
|
|
||||||
<a-collapse size="small" ghost>
|
|
||||||
<a-collapse-panel
|
|
||||||
v-for="(drops, stage) in userData.drop_statistics"
|
|
||||||
:key="stage"
|
|
||||||
:header="stage"
|
|
||||||
>
|
|
||||||
<a-row :gutter="8">
|
|
||||||
<a-col v-for="(count, item) in drops" :key="item" :span="6">
|
|
||||||
<a-statistic
|
|
||||||
:title="item"
|
|
||||||
:value="count"
|
|
||||||
:value-style="{ fontSize: '12px' }"
|
|
||||||
/>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="no-data">
|
||||||
<!-- 错误信息 -->
|
<a-empty description="暂无掉落数据" :image="false" />
|
||||||
<div
|
|
||||||
v-if="userData.error_info && Object.keys(userData.error_info).length > 0"
|
|
||||||
class="stat-item"
|
|
||||||
>
|
|
||||||
<h4>
|
|
||||||
<ExclamationCircleOutlined style="color: #ff4d4f" />
|
|
||||||
错误信息
|
|
||||||
</h4>
|
|
||||||
<a-list size="small" :data-source="Object.entries(userData.error_info)">
|
|
||||||
<template #renderItem="{ item }">
|
|
||||||
<a-list-item>
|
|
||||||
<a-list-item-meta>
|
|
||||||
<template #title>
|
|
||||||
<span style="color: #ff4d4f">{{
|
|
||||||
new Date(parseInt(item[0])).toLocaleString()
|
|
||||||
}}</span>
|
|
||||||
</template>
|
|
||||||
<template #description>
|
|
||||||
{{ item[1] }}
|
|
||||||
</template>
|
|
||||||
</a-list-item-meta>
|
|
||||||
</a-list-item>
|
|
||||||
</template>
|
|
||||||
</a-list>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
</a-collapse-panel>
|
|
||||||
</a-collapse>
|
|
||||||
</div>
|
|
||||||
</a-spin>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 详情弹窗 -->
|
<!-- 右侧:详细日志 -->
|
||||||
<a-modal
|
<div class="log-area">
|
||||||
v-model:open="detailModalVisible"
|
<a-card size="small" title="详细日志" class="log-card">
|
||||||
:title="`${currentUser} - ${currentDate} 详细日志`"
|
<template #extra>
|
||||||
width="80%"
|
<a-space>
|
||||||
:footer="null"
|
<FileTextOutlined />
|
||||||
class="detail-modal"
|
<a-button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="handleRefreshLog"
|
||||||
|
:loading="detailLoading"
|
||||||
>
|
>
|
||||||
|
刷新日志
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
<a-spin :spinning="detailLoading">
|
<a-spin :spinning="detailLoading">
|
||||||
<div v-if="currentDetail?.log_content" class="log-content">
|
<div v-if="currentDetail?.log_content" class="log-content">
|
||||||
<pre>{{ currentDetail.log_content }}</pre>
|
<pre>{{ currentDetail.log_content }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-log">
|
<div v-else class="no-log">
|
||||||
<a-empty description="暂无日志内容" />
|
<a-empty description="暂无日志内容" :image="false" />
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</a-modal>
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import {
|
import {
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
@@ -251,6 +332,9 @@ import {
|
|||||||
UserOutlined,
|
UserOutlined,
|
||||||
GiftOutlined,
|
GiftOutlined,
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
|
FileSearchOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
RightOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { Service } from '@/api/services/Service'
|
import { Service } from '@/api/services/Service'
|
||||||
import type { HistorySearchIn, HistoryData, HistoryDataGetIn } from '@/api/models'
|
import type { HistorySearchIn, HistoryData, HistoryDataGetIn } from '@/api/models'
|
||||||
@@ -259,10 +343,16 @@ import dayjs from 'dayjs'
|
|||||||
// 响应式数据
|
// 响应式数据
|
||||||
const searchLoading = ref(false)
|
const searchLoading = ref(false)
|
||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
const detailModalVisible = ref(false)
|
|
||||||
const activeKeys = ref<string[]>([])
|
const activeKeys = ref<string[]>([])
|
||||||
const currentPreset = ref('week') // 当前选中的快捷选项
|
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 = [
|
const timePresets = [
|
||||||
{
|
{
|
||||||
@@ -305,14 +395,14 @@ const timePresets = [
|
|||||||
label: '最近三个月',
|
label: '最近三个月',
|
||||||
startDate: () => dayjs().subtract(3, 'month').format('YYYY-MM-DD'),
|
startDate: () => dayjs().subtract(3, 'month').format('YYYY-MM-DD'),
|
||||||
endDate: () => dayjs().format('YYYY-MM-DD'),
|
endDate: () => dayjs().format('YYYY-MM-DD'),
|
||||||
mode: '按年月并' as HistorySearchIn.mode,
|
mode: '按月合并' as HistorySearchIn.mode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'halfYear',
|
key: 'halfYear',
|
||||||
label: '最近半年',
|
label: '最近半年',
|
||||||
startDate: () => dayjs().subtract(6, 'month').format('YYYY-MM-DD'),
|
startDate: () => dayjs().subtract(6, 'month').format('YYYY-MM-DD'),
|
||||||
endDate: () => dayjs().format('YYYY-MM-DD'),
|
endDate: () => dayjs().format('YYYY-MM-DD'),
|
||||||
mode: '按年月并' as HistorySearchIn.mode,
|
mode: '按月合并' as HistorySearchIn.mode,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -330,9 +420,60 @@ interface HistoryDateGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const historyData = ref<HistoryDateGroup[]>([])
|
const historyData = ref<HistoryDateGroup[]>([])
|
||||||
const currentDetail = ref<HistoryData | null>(null)
|
|
||||||
const currentUser = ref('')
|
// 计算总览数据
|
||||||
const currentDate = ref('')
|
const totalOverview = computed(() => {
|
||||||
|
let totalRecruit = 0
|
||||||
|
let totalDrop = 0
|
||||||
|
|
||||||
|
historyData.value.forEach(dateGroup => {
|
||||||
|
Object.values(dateGroup.users).forEach(userData => {
|
||||||
|
// 统计公招数据
|
||||||
|
if (userData.recruit_statistics) {
|
||||||
|
Object.values(userData.recruit_statistics).forEach((count: any) => {
|
||||||
|
totalRecruit += count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计掉落数据
|
||||||
|
if (userData.drop_statistics) {
|
||||||
|
Object.values(userData.drop_statistics).forEach((stageDrops: any) => {
|
||||||
|
Object.values(stageDrops).forEach((count: any) => {
|
||||||
|
totalDrop += count
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalRecruit,
|
||||||
|
totalDrop,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前显示的统计数据(根据是否选中记录条目来决定显示用户总计还是单条记录的数据)
|
||||||
|
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(() => {
|
onMounted(() => {
|
||||||
@@ -389,37 +530,7 @@ const handleRefresh = () => {
|
|||||||
handleSearch()
|
handleSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看详情
|
|
||||||
const handleViewDetails = async (userData: HistoryData, username: string, date: string) => {
|
|
||||||
if (!userData.index || userData.index.length === 0) {
|
|
||||||
message.warning('该记录没有详细日志文件')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
detailLoading.value = true
|
|
||||||
detailModalVisible.value = true
|
|
||||||
currentUser.value = username
|
|
||||||
currentDate.value = date
|
|
||||||
|
|
||||||
// 获取第一个JSON文件的详细内容
|
|
||||||
const jsonFile = userData.index[0].jsonFile
|
|
||||||
const response = await Service.getHistoryDataApiHistoryDataPost({
|
|
||||||
jsonPath: jsonFile,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.code === 200) {
|
|
||||||
currentDetail.value = response.data
|
|
||||||
} else {
|
|
||||||
message.error(response.message || '获取详细日志失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取历史记录详情失败:', error)
|
|
||||||
message.error('获取历史记录详情失败')
|
|
||||||
} finally {
|
|
||||||
detailLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 快捷时间选择处理
|
// 快捷时间选择处理
|
||||||
const handleQuickTimeSelect = (preset: (typeof timePresets)[0]) => {
|
const handleQuickTimeSelect = (preset: (typeof timePresets)[0]) => {
|
||||||
@@ -437,6 +548,52 @@ const handleDateChange = () => {
|
|||||||
currentPreset.value = ''
|
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 handleRefreshLog = async () => {
|
||||||
|
if (currentJsonFile.value) {
|
||||||
|
await loadUserLog(currentJsonFile.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取日期状态颜色
|
// 获取日期状态颜色
|
||||||
const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
||||||
const hasError = Object.values(users).some(
|
const hasError = Object.values(users).some(
|
||||||
@@ -470,6 +627,7 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
|||||||
|
|
||||||
.history-content {
|
.history-content {
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
|
height: calc(100vh - 200px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
@@ -477,61 +635,316 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
|||||||
padding: 60px 0;
|
padding: 60px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-list {
|
/* 新的布局样式 */
|
||||||
min-height: 400px;
|
.history-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧日期栏 */
|
||||||
|
.date-sidebar {
|
||||||
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-section {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card {
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
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 {
|
.date-panel {
|
||||||
margin-bottom: 16px;
|
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 {
|
.user-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 4px;
|
||||||
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-card {
|
.user-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item:hover {
|
||||||
|
background: var(--ant-color-bg-container-disabled);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-status {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧详情区域 */
|
||||||
|
.detail-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 记录条目区域 */
|
||||||
|
.records-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-section {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-card {
|
||||||
border: 1px solid var(--ant-color-border);
|
border: 1px solid var(--ant-color-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statistics-section {
|
.record-count {
|
||||||
display: flex;
|
font-size: 12px;
|
||||||
flex-direction: column;
|
color: var(--ant-color-text-secondary);
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item h4 {
|
.records-list {
|
||||||
margin: 0 0 8px 0;
|
max-height: 300px;
|
||||||
font-size: 14px;
|
overflow-y: auto;
|
||||||
font-weight: 600;
|
border: 1px solid var(--ant-color-border-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--ant-color-bg-layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-modal .log-content {
|
.record-item:last-child {
|
||||||
max-height: 60vh;
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-item:hover {
|
||||||
|
background: var(--ant-color-bg-container-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
border: 1px solid var(--ant-color-error-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日志区域 */
|
||||||
|
.log-area {
|
||||||
|
width: 400px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
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-y: auto;
|
||||||
background: var(--ant-color-bg-container);
|
background: var(--ant-color-bg-layout);
|
||||||
border: 1px solid var(--ant-color-border);
|
border: 1px solid var(--ant-color-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.detail-modal .log-content pre {
|
|
||||||
margin: 0;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-content pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.no-log {
|
.no-log {
|
||||||
text-align: center;
|
flex: 1;
|
||||||
padding: 40px 0;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 按钮样式 */
|
/* 按钮样式 */
|
||||||
@@ -544,4 +957,60 @@ const getDateStatusColor = (users: Record<string, HistoryData>) => {
|
|||||||
border-color: var(--ant-color-primary);
|
border-color: var(--ant-color-primary);
|
||||||
color: var(--ant-color-primary);
|
color: var(--ant-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@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%;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计数据标题样式 */
|
||||||
|
.stat-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.date-list::-webkit-scrollbar,
|
||||||
|
.log-content::-webkit-scrollbar,
|
||||||
|
.records-list::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-list::-webkit-scrollbar-track,
|
||||||
|
.log-content::-webkit-scrollbar-track,
|
||||||
|
.records-list::-webkit-scrollbar-track {
|
||||||
|
background: var(--ant-color-bg-container);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-list::-webkit-scrollbar-thumb,
|
||||||
|
.log-content::-webkit-scrollbar-thumb,
|
||||||
|
.records-list::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--ant-color-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-list::-webkit-scrollbar-thumb:hover,
|
||||||
|
.log-content::-webkit-scrollbar-thumb:hover,
|
||||||
|
.records-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--ant-color-border-secondary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user