43 lines
4.4 KiB
Vue
43 lines
4.4 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted, ref } from 'vue'
|
||
import { api, errorMessage, type SessionRecord } from '../api'
|
||
const rows = ref<SessionRecord[]>([]), total = ref(0), page = ref(1), keyword = ref(''), status = ref(''), days = ref(1), busy = ref(false), error = ref('')
|
||
const labels: Record<string, string> = { connecting: '连接中', connected: '已连接', ended: '正常结束', interrupted: '连接中断' }
|
||
const reasons: Record<string, string> = { client_ended: '终端结束', replaced: '新连接替代', heartbeat_timeout: '心跳超时', identity_reset: '设备身份重置' }
|
||
const time = (v?: string | null) => v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '—'
|
||
function duration(row: SessionRecord) {
|
||
if (!row.connectedAt) return '未就绪'
|
||
const seconds = Math.max(0, Math.floor((new Date(row.endedAt || row.lastReportedAt).getTime() - new Date(row.connectedAt).getTime()) / 1000))
|
||
return `${Math.floor(seconds / 60)} 分 ${seconds % 60} 秒`
|
||
}
|
||
async function load(reset = false) {
|
||
if (busy.value) return
|
||
if (reset) page.value = 1
|
||
busy.value = true; error.value = ''
|
||
try {
|
||
const to = new Date(), from = new Date(to.getTime() - days.value * 86400000)
|
||
const result = await api.sessionHistory({ page: page.value, keyword: keyword.value, status: status.value, from: from.toISOString(), to: to.toISOString() })
|
||
rows.value = result.items; total.value = result.total
|
||
} catch (e) { error.value = errorMessage(e) } finally { busy.value = false }
|
||
}
|
||
onMounted(() => { void load() })
|
||
</script>
|
||
<template>
|
||
<section class="panel">
|
||
<div class="panel-heading"><div><h2>会话历史</h2><p class="subtext">保留 90 天。连接时长从画面就绪算起,活动会话统计到最近心跳。</p></div></div>
|
||
<div class="history-filters"><el-input v-model="keyword" aria-label="搜索历史会话" placeholder="终端、桌面或用户" clearable @keyup.enter="load(true)" /><el-select v-model="status" aria-label="历史会话状态"><el-option label="全部状态" value="" /><el-option v-for="(label, value) in labels" :key="value" :label="label" :value="value" /></el-select><el-select v-model="days" aria-label="历史时间范围"><el-option :value="1" label="最近 24 小时" /><el-option :value="7" label="最近 7 天" /><el-option :value="30" label="最近 30 天" /><el-option :value="90" label="最近 90 天" /></el-select><el-button :loading="busy" @click="load(true)">查询历史</el-button></div>
|
||
<el-alert v-if="error" type="error" :title="error" :closable="false" class="notice" />
|
||
<el-table v-loading="busy" :data="rows" empty-text="所选时间范围内没有会话记录">
|
||
<el-table-column label="终端 / 用户" min-width="170"><template #default="{ row }"><strong>{{ row.terminalName }}</strong><div class="subtext">{{ row.displayName || row.username || '未关联用户' }}</div></template></el-table-column>
|
||
<el-table-column prop="desktopName" label="桌面" min-width="150" /><el-table-column label="状态" width="120"><template #default="{ row }"><el-tag :type="row.state === 'connected' ? 'success' : row.state === 'interrupted' ? 'danger' : 'info'">{{ labels[row.state] }}</el-tag></template></el-table-column>
|
||
<el-table-column label="开始 / 就绪" min-width="185"><template #default="{ row }"><div>{{ time(row.reservedAt) }}</div><div class="subtext">就绪 {{ time(row.connectedAt) }}</div><div v-if="row.source === 'observed'" class="subtext">从首次观测开始记录</div></template></el-table-column>
|
||
<el-table-column label="结束 / 原因" min-width="185"><template #default="{ row }"><div>{{ time(row.endedAt) }}</div><div class="subtext">{{ reasons[row.endReason] || (row.endedAt ? row.endReason : '会话进行中') }}</div></template></el-table-column>
|
||
<el-table-column label="连接时长" min-width="125"><template #default="{ row }">{{ duration(row) }}</template></el-table-column>
|
||
</el-table><el-pagination v-model:current-page="page" :page-size="20" :total="total" layout="prev, pager, next, total" @current-change="load()" />
|
||
</section>
|
||
</template>
|
||
<style scoped>
|
||
.history-filters{display:flex;gap:12px;flex-wrap:wrap;margin:20px 0}.history-filters .el-input{width:240px}.history-filters .el-select{width:160px}.el-pagination{margin-top:20px;flex-wrap:wrap}.subtext{line-height:1.7}
|
||
@media(max-width:600px){.history-filters>*{flex:1;min-width:130px}.history-filters .el-input{width:100%;flex-basis:100%}}
|
||
</style>
|