feat(HEL-560): 数据中枢接管数据源/模型池/会员,注册改一次性邀请码
主站 - 新增 m0006 invite_codes 迁移;注册强制邀请码(首个管理员除外),消码与建号 同一事务,并发提交只有一个能成功 - 新增 /api/hub-admin/* 服务端点(共享 HUB_ADMIN_TOKEN,先于鉴权校验),供数据 中枢桥接读写会话/密码/模型池/会员/邀请码,并提供供应商模型列表拉取 - 前端:注册表单加邀请码(桌面 login、index.html、移动端);「系统管理」改为 「数据中枢」入口指向 8766,原模型池与会员管理分区移除,仅留「行情管理」; 随之清理陈旧 CSS 数据中枢 - 取消独立账号:删除 hub_admin/hub_sessions 与登录、改密、锁定逻辑,改为校验 主站 xiaobai_session,仅管理员可进,CSRF 由会话派生,危险操作二次确认走主站 - 控制台新增数据源凭证可编辑区(原有内容一项不删)、供应商制模型池(自动拉取 /models,失败退回卡内手动录入)、会员管理与邀请码页 - 日夜双主题:颜色收敛为同名 token 换值,SVG 改用 inline style 以吃到变量 自测 - 主站 verify_baseline 通过(498 项);数据中枢 235 项通过 - tools/verify_datahub_console.py 端到端跑通两服务真实对话; tools/verify_datahub_console_ui.py 浏览器跑通门禁/凭证/模型池/会员/主题/1030 窄屏 Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
+12
-188
@@ -39,21 +39,26 @@ async function openAdminSettings(refreshOnly = false) {
|
||||
const payload = await apiRequest("/api/admin/settings");
|
||||
const data = payload.data || {};
|
||||
const ifind = data.ifind || {};
|
||||
const llm = payload.llm || {};
|
||||
const membership = payload.membership || {};
|
||||
status.textContent = `数据中枢 ${data.configured ? "已连接" : "未连接"} · iFinD ${ifind.configured ? "已配置" : "未配置"} · ${number(data.snapshot_dates)} 个交易日`;
|
||||
status.classList.toggle("connected", Boolean(data.configured));
|
||||
setText("systemDataStatus", data.background_refresh_enabled ? "后台刷新已启用" : "后台刷新已暂停");
|
||||
renderDatahubRouteStatus(data.datahub || {});
|
||||
document.querySelector("#systemBackgroundRefresh").checked = Boolean(data.background_refresh_enabled);
|
||||
document.querySelector("#memberDailyLimit").value = number(membership.member_daily_limit) || 50;
|
||||
renderModelPool(llm.models || [], llm.primary_model_id || "", llm.fallback_model_id || "");
|
||||
renderAdminUsers(payload.users || []);
|
||||
} catch (error) {
|
||||
status.textContent = error.message || "系统配置读取失败";
|
||||
}
|
||||
}
|
||||
|
||||
function dataHubConsoleUrl() {
|
||||
if (window.XIAOBAI_DATAHUB_URL) return String(window.XIAOBAI_DATAHUB_URL);
|
||||
return `${window.location.protocol}//${window.location.hostname}:8766/admin/`;
|
||||
}
|
||||
|
||||
function openDataHubConsole() {
|
||||
if (state.user?.role !== "admin") return;
|
||||
window.open(dataHubConsoleUrl(), "_blank", "noopener");
|
||||
}
|
||||
|
||||
function renderDatahubRouteStatus(hub) {
|
||||
const box = document.querySelector("#datahubRouteStatus");
|
||||
if (!box) return;
|
||||
@@ -74,129 +79,6 @@ function renderDatahubRouteStatus(hub) {
|
||||
}
|
||||
}
|
||||
|
||||
function selectAdminPanel(panel) {
|
||||
const selected = ["market", "models", "members"].includes(panel) ? panel : "market";
|
||||
document.querySelector("#adminSectionSelect").value = selected;
|
||||
document.querySelectorAll("[data-admin-panel]").forEach((item) => {
|
||||
item.hidden = item.dataset.adminPanel !== selected;
|
||||
});
|
||||
}
|
||||
|
||||
function renderModelPool(models, primaryId = "", fallbackId = "") {
|
||||
state.adminModels = models.map((item) => ({ ...item, api_key: item.api_key || "" }));
|
||||
const container = document.querySelector("#modelPoolList");
|
||||
container.innerHTML = state.adminModels.map((item, index) => `
|
||||
<article class="model-pool-row" data-model-id="${escapeHtml(item.id)}">
|
||||
<div class="model-pool-heading"><strong>${escapeHtml(item.name || `模型 ${index + 1}`)}</strong><span>${item.configured ? "已保存密钥" : "待配置"}</span></div>
|
||||
<div class="model-pool-fields">
|
||||
<label class="form-field"><span>显示名称 *</span><input data-model-field="name" maxlength="50" value="${escapeHtml(item.name || "")}" required></label>
|
||||
<label class="form-field"><span>API Base URL *</span><input data-model-field="base_url" type="url" value="${escapeHtml(item.base_url || "https://api.openai.com/v1")}" required></label>
|
||||
<label class="form-field"><span>模型标识 *</span><input data-model-field="model" maxlength="100" value="${escapeHtml(item.model || "")}" required></label>
|
||||
<label class="form-field"><span>API Key${item.configured ? "" : " *"}</span><input data-model-field="api_key" type="password" autocomplete="off" maxlength="300" placeholder="${item.configured ? "留空保留已保存的 Key" : "输入 API Key"}" ${item.configured ? "" : "required"}></label>
|
||||
</div>
|
||||
<div class="model-test-row"><button class="button" type="button" data-test-model>测试连接</button><span class="model-test-status" aria-live="polite">未测试</span><button class="icon-button model-delete-button" type="button" data-delete-model aria-label="删除模型" title="删除模型"><i data-lucide="trash-2"></i></button></div>
|
||||
</article>
|
||||
`).join("") || emptyStateHtml("模型池为空,请先添加模型");
|
||||
updateModelRoleOptions(primaryId, fallbackId);
|
||||
container.querySelectorAll("[data-test-model]").forEach((button) => button.addEventListener("click", () => testPlatformModel(button.closest("[data-model-id]"))));
|
||||
container.querySelectorAll("[data-delete-model]").forEach((button) => button.addEventListener("click", () => deletePlatformModel(button.closest("[data-model-id]"))));
|
||||
container.querySelectorAll("[data-model-field='name']").forEach((input) => input.addEventListener("input", updateModelRoleLabels));
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
function collectModelPool() {
|
||||
const saved = new Map(state.adminModels.map((item) => [item.id, item]));
|
||||
return [...document.querySelectorAll("#modelPoolList [data-model-id]")].map((row) => ({
|
||||
id: row.dataset.modelId,
|
||||
name: row.querySelector("[data-model-field='name']").value.trim(),
|
||||
base_url: row.querySelector("[data-model-field='base_url']").value.trim(),
|
||||
model: row.querySelector("[data-model-field='model']").value.trim(),
|
||||
api_key: row.querySelector("[data-model-field='api_key']").value.trim(),
|
||||
configured: Boolean(saved.get(row.dataset.modelId)?.configured),
|
||||
}));
|
||||
}
|
||||
|
||||
function updateModelRoleOptions(primaryId = document.querySelector("#platformPrimaryModelSelect").value, fallbackId = document.querySelector("#platformFallbackModelSelect").value) {
|
||||
const models = collectModelPool();
|
||||
const options = models.map((item) => `<option value="${escapeHtml(item.id)}">${escapeHtml(item.name || item.model || "未命名模型")}</option>`).join("");
|
||||
const primary = document.querySelector("#platformPrimaryModelSelect");
|
||||
const fallback = document.querySelector("#platformFallbackModelSelect");
|
||||
primary.innerHTML = models.length ? options : '<option value="">暂无模型</option>';
|
||||
fallback.innerHTML = `<option value="">不启用辅助模型</option>${options}`;
|
||||
primary.value = models.some((item) => item.id === primaryId) ? primaryId : models[0]?.id || "";
|
||||
fallback.value = models.some((item) => item.id === fallbackId) && fallbackId !== primary.value ? fallbackId : "";
|
||||
}
|
||||
|
||||
function updateModelRoleLabels() {
|
||||
updateModelRoleOptions();
|
||||
}
|
||||
|
||||
function addPlatformModel() {
|
||||
const models = collectModelPool();
|
||||
const id = `model-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
models.push({ id, name: `模型 ${models.length + 1}`, base_url: "https://api.openai.com/v1", model: "", api_key: "", configured: false });
|
||||
renderModelPool(models, document.querySelector("#platformPrimaryModelSelect").value || id, document.querySelector("#platformFallbackModelSelect").value);
|
||||
document.querySelector(`[data-model-id="${CSS.escape(id)}"] [data-model-field="name"]`)?.focus();
|
||||
}
|
||||
|
||||
function deletePlatformModel(row) {
|
||||
if (!row) return;
|
||||
const id = row.dataset.modelId;
|
||||
const primary = document.querySelector("#platformPrimaryModelSelect").value;
|
||||
const fallback = document.querySelector("#platformFallbackModelSelect").value;
|
||||
if (id === primary || id === fallback) {
|
||||
showToast("请先为主模型或辅助模型选择其他模型,再删除当前模型");
|
||||
return;
|
||||
}
|
||||
const models = collectModelPool().filter((item) => item.id !== id);
|
||||
renderModelPool(models, primary, fallback);
|
||||
}
|
||||
|
||||
function renderAdminUsers(users) {
|
||||
const container = document.querySelector("#adminUsersList");
|
||||
container.innerHTML = users.map((user) => {
|
||||
const admin = user.role === "admin";
|
||||
const member = Boolean(user.membership_subscribed);
|
||||
const identityLabels = [admin ? "管理员" : "", member ? "会员有效" : "普通用户"].filter(Boolean).join(" · ");
|
||||
const expiry = member
|
||||
? (user.membership_expires_at ? `有效至 ${membershipDateDisplay(user.membership_expires_at)}` : "永久有效")
|
||||
: user.membership_status === "suspended"
|
||||
? "会员已停用"
|
||||
: user.membership_status === "active" && user.membership_expires_at
|
||||
? `已于 ${membershipDateDisplay(user.membership_expires_at)} 到期`
|
||||
: "尚未开通";
|
||||
return `<article class="admin-user-row" data-admin-user="${number(user.id)}">
|
||||
<div class="admin-user-identity"><strong>${escapeHtml(user.username)}</strong><span>${escapeHtml(identityLabels)}</span><small>${escapeHtml(expiry)}</small></div>
|
||||
<div class="admin-user-usage">今日调用 <b>${number(user.used_today)}</b></div>
|
||||
<form class="membership-form">
|
||||
<input type="hidden" name="user_id" value="${number(user.id)}">
|
||||
<label><span>状态</span><select name="status"><option value="inactive" ${user.membership_status === "inactive" ? "selected" : ""}>未开通</option><option value="active" ${user.membership_status === "active" ? "selected" : ""}>有效</option><option value="suspended" ${user.membership_status === "suspended" ? "selected" : ""}>停用</option></select></label>
|
||||
<label><span>开通 / 续期时长</span><select name="duration"><option value="">选择时长</option><option value="1_month">1个月</option><option value="3_months">3个月</option><option value="12_months">12个月</option><option value="3_years">3年</option><option value="permanent">永久</option></select></label>
|
||||
<div class="membership-expiry"><span>当前到期</span><strong>${escapeHtml(expiry)}</strong></div>
|
||||
<button class="button" type="submit">应用</button>
|
||||
</form>
|
||||
</article>`;
|
||||
}).join("") || emptyStateHtml("暂无注册用户");
|
||||
container.querySelectorAll(".membership-form").forEach((form) => form.addEventListener("submit", saveMembership));
|
||||
}
|
||||
|
||||
async function saveMembership(event) {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget;
|
||||
const data = Object.fromEntries(new FormData(form).entries());
|
||||
const button = form.querySelector("button[type='submit']");
|
||||
button.disabled = true;
|
||||
try {
|
||||
const payload = await apiRequest("/api/admin/membership", "POST", data);
|
||||
renderAdminUsers(payload.users || []);
|
||||
showToast("会员状态已更新");
|
||||
} catch (error) {
|
||||
showToast(error.message || "会员状态保存失败");
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMarketSettings(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget.querySelector("button[type='submit']");
|
||||
@@ -214,70 +96,12 @@ async function saveMarketSettings(event) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveModelPool(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget.querySelector("button[type='submit']");
|
||||
button.disabled = true;
|
||||
try {
|
||||
await apiRequest("/api/admin/settings", "POST", {
|
||||
models: collectModelPool(),
|
||||
primary_model_id: document.querySelector("#platformPrimaryModelSelect").value,
|
||||
fallback_model_id: document.querySelector("#platformFallbackModelSelect").value,
|
||||
});
|
||||
showToast("模型池已保存");
|
||||
await openAdminSettings(true);
|
||||
} catch (error) {
|
||||
showToast(error.message || "模型池保存失败");
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMembershipSettings(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget.querySelector("button[type='submit']");
|
||||
button.disabled = true;
|
||||
try {
|
||||
await apiRequest("/api/admin/settings", "POST", {
|
||||
member_daily_limit: number(document.querySelector("#memberDailyLimit").value),
|
||||
});
|
||||
showToast("会员调用额度已保存");
|
||||
await openAdminSettings(true);
|
||||
} catch (error) {
|
||||
showToast(error.message || "会员调用额度保存失败");
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testPlatformModel(row) {
|
||||
if (!row) return;
|
||||
const button = row.querySelector("[data-test-model]");
|
||||
const status = row.querySelector(".model-test-status");
|
||||
const profile = collectModelPool().find((item) => item.id === row.dataset.modelId) || {};
|
||||
button.disabled = true;
|
||||
status.textContent = "连接中";
|
||||
try {
|
||||
const payload = await apiRequest("/api/admin/settings/test", "POST", { model_id: row.dataset.modelId, profile });
|
||||
status.textContent = `已连通 · ${number(payload.result.latency_ms)} ms`;
|
||||
status.className = "model-test-status success";
|
||||
} catch (error) {
|
||||
status.textContent = error.message;
|
||||
status.className = "model-test-status failure";
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function bindAdminEvents() {
|
||||
document.querySelector("#settingsButton").addEventListener("click", () => openAdminSettings());
|
||||
document.querySelector("#settingsButton").addEventListener("click", openDataHubConsole);
|
||||
document.querySelector("#marketAdminButton").addEventListener("click", () => openAdminSettings());
|
||||
document.querySelector("#closeAdminDialog").addEventListener("click", () => elements.adminDialog.close());
|
||||
document.querySelector("#backfillButton").addEventListener("click", backfillData);
|
||||
document.querySelector("#adminSectionSelect").addEventListener("change", (event) => selectAdminPanel(event.target.value));
|
||||
document.querySelector("#systemMarketForm").addEventListener("submit", saveMarketSettings);
|
||||
document.querySelector("#systemModelsForm").addEventListener("submit", saveModelPool);
|
||||
document.querySelector("#membershipSettingsForm").addEventListener("submit", saveMembershipSettings);
|
||||
document.querySelector("#addPlatformModel").addEventListener("click", addPlatformModel);
|
||||
document.querySelector("#adminRefreshButton").addEventListener("click", startAdminRefresh);
|
||||
}
|
||||
|
||||
@@ -125,50 +125,6 @@
|
||||
width: min(1060px, -24px + 100vw);
|
||||
}
|
||||
|
||||
.admin-section-picker {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 96px minmax(220px, 360px);
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.admin-section-picker label {
|
||||
color: var(--text-secondary);
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-section-picker select {
|
||||
width: 100%;
|
||||
|
||||
min-height: 32px;
|
||||
|
||||
padding: 0px 11px;
|
||||
|
||||
border: 1px solid var(--border-strong);
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
background: var(--surface);
|
||||
|
||||
color: var(--text-primary);
|
||||
|
||||
font-size: var(--font-size-label);
|
||||
}
|
||||
|
||||
.admin-section-picker select:focus-visible {
|
||||
outline: 2px solid var(--action);
|
||||
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.admin-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@@ -220,161 +176,6 @@
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.compact-number-field {
|
||||
width: min(260px, 100%);
|
||||
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.admin-users-list {
|
||||
display: grid;
|
||||
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.admin-user-row {
|
||||
min-width: 0px;
|
||||
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 160px 100px minmax(0px, 1fr);
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 14px;
|
||||
|
||||
padding: 13px 0px;
|
||||
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.admin-user-identity strong {
|
||||
display: block;
|
||||
|
||||
color: var(--text-primary);
|
||||
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-user-identity small,
|
||||
.admin-user-identity span {
|
||||
display: block;
|
||||
|
||||
margin-top: 4px;
|
||||
|
||||
color: var(--text-muted);
|
||||
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.admin-user-usage {
|
||||
margin-top: 4px;
|
||||
|
||||
color: var(--text-muted);
|
||||
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.membership-form {
|
||||
padding: 0px;
|
||||
|
||||
min-width: 0px;
|
||||
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 100px minmax(170px, 0.8fr) minmax(160px, 1fr) auto;
|
||||
|
||||
align-items: end;
|
||||
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.membership-form label {
|
||||
min-width: 0px;
|
||||
|
||||
display: grid;
|
||||
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.membership-form label span {
|
||||
color: var(--text-muted);
|
||||
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.membership-form input,
|
||||
.membership-form select {
|
||||
width: 100%;
|
||||
|
||||
min-width: 0px;
|
||||
|
||||
height: 32px;
|
||||
|
||||
padding: 0px 8px;
|
||||
|
||||
border: 1px solid var(--border-strong);
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.membership-expiry {
|
||||
min-width: 0px;
|
||||
|
||||
display: grid;
|
||||
|
||||
gap: 5px;
|
||||
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.membership-expiry span {
|
||||
color: var(--text-muted);
|
||||
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.membership-expiry strong {
|
||||
min-height: 34px;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
color: var(--text-secondary);
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.admin-user-row {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.admin-user-row .membership-form {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.membership-form {
|
||||
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
||||
}
|
||||
|
||||
.membership-form .button {
|
||||
align-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
||||
.membership-form {
|
||||
grid-template-columns: minmax(0px, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
margin: 18px 18px 0px;
|
||||
|
||||
@@ -399,7 +200,6 @@
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.settings-dialog .membership-form,
|
||||
.settings-dialog .settings-section > form,
|
||||
.settings-dialog.admin-dialog > form {
|
||||
padding: 0px;
|
||||
@@ -559,7 +359,6 @@
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.settings-dialog .membership-form,
|
||||
.settings-dialog .settings-section > form,
|
||||
.settings-dialog.admin-dialog > form {
|
||||
padding: 0px;
|
||||
@@ -1173,12 +972,6 @@ button.account-role-badge:focus-visible {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.admin-section-picker {
|
||||
grid-template-columns: minmax(0px, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.settings-dialog .form-field {
|
||||
width: 100%;
|
||||
@@ -1520,32 +1313,6 @@ button.account-role-badge:focus-visible {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.admin-dialog .admin-section-picker {
|
||||
grid-template-columns: auto minmax(220px, 300px);
|
||||
|
||||
gap: 14px;
|
||||
|
||||
padding: 12px 20px;
|
||||
|
||||
border-bottom: 1px solid var(--dialog-line);
|
||||
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
|
||||
.admin-dialog .admin-section-picker label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.admin-dialog .admin-section-picker select {
|
||||
min-height: 32px;
|
||||
|
||||
border-color: var(--border-strong);
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
font-size: var(--font-size-label);
|
||||
}
|
||||
|
||||
.admin-dialog .admin-panel {
|
||||
max-width: 860px;
|
||||
|
||||
@@ -1556,24 +1323,6 @@ button.account-role-badge:focus-visible {
|
||||
border-top: 1px solid var(--dialog-line);
|
||||
}
|
||||
|
||||
.admin-dialog .admin-users-list {
|
||||
border-color: var(--dialog-line);
|
||||
}
|
||||
|
||||
.admin-dialog .admin-user-row {
|
||||
grid-template-columns: 150px 90px minmax(0px, 1fr);
|
||||
|
||||
gap: 12px;
|
||||
|
||||
padding: 12px 0px;
|
||||
|
||||
border-color: var(--dialog-line);
|
||||
}
|
||||
|
||||
.admin-dialog .membership-form {
|
||||
grid-template-columns: 92px minmax(135px, 0.8fr) minmax(140px, 1fr) auto;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.account-settings-dialog .membership-status-grid {
|
||||
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
||||
@@ -1582,22 +1331,6 @@ button.account-role-badge:focus-visible {
|
||||
.account-settings-dialog .account-birth-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-dialog .admin-section-picker {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-dialog .admin-user-row {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.admin-dialog .admin-user-row .membership-form {
|
||||
grid-column: 1 / -1;
|
||||
|
||||
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
@@ -1612,10 +1345,6 @@ button.account-role-badge:focus-visible {
|
||||
.account-settings-dialog .membership-comparison > div {
|
||||
min-width: 510px;
|
||||
}
|
||||
|
||||
.admin-dialog .admin-user-row .membership-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] :is(.header-command-group, .account-dropdown) {
|
||||
|
||||
@@ -1028,64 +1028,6 @@
|
||||
vertical-align: -1px;
|
||||
}
|
||||
|
||||
.model {
|
||||
padding: 12px 14px;
|
||||
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.model:hover {
|
||||
background: var(--table-hover);
|
||||
}
|
||||
|
||||
.model .r1 {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model .r1 .nm {
|
||||
font-weight: 700;
|
||||
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.model .r1 .lv {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.model .r2 {
|
||||
font-size: 11.5px;
|
||||
|
||||
color: var(--sub);
|
||||
|
||||
margin-top: 4px;
|
||||
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.model .r3 {
|
||||
display: flex;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
margin-top: 6px;
|
||||
|
||||
font-size: 10.5px;
|
||||
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
.model .r3 .q {
|
||||
border-bottom: 1px dashed var(--faint);
|
||||
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] :is(.card-h, .performance-stage-card header, .rotation-card-head, .auction-card-head-v2, .theme-card-head-v2, .dragon-detail-header, .screener-card-head, .mentor-page-header) {
|
||||
border-color: var(--line-soft);
|
||||
|
||||
|
||||
@@ -29,22 +29,6 @@
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.admin-dialog .model-role-selectors {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-dialog .model-pool-list {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.admin-dialog .model-row {
|
||||
border-color: var(--border);
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
|
||||
@keyframes overlay-enter {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
||||
@@ -6,6 +6,8 @@ function selectAuthMode(mode) {
|
||||
const registering = state.authMode === "register";
|
||||
document.querySelector("#authConfirmField").hidden = !registering;
|
||||
document.querySelector("#authPasswordConfirm").required = registering;
|
||||
document.querySelector("#authInviteField").hidden = !registering;
|
||||
document.querySelector("#authInviteCode").required = registering;
|
||||
document.querySelector("#authPassword").autocomplete = registering ? "new-password" : "current-password";
|
||||
document.querySelector("#authSubmitButton").textContent = registering ? "注册并进入" : "登录";
|
||||
document.querySelector("#authError").hidden = true;
|
||||
@@ -24,7 +26,11 @@ async function submitAuthForm(event) {
|
||||
const button = document.querySelector("#authSubmitButton");
|
||||
button.disabled = true;
|
||||
try {
|
||||
const session = await apiRequest(`/api/auth/${state.authMode}`, "POST", { username, password });
|
||||
const registering = state.authMode === "register";
|
||||
const credentials = registering
|
||||
? { username, password, invite_code: document.querySelector("#authInviteCode").value.trim() }
|
||||
: { username, password };
|
||||
const session = await apiRequest(`/api/auth/${state.authMode}`, "POST", credentials);
|
||||
document.querySelector("#authForm").reset();
|
||||
await applyAuthenticatedSession(session);
|
||||
} catch (error) {
|
||||
@@ -42,6 +48,7 @@ async function applyAuthenticatedSession(session) {
|
||||
const isAdmin = session.user?.role === "admin";
|
||||
updateAccountIdentityBadges(session.user?.membership || {});
|
||||
document.querySelector("#settingsButton").hidden = !isAdmin;
|
||||
document.querySelector("#marketAdminButton").hidden = !isAdmin;
|
||||
document.querySelector("#syncButton").hidden = !isAdmin;
|
||||
document.querySelector("#reasonForm").hidden = !isAdmin;
|
||||
document.querySelector("#sectorPhaseManager").hidden = !isAdmin;
|
||||
|
||||
Reference in New Issue
Block a user