Files
V2_micropython/websocket_server/templates/admin.html
jeremygan2021 88bb27569a
All checks were successful
Deploy WebSocket Server / deploy (push) Successful in 4s
mode bug
2026-03-20 18:04:44 +08:00

365 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Image Generator Admin</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; padding: 20px; }
.container { max-width: 1100px; margin: 0 auto; }
h1 { text-align: center; margin-bottom: 30px; color: #00d4ff; }
.card { background: #16213e; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
.card h2 { margin-bottom: 16px; color: #00d4ff; font-size: 18px; }
.current-status { background: #0f3460; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
.current-status .label { color: #888; font-size: 14px; }
.current-status .value { color: #00d4ff; font-size: 20px; font-weight: bold; margin-top: 4px; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 8px; color: #ccc; }
select, input[type="number"] { width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #333; background: #0f3460; color: #fff; font-size: 16px; }
select:focus, input:focus { outline: none; border-color: #00d4ff; }
input[type="checkbox"] { width: 20px; height: 20px; margin-right: 8px; }
.btn { display: inline-block; padding: 12px 24px; border-radius: 8px; border: none; cursor: pointer; font-size: 16px; font-weight: bold; transition: all 0.3s; }
.btn-primary { background: #00d4ff; color: #1a1a2e; }
.btn-primary:hover { background: #00b8e6; }
.btn-danger { background: #e74c3c; color: #fff; }
.btn-danger:hover { background: #c0392b; }
.btn-small { padding: 6px 12px; font-size: 14px; }
.model-list { margin-top: 16px; }
.model-item { background: #0f3460; padding: 12px 16px; border-radius: 8px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; }
.model-item.active { border: 2px solid #00d4ff; }
.model-item .name { font-weight: bold; }
.model-item .provider { color: #888; font-size: 14px; }
.test-section { margin-top: 20px; }
.test-input { width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #333; background: #0f3460; color: #fff; font-size: 14px; resize: vertical; min-height: 80px; margin-bottom: 12px; }
.test-input:focus { outline: none; border-color: #00d4ff; }
.message { padding: 12px; border-radius: 8px; margin-top: 12px; display: none; }
.message.success { background: #27ae60; display: block; }
.message.error { background: #e74c3c; display: block; }
.loading { text-align: center; padding: 20px; display: none; }
.loading.show { display: block; }
.spinner { border: 3px solid #333; border-top: 3px solid #00d4ff; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.auto-delete-settings { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.auto-delete-settings label { display: flex; align-items: center; color: #ccc; }
.auto-delete-settings input[type="number"] { width: 80px; }
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-top: 16px; }
.gallery-item { background: #0f3460; border-radius: 8px; overflow: hidden; position: relative; }
.gallery-item img { width: 100%; height: 180px; object-fit: cover; display: block; }
.gallery-item .info { padding: 12px; }
.gallery-item .filename { font-size: 12px; color: #888; word-break: break-all; }
.gallery-item .size { font-size: 12px; color: #666; margin-top: 4px; }
.gallery-item .delete-btn { position: absolute; top: 8px; right: 8px; background: rgba(231,76,60,0.9); color: white; border: none; border-radius: 50%; width: 28px; height: 28px; cursor: pointer; font-size: 16px; line-height: 28px; }
.gallery-item .delete-btn:hover { background: #c0392b; }
.empty-gallery { text-align: center; padding: 40px; color: #666; }
.flex-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.tab-nav { display: flex; gap: 4px; margin-bottom: 20px; background: #0f3460; border-radius: 8px; padding: 4px; }
.tab-nav button { flex: 1; padding: 12px; border: none; background: transparent; color: #888; cursor: pointer; border-radius: 6px; font-size: 16px; transition: all 0.3s; }
.tab-nav button.active { background: #00d4ff; color: #1a1a2e; font-weight: bold; }
.tab-content { display: none; }
.tab-content.active { display: block; }
</style>
</head>
<body>
<div class="container">
<h1>AI Image Generator Admin</h1>
<div class="tab-nav">
<button class="active" onclick="showTab('settings')">设置</button>
<button onclick="showTab('gallery')">图片库</button>
</div>
<div id="tab-settings" class="tab-content active">
<div class="card">
<h2>当前状态</h2>
<div class="current-status">
<div class="label">当前 Provider</div>
<div class="value" id="currentProvider">加载中...</div>
<div class="label" style="margin-top: 12px;">当前模型</div>
<div class="value" id="currentModel">加载中...</div>
</div>
</div>
<div class="card">
<h2>切换 Provider</h2>
<div class="form-group">
<select id="providerSelect">
<option value="doubao">豆包 (Doubao)</option>
<option value="dashscope">阿里云 (DashScope)</option>
</select>
</div>
<button class="btn btn-primary" onclick="switchProvider()">切换 Provider</button>
</div>
<div class="card">
<h2>豆包模型</h2>
<div class="model-list">
<div class="model-item" data-provider="doubao" data-model="doubao-seedream-4.0">
<div>
<div class="name">doubao-seedream-4.0</div>
<div class="provider">豆包</div>
</div>
<button class="btn btn-primary" onclick="setModel('doubao', 'doubao-seedream-4.0')">使用</button>
</div>
<div class="model-item" data-provider="doubao" data-model="doubao-seedream-5-0-260128">
<div>
<div class="name">doubao-seedream-5-0-260128</div>
<div class="provider">豆包</div>
</div>
<button class="btn btn-primary" onclick="setModel('doubao', 'doubao-seedream-5-0-260128')">使用</button>
</div>
</div>
</div>
<div class="card">
<h2>阿里云模型 (DashScope)</h2>
<div class="model-list">
<div class="model-item" data-provider="dashscope" data-model="wanx2.0-t2i-turbo">
<div>
<div class="name">wanx2.0-t2i-turbo</div>
<div class="provider">阿里云</div>
</div>
<button class="btn btn-primary" onclick="setModel('dashscope', 'wanx2.0-t2i-turbo')">使用</button>
</div>
<div class="model-item" data-provider="dashscope" data-model="qwen-image-plus">
<div>
<div class="name">qwen-image-plus</div>
<div class="provider">阿里云</div>
</div>
<button class="btn btn-primary" onclick="setModel('dashscope', 'qwen-image-plus')">使用</button>
</div>
<div class="model-item" data-provider="dashscope" data-model="qwen-image-v1">
<div>
<div class="name">qwen-image-v1</div>
<div class="provider">阿里云</div>
</div>
<button class="btn btn-primary" onclick="setModel('dashscope', 'qwen-image-v1')">使用</button>
</div>
</div>
</div>
<div class="card">
<h2>测试图片生成</h2>
<textarea class="test-input" id="testPrompt" placeholder="输入提示词...">A cute cat, black and white line art, cartoon style</textarea>
<button class="btn btn-primary" onclick="testGenerate()">生成图片</button>
<div class="loading" id="loading">
<div class="spinner"></div>
<p style="margin-top: 10px;">生成中...</p>
</div>
<div class="message" id="message"></div>
<div id="resultArea" style="margin-top: 16px; display: none;">
<img id="resultImage" style="max-width: 100%; max-height: 300px; border-radius: 8px;">
</div>
</div>
</div>
<div id="tab-gallery" class="tab-content">
<div class="card">
<h2>图片库</h2>
<div class="auto-delete-settings">
<label><input type="checkbox" id="autoDeleteEnabled" onchange="updateAutoDelete()"> 自动删除</label>
<label><input type="number" id="autoDeleteHours" min="1" max="168" value="24" onchange="updateAutoDelete()"> 小时后删除</label>
<button class="btn btn-primary btn-small" onclick="loadGallery()">刷新</button>
<button class="btn btn-danger btn-small" onclick="deleteAllImages()">删除全部</button>
</div>
<div class="gallery" id="gallery"></div>
</div>
</div>
</div>
<script>
function showTab(tab) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tab-nav button').forEach(el => el.classList.remove('active'));
document.getElementById('tab-' + tab).classList.add('active');
event.target.classList.add('active');
if (tab === 'gallery') loadGallery();
}
async function loadStatus() {
try {
const res = await fetch('/api/admin/status');
const data = await res.json();
document.getElementById('currentProvider').textContent = data.provider;
document.getElementById('currentModel').textContent = data.model;
document.getElementById('providerSelect').value = data.provider;
updateActiveModel(data.provider, data.model);
} catch (e) {
console.error('Failed to load status:', e);
}
}
async function loadAutoDelete() {
try {
const res = await fetch('/api/admin/auto-delete');
const data = await res.json();
document.getElementById('autoDeleteEnabled').checked = data.enabled;
document.getElementById('autoDeleteHours').value = data.hours;
} catch (e) {
console.error('Failed to load auto-delete settings:', e);
}
}
async function updateAutoDelete() {
const enabled = document.getElementById('autoDeleteEnabled').checked;
const hours = document.getElementById('autoDeleteHours').value;
try {
await fetch('/api/admin/auto-delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled, hours: parseInt(hours) })
});
} catch (e) {
console.error('Failed to update auto-delete:', e);
}
}
function updateActiveModel(provider, model) {
document.querySelectorAll('.model-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.provider === provider && item.dataset.model === model) {
item.classList.add('active');
item.querySelector('button').textContent = '使用中';
item.querySelector('button').disabled = true;
} else {
item.querySelector('button').textContent = '使用';
item.querySelector('button').disabled = false;
}
});
}
async function switchProvider() {
const provider = document.getElementById('providerSelect').value;
try {
const res = await fetch('/api/admin/switch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider })
});
const data = await res.json();
showMessage(data.message, data.success);
if (data.success) loadStatus();
} catch (e) {
showMessage('切换失败: ' + e.message, false);
}
}
async function setModel(provider, model) {
try {
const res = await fetch('/api/admin/model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider, model })
});
const data = await res.json();
showMessage(data.message, data.success);
if (data.success) loadStatus();
} catch (e) {
showMessage('设置失败: ' + e.message, false);
}
}
async function testGenerate() {
const prompt = document.getElementById('testPrompt').value;
if (!prompt) return;
document.getElementById('loading').classList.add('show');
document.getElementById('message').style.display = 'none';
document.getElementById('resultArea').style.display = 'none';
try {
const res = await fetch('/api/admin/test-generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
});
const data = await res.json();
if (data.success && data.image_url) {
document.getElementById('resultImage').src = data.image_url;
document.getElementById('resultArea').style.display = 'block';
showMessage('生成成功', true);
} else {
showMessage(data.message || '生成失败', false);
}
} catch (e) {
showMessage('生成失败: ' + e.message, false);
} finally {
document.getElementById('loading').classList.remove('show');
}
}
async function loadGallery() {
try {
const res = await fetch('/api/admin/images');
const data = await res.json();
const gallery = document.getElementById('gallery');
if (!data.images || data.images.length === 0) {
gallery.innerHTML = '<div class="empty-gallery">暂无图片</div>';
return;
}
gallery.innerHTML = data.images.map(img => `
<div class="gallery-item">
<button class="delete-btn" onclick="deleteImage('${img.name}')">×</button>
<img src="${img.url}" alt="${img.name}" onclick="window.open('${img.url}', '_blank')">
<div class="info">
<div class="filename">${img.name}</div>
<div class="size">${formatSize(img.size)}</div>
</div>
</div>
`).join('');
} catch (e) {
console.error('Failed to load gallery:', e);
}
}
async function deleteImage(filename) {
if (!confirm('确定要删除这张图片吗?')) return;
try {
const res = await fetch(`/api/admin/images/${encodeURIComponent(filename)}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
loadGallery();
} else {
alert(data.message);
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
async function deleteAllImages() {
if (!confirm('确定要删除所有图片吗?此操作不可恢复!')) return;
try {
const res = await fetch('/api/admin/images');
const data = await res.json();
for (const img of data.images) {
await fetch(`/api/admin/images/${encodeURIComponent(img.name)}`, { method: 'DELETE' });
}
loadGallery();
} catch (e) {
alert('删除失败: ' + e.message);
}
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function showMessage(msg, success) {
const el = document.getElementById('message');
el.textContent = msg;
el.className = 'message ' + (success ? 'success' : 'error');
}
loadStatus();
loadAutoDelete();
</script>
</body>
</html>