-
+
@@ -307,7 +387,7 @@
-
+
|
{{ formatDate(record.timestamp).split(' ')[0] }}
{{ formatDate(record.timestamp).split(' ')[1] }}
@@ -320,9 +400,15 @@
|
- {{ record.prompt }}
+ {{ record.prompt }}
-
+
+
+ 优化后 Prompt
+
+ {{ record.final_prompt }}
+
+
{{ record.details }}
@@ -344,13 +430,13 @@
|
-
+
|
|
@@ -396,12 +482,14 @@
{{ file.name }}
+ {{ formatBytes(file.size) }}
{{ file.name }}
+ {{ formatBytes(file.size) }}
@@ -440,6 +528,134 @@
+
+
+
+
+
+
无法获取 GPU 信息
+
{{ gpuStatus.error || '未检测到 NVIDIA GPU 或 nvidia-smi 不可用' }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ gpuStatus.gpu_util }}%
+
+
+
+
+
+
+
+
+
+
{{ (gpuStatus.mem_used / 1024).toFixed(2) }}GB
+
of {{ (gpuStatus.mem_total / 1024).toFixed(2) }} GB
+
+
+
+
+
+
+
+
{{ gpuStatus.temperature }}°C
+
+
+ {{ gpuStatus.temperature > 80 ? 'High' : 'Normal' }}
+
+
+
+
+
+
+
+
{{ gpuStatus.power_draw }}W
+
Limit: {{ gpuStatus.power_limit }} W
+
+
+
+
+
+
+
+
+
+ GPU & Memory Utilization
+
+
+
+
+
+
+ Temperature & Power
+
+
+
+
+
+
+
+
+
设备详细信息
+
+
+
+
Product Name
+
{{ gpuStatus.name }}
+
+
+
Driver Version
+
{{ gpuStatus.driver_version }}
+
+
+
CUDA Version
+
{{ gpuStatus.cuda_version }}
+
+
+
Data Source
+
+
+ {{ gpuStatus.source }}
+
+
+
+
+
+
+
@@ -568,29 +784,107 @@
const availableModels = ref([]);
const prompts = ref({});
const cleanupConfig = ref({ enabled: true, lifetime: 3600, interval: 600 });
+ const gpuStatus = ref({});
+ const gpuHistory = ref([]);
+ let gpuInterval = null;
+
+ // Filters
+ const selectedTimeRange = ref('all');
+ const selectedType = ref('all');
// Charts Refs
const barChartRef = ref(null);
const pieChartRef = ref(null);
+ const promptPieChartRef = ref(null);
+ const promptBarChartRef = ref(null);
+ const wordCloudRef = ref(null);
+ const gpuUtilChartRef = ref(null);
+ const gpuTempChartRef = ref(null);
+
let barChartInst = null;
let pieChartInst = null;
+ let promptPieChartInst = null;
+ let promptBarChartInst = null;
+ let wordCloudInst = null;
+ let gpuUtilChartInst = null;
+ let gpuTempChartInst = null;
+
+ // Computed Properties
+ const uniqueTypes = computed(() => {
+ const types = new Set(history.value.map(h => h.type));
+ return Array.from(types);
+ });
+
+ const filteredHistory = computed(() => {
+ let filtered = history.value;
+ const now = Date.now() / 1000; // current timestamp in seconds
+
+ // Filter by Time Range
+ if (selectedTimeRange.value !== 'all') {
+ let cutoff = 0;
+ if (selectedTimeRange.value === '24h') cutoff = now - 24 * 3600;
+ if (selectedTimeRange.value === '7d') cutoff = now - 7 * 24 * 3600;
+ if (selectedTimeRange.value === '30d') cutoff = now - 30 * 24 * 3600;
+ filtered = filtered.filter(h => h.timestamp >= cutoff);
+ }
+
+ // Filter by Type
+ if (selectedType.value !== 'all') {
+ filtered = filtered.filter(h => h.type === selectedType.value);
+ }
+
+ return filtered;
+ });
- // Stats Computed
const stats = computed(() => {
- if (!history.value.length) return { totalCount: 0, successRate: 0, avgDuration: 0 };
+ const data = filteredHistory.value;
+ if (!data.length) return { totalCount: 0, successRate: 0, avgDuration: 0, todayCount: 0, failCount: 0, maxDuration: 0, lastActivity: '-' };
- const total = history.value.length;
- const success = history.value.filter(h => h.status === 'success').length;
- const totalDuration = history.value.reduce((acc, curr) => acc + (curr.duration || 0), 0);
- const countWithDuration = history.value.filter(h => h.duration).length;
+ const total = data.length;
+ const success = data.filter(h => h.status === 'success').length;
+ const fail = total - success;
+ const totalDuration = data.reduce((acc, curr) => acc + (curr.duration || 0), 0);
+ const countWithDuration = data.filter(h => h.duration).length;
+ const maxDuration = data.reduce((max, curr) => Math.max(max, curr.duration || 0), 0);
+
+ // Today's Count
+ const todayStart = new Date();
+ todayStart.setHours(0,0,0,0);
+ const todayTs = todayStart.getTime() / 1000;
+ const todayCount = data.filter(h => h.timestamp >= todayTs).length;
+
+ // Last Activity
+ const lastTs = data.length > 0 ? Math.max(...data.map(h => h.timestamp)) : 0;
+ const lastActivity = lastTs ? new Date(lastTs * 1000).toLocaleTimeString() : '-';
return {
totalCount: total,
- successRate: Math.round((success / total) * 100),
- avgDuration: countWithDuration ? (totalDuration / countWithDuration).toFixed(2) : 0
+ successRate: total ? Math.round((success / total) * 100) : 0,
+ avgDuration: countWithDuration ? (totalDuration / countWithDuration).toFixed(2) : 0,
+ todayCount,
+ failCount: fail,
+ maxDuration: maxDuration.toFixed(2),
+ lastActivity
};
});
+ const promptStats = computed(() => {
+ const counts = {};
+ filteredHistory.value.forEach(h => {
+ if (h.prompt) {
+ const p = h.prompt.trim();
+ counts[p] = (counts[p] || 0) + 1;
+ }
+ });
+
+ // Sort by count desc
+ const sorted = Object.entries(counts)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 10); // Top 10
+
+ return sorted.map(([name, value]) => ({ name, value }));
+ });
+
// --- Auth ---
const checkLogin = () => {
const token = localStorage.getItem('admin_token');
@@ -663,6 +957,7 @@
currentModel.value = res.data.current_qwen_model;
availableModels.value = res.data.available_qwen_models;
if (res.data.cleanup_config) cleanupConfig.value = res.data.cleanup_config;
+ if (res.data.gpu_status) gpuStatus.value = res.data.gpu_status;
} catch (e) { console.error(e); }
};
@@ -673,6 +968,119 @@
} catch (e) { console.error(e); }
};
+ // --- GPU Monitoring ---
+ const fetchGpuStatus = async () => {
+ try {
+ const res = await axios.get('/admin/api/gpu/status');
+ const data = res.data;
+ gpuStatus.value = data;
+
+ // Update History
+ const now = new Date();
+ const timeStr = now.toLocaleTimeString();
+
+ if (gpuHistory.value.length > 60) gpuHistory.value.shift();
+ gpuHistory.value.push({
+ time: timeStr,
+ gpu_util: data.gpu_util,
+ mem_util: data.mem_util,
+ temp: data.temperature,
+ power: data.power_draw
+ });
+
+ updateGpuCharts();
+ } catch (e) { console.error(e); }
+ };
+
+ const startGpuMonitoring = () => {
+ if (gpuInterval) clearInterval(gpuInterval);
+ // Use setTimeout to ensure DOM is ready after v-if switch
+ setTimeout(() => {
+ initGpuCharts();
+ fetchGpuStatus(); // Initial fetch
+ gpuInterval = setInterval(fetchGpuStatus, 2000); // Every 2s
+ }, 300);
+ };
+
+ const stopGpuMonitoring = () => {
+ if (gpuInterval) {
+ clearInterval(gpuInterval);
+ gpuInterval = null;
+ }
+ };
+
+ const initGpuCharts = () => {
+ if (!gpuUtilChartRef.value || !gpuTempChartRef.value) return;
+
+ const commonOption = {
+ tooltip: { trigger: 'axis' },
+ grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+ xAxis: { type: 'category', boundaryGap: false, data: [] },
+ yAxis: { type: 'value', min: 0, max: 100 },
+ animation: false
+ };
+
+ // Util Chart
+ if (gpuUtilChartInst) gpuUtilChartInst.dispose();
+ gpuUtilChartInst = echarts.init(gpuUtilChartRef.value);
+ gpuUtilChartInst.setOption({
+ ...commonOption,
+ legend: { data: ['GPU Core', 'Memory Controller'] },
+ series: [
+ { name: 'GPU Core', type: 'line', smooth: true, showSymbol: false, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#3b82f6' }, data: [] },
+ { name: 'Memory Controller', type: 'line', smooth: true, showSymbol: false, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#a855f7' }, data: [] }
+ ]
+ });
+
+ // Temp/Power Chart
+ if (gpuTempChartInst) gpuTempChartInst.dispose();
+ gpuTempChartInst = echarts.init(gpuTempChartRef.value);
+ gpuTempChartInst.setOption({
+ tooltip: { trigger: 'axis' },
+ grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+ legend: { data: ['Temperature (°C)', 'Power (W)'] },
+ xAxis: { type: 'category', boundaryGap: false, data: [] },
+ yAxis: [
+ { type: 'value', name: 'Temp', min: 0, max: 100, position: 'left' },
+ { type: 'value', name: 'Power', min: 0, position: 'right' }
+ ],
+ animation: false,
+ series: [
+ { name: 'Temperature (°C)', type: 'line', smooth: true, showSymbol: false, itemStyle: { color: '#f97316' }, yAxisIndex: 0, data: [] },
+ { name: 'Power (W)', type: 'line', smooth: true, showSymbol: false, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#22c55e' }, yAxisIndex: 1, data: [] }
+ ]
+ });
+
+ // Render initial data if available
+ updateGpuCharts();
+ };
+
+ const updateGpuCharts = () => {
+ if (!gpuUtilChartInst || !gpuTempChartInst) return;
+
+ const times = gpuHistory.value.map(h => h.time);
+ const gpuUtils = gpuHistory.value.map(h => h.gpu_util);
+ const memUtils = gpuHistory.value.map(h => h.mem_util);
+ const temps = gpuHistory.value.map(h => h.temp);
+ const powers = gpuHistory.value.map(h => h.power);
+
+ gpuUtilChartInst.setOption({
+ xAxis: { data: times },
+ series: [
+ { data: gpuUtils },
+ { data: memUtils }
+ ]
+ });
+
+ gpuTempChartInst.setOption({
+ xAxis: { data: times },
+ series: [
+ { data: temps },
+ { data: powers }
+ ]
+ });
+ };
+
// --- Actions ---
const saveCleanupConfig = async () => {
try {
@@ -729,11 +1137,24 @@
// --- Navigation & Helpers ---
const switchTab = (tab) => {
+ const prevTab = currentTab.value;
currentTab.value = tab;
+
+ // GPU Monitor Logic
+ if (tab === 'gpu') {
+ startGpuMonitoring();
+ } else if (prevTab === 'gpu') {
+ stopGpuMonitoring();
+ }
+
if (tab === 'files') fetchFiles();
if (tab === 'dashboard') {
fetchHistory();
- nextTick(initCharts);
+ // Wait for transition to complete before initializing charts
+ // CSS transition is around 0.3s-0.4s
+ setTimeout(() => {
+ nextTick(initCharts);
+ }, 400);
}
};
@@ -770,6 +1191,15 @@
const isImage = (name) => /\.(jpg|jpeg|png|gif|webp)$/i.test(name);
const formatDate = (ts) => new Date(ts * 1000).toLocaleString();
+ const formatBytes = (bytes, decimals = 2) => {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const dm = decimals < 0 ? 0 : decimals;
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+ };
+
const getPromptDescription = (key) => {
const map = {
'translate': 'Prompt 翻译 (CN->EN)',
@@ -797,7 +1227,8 @@
'history': '识别记录',
'files': '文件资源管理',
'prompts': '提示词工程',
- 'settings': '全局配置'
+ 'settings': '全局配置',
+ 'gpu': 'GPU 监控'
};
return map[tab];
};
@@ -808,75 +1239,193 @@
'history': '所有视觉识别任务的历史流水',
'files': '查看和管理生成的图像及JSON结果',
'prompts': '调整各个识别场景的 System Prompt',
- 'settings': '系统环境参数与自动清理策略'
+ 'settings': '系统环境参数与自动清理策略',
+ 'gpu': '实时监控 GPU 核心、显存、温度与功耗'
};
return map[tab];
};
// --- Charts ---
const initCharts = () => {
- if (!barChartRef.value || !pieChartRef.value) return;
+ const data = filteredHistory.value;
+
+ // 1. Common Chart Config
+ const commonGrid = { left: '3%', right: '4%', bottom: '3%', containLabel: true };
- // 1. Pie Chart Data
- const typeCounts = {};
- history.value.forEach(h => {
- typeCounts[h.type] = (typeCounts[h.type] || 0) + 1;
- });
- const pieData = Object.keys(typeCounts).map(k => ({ value: typeCounts[k], name: k }));
-
// 2. Bar Chart Data (Group by Date)
const dateCounts = {};
- // Reverse history to get chronological order for chart
- const sortedHistory = [...history.value].sort((a, b) => a.timestamp - b.timestamp);
+ // Sort by timestamp asc for chart
+ const sortedHistory = [...data].sort((a, b) => a.timestamp - b.timestamp);
sortedHistory.forEach(h => {
const date = new Date(h.timestamp * 1000).toLocaleDateString();
dateCounts[date] = (dateCounts[date] || 0) + 1;
});
- const barCategories = Object.keys(dateCounts);
- const barData = Object.values(dateCounts);
+
+ if (barChartRef.value) {
+ if (barChartInst) barChartInst.dispose();
+ barChartInst = echarts.init(barChartRef.value);
+ barChartInst.setOption({
+ tooltip: { trigger: 'axis' },
+ grid: commonGrid,
+ xAxis: { type: 'category', data: Object.keys(dateCounts) },
+ yAxis: { type: 'value' },
+ series: [{
+ data: Object.values(dateCounts),
+ type: 'bar',
+ barWidth: '40%',
+ itemStyle: { color: '#3b82f6', borderRadius: [4, 4, 0, 0] }
+ }]
+ });
+ }
- // Render Pie
- if (pieChartInst) pieChartInst.dispose();
- pieChartInst = echarts.init(pieChartRef.value);
- pieChartInst.setOption({
- tooltip: { trigger: 'item' },
- legend: { bottom: '0%', left: 'center' },
- color: ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b'],
- series: [{
- name: '识别类型',
- type: 'pie',
- radius: ['40%', '70%'],
- avoidLabelOverlap: false,
- itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
- label: { show: false, position: 'center' },
- emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
- data: pieData
- }]
- });
+ // 3. Pie Chart (Type Distribution)
+ const typeCounts = {};
+ data.forEach(h => { typeCounts[h.type] = (typeCounts[h.type] || 0) + 1; });
+ const pieData = Object.keys(typeCounts).map(k => ({ value: typeCounts[k], name: k }));
- // Render Bar
- if (barChartInst) barChartInst.dispose();
- barChartInst = echarts.init(barChartRef.value);
- barChartInst.setOption({
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
- grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
- xAxis: { type: 'category', data: barCategories, axisLine: { show: false }, axisTick: { show: false } },
- yAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed' } } },
- series: [{
- name: '识别数量',
- type: 'bar',
- barWidth: '40%',
- data: barData,
- itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#3b82f6' }, { offset: 1, color: '#60a5fa' }]), borderRadius: [4, 4, 0, 0] }
- }]
- });
+ if (pieChartRef.value) {
+ if (pieChartInst) pieChartInst.dispose();
+ pieChartInst = echarts.init(pieChartRef.value);
+ pieChartInst.setOption({
+ tooltip: { trigger: 'item' },
+ legend: { bottom: '0%' },
+ series: [{
+ type: 'pie',
+ radius: ['40%', '70%'],
+ itemStyle: { borderRadius: 5, borderColor: '#fff', borderWidth: 2 },
+ data: pieData
+ }]
+ });
+ }
+
+ // 4. Prompt Charts
+ const topPrompts = promptStats.value;
+
+ // Prompt Pie
+ if (promptPieChartRef.value) {
+ if (promptPieChartInst) promptPieChartInst.dispose();
+ promptPieChartInst = echarts.init(promptPieChartRef.value);
+ promptPieChartInst.setOption({
+ tooltip: {
+ trigger: 'item',
+ formatter: function(params) {
+ const val = params.data.name;
+ const shortName = val.length > 50 ? val.substring(0, 50) + '...' : val;
+ return `${params.marker} ${shortName}
${params.value} 次 (${params.percent}%)`;
+ }
+ },
+ legend: { show: false }, // Too long for legend
+ series: [{
+ type: 'pie',
+ radius: '70%',
+ center: ['50%', '50%'],
+ data: topPrompts,
+ itemStyle: { borderRadius: 5 }
+ }]
+ });
+ }
+
+ // Prompt Bar (Horizontal)
+ if (promptBarChartRef.value) {
+ if (promptBarChartInst) promptBarChartInst.dispose();
+ promptBarChartInst = echarts.init(promptBarChartRef.value);
+ promptBarChartInst.setOption({
+ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+ grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+ xAxis: { type: 'value' },
+ yAxis: {
+ type: 'category',
+ data: topPrompts.map(p => p.name.length > 15 ? p.name.substring(0, 15) + '...' : p.name),
+ axisLabel: { interval: 0 }
+ },
+ series: [{
+ type: 'bar',
+ data: topPrompts.map(p => p.value),
+ itemStyle: { color: '#f97316', borderRadius: [0, 4, 4, 0] },
+ label: { show: true, position: 'right' }
+ }]
+ });
+ }
+
+ // 5. Word Cloud
+ if (wordCloudRef.value) {
+ if (wordCloudInst) wordCloudInst.dispose();
+ wordCloudInst = echarts.init(wordCloudRef.value);
+
+ // Calculate word frequencies
+ const wordCounts = {};
+ data.forEach(h => {
+ if (h.prompt) {
+ // Split by space and common separators
+ const words = h.prompt.split(/[\s,,.。!!??]+/);
+ words.forEach(w => {
+ w = w.trim();
+ if (w.length > 1 && !['the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'and', 'or', 'with', 'is', 'are'].includes(w.toLowerCase())) {
+ wordCounts[w] = (wordCounts[w] || 0) + 1;
+ }
+ });
+ }
+ });
+
+ const cloudData = Object.entries(wordCounts)
+ .map(([name, value]) => ({ name, value }))
+ .sort((a, b) => b.value - a.value)
+ .slice(0, 100);
+
+ wordCloudInst.setOption({
+ tooltip: { show: true },
+ series: [{
+ type: 'wordCloud',
+ shape: 'circle',
+ left: 'center',
+ top: 'center',
+ width: '90%',
+ height: '90%',
+ sizeRange: [12, 60],
+ rotationRange: [-90, 90],
+ rotationStep: 45,
+ gridSize: 8,
+ drawOutOfBound: false,
+ layoutAnimation: true,
+ textStyle: {
+ fontFamily: 'sans-serif',
+ fontWeight: 'bold',
+ color: function () {
+ return 'rgb(' + [
+ Math.round(Math.random() * 160),
+ Math.round(Math.random() * 160),
+ Math.round(Math.random() * 160)
+ ].join(',') + ')';
+ }
+ },
+ emphasis: {
+ focus: 'self',
+ textStyle: {
+ shadowBlur: 10,
+ shadowColor: '#333'
+ }
+ },
+ data: cloudData
+ }]
+ });
+ }
};
+ // Watchers for filtering
+ watch([selectedTimeRange, selectedType], () => {
+ nextTick(initCharts);
+ });
+
// Watch window resize for charts
window.addEventListener('resize', () => {
barChartInst && barChartInst.resize();
pieChartInst && pieChartInst.resize();
+ promptPieChartInst && promptPieChartInst.resize();
+ promptBarChartInst && promptBarChartInst.resize();
+ wordCloudInst && wordCloudInst.resize();
+ gpuUtilChartInst && gpuUtilChartInst.resize();
+ gpuTempChartInst && gpuTempChartInst.resize();
});
onMounted(() => {
@@ -893,8 +1442,13 @@
cleanupConfig, saveCleanupConfig,
prompts, savePrompt, getPromptDescription,
getPageTitle, getPageSubtitle,
- isLoading, refreshData, stats,
- barChartRef, pieChartRef
+ isLoading, refreshData,
+ // New/Updated exports
+ stats, filteredHistory, uniqueTypes,
+ selectedTimeRange, selectedType,
+ barChartRef, pieChartRef, promptPieChartRef, promptBarChartRef, wordCloudRef,
+ formatBytes, gpuStatus,
+ gpuUtilChartRef, gpuTempChartRef
};
}
}).mount('#app');
diff --git a/static/results/seg_01ad9bff76274811a6b6e5a8a16cf01f.jpg b/static/results/seg_01ad9bff76274811a6b6e5a8a16cf01f.jpg
new file mode 100644
index 0000000..34d5d9f
Binary files /dev/null and b/static/results/seg_01ad9bff76274811a6b6e5a8a16cf01f.jpg differ