first commit
This commit is contained in:
commit
243376e785
24
.gitignore
vendored
Executable file
24
.gitignore
vendored
Executable 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
24
index.html
Executable 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
BIN
public/favicon.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
108
server.py
Executable file
108
server.py
Executable 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
113
src/components/LicenseBar.js
Executable 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
33
src/components/QueueDisplay.js
Executable 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
41
src/main.js
Executable 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
49
src/services/licenseService.js
Executable 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
17
src/services/queueService.js
Executable 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
26
src/utils/dataTransformer.js
Executable 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
10
src/utils/timeConverter.js
Executable 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
12
src/utils/timeFormatter.js
Executable 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
217
style.css
Executable 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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user