ad-user-creator/ad_user_creator/templates/index.html

435 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>AD 用户创建</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50 text-gray-900 font-sans min-h-screen py-10">
<div class="max-w-4xl mx-auto bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100">
<!-- Header -->
<div class="bg-gradient-to-r from-blue-600 to-indigo-700 px-8 py-6">
<h1 class="text-2xl font-bold text-white tracking-tight">AD 域用户自动创建系统</h1>
<p class="text-blue-100 text-sm mt-1">请填写以下信息以预览或创建新用户</p>
</div>
<!-- Main Content -->
<div class="p-8">
<form id="form" class="space-y-6">
<!-- Grid: Basic Info -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">显示名称 (姓名)</label>
<input type="text" name="display_name" required
class="block w-full rounded-md border-gray-300 shadow-sm border p-2.5 focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-gray-50 focus:bg-white transition-colors"
placeholder="例如:张三" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">用户名 (sAMAccountName)</label>
<input type="text" name="sam_account_name" required
class="block w-full rounded-md border-gray-300 shadow-sm border p-2.5 focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-gray-50 focus:bg-white transition-colors"
placeholder="例如zhangsan" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱 (mail)</label>
<input type="email" name="email" required
class="block w-full rounded-md border-gray-300 shadow-sm border p-2.5 focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-gray-50 focus:bg-white transition-colors"
placeholder="例如zhangsan@example.com" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">部门 OU</label>
<select name="dept_ou" id="dept_ou" required
class="block w-full rounded-md border-gray-300 shadow-sm border p-2.5 focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-gray-50 focus:bg-white transition-colors">
<option value="">请选择部门 OU...</option>
</select>
</div>
</div>
<hr class="border-gray-200" />
<!-- Grid: Groups -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">基础组</label>
<select name="base_group" id="base_group"
class="block w-full rounded-md border-gray-300 shadow-sm border p-2.5 focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-gray-50 focus:bg-white transition-colors">
<option value="">(默认 staff)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">项目组</label>
<!-- Checkboxes will be injected here -->
<div id="project_groups_container" class="space-y-2 mt-2 max-h-40 overflow-y-auto p-2 border border-gray-200 rounded-md bg-gray-50">
<span class="text-sm text-gray-400">加载中...</span>
</div>
<!-- Hidden input to store comma-separated values -->
<input type="hidden" name="project_groups" id="project_groups_input" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">资源组</label>
<div id="resource_groups_container" class="space-y-2 mt-2 max-h-40 overflow-y-auto p-2 border border-gray-200 rounded-md bg-gray-50">
<span class="text-sm text-gray-400">加载中...</span>
</div>
<input type="hidden" name="resource_groups" id="resource_groups_input" />
</div>
</div>
<!-- Actions -->
<div class="pt-4 flex items-center justify-between">
<button type="button" id="btnConfig" class="inline-flex justify-center rounded-md border border-gray-300 bg-gray-50 px-4 py-2 text-sm font-medium text-gray-600 shadow-sm hover:bg-gray-100 focus:outline-none transition-all">
⚙️ 组别配置
</button>
<div class="flex space-x-4">
<button type="button" id="btnPreview"
class="inline-flex justify-center rounded-md border border-gray-300 bg-white px-6 py-2.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all">
预览
</button>
<button type="button" id="btnCreate"
class="inline-flex justify-center rounded-md border border-transparent bg-blue-600 px-6 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all">
确认创建
</button>
</div>
</div>
</form>
<!-- Output Area -->
<div id="output" class="mt-8 hidden">
<!-- Content will be injected here by JS -->
</div>
</div>
</div>
<!-- Config Modal -->
<div id="configModal" class="fixed inset-0 z-50 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true" id="modalBackdrop"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full sm:p-6">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">组别与 OU 配置</h3>
<p class="text-sm text-gray-500 mt-1">每行填写一个选项。保存后将写入配置文件并即时生效。</p>
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">部门 OU 列表</label>
<textarea id="config_ou_list" rows="4" class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm p-2 sm:text-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">基础组列表</label>
<textarea id="config_base_group_list" rows="4" class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm p-2 sm:text-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">项目组列表</label>
<textarea id="config_project_group_list" rows="4" class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm p-2 sm:text-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">资源组列表</label>
<textarea id="config_resource_group_list" rows="4" class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm p-2 sm:text-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6 sm:flex sm:flex-row-reverse">
<button type="button" id="btnSaveConfig" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm transition-all">保存配置</button>
<button type="button" id="btnCancelConfig" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:w-auto sm:text-sm transition-all">取消</button>
</div>
</div>
</div>
</div>
<script>
const form = document.getElementById('form');
const output = document.getElementById('output');
const btnPreview = document.getElementById('btnPreview');
const btnCreate = document.getElementById('btnCreate');
// Config elements
const btnConfig = document.getElementById('btnConfig');
const configModal = document.getElementById('configModal');
const btnSaveConfig = document.getElementById('btnSaveConfig');
const btnCancelConfig = document.getElementById('btnCancelConfig');
const modalBackdrop = document.getElementById('modalBackdrop');
// UI options data
let currentUIOptions = {
ou_list: [],
base_group_list: [],
project_group_list: [],
resource_group_list: []
};
function updateCheckboxValues(containerId, inputId) {
const container = document.getElementById(containerId);
const input = document.getElementById(inputId);
const checkedBoxes = container.querySelectorAll('input[type="checkbox"]:checked');
const values = Array.from(checkedBoxes).map(cb => cb.value);
input.value = values.join(',');
}
function renderCheckboxes(containerId, inputId, items) {
const container = document.getElementById(containerId);
if (!items || items.length === 0) {
container.innerHTML = '<span class="text-sm text-gray-400">无可用选项</span>';
return;
}
container.innerHTML = '';
items.forEach(item => {
const div = document.createElement('div');
div.className = 'flex items-center';
div.innerHTML = `
<input type="checkbox" value="${item}" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" onchange="updateCheckboxValues('${containerId}', '${inputId}')">
<label class="ml-2 block text-sm text-gray-900">${item}</label>
`;
container.appendChild(div);
});
}
function renderSelect(selectId, items, defaultPlaceholder) {
const select = document.getElementById(selectId);
select.innerHTML = `<option value="">${defaultPlaceholder}</option>`;
if (items && items.length > 0) {
items.forEach(item => {
const opt = document.createElement('option');
opt.value = item;
opt.textContent = item;
select.appendChild(opt);
});
}
}
async function loadUIOptions() {
try {
const res = await fetch('/api/config/ui-options');
if (res.ok) {
currentUIOptions = await res.json();
renderSelect('dept_ou', currentUIOptions.ou_list, '请选择部门 OU...');
renderSelect('base_group', currentUIOptions.base_group_list, '请选择基础组...');
// 如果列表包含 staff设置为默认选项
if (currentUIOptions.base_group_list && currentUIOptions.base_group_list.includes('staff')) {
document.getElementById('base_group').value = 'staff';
}
renderCheckboxes('project_groups_container', 'project_groups_input', currentUIOptions.project_group_list);
renderCheckboxes('resource_groups_container', 'resource_groups_input', currentUIOptions.resource_group_list);
}
} catch (err) {
console.error("加载 UI 配置失败", err);
}
}
// Modal behavior
function openConfigModal() {
document.getElementById('config_ou_list').value = currentUIOptions.ou_list.join('\n');
document.getElementById('config_base_group_list').value = currentUIOptions.base_group_list.join('\n');
document.getElementById('config_project_group_list').value = currentUIOptions.project_group_list.join('\n');
document.getElementById('config_resource_group_list').value = currentUIOptions.resource_group_list.join('\n');
configModal.classList.remove('hidden');
}
function closeConfigModal() {
configModal.classList.add('hidden');
}
async function saveConfig() {
const newVal = {
ou_list: document.getElementById('config_ou_list').value.split('\n'),
base_group_list: document.getElementById('config_base_group_list').value.split('\n'),
project_group_list: document.getElementById('config_project_group_list').value.split('\n'),
resource_group_list: document.getElementById('config_resource_group_list').value.split('\n')
};
btnSaveConfig.disabled = true;
btnSaveConfig.textContent = '保存中...';
try {
const res = await fetch('/api/config/ui-options', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newVal)
});
if (res.ok) {
closeConfigModal();
await loadUIOptions(); // reload UI
} else {
alert('保存失败');
}
} catch (err) {
alert('网络错误');
} finally {
btnSaveConfig.disabled = false;
btnSaveConfig.textContent = '保存配置';
}
}
btnConfig.addEventListener('click', openConfigModal);
btnCancelConfig.addEventListener('click', closeConfigModal);
modalBackdrop.addEventListener('click', closeConfigModal);
btnSaveConfig.addEventListener('click', saveConfig);
// Initial load
loadUIOptions();
function getBody() {
const fd = new FormData(form);
return {
display_name: (fd.get('display_name') || '').trim(),
sam_account_name: (fd.get('sam_account_name') || '').trim(),
email: (fd.get('email') || '').trim(),
dept_ou: (fd.get('dept_ou') || '').trim(),
base_group: (fd.get('base_group') || '').trim(),
project_groups: (fd.get('project_groups') || '').trim(),
resource_groups: (fd.get('resource_groups') || '').trim(),
};
}
// 格式化值的辅助函数,处理嵌套对象或数组
function formatValue(val, key, fullData) {
if (val === null || val === undefined) return '<span class="text-gray-400 italic">null</span>';
if (typeof val === 'boolean') {
return val
? '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">True</span>'
: '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">False</span>';
}
if (Array.isArray(val)) {
if (val.length === 0) return '<span class="text-gray-400 italic">空数组</span>';
return '<ul class="list-disc pl-4 space-y-1 text-sm">' + val.map(v => {
let itemHtml = formatValue(v, key, fullData);
// 特殊处理组的展示状态
if (fullData && fullData.groups_exist_in_ldap && fullData.groups_exist_in_ldap[v] !== undefined) {
const exists = fullData.groups_exist_in_ldap[v];
const badge = exists
? '<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">已存在</span>'
: '<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">不存在</span>';
return `<li>${itemHtml} ${badge}</li>`;
}
return `<li>${itemHtml}</li>`;
}).join('') + '</ul>';
}
if (typeof val === 'object') {
return `<pre class="text-xs bg-gray-50 p-2 rounded border border-gray-100 overflow-x-auto">${JSON.stringify(val, null, 2)}</pre>`;
}
let html = `<span class="text-sm text-gray-900">${val}</span>`;
// 特殊处理基础组
if (key === 'base_group' && fullData && fullData.groups_exist_in_ldap && fullData.groups_exist_in_ldap[val] !== undefined) {
const exists = fullData.groups_exist_in_ldap[val];
const badge = exists
? '<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">已存在</span>'
: '<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">不存在</span>';
html += badge;
}
return html;
}
function show(data, isError) {
output.classList.remove('hidden');
output.innerHTML = ''; // 清空内容
if (isError) {
// 渲染错误警告框
const errorMsg = typeof data === 'string' ? data : JSON.stringify(data);
output.innerHTML = `
<div class="rounded-md bg-red-50 p-4 border border-red-200">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">操作失败</h3>
<div class="mt-2 text-sm text-red-700 whitespace-pre-wrap">${errorMsg}</div>
</div>
</div>
</div>
`;
return;
}
// 渲染成功的表格 (如果是对象的话)
if (typeof data === 'object' && data !== null) {
let rowsHtml = '';
for (const [key, value] of Object.entries(data)) {
if (key === 'groups_exist_in_ldap') continue; // hide this helper property from table
rowsHtml += `
<tr class="hover:bg-gray-50 transition-colors">
<td class="whitespace-nowrap py-3 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 border-b border-gray-200">${key}</td>
<td class="px-3 py-3 text-sm text-gray-500 border-b border-gray-200">${formatValue(value, key, data)}</td>
</tr>
`;
}
output.innerHTML = `
<div class="mt-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">执行结果</h3>
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 w-1/3">属性 (Key)</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">值 (Value)</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
${rowsHtml}
</tbody>
</table>
</div>
</div>
`;
} else {
// 如果后端返回的不是对象,降级显示为文本
output.innerHTML = `<div class="p-4 bg-gray-50 rounded-md border border-gray-200 text-sm whitespace-pre-wrap">${data}</div>`;
}
}
async function post(url) {
// 禁用按钮并显示加载状态(可选,提升体验)
const prevTextCreate = btnCreate.textContent;
const prevTextPreview = btnPreview.textContent;
btnCreate.disabled = true;
btnPreview.disabled = true;
btnCreate.classList.add('opacity-50', 'cursor-not-allowed');
btnPreview.classList.add('opacity-50', 'cursor-not-allowed');
if (url.includes('create')) btnCreate.textContent = '处理中...';
if (url.includes('preview')) btnPreview.textContent = '预览中...';
try {
const body = getBody();
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
let json;
try {
json = await res.json();
} catch(e) {
json = {};
}
if (!res.ok) {
show(json.detail || res.statusText || '请求失败', true);
return;
}
show(json);
} catch (err) {
show(err.message || '网络或未知错误', true);
} finally {
// 恢复按钮状态
btnCreate.disabled = false;
btnPreview.disabled = false;
btnCreate.classList.remove('opacity-50', 'cursor-not-allowed');
btnPreview.classList.remove('opacity-50', 'cursor-not-allowed');
btnCreate.textContent = prevTextCreate;
btnPreview.textContent = prevTextPreview;
}
}
btnPreview.addEventListener('click', () => post('/api/preview'));
btnCreate.addEventListener('click', () => post('/api/create'));
</script>
</body>
</html>