Files
WebAI2API/webui/src/components/dash.vue
T
2026-01-11 19:25:34 +08:00

274 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
import { ref, onMounted, onUnmounted } from 'vue';
import { useSystemStore } from '@/stores/system';
import { useSettingsStore } from '@/stores/settings';
import {
DesktopOutlined,
PieChartOutlined,
ChromeOutlined,
FieldTimeOutlined,
LineChartOutlined,
SyncOutlined,
ExclamationCircleOutlined,
CheckCircleOutlined,
CloseCircleOutlined
} from '@ant-design/icons-vue';
const systemStore = useSystemStore();
const queueData = ref([]);
const timer = ref(null);
const queueStats = ref({ processing: 0, waiting: 0, total: 0 });
// 获取队列数据
const fetchQueue = async () => {
const settingsStore = useSettingsStore(); // 获取store
try {
const res = await fetch('/admin/queue', { headers: settingsStore.getHeaders() });
if (res.ok) {
const data = await res.json();
// 更新统计信息
queueStats.value = {
processing: data.processing || 0,
waiting: data.waiting || 0,
total: data.total || 0
};
const processing = (data.processingTasks || []).map(t => ({ ...t, status: 'processing' }));
const waiting = (data.waitingTasks || []).map(t => ({ ...t, status: 'waiting' }));
queueData.value = [...processing, ...waiting];
}
} catch (e) {
console.error('Fetch queue failed', e);
}
};
const refreshData = async () => {
await Promise.all([
systemStore.fetchStatus(),
systemStore.fetchStats(),
fetchQueue()
]);
};
const formatUptime = (seconds) => {
const d = Math.floor(seconds / (3600 * 24));
const h = Math.floor((seconds % (3600 * 24)) / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (d > 0) return `${d}${h}小时 ${m}`;
if (h > 0) return `${h}小时 ${m}`;
return `${m}`;
};
const formatMemory = (mb) => {
if (!mb || mb === 0) return '0 MB';
if (mb > 1024) {
return parseFloat((mb / 1024).toFixed(2)) + ' GB';
}
return parseFloat(Number(mb).toFixed(2)) + ' MB';
};
const getLoadColor = (usage) => {
if (usage < 50) return '#52c41a'; // 绿色
if (usage < 80) return '#faad14'; // 橙色
return '#f5222d'; // 红色
};
// 状态映射
const getStatusConfig = (status) => {
const map = {
'normal': { color: 'green', text: '正常模式 (Normal)' },
'headless': { color: 'blue', text: '无头模式 (Headless)' },
'xvfb': { color: 'purple', text: '虚拟显示 (Xvfb)' }
};
return map[status] || { color: 'red', text: '未运行' };
};
onMounted(() => {
refreshData();
timer.value = setInterval(refreshData, 5000); // 每5秒轮询
});
onUnmounted(() => {
if (timer.value) clearInterval(timer.value);
});
</script>
<template>
<a-layout style="width: 100%; background: transparent;">
<!-- 安全模式告警横幅 -->
<a-alert v-if="systemStore.safeMode?.enabled" type="error" show-icon style="margin-bottom: 16px;" closable>
<template #message>
<span style="font-weight: 600;"> 安全模式</span>
</template>
<template #description>
<div>
<p style="margin-bottom: 8px;">
服务因初始化失败进入安全模式OpenAI API 不可用
</p>
<p style="margin-bottom: 8px; color: #cf1322;">
<b>原因</b>{{ systemStore.safeMode.reason }}
</p>
<p style="margin: 0;">
请前往系统设置修改正确的配置后重启服务
</p>
</div>
</template>
</a-alert>
<!-- 响应式布局手机竖向电脑横向 -->
<a-row :gutter="[16, 16]" style="margin-bottom: 24px">
<!-- 系统信息卡片 -->
<a-col :xs="24" :md="12">
<a-card title="系统状态" :bordered="false" style="height: 100%">
<a-space direction="vertical" style="width: 100%" size="middle">
<div style="display: flex; justify-content: space-between;">
<span>
<DesktopOutlined /> 系统版本:
</span>
<b>{{ systemStore.systemVersion }}</b>
</div>
<div style="display: flex; justify-content: space-between;">
<span>
<FieldTimeOutlined /> 运行时间:
</span>
<b>{{ formatUptime(systemStore.uptime) }}</b>
</div>
<div style="display: flex; justify-content: space-between;">
<span>
<ChromeOutlined /> 状态:
</span>
<a-tag :color="getStatusConfig(systemStore.status).color">
{{ getStatusConfig(systemStore.status).text }}
</a-tag>
</div>
<div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>
<LineChartOutlined /> CPU 使用率:
</span>
<span>{{ systemStore.cpuUsage }}%</span>
</div>
<a-progress :percent="systemStore.cpuUsage"
:stroke-color="getLoadColor(systemStore.cpuUsage)" :show-info="false" />
</div>
<div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>
<PieChartOutlined /> 内存使用:
</span>
<span>{{ formatMemory(systemStore.memoryUsage.used) }} / {{
formatMemory(systemStore.memoryUsage.total) }}</span>
</div>
<a-progress
:percent="Math.round((systemStore.memoryUsage.used / systemStore.memoryUsage.total) * 100) || 0"
:stroke-color="getLoadColor((systemStore.memoryUsage.used / systemStore.memoryUsage.total) * 100)"
:show-info="false" />
</div>
</a-space>
</a-card>
</a-col>
<!-- 统计数据卡片 -->
<a-col :xs="24" :md="12">
<a-card title="业务统计" :bordered="false" style="height: 100%">
<a-row :gutter="16" style="margin-bottom: 24px">
<a-col :span="12">
<a-statistic title="窗口数量" :value="systemStore.stats.workers || 0">
<template #suffix>
<span style="font-size: 14px; color: #8c8c8c;"></span>
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic title="实例数量" :value="systemStore.stats.instances || 0">
<template #suffix>
<span style=" font-size: 14px; color: #8c8c8c;"></span>
</template>
</a-statistic>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-statistic title="正在进行" :value="queueStats.processing">
<template #suffix>
<span style="font-size: 14px; color: #8c8c8c;">/ {{ queueStats.total }}</span>
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic title="等待排队" :value="queueStats.waiting">
<template #suffix>
<span style="font-size: 14px; color: #8c8c8c;">/ {{ queueStats.total }}</span>
</template>
</a-statistic>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px">
<a-col :span="12">
<a-statistic title="今日成功" :value="systemStore.stats.success || 0">
<template #prefix>
<CheckCircleOutlined style="color: #52c41a" />
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic title="今日失败" :value="systemStore.stats.failed || 0">
<template #prefix>
<CloseCircleOutlined style="color: #ff4d4f" />
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
<!-- 任务队列列表 -->
<a-card title="任务队列实时监控" :bordered="false" style="width: 100%" :bodyStyle="{ padding: '0 24px' }">
<template #extra>
<div style="color: #8c8c8c; font-size: 12px;">
<SyncOutlined :spin="true" style="margin-right: 4px" /> 实时刷新中
</div>
</template>
<a-list item-layout="horizontal" :data-source="queueData">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :description="`ID: ${item.id}`">
<template #title>
<span style="font-weight: 500; margin-right: 8px;">{{ item.model }}</span>
<a-tag v-if="item.worker" color="blue">{{ item.worker }}</a-tag>
</template>
</a-list-item-meta>
<div>
<a-tag v-if="item.status === 'processing'" color="processing">
<template #icon>
<SyncOutlined :spin="true" />
</template>
进行中
</a-tag>
<a-tag v-else-if="item.status === 'waiting'" color="warning">
<template #icon>
<ExclamationCircleOutlined />
</template>
等待中
</a-tag>
<a-tag v-else-if="item.status === 'success'" color="success">
<template #icon>
<CheckCircleOutlined />
</template>
已完成
</a-tag>
</div>
</a-list-item>
</template>
<div v-if="queueData.length === 0" style="text-align: center; padding: 24px; color: #8c8c8c;">
暂无任务
</div>
</a-list>
</a-card>
</a-layout>
</template>