840 lines
28 KiB
JavaScript
840 lines
28 KiB
JavaScript
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 `<span class="status-pill ${klass}">${label}</span>`;
|
||
}
|
||
|
||
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 = `<tr><td colspan="7" class="empty-card">没有匹配到用户</td></tr>`;
|
||
updateSelectionUi();
|
||
return;
|
||
}
|
||
|
||
elements.usersTableBody.innerHTML = state.users.map((user) => {
|
||
const identifier = user.userPrincipalName || "";
|
||
const checked = state.selectedUsers.has(identifier) ? "checked" : "";
|
||
return `
|
||
<tr>
|
||
<td class="check-cell">
|
||
<input type="checkbox" data-select-identifier="${encodeURIComponent(identifier)}" ${checked}>
|
||
</td>
|
||
<td>${escapeHtml(user.displayName || "-")}</td>
|
||
<td>${escapeHtml(identifier || "-")}</td>
|
||
<td>
|
||
<div>${escapeHtml(user.department || "-")}</div>
|
||
<div class="muted">${escapeHtml(user.jobTitle || "-")}</div>
|
||
</td>
|
||
<td>${statusPill(user.accountEnabled)}</td>
|
||
<td>${user.assignedLicensesCount || 0}</td>
|
||
<td>
|
||
<div class="actions-inline">
|
||
<button class="btn btn-secondary" data-action="view" data-identifier="${encodeURIComponent(identifier)}">查看</button>
|
||
<button class="btn btn-ghost" data-action="reset" data-identifier="${encodeURIComponent(identifier)}">改密</button>
|
||
<button class="btn btn-danger" data-action="delete" data-identifier="${encodeURIComponent(identifier)}">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).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 = `<div class="empty-card">未获取到许可证信息</div>`;
|
||
return;
|
||
}
|
||
|
||
elements.licenseList.innerHTML = state.licenses.map((item) => `
|
||
<article class="license-card">
|
||
<strong>${escapeHtml(item.skuPartNumber || "UNKNOWN")}</strong>
|
||
<div>可用席位:${item.availableUnits}</div>
|
||
<div>已用席位:${item.consumedUnits}</div>
|
||
<div>总席位:${item.totalUnits}</div>
|
||
</article>
|
||
`).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();
|