refactor: establish standalone application boundary
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
function selectAuthMode(mode) {
|
||||
state.authMode = mode === "register" ? "register" : "login";
|
||||
document.querySelectorAll("[data-auth-mode]").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.authMode === state.authMode);
|
||||
});
|
||||
const registering = state.authMode === "register";
|
||||
document.querySelector("#authConfirmField").hidden = !registering;
|
||||
document.querySelector("#authPasswordConfirm").required = registering;
|
||||
document.querySelector("#authPassword").autocomplete = registering ? "new-password" : "current-password";
|
||||
document.querySelector("#authSubmitButton").textContent = registering ? "注册并进入" : "登录";
|
||||
document.querySelector("#authError").hidden = true;
|
||||
}
|
||||
|
||||
async function submitAuthForm(event) {
|
||||
event.preventDefault();
|
||||
const username = document.querySelector("#authUsername").value.trim();
|
||||
const password = document.querySelector("#authPassword").value;
|
||||
const errorElement = document.querySelector("#authError");
|
||||
if (state.authMode === "register" && password !== document.querySelector("#authPasswordConfirm").value) {
|
||||
errorElement.textContent = "两次输入的密码不一致。";
|
||||
errorElement.hidden = false;
|
||||
return;
|
||||
}
|
||||
const button = document.querySelector("#authSubmitButton");
|
||||
button.disabled = true;
|
||||
try {
|
||||
const session = await apiRequest(`/api/auth/${state.authMode}`, "POST", { username, password });
|
||||
document.querySelector("#authForm").reset();
|
||||
await applyAuthenticatedSession(session);
|
||||
} catch (error) {
|
||||
errorElement.textContent = error.message || "账号操作失败";
|
||||
errorElement.hidden = false;
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyAuthenticatedSession(session) {
|
||||
state.user = session.user;
|
||||
state.csrfToken = session.csrf_token || "";
|
||||
setText("accountName", session.user?.username || "账号");
|
||||
const isAdmin = session.user?.role === "admin";
|
||||
updateAccountIdentityBadges(session.user?.membership || {});
|
||||
document.querySelector("#settingsButton").hidden = !isAdmin;
|
||||
document.querySelector("#syncButton").hidden = !isAdmin;
|
||||
document.querySelector("#reasonForm").hidden = !isAdmin;
|
||||
document.querySelector("#sectorPhaseManager").hidden = !isAdmin;
|
||||
document.querySelector("#authGate").hidden = true;
|
||||
applyMembershipAccess();
|
||||
await startAuthenticatedApp();
|
||||
}
|
||||
|
||||
function showAuthGate(message = "") {
|
||||
state.user = null;
|
||||
state.csrfToken = "";
|
||||
const gate = document.querySelector("#authGate");
|
||||
gate.hidden = false;
|
||||
const errorElement = document.querySelector("#authError");
|
||||
errorElement.textContent = message;
|
||||
errorElement.hidden = !message;
|
||||
document.querySelector("#authUsername").focus();
|
||||
}
|
||||
|
||||
async function logoutAccount() {
|
||||
toggleAccountDropdown(false);
|
||||
try {
|
||||
await apiRequest("/api/auth/logout", "POST", {});
|
||||
} catch (error) {
|
||||
showToast(error.message || "退出失败");
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
|
||||
function hasMemberAccess() {
|
||||
return state.user?.role === "admin" || Boolean(state.user?.membership?.active);
|
||||
}
|
||||
|
||||
function updateAccountIdentityBadges(membership = {}) {
|
||||
const isAdmin = state.user?.role === "admin" || Boolean(membership.is_admin);
|
||||
const subscribed = Boolean(membership.subscribed);
|
||||
document.querySelector("#accountAdminBadge").hidden = !isAdmin;
|
||||
const vipBadge = document.querySelector("#accountVipBadge");
|
||||
vipBadge.hidden = false;
|
||||
vipBadge.classList.toggle("is-nonmember", !subscribed);
|
||||
setText("accountVipLabel", subscribed ? "会员" : "非会员");
|
||||
vipBadge.title = subscribed ? "查看会员状态" : "查看会员权益";
|
||||
}
|
||||
|
||||
function applyMembershipAccess() {
|
||||
const unlocked = hasMemberAccess();
|
||||
document.querySelectorAll(".member-feature-view").forEach((view) => {
|
||||
view.classList.toggle("member-locked", !unlocked);
|
||||
const gate = view.querySelector(".member-gate");
|
||||
if (gate) gate.hidden = unlocked;
|
||||
view.querySelectorAll("button, input, textarea, select").forEach((control) => {
|
||||
if (control.closest(".member-gate") || control.hasAttribute("data-member-navigation")) return;
|
||||
control.disabled = !unlocked;
|
||||
});
|
||||
});
|
||||
const assistantButton = document.querySelector("#assistantButton");
|
||||
assistantButton.classList.toggle("member-locked-control", !unlocked);
|
||||
assistantButton.title = unlocked ? "复盘助手" : "复盘助手(会员可用)";
|
||||
updateAssistantControls();
|
||||
}
|
||||
|
||||
|
||||
function selectAccountPanel(panel) {
|
||||
const selected = ["profile", "membership", "password"].includes(panel) ? panel : "profile";
|
||||
const titles = { profile: "个人资料", membership: "会员状态", password: "修改密码" };
|
||||
setText("accountDialogTitle", titles[selected]);
|
||||
document.querySelectorAll("[data-account-panel-content]").forEach((section) => {
|
||||
section.hidden = section.dataset.accountPanelContent !== selected;
|
||||
});
|
||||
document.querySelector("#connectionStatus").hidden = selected !== "membership";
|
||||
return selected;
|
||||
}
|
||||
|
||||
async function openSettings(panel = "profile") {
|
||||
selectAccountPanel(panel);
|
||||
toggleAccountDropdown(false);
|
||||
toggleHeaderCommandMenu(false);
|
||||
const status = document.querySelector("#connectionStatus");
|
||||
status.className = "connection-status";
|
||||
status.textContent = "正在读取账号状态";
|
||||
openModalDialog(elements.settingsDialog);
|
||||
try {
|
||||
const payload = await apiRequest("/api/account/status");
|
||||
const access = payload.llm_access || {};
|
||||
const membership = access.membership || {};
|
||||
if (state.user) {
|
||||
state.user.membership = membership;
|
||||
updateAccountIdentityBadges(membership);
|
||||
applyMembershipAccess();
|
||||
}
|
||||
status.textContent = membership.active ? "账户权益已同步" : "账户信息已同步";
|
||||
status.classList.toggle("connected", true);
|
||||
setText("membershipBadge", membership.subscribed ? "会员有效" : membership.is_admin ? "管理员权限" : "普通用户");
|
||||
setText("membershipStateValue", membership.subscribed ? "已开通" : membership.is_admin ? "管理员可用" : "未开通");
|
||||
setText("membershipRemainingValue", membership.subscribed && membership.expires_at
|
||||
? `${number(membership.remaining_days)} 天`
|
||||
: membership.is_admin || membership.subscribed ? "长期有效" : "--");
|
||||
setText("membershipDetail", membership.subscribed
|
||||
? `${membership.plan || "会员"}${membership.expires_at ? ` · 有效至 ${membershipDateDisplay(membership.expires_at, true)}` : " · 长期有效"}`
|
||||
: membership.is_admin
|
||||
? "管理员拥有智能功能管理权限,但不会因此显示为已开通会员。"
|
||||
: "开通会员后可使用智能选股、问师、问天、复盘助手等智能功能。");
|
||||
setText("membershipQuotaHint", `会员默认每日智能分析额度 ${number(access.daily_limit)} 次,由管理员统一设置。`);
|
||||
setText("membershipUsage", membership.active
|
||||
? `今日已用 ${number(access.used_today)} 次`
|
||||
: "今日智能分析:--");
|
||||
setText("membershipUsageSummary", membership.active ? `${number(access.used_today)} / ${number(access.daily_limit)}` : "--");
|
||||
setText("membershipRemainingUsage", membership.is_admin ? "不限" : membership.active ? `${number(access.remaining_calls)} 次` : "--");
|
||||
const birth = payload.birth_profile || {};
|
||||
if (birth.birth_datetime) {
|
||||
const [birthDate, birthTime] = String(birth.birth_datetime).split("T");
|
||||
document.querySelector("#accountBirthDate").value = birthDate || "";
|
||||
document.querySelector("#accountBirthTime").value = (birthTime || "").slice(0, 5);
|
||||
document.querySelector("#accountBirthGender").value = birth.gender || "unspecified";
|
||||
}
|
||||
setText("birthProfileStatus", payload.birth_profile_configured ? "已加密保存" : "尚未设置");
|
||||
document.querySelector("#deleteBirthProfileButton").disabled = !payload.birth_profile_configured;
|
||||
} catch (error) {
|
||||
status.hidden = false;
|
||||
status.textContent = "账户状态暂时无法同步";
|
||||
showToast(error.message || "账号信息加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function changeAccountPassword(event) {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget;
|
||||
const button = form.querySelector("button[type='submit']");
|
||||
button.disabled = true;
|
||||
try {
|
||||
await apiRequest("/api/account/password", "POST", {
|
||||
current_password: document.querySelector("#currentPassword").value,
|
||||
new_password: document.querySelector("#newPassword").value,
|
||||
confirm_password: document.querySelector("#confirmPassword").value,
|
||||
});
|
||||
form.reset();
|
||||
showToast("密码已更新");
|
||||
} catch (error) {
|
||||
showToast(error.message || "密码更新失败");
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function switchAccount() {
|
||||
const button = document.querySelector("#switchAccountMenuButton");
|
||||
button.disabled = true;
|
||||
toggleAccountDropdown(false);
|
||||
try {
|
||||
await apiRequest("/api/auth/logout", "POST", {});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
showToast(error.message || "切换账号失败");
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function membershipDateDisplay(value) {
|
||||
if (!value) return "";
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) return String(value).slice(0, 10);
|
||||
return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" }).format(parsed);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function toggleAccountDropdown(force, returnFocus = false) {
|
||||
const menu = document.querySelector("#accountDropdown");
|
||||
const button = document.querySelector("#accountButton");
|
||||
if (!menu || !button) return;
|
||||
const open = typeof force === "boolean" ? force : menu.hidden;
|
||||
menu.hidden = !open;
|
||||
button.setAttribute("aria-expanded", String(open));
|
||||
document.querySelector(".account-menu-shell")?.classList.toggle("is-open", open);
|
||||
if (open) {
|
||||
setText("accountMenuName", state.user?.username || "当前账号");
|
||||
const membership = state.user?.membership || {};
|
||||
setText("accountMenuRole", state.user?.role === "admin" ? (membership.subscribed ? "管理员 · 会员" : "管理员") : membership.subscribed ? "会员用户" : "普通用户");
|
||||
} else if (returnFocus) {
|
||||
button.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function handleAccountMenuKeydown(event) {
|
||||
const menu = document.querySelector("#accountDropdown");
|
||||
if (!menu) return;
|
||||
if (menu.hidden) {
|
||||
if (document.activeElement?.id === "accountButton" && event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
toggleAccountDropdown(true);
|
||||
menu.querySelector('[role="menuitem"]')?.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const items = [...menu.querySelectorAll('[role="menuitem"]:not(:disabled)')];
|
||||
if (!items.length) return;
|
||||
const current = items.indexOf(document.activeElement);
|
||||
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
const offset = event.key === "ArrowDown" ? 1 : -1;
|
||||
items[(current + offset + items.length) % items.length].focus();
|
||||
} else if (event.key === "Home" || event.key === "End") {
|
||||
event.preventDefault();
|
||||
items[event.key === "Home" ? 0 : items.length - 1].focus();
|
||||
}
|
||||
}
|
||||
|
||||
function bindSessionEvents() {
|
||||
document.querySelectorAll("[data-auth-mode]").forEach((button) => {
|
||||
button.addEventListener("click", () => selectAuthMode(button.dataset.authMode));
|
||||
});
|
||||
document.querySelector("#authForm").addEventListener("submit", submitAuthForm);
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!event.target.closest(".account-menu-shell")) toggleAccountDropdown(false);
|
||||
});
|
||||
window.addEventListener("keydown", handleGlobalSearchShortcut);
|
||||
document.querySelectorAll("[data-open-account]").forEach((button) => {
|
||||
button.addEventListener("click", () => openSettings("membership"));
|
||||
});
|
||||
document.querySelector("#accountButton").addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
toggleAccountDropdown();
|
||||
});
|
||||
document.querySelector("#accountVipBadge").addEventListener("click", () => openSettings("membership"));
|
||||
document.querySelectorAll("[data-account-panel]").forEach((button) => {
|
||||
button.addEventListener("click", () => openSettings(button.dataset.accountPanel));
|
||||
});
|
||||
document.querySelector("#switchAccountMenuButton").addEventListener("click", switchAccount);
|
||||
document.querySelector("#logoutMenuButton").addEventListener("click", logoutAccount);
|
||||
document.querySelector("#closeSettingsDialog").addEventListener("click", () => elements.settingsDialog.close());
|
||||
document.querySelector("#accountBirthForm").addEventListener("submit", saveAccountBirthProfile);
|
||||
document.querySelector("#deleteBirthProfileButton").addEventListener("click", deleteAccountBirthProfile);
|
||||
document.querySelector("#passwordForm").addEventListener("submit", changeAccountPassword);
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") toggleAccountDropdown(false, true);
|
||||
handleAccountMenuKeydown(event);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user