435 lines
21 KiB
HTML
435 lines
21 KiB
HTML
<!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">​</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>
|