286 lines
12 KiB
JavaScript
286 lines
12 KiB
JavaScript
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);
|
|
});
|
|
}
|