Initial commit: Office365 web management platform
This commit is contained in:
839
office365_admin/static/app.js
Normal file
839
office365_admin/static/app.js
Normal file
@@ -0,0 +1,839 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user