const state = { bootstrap: window.APP_BOOTSTRAP || {}, authenticated: false, users: [], page: 1, pageSize: (window.APP_BOOTSTRAP && window.APP_BOOTSTRAP.pageSize) || 25, total: 0, totalBeforeSearch: 0, summary: { active: 0, disabled: 0 }, search: "", licenses: [], selectedUser: null, selectedUsers: new Set(), activeTaskId: "", activeTaskTimer: null, }; const elements = { platformStatus: document.getElementById("platform-status"), platformSubstatus: document.getElementById("platform-substatus"), metricTotal: document.getElementById("metric-total"), metricActive: document.getElementById("metric-active"), metricDisabled: document.getElementById("metric-disabled"), metricLicense: document.getElementById("metric-license"), usersTableBody: document.getElementById("users-table-body"), paginationInfo: document.getElementById("pagination-info"), licenseList: document.getElementById("license-list"), resultConsole: document.getElementById("result-console"), loginSection: document.getElementById("login-section"), loginForm: document.getElementById("login-form"), logoutBtn: document.getElementById("logout-btn"), refreshAllBtn: document.getElementById("refresh-all-btn"), searchForm: document.getElementById("search-form"), searchInput: document.getElementById("search-input"), prevPageBtn: document.getElementById("prev-page-btn"), nextPageBtn: document.getElementById("next-page-btn"), userForm: document.getElementById("user-form"), selectedUserId: document.getElementById("selected-user-id"), deleteUserBtn: document.getElementById("delete-user-btn"), newUserBtn: document.getElementById("new-user-btn"), clearFormBtn: document.getElementById("clear-form-btn"), resetPasswordBtn: document.getElementById("reset-password-btn"), selectedCount: document.getElementById("selected-count"), selectAllResultsBtn: document.getElementById("select-all-results-btn"), clearSelectionBtn: document.getElementById("clear-selection-btn"), bulkEnableBtn: document.getElementById("bulk-enable-btn"), bulkDisableBtn: document.getElementById("bulk-disable-btn"), bulkResetBtn: document.getElementById("bulk-reset-btn"), bulkDeleteBtn: document.getElementById("bulk-delete-btn"), selectPageCheckbox: document.getElementById("select-page-checkbox"), taskStatusCard: document.getElementById("task-status-card"), taskStatusTitle: document.getElementById("task-status-title"), taskStatusState: document.getElementById("task-status-state"), taskProgressFill: document.getElementById("task-progress-fill"), taskStatusText: document.getElementById("task-status-text"), }; const TASK_LABELS = { "create": "批量创建", "update": "批量更新", "delete": "批量删除", "reset-password": "批量改密", }; async function api(path, options = {}) { const config = { ...options }; config.headers = config.headers || {}; if (config.body && !(config.body instanceof FormData)) { config.headers["Content-Type"] = "application/json"; } const response = await fetch(path, config); const payload = await response.json().catch(() => ({ success: false, message: "接口返回了无法解析的响应。", })); if (response.status === 401) { state.authenticated = false; updateAuthView(); } if (!response.ok || !payload.success) { throw new Error(payload.message || "请求失败"); } return payload.data; } function updatePlatformStatus() { if (state.bootstrap.graphReady) { elements.platformStatus.textContent = "Graph 已就绪"; elements.platformSubstatus.textContent = state.bootstrap.graphFlavor || "Microsoft Graph Global"; } else { elements.platformStatus.textContent = "等待配置"; const errors = state.bootstrap.validationErrors || []; elements.platformSubstatus.textContent = errors.join(";") || "请补充 .env"; } } function updateAuthView() { const needsLogin = state.bootstrap.authEnabled && !state.authenticated; elements.loginSection.classList.toggle("hidden", !needsLogin); } function setConsole(content) { elements.resultConsole.textContent = content; } function appendConsole(title, summary) { const lines = [ `[${new Date().toLocaleString("zh-CN")}] ${title}`, String(summary || ""), "", ]; elements.resultConsole.textContent = lines.join("\n") + elements.resultConsole.textContent; } function formatTaskLabel(operation) { return TASK_LABELS[operation] || operation; } function formatTaskState(task) { if (task.status === "queued") { return "已提交"; } if (task.status === "running") { return "执行中"; } if (task.status === "succeeded") { return "已完成"; } if (task.status === "failed") { return "失败"; } return task.status || "未知"; } function summarizeTask(task) { const base = `${formatTaskLabel(task.operation)}:${task.completed}/${task.total},成功 ${task.successCount},失败 ${task.failureCount}`; if (task.failureCount > 0) { return `${base}。详细失败项请查看 logs/office365_admin.log`; } return base; } function showTaskCard(task) { if (!elements.taskStatusCard) { return; } elements.taskStatusCard.classList.remove("hidden"); elements.taskStatusTitle.textContent = formatTaskLabel(task.operation); elements.taskStatusState.textContent = formatTaskState(task); elements.taskProgressFill.style.width = `${task.progressPercent || 0}%`; const statusText = [ `进度 ${task.completed || 0} / ${task.total || 0}`, `成功 ${task.successCount || 0}`, `失败 ${task.failureCount || 0}`, ].join(" · "); const currentText = task.currentItem ? ` · 当前 ${task.currentItem}` : ""; elements.taskStatusText.textContent = `${statusText}${currentText}`; } function showSingleActionStatus(title, text) { if (!elements.taskStatusCard) { return; } elements.taskStatusCard.classList.remove("hidden"); elements.taskStatusTitle.textContent = title; elements.taskStatusState.textContent = "处理中"; elements.taskProgressFill.style.width = "30%"; elements.taskStatusText.textContent = text; } function completeSingleActionStatus(title, text) { if (!elements.taskStatusCard) { return; } elements.taskStatusCard.classList.remove("hidden"); elements.taskStatusTitle.textContent = title; elements.taskStatusState.textContent = "已完成"; elements.taskProgressFill.style.width = "100%"; elements.taskStatusText.textContent = text; } function showFailedActionStatus(title, text) { if (!elements.taskStatusCard) { return; } elements.taskStatusCard.classList.remove("hidden"); elements.taskStatusTitle.textContent = title; elements.taskStatusState.textContent = "失败"; elements.taskProgressFill.style.width = "100%"; elements.taskStatusText.textContent = text; } function clearTaskPolling() { if (state.activeTaskTimer) { window.clearTimeout(state.activeTaskTimer); state.activeTaskTimer = null; } } async function pollTask(taskId) { try { const task = await api(`/api/tasks/${encodeURIComponent(taskId)}`); showTaskCard(task); if (task.status === "queued" || task.status === "running") { clearTaskPolling(); state.activeTaskTimer = window.setTimeout(() => pollTask(taskId), 1000); return; } state.activeTaskId = ""; clearTaskPolling(); appendConsole( task.status === "failed" ? `${formatTaskLabel(task.operation)}失败` : `${formatTaskLabel(task.operation)}完成`, summarizeTask(task), ); await refreshAll(); } catch (error) { state.activeTaskId = ""; clearTaskPolling(); showFailedActionStatus("任务轮询失败", error.message); appendConsole("任务轮询失败", error.message); } } function startTask(task, submittedMessage) { state.activeTaskId = task.id; clearTaskPolling(); showTaskCard(task); appendConsole("任务已提交", submittedMessage); pollTask(task.id); } function statusPill(enabled) { const klass = enabled ? "active" : "disabled"; const label = enabled ? "启用" : "停用"; return `${label}`; } function selectedIdentifiers() { return Array.from(state.selectedUsers); } function updateSelectionUi() { const count = state.selectedUsers.size; if (elements.selectedCount) { elements.selectedCount.textContent = count ? `已选择 ${count} 个账号` : "未选择账号"; } const currentPageIds = state.users.map((user) => user.userPrincipalName).filter(Boolean); const selectedOnPage = currentPageIds.filter((identifier) => state.selectedUsers.has(identifier)).length; if (elements.selectPageCheckbox) { elements.selectPageCheckbox.checked = currentPageIds.length > 0 && selectedOnPage === currentPageIds.length; elements.selectPageCheckbox.indeterminate = selectedOnPage > 0 && selectedOnPage < currentPageIds.length; elements.selectPageCheckbox.disabled = currentPageIds.length === 0; } [ elements.clearSelectionBtn, elements.bulkEnableBtn, elements.bulkDisableBtn, elements.bulkResetBtn, elements.bulkDeleteBtn, ].forEach((button) => { if (button) { button.disabled = count === 0; } }); if (elements.selectAllResultsBtn) { elements.selectAllResultsBtn.disabled = state.total === 0; } } function renderUsers() { if (!state.users.length) { elements.usersTableBody.innerHTML = `没有匹配到用户`; updateSelectionUi(); return; } elements.usersTableBody.innerHTML = state.users.map((user) => { const identifier = user.userPrincipalName || ""; const checked = state.selectedUsers.has(identifier) ? "checked" : ""; return ` ${escapeHtml(user.displayName || "-")} ${escapeHtml(identifier || "-")}
${escapeHtml(user.department || "-")}
${escapeHtml(user.jobTitle || "-")}
${statusPill(user.accountEnabled)} ${user.assignedLicensesCount || 0}
`; }).join(""); elements.paginationInfo.textContent = `第 ${state.page} 页,当前 ${state.users.length} / 共 ${state.total} 条`; updateSelectionUi(); } function renderMetrics() { elements.metricTotal.textContent = String(state.total); elements.metricActive.textContent = String(state.summary.active || 0); elements.metricDisabled.textContent = String(state.summary.disabled || 0); const available = state.licenses.reduce((sum, item) => sum + (item.availableUnits || 0), 0); elements.metricLicense.textContent = String(available); const totalFoot = state.search ? `搜索结果 ${state.total} / 全量 ${state.totalBeforeSearch}` : `全量账号 ${state.totalBeforeSearch}`; document.getElementById("metric-total-foot").textContent = totalFoot; } function renderLicenses() { if (!state.licenses.length) { elements.licenseList.innerHTML = `
未获取到许可证信息
`; return; } elements.licenseList.innerHTML = state.licenses.map((item) => `
${escapeHtml(item.skuPartNumber || "UNKNOWN")}
可用席位:${item.availableUnits}
已用席位:${item.consumedUnits}
总席位:${item.totalUnits}
`).join(""); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } async function fetchSession() { const data = await api("/api/session"); state.authenticated = data.authenticated; updateAuthView(); } async function fetchUsers() { const query = new URLSearchParams({ page: String(state.page), pageSize: String(state.pageSize), search: state.search, }); const data = await api(`/api/users?${query.toString()}`); state.users = data.items; state.total = data.total; state.totalBeforeSearch = data.totalBeforeSearch; state.summary = data.summary; renderUsers(); renderMetrics(); } async function fetchLicenses() { const data = await api("/api/licenses"); state.licenses = data; renderLicenses(); renderMetrics(); } async function fetchUserDetail(identifier) { const data = await api(`/api/users/${encodeURIComponent(identifier)}`); state.selectedUser = data; fillUserForm(data); appendConsole("已加载用户", data.userPrincipalName || identifier); } function formPayload() { return { userPrincipalName: document.getElementById("userPrincipalName").value.trim(), displayName: document.getElementById("displayName").value.trim(), givenName: document.getElementById("givenName").value.trim(), surname: document.getElementById("surname").value.trim(), department: document.getElementById("department").value.trim(), jobTitle: document.getElementById("jobTitle").value.trim(), officeLocation: document.getElementById("officeLocation").value.trim(), mobilePhone: document.getElementById("mobilePhone").value.trim(), usageLocation: document.getElementById("usageLocation").value.trim(), skuPartNumber: document.getElementById("skuPartNumber").value.trim(), password: document.getElementById("password").value, accountEnabled: document.getElementById("accountEnabled").checked, forceChangePasswordNextSignIn: document.getElementById("forceChangePasswordNextSignIn").checked, }; } function fillUserForm(user) { elements.selectedUserId.value = user.id || ""; document.getElementById("userPrincipalName").value = user.userPrincipalName || ""; document.getElementById("displayName").value = user.displayName || ""; document.getElementById("givenName").value = user.givenName || ""; document.getElementById("surname").value = user.surname || ""; document.getElementById("department").value = user.department || ""; document.getElementById("jobTitle").value = user.jobTitle || ""; document.getElementById("officeLocation").value = user.officeLocation || ""; document.getElementById("mobilePhone").value = user.mobilePhone || ""; document.getElementById("usageLocation").value = user.usageLocation || state.bootstrap.defaultUsageLocation || ""; document.getElementById("skuPartNumber").value = (user.licenseLabels && user.licenseLabels[0]) || ""; document.getElementById("password").value = ""; document.getElementById("accountEnabled").checked = Boolean(user.accountEnabled); document.getElementById("forceChangePasswordNextSignIn").checked = state.bootstrap.forceChangePassword; } function clearUserForm() { state.selectedUser = null; elements.selectedUserId.value = ""; elements.userForm.reset(); document.getElementById("accountEnabled").checked = true; document.getElementById("forceChangePasswordNextSignIn").checked = state.bootstrap.forceChangePassword; document.getElementById("usageLocation").value = state.bootstrap.defaultUsageLocation || ""; } function toggleUserSelection(identifier, checked) { if (!identifier) { return; } if (checked) { state.selectedUsers.add(identifier); } else { state.selectedUsers.delete(identifier); } updateSelectionUi(); } function clearSelection() { state.selectedUsers.clear(); updateSelectionUi(); renderUsers(); } function toggleCurrentPageSelection(checked) { state.users.forEach((user) => { if (!user.userPrincipalName) { return; } if (checked) { state.selectedUsers.add(user.userPrincipalName); } else { state.selectedUsers.delete(user.userPrincipalName); } }); renderUsers(); } async function selectAllMatchingUsers() { const query = new URLSearchParams({ search: state.search }); const data = await api(`/api/users/selection?${query.toString()}`); state.selectedUsers = new Set(data.identifiers || []); renderUsers(); appendConsole("已选中搜索结果", `共 ${state.selectedUsers.size} 个账号`); } async function saveUser(event) { event.preventDefault(); const payload = formPayload(); const identifier = payload.userPrincipalName; if (!identifier) { throw new Error("请先填写账号或邮箱。"); } showSingleActionStatus("保存用户", identifier); let data; if (state.selectedUser && state.selectedUser.userPrincipalName) { data = await api(`/api/users/${encodeURIComponent(state.selectedUser.userPrincipalName)}`, { method: "PATCH", body: JSON.stringify(payload), }); appendConsole("用户已更新", data.user.userPrincipalName || identifier); } else { data = await api("/api/users", { method: "POST", body: JSON.stringify(payload), }); appendConsole("用户已创建", data.user.userPrincipalName || identifier); } completeSingleActionStatus("保存用户", data.user.userPrincipalName || identifier); await refreshAll(); if (data.user && data.user.userPrincipalName) { await fetchUserDetail(data.user.userPrincipalName); } } async function deleteCurrentUser(identifier) { if (!identifier) { throw new Error("请先选择一个用户。"); } const confirmDelete = window.confirm(`确认删除 ${identifier} 吗?此操作不可撤销。`); if (!confirmDelete) { return; } showSingleActionStatus("删除用户", identifier); await api(`/api/users/${encodeURIComponent(identifier)}`, { method: "DELETE" }); state.selectedUsers.delete(identifier); appendConsole("用户已删除", identifier); completeSingleActionStatus("删除用户", identifier); clearUserForm(); await refreshAll(); } async function resetPassword(identifier) { if (!identifier) { throw new Error("请先选择一个用户。"); } const payload = { password: document.getElementById("password").value, forceChangePasswordNextSignIn: document.getElementById("forceChangePasswordNextSignIn").checked, }; showSingleActionStatus("重置密码", identifier); await api(`/api/users/${encodeURIComponent(identifier)}/reset-password`, { method: "POST", body: JSON.stringify(payload), }); appendConsole("密码已重置", identifier); completeSingleActionStatus("重置密码", identifier); } async function submitBatchTask(action, body, summaryText) { const task = await api(`/api/users/batch/${action}`, { method: "POST", body: JSON.stringify(body), }); startTask(task, summaryText); } async function runSelectedBulk(action) { const identifiers = selectedIdentifiers(); if (!identifiers.length) { throw new Error("请先勾选至少一个用户。"); } if (action === "delete") { const confirmed = window.confirm(`确认批量删除这 ${identifiers.length} 个账号吗?此操作不可撤销。`); if (!confirmed) { return; } await submitBatchTask("delete", { identifiers }, `批量删除任务已提交,共 ${identifiers.length} 个账号`); } else if (action === "enable" || action === "disable") { const rows = identifiers.map((userPrincipalName) => ({ userPrincipalName, accountEnabled: action === "enable", })); await submitBatchTask("update", { rows }, `${action === "enable" ? "批量启用" : "批量停用"}任务已提交,共 ${identifiers.length} 个账号`); } else if (action === "reset-password") { const promptedPassword = window.prompt("输入统一临时密码;留空则使用系统默认密码。", ""); if (promptedPassword === null) { return; } const password = promptedPassword.trim(); const rows = identifiers.map((userPrincipalName) => ({ userPrincipalName, ...(password ? { password } : {}), forceChangePasswordNextSignIn: true, })); await submitBatchTask("reset-password", { rows }, `批量改密任务已提交,共 ${identifiers.length} 个账号`); } else { throw new Error("不支持的批量操作。"); } clearSelection(); } async function runBatch(action) { const textareaMap = { "create": document.getElementById("batch-create-content"), "update": document.getElementById("batch-update-content"), "delete": document.getElementById("batch-delete-content"), "reset-password": document.getElementById("batch-reset-content"), }; const textarea = textareaMap[action]; const content = textarea.value.trim(); if (!content) { throw new Error("请先粘贴批量内容或上传文件。"); } await submitBatchTask(action, { content }, `${formatTaskLabel(action)}任务已提交,等待后台处理`); } async function refreshAll() { updatePlatformStatus(); if (state.bootstrap.authEnabled && !state.authenticated) { return; } if (!state.bootstrap.graphReady) { return; } await Promise.all([fetchUsers(), fetchLicenses()]); } function bindEvents() { elements.loginForm?.addEventListener("submit", async (event) => { event.preventDefault(); try { showSingleActionStatus("登录后台", "正在验证管理员账号"); await api("/api/login", { method: "POST", body: JSON.stringify({ username: document.getElementById("login-username").value.trim(), password: document.getElementById("login-password").value, }), }); await fetchSession(); await refreshAll(); appendConsole("登录成功", "现在可以开始管理租户账号"); completeSingleActionStatus("登录后台", "登录成功"); } catch (error) { showFailedActionStatus("登录后台", error.message); appendConsole("登录失败", error.message); } }); elements.logoutBtn?.addEventListener("click", async () => { await api("/api/logout", { method: "POST" }); state.authenticated = false; updateAuthView(); appendConsole("已退出后台", "如需继续操作,请重新登录"); }); elements.refreshAllBtn?.addEventListener("click", async () => { try { showSingleActionStatus("刷新数据", "正在重新加载用户与许可证"); await refreshAll(); appendConsole("刷新完成", "用户列表和许可证信息已更新"); completeSingleActionStatus("刷新数据", "刷新完成"); } catch (error) { showFailedActionStatus("刷新数据", error.message); appendConsole("刷新失败", error.message); } }); elements.searchForm?.addEventListener("submit", async (event) => { event.preventDefault(); state.search = elements.searchInput.value.trim(); state.page = 1; try { showSingleActionStatus("搜索用户", state.search || "全部用户"); await fetchUsers(); appendConsole("搜索完成", `${state.total} 条匹配结果`); completeSingleActionStatus("搜索用户", `${state.total} 条匹配结果`); } catch (error) { showFailedActionStatus("搜索用户", error.message); appendConsole("搜索失败", error.message); } }); elements.prevPageBtn?.addEventListener("click", async () => { if (state.page <= 1) { return; } state.page -= 1; await fetchUsers(); }); elements.nextPageBtn?.addEventListener("click", async () => { if (state.page * state.pageSize >= state.total) { return; } state.page += 1; await fetchUsers(); }); elements.selectPageCheckbox?.addEventListener("change", (event) => { toggleCurrentPageSelection(event.target.checked); }); elements.selectAllResultsBtn?.addEventListener("click", async () => { try { await selectAllMatchingUsers(); } catch (error) { appendConsole("全选失败", error.message); } }); elements.clearSelectionBtn?.addEventListener("click", () => { clearSelection(); appendConsole("已清空选择", "当前未勾选任何账号"); }); elements.bulkEnableBtn?.addEventListener("click", async () => { try { await runSelectedBulk("enable"); } catch (error) { appendConsole("批量启用失败", error.message); } }); elements.bulkDisableBtn?.addEventListener("click", async () => { try { await runSelectedBulk("disable"); } catch (error) { appendConsole("批量停用失败", error.message); } }); elements.bulkResetBtn?.addEventListener("click", async () => { try { await runSelectedBulk("reset-password"); } catch (error) { appendConsole("批量改密失败", error.message); } }); elements.bulkDeleteBtn?.addEventListener("click", async () => { try { await runSelectedBulk("delete"); } catch (error) { appendConsole("批量删除失败", error.message); } }); elements.usersTableBody?.addEventListener("change", (event) => { const checkbox = event.target.closest("input[data-select-identifier]"); if (!checkbox) { return; } const identifier = decodeURIComponent(checkbox.dataset.selectIdentifier || ""); toggleUserSelection(identifier, checkbox.checked); }); elements.usersTableBody?.addEventListener("click", async (event) => { const button = event.target.closest("button[data-action]"); if (!button) { return; } const action = button.dataset.action; const identifier = decodeURIComponent(button.dataset.identifier || ""); try { if (action === "view") { await fetchUserDetail(identifier); } else if (action === "delete") { await deleteCurrentUser(identifier); } else if (action === "reset") { await resetPassword(identifier); } } catch (error) { appendConsole("用户操作失败", error.message); showFailedActionStatus("用户操作失败", error.message); } }); elements.userForm?.addEventListener("submit", async (event) => { try { await saveUser(event); } catch (error) { appendConsole("保存失败", error.message); showFailedActionStatus("保存用户", error.message); } }); elements.newUserBtn?.addEventListener("click", () => clearUserForm()); elements.clearFormBtn?.addEventListener("click", () => clearUserForm()); elements.deleteUserBtn?.addEventListener("click", async () => { try { await deleteCurrentUser(state.selectedUser && state.selectedUser.userPrincipalName); } catch (error) { appendConsole("删除失败", error.message); showFailedActionStatus("删除用户", error.message); } }); elements.resetPasswordBtn?.addEventListener("click", async () => { try { await resetPassword(state.selectedUser && state.selectedUser.userPrincipalName); } catch (error) { appendConsole("重置密码失败", error.message); showFailedActionStatus("重置密码", error.message); } }); document.querySelectorAll("[data-batch-action]").forEach((button) => { button.addEventListener("click", async () => { try { await runBatch(button.dataset.batchAction); } catch (error) { appendConsole("批量任务提交失败", error.message); showFailedActionStatus("批量任务提交失败", error.message); } }); }); document.querySelectorAll(".file-input").forEach((input) => { input.addEventListener("change", async (event) => { const file = event.target.files && event.target.files[0]; if (!file) { return; } const targetId = event.target.dataset.target; const target = document.getElementById(targetId); target.value = await file.text(); appendConsole("文件已加载", `${file.name} 已写入输入框`); }); }); } async function boot() { setConsole("等待操作..."); updatePlatformStatus(); bindEvents(); updateSelectionUi(); try { await fetchSession(); if (!state.bootstrap.authEnabled || state.authenticated) { await refreshAll(); } } catch (error) { appendConsole("初始化失败", error.message); showFailedActionStatus("初始化失败", error.message); } } boot();