feat: 增加网页后台管理
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card shadow="hover">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<h2 class="page-title">连接请求</h2>
|
||||
<span class="text-muted">
|
||||
主控端发起、等待被控端确认的连接请求(共 {{ connections.length }} 条)
|
||||
</span>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-switch v-model="autoRefresh" active-text="自动刷新" />
|
||||
<el-button :icon="'Refresh'" :loading="loading" @click="load">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!connections.length" description="当前没有待确认的连接请求" />
|
||||
|
||||
<el-table v-else :data="connections" stripe>
|
||||
<el-table-column label="主控端 (From)" prop="fromDeviceId" min-width="200" />
|
||||
<el-table-column label="被控端 (To)" prop="toDeviceId" min-width="200" />
|
||||
<el-table-column label="发起时间" min-width="180">
|
||||
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已等待" width="140">
|
||||
<template #default="{ row }">{{ (row.elapsedMs / 1000).toFixed(1) }} 秒</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default>
|
||||
<el-tag type="warning" effect="dark">待确认</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { getConnections } from '@/api/admin';
|
||||
import type { PendingConnection } from '@/api/admin';
|
||||
|
||||
const connections = ref<PendingConnection[]>([]);
|
||||
const loading = ref(false);
|
||||
const autoRefresh = ref(true);
|
||||
let timer: number | undefined;
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
connections.value = await getConnections();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
if (timer) clearInterval(timer);
|
||||
if (autoRefresh.value) timer = window.setInterval(load, 3000);
|
||||
}
|
||||
|
||||
watch(autoRefresh, startTimer);
|
||||
|
||||
function fmtTime(ts: number) {
|
||||
return new Date(ts).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
startTimer();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
188
WebRTCSignalServerWeb/src/views/dashboard/Dashboard.vue
Normal file
188
WebRTCSignalServerWeb/src/views/dashboard/Dashboard.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2 class="page-title">仪表盘</h2>
|
||||
<span class="text-muted">实时展示信令服务器运行态,每 5 秒自动刷新</span>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<span class="text-muted">最后更新:{{ lastUpdate }}</span>
|
||||
<el-button :icon="'Refresh'" :loading="loading" @click="load">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<StatCard title="在线设备" :value="data?.onlineDevices ?? 0" icon="Monitor" color="#3b82f6" />
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<StatCard title="在线主控端" :value="data?.onlineControllers ?? 0" icon="VideoCamera" color="#67c23a" />
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<StatCard title="在线被控端" :value="data?.onlineControlled ?? 0" icon="Cellphone" color="#e6a23c" />
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<StatCard title="待确认连接" :value="data?.pendingConnections ?? 0" icon="Connection" color="#f56c6c" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="charts-row">
|
||||
<el-col :xs="24" :md="10">
|
||||
<el-card shadow="hover" header="设备类型分布">
|
||||
<div ref="pieRef" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="14">
|
||||
<el-card shadow="hover" header="信令流量统计">
|
||||
<div ref="barRef" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="hover" header="最近在线设备">
|
||||
<el-table :data="devices.slice(0, 8)" stripe size="default">
|
||||
<el-table-column prop="deviceId" label="设备 ID" min-width="180" />
|
||||
<el-table-column label="类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.deviceType === 'CONTROLLER'" type="success">主控端</el-tag>
|
||||
<el-tag v-else-if="row.deviceType === 'CONTROLLED'" type="warning">被控端</el-tag>
|
||||
<el-tag v-else type="info">未知</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.online ? 'success' : 'danger'" effect="dark">
|
||||
{{ row.online ? '在线' : '离线' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上线时间" min-width="180">
|
||||
<template #default="{ row }">{{ fmtTime(row.connectedAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { getDashboard, getDevices } from '@/api/admin';
|
||||
import type { DashboardData, DeviceInfo } from '@/api/admin';
|
||||
import StatCard from '@/components/StatCard.vue';
|
||||
|
||||
const data = ref<DashboardData | null>(null);
|
||||
const devices = ref<DeviceInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const lastUpdate = ref('—');
|
||||
const pieRef = ref<HTMLDivElement>();
|
||||
const barRef = ref<HTMLDivElement>();
|
||||
let pieChart: echarts.ECharts | null = null;
|
||||
let barChart: echarts.ECharts | null = null;
|
||||
let timer: number | undefined;
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [d, devs] = await Promise.all([getDashboard(), getDevices()]);
|
||||
data.value = d;
|
||||
devices.value = devs;
|
||||
lastUpdate.value = new Date().toLocaleTimeString('zh-CN');
|
||||
renderCharts();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderCharts() {
|
||||
if (!data.value) return;
|
||||
const d = data.value;
|
||||
pieChart?.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: 0 },
|
||||
series: [
|
||||
{
|
||||
name: '设备分布',
|
||||
type: 'pie',
|
||||
radius: ['42%', '70%'],
|
||||
avoidLabelOverlap: true,
|
||||
label: { show: true, formatter: '{b}: {c}' },
|
||||
data: [
|
||||
{ value: d.onlineControllers, name: '在线主控端', itemStyle: { color: '#67c23a' } },
|
||||
{ value: d.onlineControlled, name: '在线被控端', itemStyle: { color: '#e6a23c' } },
|
||||
{ value: d.offlineDevices, name: '离线设备', itemStyle: { color: '#c0c4cc' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const m = d.metrics;
|
||||
barChart?.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 40, right: 20, top: 30, bottom: 30 },
|
||||
xAxis: { type: 'category', data: ['注册', 'OFFER', 'ANSWER', '拒绝', '消息总量'] },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: '48%',
|
||||
data: [m.totalRegistered, m.totalOffers, m.totalAnswers, m.totalRejected, m.totalMessages],
|
||||
itemStyle: { borderRadius: [4, 4, 0, 0], color: '#3b82f6' },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
if (pieRef.value) pieChart = echarts.init(pieRef.value);
|
||||
if (barRef.value) barChart = echarts.init(barRef.value);
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
pieChart?.resize();
|
||||
barChart?.resize();
|
||||
}
|
||||
|
||||
function fmtTime(ts: number | null) {
|
||||
return ts ? new Date(ts).toLocaleString('zh-CN') : '—';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCharts();
|
||||
load();
|
||||
timer = window.setInterval(load, 5000);
|
||||
window.addEventListener('resize', onResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
window.removeEventListener('resize', onResize);
|
||||
pieChart?.dispose();
|
||||
barChart?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.page-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.charts-row {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.chart {
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
131
WebRTCSignalServerWeb/src/views/devices/DeviceList.vue
Normal file
131
WebRTCSignalServerWeb/src/views/devices/DeviceList.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card shadow="hover">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<h2 class="page-title">设备管理</h2>
|
||||
<span class="text-muted">共 {{ devices.length }} 台已注册设备</span>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-select v-model="filterType" placeholder="全部类型" clearable style="width: 150px">
|
||||
<el-option label="主控端" value="CONTROLLER" />
|
||||
<el-option label="被控端" value="CONTROLLED" />
|
||||
</el-select>
|
||||
<el-switch v-model="autoRefresh" active-text="自动刷新" />
|
||||
<el-button :icon="'Refresh'" :loading="loading" @click="load">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="devices" stripe>
|
||||
<el-table-column prop="deviceId" label="设备 ID" min-width="200" />
|
||||
<el-table-column label="类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.deviceType === 'CONTROLLER'" type="success">主控端</el-tag>
|
||||
<el-tag v-else-if="row.deviceType === 'CONTROLLED'" type="warning">被控端</el-tag>
|
||||
<el-tag v-else type="info">未知</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="status-dot" :class="row.online ? 'online' : 'offline'"></span>
|
||||
<el-tag :type="row.online ? 'success' : 'danger'" effect="dark" size="small">
|
||||
{{ row.online ? '在线' : '离线' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上线时间" min-width="180">
|
||||
<template #default="{ row }">{{ fmtTime(row.connectedAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :icon="'CopyDocument'" @click="copyId(row.deviceId)">
|
||||
复制 ID
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { getDevices } from '@/api/admin';
|
||||
import type { DeviceInfo } from '@/api/admin';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const devices = ref<DeviceInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const filterType = ref<string>('');
|
||||
const autoRefresh = ref(true);
|
||||
let timer: number | undefined;
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
devices.value = await getDevices(filterType.value || undefined);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
if (timer) clearInterval(timer);
|
||||
if (autoRefresh.value) timer = window.setInterval(load, 5000);
|
||||
}
|
||||
|
||||
watch(autoRefresh, startTimer);
|
||||
watch(filterType, load);
|
||||
|
||||
function copyId(id: string) {
|
||||
navigator.clipboard
|
||||
?.writeText(id)
|
||||
.then(() => ElMessage.success('已复制设备 ID'))
|
||||
.catch(() => ElMessage.error('复制失败'));
|
||||
}
|
||||
|
||||
function fmtTime(ts: number | null) {
|
||||
return ts ? new Date(ts).toLocaleString('zh-CN') : '—';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
startTimer();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.status-dot.online {
|
||||
background: #67c23a;
|
||||
}
|
||||
.status-dot.offline {
|
||||
background: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
116
WebRTCSignalServerWeb/src/views/login/Login.vue
Normal file
116
WebRTCSignalServerWeb/src/views/login/Login.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="login-wrap">
|
||||
<div class="login-card">
|
||||
<div class="brand">
|
||||
<div class="brand-logo">📡</div>
|
||||
<h1>WebRTC 信令服务器</h1>
|
||||
<p>后台管理系统</p>
|
||||
</div>
|
||||
<el-form @submit.prevent="onSubmit">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="token"
|
||||
size="large"
|
||||
placeholder="请输入管理令牌 (X-Admin-Token)"
|
||||
:prefix-icon="'Key'"
|
||||
@keyup.enter="onSubmit"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="login-btn"
|
||||
:loading="loading"
|
||||
@click="onSubmit"
|
||||
>
|
||||
登 录
|
||||
</el-button>
|
||||
<p class="hint">
|
||||
默认令牌与服务器 <code>application.yml</code> 中的
|
||||
<code>admin.token</code> 一致(当前默认:<code>webrtc-admin-token</code>)。
|
||||
</p>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const user = useUserStore();
|
||||
const token = ref(import.meta.env.VITE_ADMIN_TOKEN || 'webrtc-admin-token');
|
||||
const loading = ref(false);
|
||||
|
||||
async function onSubmit() {
|
||||
if (!token.value.trim()) {
|
||||
ElMessage.warning('请输入管理令牌');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
await user.login(token.value.trim());
|
||||
ElMessage.success('登录成功');
|
||||
const redirect = (route.query.redirect as string) || '/dashboard';
|
||||
router.replace(redirect);
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { message?: string } } };
|
||||
ElMessage.error(err?.response?.data?.message || '令牌校验失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-wrap {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1b1f2d 0%, #2b3a67 100%);
|
||||
}
|
||||
.login-card {
|
||||
width: 380px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 36px 32px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.brand {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.brand-logo {
|
||||
font-size: 40px;
|
||||
}
|
||||
.brand h1 {
|
||||
font-size: 20px;
|
||||
margin: 10px 0 4px;
|
||||
color: #1f2937;
|
||||
}
|
||||
.brand p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.hint code {
|
||||
background: #f0f2f5;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
</style>
|
||||
95
WebRTCSignalServerWeb/src/views/system/ServerInfo.vue
Normal file
95
WebRTCSignalServerWeb/src/views/system/ServerInfo.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<StatCard title="当前在线" :value="data?.onlineDevices ?? 0" icon="Monitor" color="#3b82f6" />
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<StatCard title="峰值会话" :value="data?.metrics.peakSessions ?? 0" icon="TrendCharts" color="#67c23a" />
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<StatCard title="累计注册" :value="data?.metrics.totalRegistered ?? 0" icon="User" color="#e6a23c" />
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<StatCard title="消息总量" :value="data?.metrics.totalMessages ?? 0" icon="ChatDotRound" color="#f56c6c" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="hover" header="运行信息" class="info-card">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="运行状态">
|
||||
<el-tag type="success" effect="dark">运行中</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服务端口">{{ config?.serverPort ?? '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="启动时间">
|
||||
{{ data ? fmtTime(data.serverStartTime) : '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已运行时长">{{ uptime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="在线 / 总数">
|
||||
{{ data?.onlineDevices ?? 0 }} / {{ data?.totalDevices ?? 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="WebSocket 端点">
|
||||
{{ config?.websocketEndpoint ?? '—' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="hover" header="信令流量明细" class="info-card">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="注册总数">{{ data?.metrics.totalRegistered ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="OFFER 总数">{{ data?.metrics.totalOffers ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="ANSWER 总数">{{ data?.metrics.totalAnswers ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="拒绝总数">{{ data?.metrics.totalRejected ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="消息总量">{{ data?.metrics.totalMessages ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="峰值会话">{{ data?.metrics.peakSessions ?? 0 }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
|
||||
import { getDashboard, getConfig } from '@/api/admin';
|
||||
import type { DashboardData, ServerConfig } from '@/api/admin';
|
||||
import StatCard from '@/components/StatCard.vue';
|
||||
|
||||
const data = ref<DashboardData | null>(null);
|
||||
const config = ref<ServerConfig | null>(null);
|
||||
const now = ref(Date.now());
|
||||
let timer: number | undefined;
|
||||
|
||||
const uptime = computed(() => {
|
||||
if (!data.value) return '—';
|
||||
return fmtDuration(now.value - data.value.serverStartTime);
|
||||
});
|
||||
|
||||
function fmtDuration(ms: number) {
|
||||
const s = Math.floor(ms / 1000);
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
return `${h} 小时 ${m} 分 ${sec} 秒`;
|
||||
}
|
||||
|
||||
function fmtTime(ts: number) {
|
||||
return new Date(ts).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [d, c] = await Promise.all([getDashboard(), getConfig()]);
|
||||
data.value = d;
|
||||
config.value = c;
|
||||
timer = window.setInterval(() => {
|
||||
now.value = Date.now();
|
||||
}, 1000);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
60
WebRTCSignalServerWeb/src/views/system/Settings.vue
Normal file
60
WebRTCSignalServerWeb/src/views/system/Settings.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card shadow="hover" header="连接治理配置" class="info-card">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="WebSocket 端点">
|
||||
{{ config?.websocketEndpoint ?? '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服务端口">{{ config?.serverPort ?? '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="OFFER 去重窗口">
|
||||
{{ config ? (config.dedupWindowMs / 1000).toFixed(0) + ' 秒' : '—' }}
|
||||
<span class="text-muted">(同一连接在此窗口内的重复 OFFER 将被忽略)</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="连接确认超时">
|
||||
{{ config ? (config.requestTimeoutMs / 1000).toFixed(0) + ' 秒' : '—' }}
|
||||
<span class="text-muted">(超时未确认则通知主控端请求失败)</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="hover" header="管理令牌 (admin.token)" class="info-card">
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
<template #title>如何修改后台登录令牌</template>
|
||||
<p>
|
||||
管理接口受 <code>X-Admin-Token</code> 头保护,令牌由服务器
|
||||
<code>application.yml</code> 中的 <code>admin.token</code> 配置,
|
||||
也可通过环境变量 <code>ADMIN_TOKEN</code> 覆盖。
|
||||
</p>
|
||||
<p>当前前端默认令牌取自 <code>.env</code> 的 <code>VITE_ADMIN_TOKEN</code>,需与服务器保持一致。</p>
|
||||
<p>示例:</p>
|
||||
<pre>admin:
|
||||
token: ${ADMIN_TOKEN:webrtc-admin-token}</pre>
|
||||
<p class="text-muted">修改后重启信令服务器即可生效。</p>
|
||||
</el-alert>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { getConfig } from '@/api/admin';
|
||||
import type { ServerConfig } from '@/api/admin';
|
||||
|
||||
const config = ref<ServerConfig | null>(null);
|
||||
onMounted(async () => {
|
||||
config.value = await getConfig();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
pre {
|
||||
background: #f0f2f5;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
overflow: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user