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:
+5
-32
@@ -73,6 +73,7 @@
|
||||
<label class="form-field"><span>账号名</span><input id="authUsername" type="text" minlength="3" maxlength="30" autocomplete="username" required></label>
|
||||
<label class="form-field"><span>密码</span><input id="authPassword" type="password" minlength="8" maxlength="128" autocomplete="current-password" required></label>
|
||||
<label id="authConfirmField" class="form-field" hidden><span>确认密码</span><input id="authPasswordConfirm" type="password" minlength="8" maxlength="128" autocomplete="new-password"></label>
|
||||
<label id="authInviteField" class="form-field" hidden><span>邀请码</span><input id="authInviteCode" type="text" maxlength="32" autocomplete="off" spellcheck="false" placeholder="XB-XXXX-XXXX-XXXX"></label>
|
||||
<p id="authError" class="auth-error" hidden></p>
|
||||
<button id="authSubmitButton" class="button primary" type="submit">登录</button>
|
||||
</form>
|
||||
@@ -156,7 +157,8 @@
|
||||
</div>
|
||||
<button id="refreshButton" class="button command-button" type="button" title="刷新"><i data-lucide="refresh-cw"></i><span>刷新</span></button>
|
||||
<button id="syncButton" class="button primary command-button" type="button" title="后台刷新" hidden><i data-lucide="cloud-download"></i><span>后台刷新</span></button>
|
||||
<button id="settingsButton" class="button command-button" type="button" title="系统管理" hidden><i data-lucide="settings-2"></i><span>系统管理</span></button>
|
||||
<button id="marketAdminButton" class="button command-button" type="button" title="行情管理" hidden><i data-lucide="settings-2"></i><span>行情管理</span></button>
|
||||
<button id="settingsButton" class="button command-button" type="button" title="数据中枢" hidden><i data-lucide="database"></i><span>数据中枢</span></button>
|
||||
<div class="account-menu-shell">
|
||||
<div id="accountRoleBadges" class="account-role-badges" aria-label="账号身份">
|
||||
<span id="accountAdminBadge" class="account-role-badge admin-role-badge" title="管理员" hidden><i data-lucide="shield-check"></i><span>管理员</span></span>
|
||||
@@ -592,18 +594,11 @@
|
||||
|
||||
<dialog id="adminDialog" class="settings-dialog admin-dialog" aria-labelledby="adminDialogTitle">
|
||||
<div class="dialog-header">
|
||||
<div><span class="dialog-eyebrow">管理员</span><h2 id="adminDialogTitle">系统配置</h2></div>
|
||||
<div><span class="dialog-eyebrow">管理员</span><h2 id="adminDialogTitle">行情管理</h2></div>
|
||||
<button id="closeAdminDialog" class="icon-button" type="button" aria-label="关闭" title="关闭"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
<div id="adminConnectionStatus" class="connection-status" aria-live="polite">正在读取系统状态</div>
|
||||
<div class="admin-section-picker">
|
||||
<label for="adminSectionSelect">管理项目</label>
|
||||
<select id="adminSectionSelect">
|
||||
<option value="market">行情管理</option>
|
||||
<option value="models">模型池</option>
|
||||
<option value="members">会员管理</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="form-hint">模型池、会员与邀请码已统一在数据中枢管理,点击顶栏「数据中枢」进入。</p>
|
||||
<div class="admin-panel" data-admin-panel="market">
|
||||
<form id="systemMarketForm" class="settings-section">
|
||||
<div class="settings-section-heading"><h3>公共行情</h3><span id="systemDataStatus">待检查</span></div>
|
||||
@@ -624,28 +619,6 @@
|
||||
<div class="dialog-actions"><button id="backfillButton" class="button" type="button">开始回补</button></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="admin-panel" data-admin-panel="models" hidden>
|
||||
<form id="systemModelsForm" class="settings-section">
|
||||
<div class="settings-section-heading"><h3>平台模型池</h3><span>会员共享</span></div>
|
||||
<div class="model-role-selectors">
|
||||
<label class="form-field"><span>主模型</span><select id="platformPrimaryModelSelect"></select></label>
|
||||
<label class="form-field"><span>辅助模型</span><select id="platformFallbackModelSelect"><option value="">不启用辅助模型</option></select></label>
|
||||
</div>
|
||||
<div id="modelPoolList" class="model-pool-list"></div>
|
||||
<div class="dialog-actions admin-inline-actions"><button id="addPlatformModel" class="button" type="button"><i data-lucide="plus"></i>添加模型</button><button class="button primary" type="submit">保存模型池</button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="admin-panel" data-admin-panel="members" hidden>
|
||||
<form id="membershipSettingsForm" class="settings-section membership-limit-form">
|
||||
<div class="settings-section-heading"><h3>会员调用额度</h3><span>每日自动重置</span></div>
|
||||
<label class="form-field compact-number-field"><span>会员每日智能分析上限</span><input id="memberDailyLimit" type="number" min="1" max="1000" value="50"></label>
|
||||
<div class="dialog-actions"><button class="button primary" type="submit">保存调用额度</button></div>
|
||||
</form>
|
||||
<section class="settings-section">
|
||||
<div class="settings-section-heading"><h3>会员账号</h3><span>手动开通与续期</span></div>
|
||||
<div id="adminUsersList" class="admin-users-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<div id="loadingOverlay" class="loading-overlay" hidden>
|
||||
|
||||
+11
-2
@@ -15,6 +15,7 @@
|
||||
error: "",
|
||||
username: "",
|
||||
password: "",
|
||||
inviteCode: "",
|
||||
passwordVisible: false,
|
||||
};
|
||||
|
||||
@@ -119,6 +120,7 @@
|
||||
`<label class="form-field"><span>账号名</span><input id="loginUsername" type="text" minlength="3" maxlength="30" autocomplete="username" placeholder="请输入账号名" value="${escapeHtml(state.username)}" required></label>`,
|
||||
`<label class="form-field"><span>密码</span><span class="login-password-wrap"><input id="loginPassword" class="${invalid.trim()}" type="${passwordType}" minlength="8" maxlength="128" autocomplete="${registering ? "new-password" : "current-password"}" placeholder="请输入密码" value="${escapeHtml(state.password)}" required><button class="login-password-toggle" type="button" data-login-action="toggle-password" aria-pressed="${state.passwordVisible ? "true" : "false"}" aria-label="${state.passwordVisible ? "隐藏密码" : "显示密码"}">${passwordToggle}</button></span></label>`,
|
||||
`<label class="form-field" id="loginConfirmField"${registering ? "" : " hidden"}><span>确认密码</span><input id="loginPasswordConfirm" type="password" minlength="8" maxlength="128" autocomplete="new-password"${registering ? " required" : ""}></label>`,
|
||||
`<label class="form-field" id="loginInviteField"${registering ? "" : " hidden"}><span>邀请码</span><input id="loginInviteCode" type="text" maxlength="32" autocomplete="off" spellcheck="false" placeholder="XB-XXXX-XXXX-XXXX" value="${escapeHtml(state.inviteCode)}"${registering ? " required" : ""}></label>`,
|
||||
state.error ? `<p class="login-error">${escapeHtml(state.error)}</p>` : '<p class="login-error" hidden></p>',
|
||||
`<button class="button primary login-submit" type="submit"${state.loading ? " disabled" : ""}>`,
|
||||
state.loading ? '<span class="login-spinner" aria-hidden="true"></span>' : "",
|
||||
@@ -289,9 +291,14 @@
|
||||
event.preventDefault();
|
||||
const username = document.querySelector("#loginUsername").value.trim();
|
||||
const password = document.querySelector("#loginPassword").value;
|
||||
const registering = state.mode === "register";
|
||||
const inviteCode = registering
|
||||
? document.querySelector("#loginInviteCode").value.trim()
|
||||
: "";
|
||||
state.username = username;
|
||||
state.password = password;
|
||||
if (state.mode === "register" && password !== document.querySelector("#loginPasswordConfirm").value) {
|
||||
state.inviteCode = inviteCode;
|
||||
if (registering && password !== document.querySelector("#loginPasswordConfirm").value) {
|
||||
setError("两次输入的密码不一致。");
|
||||
render();
|
||||
return;
|
||||
@@ -300,7 +307,9 @@
|
||||
setError("");
|
||||
render();
|
||||
try {
|
||||
await api.request(`/api/auth/${state.mode}`, "POST", { username, password });
|
||||
await api.request(`/api/auth/${state.mode}`, "POST", registering
|
||||
? { username, password, invite_code: inviteCode }
|
||||
: { username, password });
|
||||
await celebrateLogin();
|
||||
enterApp();
|
||||
} catch (error) {
|
||||
|
||||
@@ -52,8 +52,7 @@
|
||||
{ key: "system/profile", label: "账号资料", icon: "user" },
|
||||
{ key: "system/password", label: "修改密码", icon: "lock" },
|
||||
{ key: "system/membership", label: "会员状态", icon: "gem" },
|
||||
{ key: "system/admin", label: "系统设置", icon: "sliders-horizontal", adminOnly: true },
|
||||
{ key: "system/members", label: "会员管理", icon: "users", adminOnly: true }
|
||||
{ key: "system/admin", label: "行情管理", icon: "sliders-horizontal", adminOnly: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
+26
-361
@@ -198,11 +198,7 @@
|
||||
system: {
|
||||
account: null,
|
||||
admin: null,
|
||||
adminTab: "market",
|
||||
models: [],
|
||||
accounts: [],
|
||||
editingModelId: "",
|
||||
editingUserId: "",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3137,7 +3133,6 @@
|
||||
"system/password": setupSystemPage,
|
||||
"system/membership": setupSystemPage,
|
||||
"system/admin": setupSystemPage,
|
||||
"system/members": setupSystemPage,
|
||||
};
|
||||
|
||||
const COMPLEX_LOADERS = {
|
||||
@@ -3160,7 +3155,6 @@
|
||||
"system/password": loadSystem,
|
||||
"system/membership": loadSystem,
|
||||
"system/admin": loadSystem,
|
||||
"system/members": loadSystem,
|
||||
};
|
||||
|
||||
function isComplexPage(key) {
|
||||
@@ -4938,7 +4932,7 @@
|
||||
return '<div class="m-state m-motion-rise-in" data-system-page="forbidden">' +
|
||||
'<span class="m-state-icon">' + icon("lock", 26) + "</span>" +
|
||||
"<p>仅管理员可访问</p>" +
|
||||
"<small>系统设置和会员管理需要管理员权限。</small>" +
|
||||
"<small>行情管理与数据中枢需要管理员权限。</small>" +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
@@ -4974,7 +4968,6 @@
|
||||
state.sort = { key: "", dir: null };
|
||||
state.sortTable = { cols: null, reapply: null };
|
||||
state.detail = null;
|
||||
if (key === "system/admin") state.system.adminTab = state.system.adminTab || "market";
|
||||
document.getElementById("m-view").classList.add("m-view-feature");
|
||||
global.MobileRouter.updateHeader({ title: findLabel(key) || key, back: true, actions: "" });
|
||||
document.getElementById("m-view").innerHTML = complexFrame(key, complexScroll(skeletonHtml(6)));
|
||||
@@ -4982,7 +4975,7 @@
|
||||
|
||||
function loadSystem() {
|
||||
const key = state.key;
|
||||
if ((key === "system/admin" || key === "system/members") && !global.MobileSession.isAdmin()) {
|
||||
if (key === "system/admin" && !global.MobileSession.isAdmin()) {
|
||||
systemFill(systemForbiddenHtml());
|
||||
return;
|
||||
}
|
||||
@@ -4991,15 +4984,12 @@
|
||||
return;
|
||||
}
|
||||
const seq = nextSeq();
|
||||
const url = (key === "system/admin" || key === "system/members") ? "/api/admin/settings" : "/api/account/status";
|
||||
global.MobileAPI.request(url).then(function (payload) {
|
||||
const url = key === "system/admin" ? "/api/admin/settings" : "/api/account/status";
|
||||
return global.MobileAPI.request(url).then(function (payload) {
|
||||
if (seq !== state.seq || state.key !== key) return;
|
||||
if (key === "system/admin" || key === "system/members") {
|
||||
if (key === "system/admin") {
|
||||
state.system.admin = payload || {};
|
||||
state.system.models = ((payload.llm && payload.llm.models) || []).map(function (item) {
|
||||
return Object.assign({}, item);
|
||||
});
|
||||
renderSystemAdmin(key);
|
||||
renderSystemAdmin();
|
||||
} else {
|
||||
state.system.account = payload || {};
|
||||
if (key === "system/profile") renderSystemProfile();
|
||||
@@ -5089,8 +5079,7 @@
|
||||
{ key: "system/membership", icon: "gem", label: "会员状态", hint: "有效期与智能分析额度" }
|
||||
];
|
||||
const adminRows = [
|
||||
{ key: "system/admin", icon: "sliders-horizontal", label: "系统设置", hint: "行情数据 · 模型池" },
|
||||
{ key: "system/members", icon: "users", label: "会员管理", hint: "开通 · 续期 · 额度" }
|
||||
{ key: "system/admin", icon: "sliders-horizontal", label: "行情管理", hint: "数据源状态 · 后台刷新 · 历史回补" }
|
||||
];
|
||||
const html =
|
||||
'<div class="m-sys-home" data-system-page="home">' +
|
||||
@@ -5104,7 +5093,7 @@
|
||||
'<h3 class="m-sys-group-title">偏好</h3>' +
|
||||
'<div class="m-card m-sys-list">' + systemThemeRowHtml() + "</div>" +
|
||||
(global.MobileSession.isAdmin()
|
||||
? '<h3 class="m-sys-group-title">管理员专区</h3><div class="m-card m-sys-list">' + adminRows.map(systemRowHtml).join("") + "</div>"
|
||||
? '<h3 class="m-sys-group-title">管理员专区</h3><div class="m-card m-sys-list">' + adminRows.map(systemRowHtml).join("") + dataHubRowHtml() + "</div>"
|
||||
: "") +
|
||||
'<h3 class="m-sys-group-title">其他</h3>' +
|
||||
'<div class="m-card m-sys-list">' +
|
||||
@@ -5117,7 +5106,7 @@
|
||||
'<span class="m-sys-row-body"><strong>退出登录</strong><small>退出后需要重新登录</small></span>' +
|
||||
'<span class="m-sys-row-chevron">' + icon("chevron-right", 16) + "</span></button>" +
|
||||
"</div>" +
|
||||
'<p class="m-sys-foot">' + (global.MobileSession.isAdmin() ? "小白复盘 · 内网个人版" : "系统设置与会员管理仅管理员可见") + "</p>" +
|
||||
'<p class="m-sys-foot">' + (global.MobileSession.isAdmin() ? "小白复盘 · 内网个人版" : "行情管理与数据中枢仅管理员可见") + "</p>" +
|
||||
"</div>";
|
||||
document.getElementById("m-view").innerHTML = html;
|
||||
}
|
||||
@@ -5207,14 +5196,6 @@
|
||||
systemFill(html);
|
||||
}
|
||||
|
||||
function adminTabHtml() {
|
||||
const tab = state.system.adminTab === "models" ? "models" : "market";
|
||||
return '<div class="m-source-tabs" role="tablist" aria-label="系统设置分类">' +
|
||||
'<button class="m-source-tab' + (tab === "market" ? " active" : "") + '" type="button" role="tab" data-system-admin-tab="market">行情管理</button>' +
|
||||
'<button class="m-source-tab' + (tab === "models" ? " active" : "") + '" type="button" role="tab" data-system-admin-tab="models">模型池</button>' +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
function statusDot(ok) {
|
||||
return '<span class="m-sys-dot' + (ok ? " m-sys-dot--ok" : "") + '"></span>';
|
||||
}
|
||||
@@ -5228,16 +5209,10 @@
|
||||
return " 未配置";
|
||||
}
|
||||
|
||||
function renderSystemAdmin(key) {
|
||||
if (key === "system/members") {
|
||||
renderSystemMembers();
|
||||
return;
|
||||
}
|
||||
function renderSystemAdmin() {
|
||||
const payload = state.system.admin || {};
|
||||
const data = payload.data || {};
|
||||
const ifind = data.ifind || {};
|
||||
const llm = payload.llm || {};
|
||||
const tab = state.system.adminTab === "models" ? "models" : "market";
|
||||
const marketHtml =
|
||||
'<div class="m-sys-body" data-system-admin-panel="market">' +
|
||||
'<div class="m-card m-sys-section"><strong>数据源状态</strong>' +
|
||||
@@ -5263,178 +5238,28 @@
|
||||
formFieldHtml("结束日期", dateInputHtml("m-sys-backfill-end", ""), false) +
|
||||
'<button class="m-btn-outline" type="button" data-system-backfill>开始回补</button>' +
|
||||
'<p class="m-sys-hint">回补用于补齐缺失的历史行情,开始前会再次确认;回补期间页面可正常使用。</p></div>' +
|
||||
'<div class="m-card m-sys-section"><strong>模型池 · 会员 · 邀请码</strong>' +
|
||||
'<p class="m-sys-hint">这些配置已统一在数据中枢管理。</p>' +
|
||||
'<button class="m-btn-outline" type="button" data-system-datahub>打开数据中枢</button></div>' +
|
||||
"</div>";
|
||||
const models = state.system.models || [];
|
||||
const modelsHtml =
|
||||
'<div class="m-sys-body" data-system-admin-panel="models">' +
|
||||
'<div class="m-card m-sys-section"><strong>模型分工</strong>' +
|
||||
formFieldHtml("主模型", '<select id="m-sys-primary-model"></select>', false) +
|
||||
formFieldHtml("辅助模型", '<select id="m-sys-fallback-model"></select>', false) +
|
||||
'<p class="m-sys-hint">主模型不可用时自动改用辅助模型</p>' +
|
||||
'<button class="m-btn-primary" type="button" data-system-save-models>保存分工</button></div>' +
|
||||
'<div class="m-card m-sys-section"><strong>模型池 · ' + models.length + " 个</strong>" +
|
||||
'<div id="m-sys-model-list">' + renderModelPoolHtml(models) + "</div>" +
|
||||
'<p class="m-sys-hint">点任意模型卡片进入编辑:改名称、地址、密钥、测试连接或删除</p>' +
|
||||
'<button class="m-btn-primary" type="button" data-system-add-model>+ 添加模型</button></div>' +
|
||||
"</div>";
|
||||
const page = document.querySelector(".m-page");
|
||||
if (!page) return;
|
||||
page.innerHTML = adminTabHtml() + '<div class="m-scroll" id="m-scroll">' + (tab === "models" ? modelsHtml : marketHtml) + "</div>";
|
||||
if (tab === "models") updateSystemModelRoleOptions(llm.primary_model_id || "", llm.fallback_model_id || "");
|
||||
systemFill(marketHtml);
|
||||
}
|
||||
|
||||
function hostOfUrl(url) {
|
||||
try {
|
||||
return new URL(url).host;
|
||||
} catch (error) {
|
||||
return String(url || "").replace(/^https?:\/\//, "").split("/")[0] || "--";
|
||||
}
|
||||
function dataHubConsoleUrl() {
|
||||
if (global.XIAOBAI_DATAHUB_URL) return String(global.XIAOBAI_DATAHUB_URL);
|
||||
return global.location.protocol + "//" + global.location.hostname + ":8766/admin/";
|
||||
}
|
||||
|
||||
function renderModelPoolHtml(models) {
|
||||
if (!models.length) {
|
||||
return '<div class="m-state"><p>模型池为空,请先添加模型</p></div>';
|
||||
}
|
||||
const llm = (state.system.admin && state.system.admin.llm) || {};
|
||||
return models.map(function (item) {
|
||||
const badges = [];
|
||||
if (item.id === llm.primary_model_id) badges.push('<span class="m-sys-badge m-sys-badge--admin">主模型</span>');
|
||||
if (item.id === llm.fallback_model_id) badges.push('<span class="m-sys-badge">辅助</span>');
|
||||
badges.push('<span class="m-sys-badge' + (item.configured ? " m-sys-badge--ok" : "") + '">' + (item.configured ? "已配置" : "待配置") + "</span>");
|
||||
return '<button class="m-sys-model-card" type="button" data-model-id="' + escapeHtml(item.id) + '" data-system-edit-model>' +
|
||||
"<div><strong>" + escapeHtml(item.name || "未命名模型") + "</strong>" +
|
||||
'<div class="m-sys-badges">' + badges.join("") + "</div>" +
|
||||
'<p class="m-sys-hint">' + escapeHtml(hostOfUrl(item.base_url) + " · " + (item.model || "未填写标识")) + "</p></div>" +
|
||||
'<span class="m-sys-row-chevron">' + icon("chevron-right", 16) + "</span></button>";
|
||||
}).join("");
|
||||
function openDataHubConsole() {
|
||||
if (!global.MobileSession.isAdmin()) return;
|
||||
global.open(dataHubConsoleUrl(), "_blank", "noopener");
|
||||
}
|
||||
|
||||
function collectSystemModelPool() {
|
||||
return (state.system.models || []).map(function (item) {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name || "",
|
||||
base_url: item.base_url || "",
|
||||
model: item.model || "",
|
||||
api_key: item.api_key || "",
|
||||
configured: Boolean(item.configured)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updateSystemModelRoleOptions(primaryId, fallbackId) {
|
||||
const models = collectSystemModelPool();
|
||||
const options = models.map(function (item) {
|
||||
return '<option value="' + escapeHtml(item.id) + '">' + escapeHtml(item.name || item.model || "未命名模型") + "</option>";
|
||||
}).join("");
|
||||
const primary = document.getElementById("m-sys-primary-model");
|
||||
const fallback = document.getElementById("m-sys-fallback-model");
|
||||
if (!primary || !fallback) return;
|
||||
primary.innerHTML = models.length ? options : '<option value="">暂无模型</option>';
|
||||
fallback.innerHTML = '<option value="">不启用辅助模型</option>' + options;
|
||||
const keepPrimary = models.some(function (item) { return item.id === primaryId; }) ? primaryId : (models[0] && models[0].id) || "";
|
||||
primary.value = keepPrimary;
|
||||
fallback.value = models.some(function (item) { return item.id === fallbackId; }) && fallbackId !== keepPrimary ? fallbackId : "";
|
||||
}
|
||||
|
||||
function openModelEditSheet(modelId) {
|
||||
const models = state.system.models || [];
|
||||
const item = models.find(function (row) { return row.id === modelId; }) || {};
|
||||
state.system.editingModelId = modelId;
|
||||
openSheet(
|
||||
'<div class="m-sheet-head"><h2>编辑模型</h2>' +
|
||||
'<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
|
||||
'<div class="m-sheet-body" data-model-id="' + escapeHtml(modelId) + '">' +
|
||||
formFieldHtml("显示名称", '<input data-model-field="name" maxlength="50" value="' + escapeHtml(item.name || "") + '">', true) +
|
||||
formFieldHtml("API Base URL", '<input data-model-field="base_url" type="url" value="' + escapeHtml(item.base_url || "https://api.openai.com/v1") + '">', true) +
|
||||
formFieldHtml("模型标识", '<input data-model-field="model" maxlength="100" value="' + escapeHtml(item.model || "") + '">', true) +
|
||||
formFieldHtml("API Key", '<input data-model-field="api_key" type="password" autocomplete="off" maxlength="300" placeholder="' + (item.configured ? "留空则保留已保存的 Key" : "输入 API Key") + '">', !item.configured) +
|
||||
'<div class="m-sys-test-row"><button class="m-btn-outline" type="button" data-system-test-model>测试连接</button>' +
|
||||
'<span data-model-test-status>未测试</span></div>' +
|
||||
'<div class="m-sys-sheet-actions">' +
|
||||
'<button class="m-btn-outline-danger" type="button" data-system-delete-model>删除模型</button>' +
|
||||
'<button class="m-btn-primary" type="button" data-system-save-model>保存</button>' +
|
||||
"</div></div>"
|
||||
);
|
||||
}
|
||||
|
||||
function readModelSheetFields(root) {
|
||||
function fieldValue(name) {
|
||||
const input = root.querySelector("[data-model-field='" + name + "']");
|
||||
return String(input && input.value != null ? input.value : "").trim();
|
||||
}
|
||||
return {
|
||||
name: fieldValue("name"),
|
||||
base_url: fieldValue("base_url"),
|
||||
model: fieldValue("model"),
|
||||
api_key: fieldValue("api_key")
|
||||
};
|
||||
}
|
||||
|
||||
function renderSystemMembers() {
|
||||
const payload = state.system.admin || {};
|
||||
const membership = payload.membership || {};
|
||||
const users = payload.users || [];
|
||||
const userHtml = users.map(function (user) {
|
||||
const admin = user.role === "admin";
|
||||
const member = Boolean(user.membership_subscribed);
|
||||
const badges = [];
|
||||
if (admin) badges.push('<span class="m-sys-badge m-sys-badge--admin">管理员</span>');
|
||||
if (member) badges.push('<span class="m-sys-badge m-sys-badge--ok">会员</span>');
|
||||
else badges.push('<span class="m-sys-badge">普通用户</span>');
|
||||
const expiry = member
|
||||
? (user.membership_expires_at ? "有效至 " + membershipDateLabel(user.membership_expires_at) : "永久有效")
|
||||
: user.membership_status === "suspended"
|
||||
? "会员已停用"
|
||||
: "尚未开通";
|
||||
return '<div class="m-sys-user-row" data-admin-user="' + number(user.id) + '">' +
|
||||
'<div class="m-sys-row-body"><strong>' + escapeHtml(user.username) + "</strong>" +
|
||||
'<div class="m-sys-badges">' + badges.join("") + "</div>" +
|
||||
'<p class="m-sys-hint">' + escapeHtml(expiry) + " · 今日已用 " + number(user.used_today) + " 次</p></div>" +
|
||||
'<button class="m-btn-outline" type="button" data-system-open-member>管理</button></div>';
|
||||
}).join("") || '<div class="m-state"><p>暂无注册用户</p></div>';
|
||||
const html =
|
||||
'<div class="m-sys-body" data-system-page="members">' +
|
||||
'<div class="m-card m-sys-section"><strong>全局额度</strong>' +
|
||||
formFieldHtml("会员每日智能分析上限", '<input id="m-sys-member-limit" type="number" min="1" max="1000" value="' + (number(membership.member_daily_limit) || 50) + '">', false) +
|
||||
'<p class="m-sys-hint">对所有会员生效,每日 0 点自动重置。</p>' +
|
||||
'<button class="m-btn-primary" type="button" data-system-save-limit>保存额度</button></div>' +
|
||||
'<div class="m-card m-sys-section"><strong>会员账号 · ' + users.length + " 个</strong>" +
|
||||
userHtml +
|
||||
'<p class="m-sys-hint">点「管理」为对应账号开通、续期或停用会员。</p></div>' +
|
||||
"</div>";
|
||||
systemFill(html);
|
||||
}
|
||||
|
||||
function openMemberManageSheet(userId) {
|
||||
const users = (state.system.admin && state.system.admin.users) || [];
|
||||
const user = users.find(function (item) { return String(item.id) === String(userId); });
|
||||
if (!user) return;
|
||||
state.system.editingUserId = String(userId);
|
||||
const member = Boolean(user.membership_subscribed);
|
||||
const expiry = member
|
||||
? (user.membership_expires_at ? "有效至 " + membershipDateLabel(user.membership_expires_at) : "永久有效")
|
||||
: user.membership_status === "suspended" ? "会员已停用" : "尚未开通";
|
||||
openSheet(
|
||||
'<div class="m-sheet-head"><h2>管理会员 · ' + escapeHtml(user.username) + "</h2>" +
|
||||
'<button class="m-sheet-close" type="button" data-sheet-close aria-label="关闭">' + icon("close", 20) + "</button></div>" +
|
||||
'<div class="m-sheet-body" data-admin-user="' + number(user.id) + '">' +
|
||||
'<p class="m-sys-lead">当前状态:' + escapeHtml(expiry) + " · 今日已用 " + number(user.used_today) + " 次</p>" +
|
||||
formFieldHtml("会员状态", '<select data-member-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>", false) +
|
||||
formFieldHtml("开通 / 续期时长", '<select data-member-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>', false) +
|
||||
'<p class="m-sys-hint">从当前时间开始顺延;已有会员则叠加续期。</p>' +
|
||||
'<div class="m-sys-sheet-actions">' +
|
||||
'<button class="m-btn-outline" type="button" data-sheet-close>取消</button>' +
|
||||
'<button class="m-btn-primary" type="button" data-system-save-member>应用</button>' +
|
||||
"</div></div>"
|
||||
);
|
||||
function dataHubRowHtml() {
|
||||
return '<button class="m-sys-row" type="button" data-system-datahub>' +
|
||||
'<span class="m-sys-row-icon">' + icon("database", 18) + "</span>" +
|
||||
'<span class="m-sys-row-body"><strong>数据中枢</strong><small>数据源 · 模型池 · 会员与邀请码</small></span>' +
|
||||
'<span class="m-sys-row-chevron">' + icon("chevron-right", 16) + "</span></button>";
|
||||
}
|
||||
|
||||
function setFieldError(inputId, message) {
|
||||
@@ -5571,97 +5396,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function saveSystemModels() {
|
||||
const button = document.querySelector("[data-system-save-models]");
|
||||
if (button) button.disabled = true;
|
||||
global.MobileAPI.request("/api/admin/settings", "POST", {
|
||||
primary_model_id: (document.getElementById("m-sys-primary-model") || {}).value || "",
|
||||
fallback_model_id: (document.getElementById("m-sys-fallback-model") || {}).value || "",
|
||||
}).then(function () {
|
||||
showToast("模型分工已保存");
|
||||
loadSystem();
|
||||
}).catch(function (error) {
|
||||
showToast(error && error.message ? error.message : "模型分工保存失败");
|
||||
}).then(function () {
|
||||
if (button) button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function persistSystemModels(models, message) {
|
||||
return global.MobileAPI.request("/api/admin/settings", "POST", { models: models }).then(function () {
|
||||
showToast(message || "模型池已保存");
|
||||
closeSheet();
|
||||
loadSystem();
|
||||
});
|
||||
}
|
||||
|
||||
function saveEditedSystemModel() {
|
||||
const sheet = document.querySelector(".m-sheet-body[data-model-id]");
|
||||
if (!sheet) return;
|
||||
const id = sheet.dataset.modelId;
|
||||
const fields = readModelSheetFields(sheet);
|
||||
const models = collectSystemModelPool().map(function (item) {
|
||||
if (item.id !== id) return item;
|
||||
return Object.assign({}, item, fields);
|
||||
});
|
||||
persistSystemModels(models, "模型已保存").catch(function (error) {
|
||||
showToast(error && error.message ? error.message : "模型保存失败");
|
||||
});
|
||||
}
|
||||
|
||||
function addSystemModel() {
|
||||
const models = collectSystemModelPool();
|
||||
const id = "model-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
|
||||
const created = { id: id, name: "模型 " + (models.length + 1), base_url: "https://api.openai.com/v1", model: "", api_key: "", configured: false };
|
||||
state.system.models = models.concat([created]);
|
||||
const list = document.getElementById("m-sys-model-list");
|
||||
if (list) list.innerHTML = renderModelPoolHtml(state.system.models);
|
||||
updateSystemModelRoleOptions(id, (document.getElementById("m-sys-fallback-model") || {}).value || "");
|
||||
openModelEditSheet(id);
|
||||
}
|
||||
|
||||
function deleteSystemModel(row) {
|
||||
if (!row) return;
|
||||
const id = row.dataset.modelId;
|
||||
const primary = (document.getElementById("m-sys-primary-model") || {}).value;
|
||||
const fallback = (document.getElementById("m-sys-fallback-model") || {}).value;
|
||||
const llm = (state.system.admin && state.system.admin.llm) || {};
|
||||
if (id === primary || id === fallback || id === llm.primary_model_id || id === llm.fallback_model_id) {
|
||||
showToast("请先为主模型或辅助模型选择其他模型,再删除当前模型");
|
||||
return;
|
||||
}
|
||||
openConfirmSheet("删除模型?", "删除后该模型将从模型池移除,此操作不可恢复。", {
|
||||
danger: true,
|
||||
centered: true,
|
||||
confirmLabel: "删除",
|
||||
onConfirm: function () {
|
||||
const models = collectSystemModelPool().filter(function (item) { return item.id !== id; });
|
||||
persistSystemModels(models, "模型已删除").catch(function (error) {
|
||||
showToast(error && error.message ? error.message : "删除失败");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function testSystemModel(row) {
|
||||
if (!row) return;
|
||||
const status = document.querySelector("[data-model-test-status]");
|
||||
const button = document.querySelector("[data-system-test-model]");
|
||||
const sheet = document.querySelector(".m-sheet-body[data-model-id]");
|
||||
const id = (sheet && sheet.dataset.modelId) || row.dataset.modelId;
|
||||
const fields = sheet ? readModelSheetFields(sheet) : {};
|
||||
const profile = Object.assign({}, collectSystemModelPool().find(function (item) { return item.id === id; }) || {}, fields, { id: id });
|
||||
if (button) button.disabled = true;
|
||||
if (status) status.textContent = "连接中";
|
||||
global.MobileAPI.request("/api/admin/settings/test", "POST", { model_id: id, profile: profile }).then(function (payload) {
|
||||
if (status) status.textContent = "上次测试:成功 · " + number(payload.result && payload.result.latency_ms) + "ms";
|
||||
}).catch(function (error) {
|
||||
if (status) status.textContent = error && error.message ? error.message : "测试失败";
|
||||
}).then(function () {
|
||||
if (button) button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function startSystemRefresh() {
|
||||
const button = document.querySelector("[data-system-refresh]");
|
||||
if (button) button.disabled = true;
|
||||
@@ -5699,56 +5433,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function saveSystemMemberLimit() {
|
||||
const button = document.querySelector("[data-system-save-limit]");
|
||||
if (button) button.disabled = true;
|
||||
global.MobileAPI.request("/api/admin/settings", "POST", {
|
||||
member_daily_limit: number((document.getElementById("m-sys-member-limit") || {}).value),
|
||||
}).then(function () {
|
||||
showToast("会员调用额度已保存");
|
||||
loadSystem();
|
||||
}).catch(function (error) {
|
||||
showToast(error && error.message ? error.message : "会员调用额度保存失败");
|
||||
}).then(function () {
|
||||
if (button) button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function saveSystemMember(card) {
|
||||
if (!card) return;
|
||||
const status = (card.querySelector("[data-member-status]") || {}).value;
|
||||
const apply = function () {
|
||||
const button = card.querySelector("[data-system-save-member]");
|
||||
if (button) button.disabled = true;
|
||||
global.MobileAPI.request("/api/admin/membership", "POST", {
|
||||
user_id: card.dataset.adminUser,
|
||||
status: status,
|
||||
duration: (card.querySelector("[data-member-duration]") || {}).value,
|
||||
}).then(function (payload) {
|
||||
if (state.system.admin) state.system.admin.users = payload.users || [];
|
||||
closeSheet();
|
||||
showToast("会员状态已更新");
|
||||
renderSystemMembers();
|
||||
}).catch(function (error) {
|
||||
showToast(error && error.message ? error.message : "会员状态保存失败");
|
||||
}).then(function () {
|
||||
if (button) button.disabled = false;
|
||||
});
|
||||
};
|
||||
if (status === "suspended") {
|
||||
openConfirmSheet("停用该会员?", "停用后该账号将无法使用会员智能功能,可稍后重新开通。", {
|
||||
danger: true,
|
||||
centered: true,
|
||||
confirmLabel: "停用",
|
||||
onConfirm: apply
|
||||
});
|
||||
return;
|
||||
}
|
||||
apply();
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- events */
|
||||
|
||||
function bindEvents() {
|
||||
window.addEventListener("hashchange", closeSheet);
|
||||
|
||||
@@ -5876,12 +5560,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const systemAdminTab = event.target.closest("[data-system-admin-tab]");
|
||||
if (systemAdminTab) {
|
||||
state.system.adminTab = systemAdminTab.dataset.systemAdminTab === "models" ? "models" : "market";
|
||||
renderSystemAdmin("system/admin");
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("[data-system-save-birth]")) { saveSystemBirth(); return; }
|
||||
if (event.target.closest("[data-system-delete-birth]")) { deleteSystemBirth(); return; }
|
||||
if (event.target.closest("[data-system-save-password]")) { saveSystemPassword(); return; }
|
||||
@@ -5889,22 +5567,9 @@
|
||||
if (event.target.closest("[data-system-logout]")) { logoutSystemAccount(); return; }
|
||||
if (event.target.closest("[data-system-save-market]")) { saveSystemMarket(); return; }
|
||||
if (event.target.closest("[data-system-toggle-refresh]")) { toggleSystemRefresh(); return; }
|
||||
if (event.target.closest("[data-system-save-models]")) { saveSystemModels(); return; }
|
||||
if (event.target.closest("[data-system-save-model]")) { saveEditedSystemModel(); return; }
|
||||
if (event.target.closest("[data-system-add-model]")) { addSystemModel(); return; }
|
||||
const editModel = event.target.closest("[data-system-edit-model]");
|
||||
if (editModel) { openModelEditSheet(editModel.dataset.modelId); return; }
|
||||
const deleteModel = event.target.closest("[data-system-delete-model]");
|
||||
if (deleteModel) { deleteSystemModel(deleteModel.closest("[data-model-id]")); return; }
|
||||
const testModel = event.target.closest("[data-system-test-model]");
|
||||
if (testModel) { testSystemModel(testModel.closest("[data-model-id]")); return; }
|
||||
if (event.target.closest("[data-system-refresh]")) { startSystemRefresh(); return; }
|
||||
if (event.target.closest("[data-system-backfill]")) { startSystemBackfill(); return; }
|
||||
if (event.target.closest("[data-system-save-limit]")) { saveSystemMemberLimit(); return; }
|
||||
const openMember = event.target.closest("[data-system-open-member]");
|
||||
if (openMember) { openMemberManageSheet(openMember.closest("[data-admin-user]").dataset.adminUser); return; }
|
||||
const saveMember = event.target.closest("[data-system-save-member]");
|
||||
if (saveMember) { saveSystemMember(saveMember.closest("[data-admin-user]")); return; }
|
||||
if (event.target.closest("[data-system-datahub]")) { openDataHubConsole(); return; }
|
||||
|
||||
// 我的复盘:新增/编辑/删除/筛选/提交等操作
|
||||
const reviewAddTrade = event.target.closest("[data-review-add-trade]");
|
||||
|
||||
@@ -263,6 +263,7 @@
|
||||
'<label class="m-form-field"><span>账号名</span><input id="m-auth-username" type="text" minlength="3" maxlength="30" autocomplete="username" required></label>',
|
||||
'<label class="m-form-field"><span>密码</span><input id="m-auth-password" type="password" minlength="8" maxlength="128" autocomplete="current-password" required></label>',
|
||||
'<label class="m-form-field" id="m-auth-confirm-field" hidden><span>确认密码</span><input id="m-auth-confirm" type="password" minlength="8" maxlength="128" autocomplete="new-password"></label>',
|
||||
'<label class="m-form-field" id="m-auth-invite-field" hidden><span>邀请码</span><input id="m-auth-invite" type="text" maxlength="32" autocomplete="off" spellcheck="false" placeholder="XB-XXXX-XXXX-XXXX"></label>',
|
||||
'<p class="m-auth-error" id="m-auth-error" hidden></p>',
|
||||
'<button class="m-btn-primary" id="m-auth-submit" type="submit"><span class="m-btn-spinner" aria-hidden="true" hidden></span><span class="m-btn-label">登录</span></button>',
|
||||
"</form>",
|
||||
@@ -280,6 +281,8 @@
|
||||
});
|
||||
document.getElementById("m-auth-confirm-field").hidden = authMode !== "register";
|
||||
document.getElementById("m-auth-confirm").required = authMode === "register";
|
||||
document.getElementById("m-auth-invite-field").hidden = authMode !== "register";
|
||||
document.getElementById("m-auth-invite").required = authMode === "register";
|
||||
document.getElementById("m-auth-password").autocomplete = authMode === "register" ? "new-password" : "current-password";
|
||||
const submitLabel = document.querySelector("#m-auth-submit .m-btn-label");
|
||||
if (submitLabel) submitLabel.textContent = authMode === "register" ? "注册并进入" : "登录";
|
||||
@@ -346,7 +349,7 @@
|
||||
label.textContent = authMode === "register" ? "注册中…" : "登录中…";
|
||||
try {
|
||||
if (authMode === "register") {
|
||||
await global.MobileSession.register(username, password);
|
||||
await global.MobileSession.register(username, password, document.getElementById("m-auth-invite").value.trim());
|
||||
} else {
|
||||
await global.MobileSession.login(username, password);
|
||||
}
|
||||
|
||||
@@ -24,10 +24,11 @@
|
||||
return applySession(payload);
|
||||
}
|
||||
|
||||
async function register(username, password) {
|
||||
async function register(username, password, inviteCode) {
|
||||
const payload = await global.MobileAPI.request("/api/auth/register", "POST", {
|
||||
username: username,
|
||||
password: password
|
||||
password: password,
|
||||
invite_code: inviteCode || ""
|
||||
});
|
||||
return applySession(payload);
|
||||
}
|
||||
|
||||
Vendored
-6
@@ -6250,12 +6250,6 @@ body[data-active-view="heavenView"] .workspace-view {
|
||||
color: var(--up);
|
||||
}
|
||||
|
||||
.model.on {
|
||||
background: var(--blue-soft);
|
||||
|
||||
box-shadow: inset 2px 0 0 var(--blue);
|
||||
}
|
||||
|
||||
.star {
|
||||
color: rgb(245, 158, 11);
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -2065,7 +2065,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] :is(.global-search-results, .assistant-messages, .trade-log-form, .alerts-toolbar, .admin-section-picker) {
|
||||
:root[data-theme="dark"] :is(.global-search-results, .assistant-messages, .trade-log-form, .alerts-toolbar) {
|
||||
border-color: var(--line-soft);
|
||||
|
||||
background: var(--surface-muted);
|
||||
|
||||
+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