feat(admin): 账号区域标识回填
This commit is contained in:
parent
34bce0f5df
commit
64585261c4
|
|
@ -55,6 +55,7 @@ def _scrub_user_payload(user: Dict[str, Any]) -> Dict[str, Any]:
|
|||
"name": user.get("service_department_name"),
|
||||
"code": user.get("service_department_code"),
|
||||
"region_id": user.get("service_department_region_id"),
|
||||
"region_name": user.get("service_department_region_name"),
|
||||
"parent_id": user.get("service_department_parent_id"),
|
||||
"phone": user.get("service_department_phone"),
|
||||
"role": user.get("department_role"),
|
||||
|
|
|
|||
|
|
@ -275,11 +275,23 @@ def admin_create_user():
|
|||
return jsonify({"success": False, "message": "用户名和密码均不能为空"}), 400
|
||||
parent_department_id = (payload.get("parent_department_id") or "").strip() or None
|
||||
service_department_id = (payload.get("service_department_id") or "").strip() or None
|
||||
region_id = (payload.get("region_id") or "").strip() or None
|
||||
department_phone = (payload.get("department_phone") or "").strip() or None
|
||||
|
||||
# 如果未显式绑定部门,则为该用户创建一个同名单位,并按父级决定层级
|
||||
created_department: Optional[Dict[str, Any]] = None
|
||||
if not service_department_id:
|
||||
# 未显式指定区域时,继承父级部门的区域
|
||||
if not region_id and parent_department_id:
|
||||
try:
|
||||
parent_dept = next(
|
||||
(dept for dept in list_service_departments() if str(dept.get("id")) == parent_department_id),
|
||||
None
|
||||
)
|
||||
if parent_dept:
|
||||
region_id = parent_dept.get("region_id")
|
||||
except Exception:
|
||||
region_id = None
|
||||
try:
|
||||
dept_name = (payload.get("display_name") or username).strip() or username
|
||||
dept_code = username.upper()
|
||||
|
|
@ -288,6 +300,7 @@ def admin_create_user():
|
|||
code=dept_code,
|
||||
phone=department_phone,
|
||||
parent_id=parent_department_id,
|
||||
region_id=region_id,
|
||||
)
|
||||
service_department_id = created_department.get("id")
|
||||
except Exception as exc:
|
||||
|
|
@ -303,6 +316,7 @@ def admin_create_user():
|
|||
service_department_id=service_department_id,
|
||||
department_role=payload.get("department_role"),
|
||||
parent_department_id=parent_department_id,
|
||||
service_department_region_id=region_id,
|
||||
service_department_phone=department_phone,
|
||||
)
|
||||
return jsonify({"success": True, "data": {"user": user, "department": created_department}})
|
||||
|
|
@ -508,6 +522,16 @@ def admin_create_service_department():
|
|||
"description": payload.get("description"),
|
||||
"grade": grade,
|
||||
}
|
||||
if not kwargs["region_id"] and kwargs["parent_id"]:
|
||||
try:
|
||||
parent_dept = next(
|
||||
(dept for dept in list_service_departments() if str(dept.get("id")) == kwargs["parent_id"]),
|
||||
None
|
||||
)
|
||||
if parent_dept:
|
||||
kwargs["region_id"] = parent_dept.get("region_id")
|
||||
except Exception:
|
||||
kwargs["region_id"] = None
|
||||
try:
|
||||
department = create_service_department(name, code=code_token, **kwargs)
|
||||
department_id = department.get("id")
|
||||
|
|
|
|||
|
|
@ -279,6 +279,7 @@ def _public_user_payload(user: Dict[str, Any]) -> Dict[str, Any]:
|
|||
"phone": user.get("service_department_phone"),
|
||||
"parent_id": user.get("service_department_parent_id"),
|
||||
"region_id": user.get("service_department_region_id"),
|
||||
"region_name": user.get("service_department_region_name"),
|
||||
"role": user.get("department_role"),
|
||||
}
|
||||
created_at = user.get("created_at")
|
||||
|
|
@ -322,9 +323,11 @@ def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
|
|||
sd.code AS service_department_code,
|
||||
sd.phone AS service_department_phone,
|
||||
sd.parent_id AS service_department_parent_id,
|
||||
sd.region_id AS service_department_region_id
|
||||
sd.region_id AS service_department_region_id,
|
||||
r.name AS service_department_region_name
|
||||
FROM auth_users au
|
||||
LEFT JOIN service_departments sd ON sd.id = au.service_department_id
|
||||
LEFT JOIN regions r ON r.id = sd.region_id
|
||||
WHERE au.username = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
|
|
@ -359,9 +362,11 @@ def get_user_by_id(user_id: str) -> Optional[Dict[str, Any]]:
|
|||
sd.code AS service_department_code,
|
||||
sd.phone AS service_department_phone,
|
||||
sd.parent_id AS service_department_parent_id,
|
||||
sd.region_id AS service_department_region_id
|
||||
sd.region_id AS service_department_region_id,
|
||||
r.name AS service_department_region_name
|
||||
FROM auth_users au
|
||||
LEFT JOIN service_departments sd ON sd.id = au.service_department_id
|
||||
LEFT JOIN regions r ON r.id = sd.region_id
|
||||
WHERE au.id = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
|
|
@ -394,9 +399,11 @@ def list_users(include_inactive: bool = False) -> List[Dict[str, Any]]:
|
|||
sd.code AS service_department_code,
|
||||
sd.phone AS service_department_phone,
|
||||
sd.parent_id AS service_department_parent_id,
|
||||
sd.region_id AS service_department_region_id
|
||||
sd.region_id AS service_department_region_id,
|
||||
r.name AS service_department_region_name
|
||||
FROM auth_users au
|
||||
LEFT JOIN service_departments sd ON sd.id = au.service_department_id
|
||||
LEFT JOIN regions r ON r.id = sd.region_id
|
||||
{where_clause}
|
||||
ORDER BY au.created_at DESC
|
||||
"""
|
||||
|
|
@ -419,6 +426,7 @@ def create_user(
|
|||
service_department_id: Optional[str] = None,
|
||||
department_role: Optional[str] = None,
|
||||
parent_department_id: Optional[str] = None,
|
||||
service_department_region_id: Optional[str] = None,
|
||||
service_department_phone: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
username_clean = (username or "").strip().lower()
|
||||
|
|
@ -436,6 +444,7 @@ def create_user(
|
|||
code=dept_code,
|
||||
phone=(service_department_phone or "").strip() or None,
|
||||
parent_id=(parent_department_id or "").strip() or None,
|
||||
region_id=(service_department_region_id or "").strip() or None,
|
||||
)
|
||||
dept_token = created.get("id")
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -1185,6 +1185,7 @@
|
|||
<th>用户名</th>
|
||||
<th>显示名</th>
|
||||
<th>部门</th>
|
||||
<th>区域</th>
|
||||
<th>角色</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
|
|
@ -1214,6 +1215,11 @@
|
|||
<label>单位电话(可选)
|
||||
<input type="text" name="department_phone" placeholder="用于自动创建的单位联系电话">
|
||||
</label>
|
||||
<label>所属区域(可选)
|
||||
<select id="userCreateRegion" class="form-select" name="region_id">
|
||||
<option value="">不选择区域(默认继承上级区域)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>上级单位(可选,不选则创建顶级市级单位)
|
||||
<select id="userCreateParent" class="form-select">
|
||||
<option value="">不选择上级(顶级单位)</option>
|
||||
|
|
@ -1400,6 +1406,7 @@ const rootDeptsEl = document.getElementById('rootDepts');
|
|||
let state = {
|
||||
users: [],
|
||||
departments: [],
|
||||
regions: [],
|
||||
themes: [],
|
||||
templateMeta: {},
|
||||
userFilter: {
|
||||
|
|
@ -1482,7 +1489,17 @@ async function loadCurrentUser() {
|
|||
document.getElementById('currentUserName').textContent = user.display_name || user.username || '未知管理员';
|
||||
document.getElementById('currentUserRole').textContent = user.role || 'admin';
|
||||
const dept = user.department;
|
||||
document.getElementById('currentUserDept').textContent = dept ? `${dept.name || ''}${dept.code ? ' · ' + dept.code : ''}` : '未绑定部门';
|
||||
const deptPieces = [];
|
||||
if (dept) {
|
||||
if (dept.name) deptPieces.push(dept.name);
|
||||
if (dept.code) deptPieces.push(dept.code);
|
||||
if (dept.region_name) {
|
||||
deptPieces.push(dept.region_name);
|
||||
} else if (dept.region_id) {
|
||||
deptPieces.push(dept.region_id);
|
||||
}
|
||||
}
|
||||
document.getElementById('currentUserDept').textContent = dept ? (deptPieces.join(' · ') || '未绑定部门') : '未绑定部门';
|
||||
} catch (err) {
|
||||
console.error('认证失败:', err);
|
||||
// 认证失败时重定向到登录页面
|
||||
|
|
@ -1502,6 +1519,7 @@ function renderDepartmentOptions() {
|
|||
deptParentSelect.innerHTML = parentOptions.join('');
|
||||
}
|
||||
renderUserCreateParentSelector();
|
||||
renderUserCreateRegionSelector();
|
||||
if (userEditDept) {
|
||||
userEditDept.innerHTML = serviceOptions.join('');
|
||||
}
|
||||
|
|
@ -1520,6 +1538,37 @@ function renderUserCreateParentSelector() {
|
|||
parentSelect.innerHTML = options.join('');
|
||||
}
|
||||
|
||||
function buildRegionOptions(selectedId = '', includeBlank = true) {
|
||||
const options = [];
|
||||
if (includeBlank) {
|
||||
options.push('<option value="">不选择区域(继承上级或稍后再设)</option>');
|
||||
}
|
||||
let hasSelected = false;
|
||||
(state.regions || []).forEach(region => {
|
||||
const value = region.id || '';
|
||||
const name = region.name || value || '未命名区域';
|
||||
const selected = String(selectedId || '') === String(value) ? 'selected' : '';
|
||||
if (selected) {
|
||||
hasSelected = true;
|
||||
}
|
||||
options.push(`<option value="${value}" ${selected}>${name}</option>`);
|
||||
});
|
||||
if (selectedId && !hasSelected && !options.find(opt => opt.includes(`value="${selectedId}"`))) {
|
||||
options.push(`<option value="${selectedId}" selected>${selectedId}</option>`);
|
||||
}
|
||||
if (!options.length) {
|
||||
options.push('<option value="">暂无区域数据</option>');
|
||||
}
|
||||
return options.join('');
|
||||
}
|
||||
|
||||
function renderUserCreateRegionSelector() {
|
||||
const select = document.getElementById('userCreateRegion');
|
||||
if (!select) return;
|
||||
const current = select.value;
|
||||
select.innerHTML = buildRegionOptions(current, true);
|
||||
}
|
||||
|
||||
function renderRoleOptions() {
|
||||
const roles = new Set(DEFAULT_ROLE_ORDER);
|
||||
state.users.forEach(user => {
|
||||
|
|
@ -1548,7 +1597,8 @@ function applyUserFilters(users) {
|
|||
const roleFilter = state.userFilter.role || '';
|
||||
return users.filter(user => {
|
||||
const deptName = user.department ? (user.department.name || '') : '';
|
||||
const text = `${user.username || ''} ${user.display_name || ''} ${deptName}`.toLowerCase();
|
||||
const regionName = user.department ? (user.department.region_name || user.department.region_id || '') : '';
|
||||
const text = `${user.username || ''} ${user.display_name || ''} ${deptName} ${regionName}`.toLowerCase();
|
||||
const matchKeyword = !keyword || text.includes(keyword);
|
||||
const matchRole = !roleFilter || user.role === roleFilter;
|
||||
return matchKeyword && matchRole;
|
||||
|
|
@ -1559,16 +1609,18 @@ function renderUserTable() {
|
|||
if (!userTableBody) return;
|
||||
const filtered = applyUserFilters(state.users);
|
||||
if (!filtered.length) {
|
||||
userTableBody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:#6b7280;padding:14px 0;">暂无用户,点击右上角“创建账号”开始添加</td></tr>`;
|
||||
userTableBody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:#6b7280;padding:14px 0;">暂无用户,点击右上角“创建账号”开始添加</td></tr>`;
|
||||
return;
|
||||
}
|
||||
userTableBody.innerHTML = filtered.map(user => {
|
||||
const dept = user.department ? user.department.name : '—';
|
||||
const region = user.department ? (user.department.region_name || user.department.region_id || '—') : '—';
|
||||
const isActive = activeUserId && String(user.id) === String(activeUserId);
|
||||
return `<tr data-id="${user.id}" class="${isActive ? 'active-row' : ''}">
|
||||
<td>${user.username}</td>
|
||||
<td>${user.display_name || '—'}</td>
|
||||
<td>${dept}</td>
|
||||
<td>${region}</td>
|
||||
<td><span class="pill">${getRoleLabel(user.role)}</span></td>
|
||||
<td>${formatDate(user.created_at)}</td>
|
||||
<td>
|
||||
|
|
@ -1634,6 +1686,12 @@ async function refreshUsers(options = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
async function refreshRegions() {
|
||||
const data = await fetchJSON(`${API_BASE}/admin/regions`, {method: 'GET'});
|
||||
state.regions = data.data.regions || [];
|
||||
renderUserCreateRegionSelector();
|
||||
}
|
||||
|
||||
async function refreshDepartments() {
|
||||
const data = await fetchJSON(`${API_BASE}/admin/service-departments`, {method: 'GET'});
|
||||
state.departments = data.data.departments || [];
|
||||
|
|
@ -1676,12 +1734,14 @@ function fillUserDrawer(user) {
|
|||
if (userSummaryBox) {
|
||||
const dept = user.department ? `${user.department.name || ''}${user.department.code ? ' · ' + user.department.code : ''}` : '未绑定';
|
||||
const deptPhone = user.department ? (user.department.phone || '—') : '—';
|
||||
const region = user.department ? (user.department.region_name || user.department.region_id || '—') : '—';
|
||||
userSummaryBox.innerHTML = `
|
||||
<div class="summary-grid">
|
||||
<div><div class="label">登录账号</div><div class="value">${user.username || '—'}</div></div>
|
||||
<div><div class="label">显示名</div><div class="value">${user.display_name || '—'}</div></div>
|
||||
<div><div class="label">角色</div><div class="value"><span class="pill">${getRoleLabel(user.role)}</span></div></div>
|
||||
<div><div class="label">绑定部门</div><div class="value">${dept}</div></div>
|
||||
<div><div class="label">所属区域</div><div class="value">${region}</div></div>
|
||||
<div><div class="label">单位电话</div><div class="value">${deptPhone}</div></div>
|
||||
<div><div class="label">创建时间</div><div class="value">${formatDate(user.created_at)}</div></div>
|
||||
</div>
|
||||
|
|
@ -1778,13 +1838,25 @@ if (userCreateForm) {
|
|||
evt.preventDefault();
|
||||
const form = evt.target;
|
||||
const parentSelect = document.getElementById('userCreateParent');
|
||||
const regionSelect = document.getElementById('userCreateRegion');
|
||||
const parentDepartmentId = parentSelect ? (parentSelect.value || '') : '';
|
||||
|
||||
let regionId = regionSelect ? regionSelect.value : '';
|
||||
if (!regionId && parentDepartmentId) {
|
||||
const parentDept = state.departments.find(dept => String(dept.id) === String(parentDepartmentId));
|
||||
if (parentDept) {
|
||||
regionId = parentDept.region_id || '';
|
||||
}
|
||||
}
|
||||
if (!regionId && !parentDepartmentId) {
|
||||
showMessage('请选择所属区域,或指定上级单位继承区域', 'error');
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
username: form.username.value.trim(),
|
||||
password: form.password.value.trim(),
|
||||
display_name: form.display_name.value.trim(),
|
||||
parent_department_id: parentDepartmentId || null,
|
||||
region_id: regionId || null,
|
||||
department_phone: (form.department_phone.value || '').trim()
|
||||
};
|
||||
try {
|
||||
|
|
@ -1799,6 +1871,9 @@ if (userCreateForm) {
|
|||
if (parentSelect) {
|
||||
parentSelect.value = '';
|
||||
}
|
||||
if (regionSelect) {
|
||||
regionSelect.value = '';
|
||||
}
|
||||
form.department_phone.value = '';
|
||||
switchUserSubtab('user-list-panel');
|
||||
await refreshDepartments();
|
||||
|
|
@ -1807,6 +1882,18 @@ if (userCreateForm) {
|
|||
showMessage(err.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
const parentSelect = document.getElementById('userCreateParent');
|
||||
const regionSelect = document.getElementById('userCreateRegion');
|
||||
if (parentSelect && regionSelect) {
|
||||
parentSelect.addEventListener('change', () => {
|
||||
if (regionSelect.value) {
|
||||
return;
|
||||
}
|
||||
const parentDept = state.departments.find(dept => String(dept.id) === String(parentSelect.value || ''));
|
||||
regionSelect.value = parentDept ? (parentDept.region_id || '') : '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (userEditForm) {
|
||||
|
|
@ -2084,6 +2171,7 @@ function flattenTree(nodes, level = 0, result = [], parentId = null) {
|
|||
name: node.name || '未知部门',
|
||||
code: node.code || '',
|
||||
region_name: node.region_name || '',
|
||||
region_id: node.region_id || '',
|
||||
level,
|
||||
node
|
||||
};
|
||||
|
|
@ -2556,6 +2644,7 @@ document.getElementById('downloadTemplateBtn').addEventListener('click', () => {
|
|||
async function bootstrap() {
|
||||
await loadCurrentUser();
|
||||
await Promise.all([
|
||||
refreshRegions(),
|
||||
refreshUsers(),
|
||||
refreshDepartments(),
|
||||
refreshThemes(),
|
||||
|
|
@ -2634,6 +2723,7 @@ function showAddChildModal(parentNode) {
|
|||
name: formData.get('name'),
|
||||
code: formData.get('code'),
|
||||
phone: formData.get('phone'),
|
||||
region_id: formData.get('region_id'),
|
||||
parent_id: parentNode.id,
|
||||
description: formData.get('description'),
|
||||
grade: autoGrade // 自动计算,无需手动选择
|
||||
|
|
@ -2662,6 +2752,12 @@ function showAddChildModal(parentNode) {
|
|||
<label>联系电话
|
||||
<input type="text" name="phone" placeholder="座机或手机号">
|
||||
</label>
|
||||
<label>所属区域
|
||||
<select name="region_id">
|
||||
${buildRegionOptions(parentNode.region_id || '', true)}
|
||||
</select>
|
||||
<small style="color: #6b7280; font-size: 12px;">不选择则默认继承上级区域</small>
|
||||
</label>
|
||||
<label>备注
|
||||
<textarea name="description" rows="2" placeholder="可填写简要职责"></textarea>
|
||||
</label>
|
||||
|
|
@ -2676,6 +2772,7 @@ function showEditModal(nodeData) {
|
|||
return {
|
||||
name: formData.get('name'),
|
||||
phone: formData.get('phone'),
|
||||
region_id: formData.get('region_id'),
|
||||
description: formData.get('description')
|
||||
// 编辑时不修改grade,grade根据层级自动计算
|
||||
};
|
||||
|
|
@ -2702,6 +2799,11 @@ function showEditModal(nodeData) {
|
|||
<label>联系电话
|
||||
<input type="text" name="phone" value="${nodeData.phone || ''}">
|
||||
</label>
|
||||
<label>所属区域
|
||||
<select name="region_id">
|
||||
${buildRegionOptions(nodeData.region_id || '', true)}
|
||||
</select>
|
||||
</label>
|
||||
<label>备注
|
||||
<textarea name="description" rows="2">${nodeData.description || ''}</textarea>
|
||||
</label>
|
||||
|
|
|
|||
Loading…
Reference in New Issue