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