first commit

This commit is contained in:
Ekko.bao 2025-01-05 09:59:40 +08:00
commit 243376e785
13 changed files with 674 additions and 0 deletions

24
.gitignore vendored Executable file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
index.html Executable file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SStar VCast License Usage Dashboard</title>
<link rel="icon" href="public/favicon.png" type="image/png">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>SStar VCast License Usage Dashboard</h1>
<div class="dashboard-layout">
<div id="chart-container" class="chart-section"></div>
<div id="queue-container" class="queue-section"></div>
</div>
<div id="update-time" class="update-time"></div>
</div>
<script type="module" src="src/main.js"></script>
</body>
</html>

BIN
public/favicon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

108
server.py Executable file
View File

@ -0,0 +1,108 @@
#!/usr/bin/python3
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import os
from pathlib import Path
sample_data = {
"27003@szmis03": {
"name": "27003@szmis03",
"total": 2,
"used": 2,
"used_info": [
{
"user": "ekko.bao",
"host": "szl3bc1808",
"login_time": "58.9h"
},
{
"user": "zzc",
"host": "szl3bc1808",
"login_time": "58.9h"
}
]
},
"27003@szmis05": {
"name": "27003@szmis05",
"total": 2,
"used": 0,
"used_info": []
},
"27003@szmis10": {
"name": "27003@szmis10",
"total": 2,
"used": 1,
"used_info": [{
"user": "zzc33333",
"host": "szl3bc1808",
"login_time": "58.9h"
}]
}
}
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
return # 重写该方法以禁用日志输出
def do_GET(self):
if self.path == '/vcast/lic_use_info':
self.send_response(200)
self.send_header('Content-type', 'text/json')
self.end_headers()
data = json.dumps(sample_data)
self.wfile.write(data.encode('utf-8'))
return
path = self.path
if path == '/':
path = '/index.html'
path = path.strip('/')
if not self.send_file(path):
self.send_404()
def send_404(self):
self.send_response(404)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(b"<h1>404 Not Found</h1><p>The page you are looking for does not exist.</p>")
def send_file(self, file_path) -> bool:
file_path =str(Path(os.path.abspath(__file__)).parent.joinpath(file_path))
print(f"clinet request: [{file_path}]")
if not os.path.isfile(file_path):
return False
self.send_response(200)
mime_type = self.get_mime_type(file_path)
self.send_header('Content-type', mime_type)
if mime_type.startswith('text/'):
self.send_header('Content-Disposition', f'inline; filename={os.path.basename(file_path)}')
else:
self.send_header('Content-Disposition', f'attachment; filename={os.path.basename(file_path)}')
self.end_headers()
with open(file_path, 'rb') as f:
self.wfile.write(f.read())
return True
def get_mime_type(self, file_path):
ext = os.path.splitext(file_path)[1]
if ext == '.txt':
return 'text/plain'
elif ext == '.js':
return 'application/javascript'
elif ext == '.html':
return 'text/html'
elif ext == '.css':
return 'text/css'
# 添加更多 MIME 类型根据需要
return 'application/octet-stream' # 默认类型
def run(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler, port=8088):
server_address = ('0.0.0.0', port)
httpd = server_class(server_address, handler_class)
print(f'Starting server on {server_address[0]}:{server_address[1]} ...')
httpd.serve_forever()
if __name__ == "__main__":
if len(os.sys.argv) > 1:
run(port=int(os.sys.argv[1]))
else:
run()

113
src/components/LicenseBar.js Executable file
View File

@ -0,0 +1,113 @@
export function createLicenseBar(license, max_duration) {
const container = document.createElement('div');
container.className = 'license-bar';
// License name and slot info
const nameSection = document.createElement('div');
nameSection.className = 'license-info';
nameSection.innerHTML = `
<div class="license-name">${license.licenseName}</div>
<div class="slot-info">${license.usedSlots}/${license.totalSlots} slots</div>
`;
// Usage bars container
const barsContainer = document.createElement('div');
barsContainer.className = 'usage-bars';
// Create bars for each slot
for (let i = 0; i < license.totalSlots; i++) {
const usage = license.usageDetails[i];
const bar = document.createElement('div');
bar.className = 'bar-container';
const barContainer = document.createElement('div');
barContainer.className = 'bar-wrapper';
if (!usage) {
const bar = document.createElement('div');
bar.className = 'bar free';
const label = document.createElement('div');
label.className = 'bar-label';
label.textContent = 'AVAILABLE';
bar.appendChild(label);
bar.style.width = '100%';
requestAnimationFrame(() => {
label.style.transform = 'translate(0%)';
});
} else {
const bar = document.createElement('div');
bar.className = 'bar';
const width = usage.duration / max_duration * 100;
// 创建标签容器
const labelContainer = document.createElement('div');
labelContainer.className = 'label-container';
// 用户信息标签
const userLabel = document.createElement('div');
userLabel.className = 'user-label';
userLabel.textContent = usage.userName;
// 时长标签
const durationLabel = document.createElement('div');
durationLabel.className = 'duration-label';
durationLabel.textContent = `${usage.duration.toFixed(1)}h`;
if (width < 50) {
labelContainer.appendChild(durationLabel);
bar.appendChild(userLabel);
} else {
bar.appendChild(userLabel);
labelContainer.appendChild(durationLabel);
}
// const label = document.createElement('div');
// label.className = 'bar-label';
// label.textContent = `${usage.userName} (${usage.duration}h)`;
// // const hostLabel = document.createElement('div');
// // hostLabel.className = 'user_info-label';
// // hostLabel.textContent = ``;
// // // hostLabel.textContent = `${usage.userName}@${usage.hostName}`;
// // barElement.appendChild(hostLabel);
// barElement.appendChild(label);
// 将标签添加到bar容器中
barContainer.appendChild(labelContainer);
barContainer.appendChild(bar);
requestAnimationFrame(() => {
const user_width = userLabel.offsetWidth;
const duration_width = labelContainer.offsetWidth
const duration_per = duration_width / bar.offsetWidth;
bar.style.width = `${width}%`;
// 根据宽度决定标签位置
if (width + duration_per >= 100) { // 如果超出bar的范围则放在里面
labelContainer.classList.add('inside');
} else {
labelContainer.classList.remove('outside');
labelContainer.classList.add('outside');
// const per = user_width * 100 / bar.offsetWidth;
const rect1 = userLabel.getBoundingClientRect();
const rect2 = durationLabel.getBoundingClientRect();
// 检查重叠
const isOverlapping = !(rect1.right < rect2.left ||
rect1.left > rect2.right);
if (isOverlapping) {
// 如果重叠,将 element2 移动到 element1 的右侧
console.log("trace", rect1.right)
labelContainer.style.left = `${userLabel.offsetLeft + userLabel.offsetWidth + 10}px`; // 10px 的间距
} else {
labelContainer.style.left = `${width + max_duration / 200}%`
}
}
});
}
barsContainer.appendChild(barContainer);
}
container.appendChild(nameSection);
container.appendChild(barsContainer);
return container;
}

33
src/components/QueueDisplay.js Executable file
View File

@ -0,0 +1,33 @@
export function createQueueDisplay(queueData) {
const container = document.createElement('div');
container.className = 'queue-container';
const title = document.createElement('h2');
title.textContent = '排队情况';
title.className = 'queue-title';
container.appendChild(title);
if (!queueData || queueData.length === 0) {
const emptyMessage = document.createElement('p');
emptyMessage.className = 'queue-empty';
emptyMessage.textContent = '当前无人排队';
container.appendChild(emptyMessage);
return container;
}
const queueList = document.createElement('div');
queueList.className = 'queue-list';
queueData.forEach(item => {
const queueItem = document.createElement('div');
queueItem.className = 'queue-item';
queueItem.innerHTML = `
<span class="queue-user">${item.userName}</span>
<span class="queue-time">${item.waitTime}</span>
`;
queueList.appendChild(queueItem);
});
container.appendChild(queueList);
return container;
}

41
src/main.js Executable file
View File

@ -0,0 +1,41 @@
import { fetchLicenseData } from './services/licenseService.js';
import { fetchQueueData } from './services/queueService.js';
import { createLicenseBar } from './components/LicenseBar.js';
import { createQueueDisplay } from './components/QueueDisplay.js';
import { formatDateTime } from './utils/timeFormatter.js';
function updateLastRefreshTime() {
const updateTimeElement = document.getElementById('update-time');
updateTimeElement.textContent = `Last Update: ${formatDateTime(new Date())}`;
}
async function renderChart() {
const [licenseData, queueData] = await Promise.all([
fetchLicenseData(),
fetchQueueData()
]);
const max_duration = licenseData.reduce((max, license) => {
return Math.max(max, license.maxDuration);
}, 0);
// 渲染许可证使用情况
const chartContainer = document.getElementById('chart-container');
chartContainer.innerHTML = '';
licenseData.forEach(license => {
const licenseBar = createLicenseBar(license, max_duration);
chartContainer.appendChild(licenseBar);
});
// 渲染排队情况
const queueContainer = document.getElementById('queue-container');
queueContainer.innerHTML = '';
queueContainer.appendChild(createQueueDisplay(queueData));
updateLastRefreshTime();
}
// 初始渲染
renderChart();
// 每1分钟更新一次数据
// setInterval(renderChart, 10000);

49
src/services/licenseService.js Executable file
View File

@ -0,0 +1,49 @@
import { transformLicenseData } from '../utils/dataTransformer.js';
// 测试用的模拟数据
const mockData = {
"27003@szmis03": {
"name": "27003@szmis03",
"total": 2,
"used": 2,
"used_info": [
{
"user": "ekko.bao",
"host": "szl3bc1808",
"login_time": "3.4h"
},
{
"user": "zzc",
"host": "szl3bc1808",
"login_time": "6.9h"
}
]
},
"27003@szmis04": {
"name": "27003@szmis04",
"total": 2,
"used": 0,
"used_info": []
},
"27003@szmis10": {
"name": "27003@szmis10",
"total": 2,
"used": 1,
"used_info": [{
"user": "zzc33333",
"host": "szl3bc1808",
"login_time": "58.9h"
}]
}
};
export async function fetchLicenseData() {
try {
const response = await fetch('/vcast/lic_use_info');
const data = await response.json();
return transformLicenseData(data);
} catch (error) {
console.error('Error fetching license data:', error);
return transformLicenseData(mockData);
}
}

17
src/services/queueService.js Executable file
View File

@ -0,0 +1,17 @@
// 测试用的模拟队列数据
const mockQueueData = [
{ userName: "Alice Chen", waitTime: "15分钟" },
{ userName: "Bob Wang", waitTime: "8分钟" },
{ userName: "Charlie Liu", waitTime: "3分钟" }
];
export async function fetchQueueData() {
try {
const response = await fetch('/vcast/queue_info');
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching queue data:', error);
return mockQueueData;
}
}

26
src/utils/dataTransformer.js Executable file
View File

@ -0,0 +1,26 @@
import { convertTimeToMinutes } from './timeConverter.js';
// 计算许可证的最大使用时长
function getMaxDuration(license) {
if (!license.used_info.length) return 0;
return Math.max(...license.used_info.map(info =>
convertTimeToMinutes(info.login_time)
));
}
export function transformLicenseData(data) {
// 转换并排序数据
return Object.values(data)
.map(license => ({
licenseName: license.name,
totalSlots: license.total,
usedSlots: license.used,
maxDuration: getMaxDuration(license),
usageDetails: license.used_info.map(info => ({
userName: info.user,
hostName: info.host,
duration: convertTimeToMinutes(info.login_time)
}))
}))
.sort((a, b) => b.maxDuration - a.maxDuration); // 按最大使用时长降序排序
}

10
src/utils/timeConverter.js Executable file
View File

@ -0,0 +1,10 @@
// 时间转换工具
export function convertTimeToMinutes(timeStr) {
const match = timeStr.match(/(\d+\.?\d*)([hm])/);
if (!match) return 0;
const value = parseFloat(match[1]);
const unit = match[2];
return unit === 'h' ? value : value / 60.0;
}

12
src/utils/timeFormatter.js Executable file
View File

@ -0,0 +1,12 @@
// Format date to locale string with seconds
export function formatDateTime(date) {
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}

217
style.css Executable file
View File

@ -0,0 +1,217 @@
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.license-bar {
display: flex;
margin-bottom: 15px;
background-color: white;
border-radius: 4px;
padding: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.license-info {
margin-left: 10px;
display: flex;
flex-direction: column;
margin-right: 10px;
}
.license-name {
margin-top: 20px;
font-size: 16px;
font-weight: bold;
color: #2c3e50;
display: flex;
align-items: center;
}
.slot-info {
font-size: 12px;
color: #666;
}
.usage-bars {
display: flex;
flex-direction: column;
height: 60px;
gap: 6px;
flex-grow: 1;
margin-right: 5px;
margin-top: 5px;
margin-bottom: 5px;
}
.bar-wrapper {
position: relative;
height: 32px;
background-color: #f8f9fa;
border-radius: 4px;
overflow: visible;
/* 允许标签显示在外部 */
}
.bar-container {
flex: 1;
background-color: #eee;
border-radius: 4px;
overflow: hidden;
}
.bar {
width: 100%;
height: 100%;
background-color: #4caf50;
display: flex;
transition: width 1s ease;
white-space: nowrap;
align-items: center;
justify-content: space-between;
border-radius: 4px;
}
.bar.free {
background-color: transparent;
border: 2px dashed #4caf50;
box-sizing: border-box;
}
.label-container {
position: absolute;
display: flex;
align-items: center;
gap: 8px;
height: 100%;
transition: all 0.3s ease;
pointer-events: none;
white-space: nowrap;
}
.label-container.outside {
color: #2c3e50;
}
.label-container.inside {
right: 8px;
color: #fff;
}
.bar-label {
color: #212121;
font-size: 16px;
white-space: nowrap;
margin: 10px;
margin-left: 10%;
}
.user-label {
margin: 10px;
color: rgba(33, 33, 33, 0.7);
font-size: 14px;
display: inline;
}
.duration-label {
font-size: 12px;
opacity: 0.9;
display: inline;
}
.free .bar-label {
color: #4caf50;
width: 100%;
text-align: center;
transform: translateY(100%);
transition: transform 1s ease;
}
.update-time {
text-align: center;
color: #666;
margin-top: 20px;
font-size: 14px;
}
.dashboard-layout {
display: flex;
gap: 20px;
}
.chart-section {
flex: 3;
background-color: #f8f9fa;
border-radius: 8px;
padding: 15px;
}
.queue-section {
flex: 1;
min-width: 250px;
}
.queue-container {
background-color: #f8f9fa;
border-radius: 8px;
padding: 15px;
}
.queue-title {
font-size: 1.2em;
color: #2c3e50;
margin: 0 0 15px 0;
padding-bottom: 10px;
border-bottom: 1px solid #e1e4e8;
}
.queue-empty {
color: #666;
text-align: center;
font-style: italic;
margin: 20px 0;
}
.queue-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.queue-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: white;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.queue-user {
font-weight: 500;
color: #2c3e50;
}
.queue-time {
color: #666;
font-size: 0.9em;
}