Files
Xiaji-go/templates/channels.html

283 lines
11 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>渠道配置 - 虾记</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f0f2f5; color: #333; min-height: 100vh; }
.header { background: linear-gradient(135deg, #ff6b6b, #ee5a24); color: #fff; padding: 20px; position: sticky; top: 0; z-index: 100; box-shadow: 0 2px 10px rgba(0,0,0,.15); display:flex; justify-content:space-between; align-items:center; gap:10px; }
.header a { color: #fff; text-decoration:none; background: rgba(255,255,255,.2); padding:6px 10px; border-radius:8px; font-size:13px; }
.header a:hover { background: rgba(255,255,255,.35); }
.wrap { max-width: 600px; margin: 0 auto; padding: 15px; }
.toolbar { display:flex; gap:8px; margin-bottom:10px; flex-wrap:wrap; }
.tip { color:#6b7280; font-size:12px; margin-bottom:10px; }
.card { background:#fff; border-radius:12px; padding:14px; margin-bottom:10px; box-shadow:0 1px 4px rgba(0,0,0,.05); }
.row { display:flex; gap:8px; align-items:center; margin:8px 0; flex-wrap:wrap; }
.row label { width:110px; color:#4b5563; font-size:13px; }
.row input, .row textarea { flex:1; min-width:220px; padding:8px; border:1px solid #ddd; border-radius:8px; font-size:13px; }
.row textarea { min-height:74px; }
.btns { display:flex; gap:8px; margin-top:8px; flex-wrap:wrap; }
button { border:none; border-radius:8px; padding:8px 12px; cursor:pointer; color:#fff; font-size:13px; }
button:disabled { opacity:.65; cursor:not-allowed; }
.btn-apply, .btn-save, .btn-publish, .btn-test, .btn-reload, .btn-enable { background:#ee5a24; }
.btn-apply:hover, .btn-save:hover, .btn-publish:hover, .btn-test:hover, .btn-reload:hover, .btn-enable:hover { background:#d63031; }
.btn-disable { background:#9b2c2c; }
.btn-disable:hover { background:#7f1d1d; }
.badge { display:inline-block; font-size:12px; border-radius:999px; padding:2px 8px; }
.state { display:inline-block; font-size:12px; border-radius:999px; padding:2px 8px; margin-left:6px; }
.ok { background:#dcfce7; color:#166534; }
.error { background:#fee2e2; color:#991b1b; }
.disabled { background:#e5e7eb; color:#374151; }
small { color:#6b7280; }
@media(max-width:640px){
.header { padding: 14px 12px 10px; align-items:flex-start; flex-direction:column; }
.header > div:last-child { display:flex; gap:8px; }
.wrap { padding:12px; }
.toolbar button { flex: 1 1 calc(50% - 4px); }
.row label { width:100%; }
.row input, .row textarea { min-width:100%; }
.btns button { flex: 1 1 calc(50% - 4px); }
}
</style>
</head>
<body>
<div class="header">
<div>🦞 渠道配置中心(草稿/发布) · {{.version}}</div>
<div><a href="/">返回首页</a><a href="/logout">退出</a></div>
</div>
<div class="wrap">
<div class="toolbar">
<button class="btn-disable" onclick="disableAll()">一键全部关闭</button>
<button class="btn-reload" onclick="reloadRuntime()">热加载运行参数</button>
</div>
<div class="tip">默认推荐:直接点“保存并立即生效”。高级场景再用“保存草稿 / 发布草稿 / 热加载”。</div>
<div id="app"></div>
</div>
<script>
const app = document.getElementById('app');
function renderError(msg) {
app.innerHTML = `<div class="card" style="border:1px solid #fecaca;background:#fef2f2;color:#991b1b;">${msg}</div>`;
}
function pretty(objStr) {
try { return JSON.stringify(JSON.parse(objStr || '{}'), null, 2); } catch { return '{}'; }
}
function statusBadge(status) {
const s = (status || 'disabled');
const cls = s === 'ok' ? 'ok' : (s === 'error' ? 'error' : 'disabled');
return `<span class="badge ${cls}">${s}</span>`;
}
function runtimeState(ch) {
if (!ch.enabled) return '<span class="state disabled">已关闭</span>';
if ((ch.status || '').toLowerCase() === 'ok') return '<span class="state ok">运行中</span>';
if ((ch.status || '').toLowerCase() === 'error') return '<span class="state error">配置异常</span>';
return '<span class="state disabled">待检测</span>';
}
function parseJSONSafe(text) {
try { return JSON.parse(text || '{}'); } catch { return null; }
}
async function fetchChannels() {
const r = await fetch('/api/v1/admin/channels');
if (!r.ok) {
throw new Error('加载渠道失败: HTTP ' + r.status);
}
const data = await r.json();
if (!Array.isArray(data)) {
throw new Error('渠道返回格式异常');
}
return data;
}
function render(channels) {
app.innerHTML = channels.map(ch => {
const draftCfg = pretty(ch.draft_config_json || ch.config_json);
const draftSec = pretty(ch.draft_secrets || ch.secrets);
return `<div class="card" data-platform="${ch.platform}">
<h3>${ch.name || ch.platform} ${statusBadge(ch.status)} ${runtimeState(ch)} ${ch.has_draft ? '<span class="badge" style="background:#fef3c7;color:#92400e">draft</span>' : ''}</h3>
<small>平台:${ch.platform} 发布:${ch.published_at || '-'} 最近检测:${ch.last_check_at || '-'}</small>
<div class="row"><label>启用</label><input type="checkbox" class="enabled" ${ch.enabled ? 'checked' : ''}></div>
<div class="row"><label>显示名称</label><input class="name" value="${(ch.name||'').replace(/"/g,'&quot;')}"></div>
<div class="row"><label>草稿 config(JSON)</label><textarea class="config">${draftCfg}</textarea></div>
<div class="row"><label>草稿 secrets(JSON)</label><textarea class="secrets">${draftSec}</textarea></div>
<div class="btns">
<button class="btn-apply" onclick="applyNow('${ch.platform}')">保存并立即生效</button>
<button class="btn-save" onclick="saveDraft('${ch.platform}')">保存草稿</button>
<button class="btn-publish" onclick="publishDraft('${ch.platform}')">发布草稿</button>
<button class="btn-test" onclick="testConn('${ch.platform}')">测试连接</button>
${ch.enabled ? `<button class="btn-disable" onclick="toggleChannel('${ch.platform}', false)">关闭通道</button>` : `<button class="btn-enable" onclick="toggleChannel('${ch.platform}', true)">开启通道</button>`}
</div>
<small class="msg"></small>
</div>`;
}).join('');
}
function collectChannelForm(platform) {
const card = document.querySelector(`[data-platform="${platform}"]`);
const name = card.querySelector('.name').value.trim();
const enabled = card.querySelector('.enabled').checked;
const configText = card.querySelector('.config').value;
const secretsText = card.querySelector('.secrets').value;
const msg = card.querySelector('.msg');
const config = parseJSONSafe(configText);
const secrets = parseJSONSafe(secretsText);
if (!config || !secrets) {
msg.textContent = 'JSON 格式错误,请检查 config/secrets';
return null;
}
return { card, msg, payload: { name, enabled, config, secrets } };
}
async function apiJSON(url, options = {}) {
const r = await fetch(url, options);
const out = await r.json().catch(() => ({}));
if (!r.ok) {
throw new Error(out.error || ('HTTP ' + r.status));
}
return out;
}
function msgOf(platform) {
return document.querySelector(`[data-platform="${platform}"] .msg`);
}
function setCardBusy(card, busy) {
if (!card) return;
card.querySelectorAll('button').forEach(btn => { btn.disabled = busy; });
}
async function applyNow(platform) {
const f = collectChannelForm(platform);
if (!f) return;
const { card, msg, payload } = f;
const applyBtn = card.querySelector('.btn-apply');
const oldText = applyBtn ? applyBtn.textContent : '';
setCardBusy(card, true);
if (applyBtn) applyBtn.textContent = '生效中...';
try {
msg.textContent = '保存并生效中...';
await apiJSON('/api/v1/admin/channels/' + platform + '/apply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
msg.textContent = '已生效';
await reload();
} catch (e) {
msg.textContent = '失败:' + (e && e.message ? e.message : e);
} finally {
setCardBusy(card, false);
if (applyBtn) applyBtn.textContent = oldText || '保存并立即生效';
}
}
async function saveDraft(platform) {
const f = collectChannelForm(platform);
if (!f) return;
const { msg, payload } = f;
try {
await apiJSON('/api/v1/admin/channels/' + platform, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
msg.textContent = '草稿已保存';
await reload();
} catch (e) {
msg.textContent = '保存失败:' + (e && e.message ? e.message : e);
}
}
async function publishDraft(platform) {
const msg = msgOf(platform);
try {
await apiJSON('/api/v1/admin/channels/' + platform + '/publish', { method: 'POST' });
msg.textContent = '发布成功,建议点“热加载运行参数”';
await reload();
} catch (e) {
msg.textContent = '发布失败:' + (e && e.message ? e.message : e);
}
}
async function testConn(platform) {
const msg = msgOf(platform);
try {
msg.textContent = '正在测试...';
const out = await apiJSON('/api/v1/admin/channels/' + platform + '/test', { method: 'POST' });
msg.textContent = `测试结果:${out.status} ${out.detail ? ' / ' + out.detail : ''}`;
await reload();
} catch (e) {
msg.textContent = '测试失败:' + (e && e.message ? e.message : e);
}
}
async function toggleChannel(platform, enable) {
const msg = msgOf(platform);
try {
msg.textContent = enable ? '正在开启...' : '正在关闭...';
await apiJSON('/api/v1/admin/channels/' + platform + (enable ? '/enable' : '/disable'), { method: 'POST' });
msg.textContent = enable ? '已开启(请点热加载生效)' : '已关闭(请点热加载生效)';
await reload();
} catch (e) {
msg.textContent = (enable ? '开启失败:' : '关闭失败:') + (e && e.message ? e.message : e);
}
}
async function reloadRuntime() {
try {
const out = await apiJSON('/api/v1/admin/channels/reload', { method: 'POST' });
alert('热加载成功:' + (out.detail || 'ok'));
await reload();
} catch (e) {
alert('热加载失败:' + (e && e.message ? e.message : e));
}
}
async function disableAll() {
if (!confirm('确认要关闭所有通道吗?')) return;
try {
const out = await apiJSON('/api/v1/admin/channels/disable-all', { method: 'POST' });
alert('已关闭通道数:' + (out.affected || 0) + ',请点热加载生效。');
await reload();
} catch (e) {
alert('批量关闭失败:' + (e && e.message ? e.message : e));
}
}
async function reload() {
try {
const channels = await fetchChannels();
render(channels);
} catch (e) {
console.error(e);
renderError('页面加载失败:' + (e && e.message ? e.message : e));
}
}
window.addEventListener('error', (e) => {
renderError('前端脚本异常:' + (e && e.message ? e.message : 'unknown'));
});
window.addEventListener('unhandledrejection', (e) => {
const msg = e && e.reason && e.reason.message ? e.reason.message : String(e.reason || 'unknown');
renderError('前端请求异常:' + msg);
});
reload();
</script>
</body>
</html>