vdi-cloud/console/src/components/SessionHistory.vue

43 lines
4.4 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.

<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>