Cleanup: Organize project structure and resolve merge conflicts

This commit is contained in:
Codex Agent 2025-12-25 19:18:37 +08:00
parent 2440a02a2d
commit 762d0c0115
13 changed files with 333 additions and 287 deletions

View File

@ -1,69 +0,0 @@
《卫星地面接收设施安装服务许可证》(换发)审批,《卫星地面接收设施安装服务许可证》(注销)审批,《卫星地面接收设施安装服务许可证》(新证)审批
《市场准入负面清单》禁止准入类:禁止违规开展金融相关经营活动“非金融机构、不从事金融活动的企业,在注册名称和经营范围中原则上不得使用与金融相关的字样”(设立依据效力层级不足允许暂时保留的禁止或许可措施)
一次性内部资料准印证核发
互联网上网服务营业场所信息网络安全审核
人力资源服务(不含职业中介活动、劳务派遣服务)备案
仅销售预包装食品备案
从事出版物零售业务许可(含音像制品、电子出版物)
从事包装装潢印刷品和其他印刷品(不含商标、票据、保密印刷)印刷经营活动企业(不含外资企业)的设立、变更审批
从事县内道路旅客运输包车经营许可
公共场所卫生许可
养老机构备案
农药经营许可
出版物发行单位在批准的经营范围内通过互联网等信息网络从事出版物发行业务的备案
出版物批发、零售单位设立不具备法人资格的分支机构,或者出版单位设立发行本版出版物的不具备法人资格的发行分支机构的备案
出版物批发单位设立、变更审批
医疗废物经营许可证核发
医疗机构(三级医院、三级妇幼保健院、急救中心、急救站、临床检验中心、中外合资合作医疗机构、港澳台独资医疗机构)设置审批
医疗机构(不含诊所)执业许可(执业登记)
印章刻制业许可证核发
危险化学品建设项目安全条件审查、安全设施设计审查
危险化学品经营许可证核发
危险废物收集经营许可证核发(广东省厅事项名称) 【国家标准名:危险废物经营许可】
巡游出租汽车经营许可
巡游出租汽车车辆运营证核发
广播电视节目制作经营许可证(载明事项变更)审批,广播电视节目制作经营许可证(新证)审批
广播电视视频点播业务许可证(乙种)审批
废弃电器电子产品处理企业资格审批
建设项目环境影响评价文件审批(广东省厅事项名称) 【国家标准名:“建设项目环境影响评价审批(海洋工程、核与辐射类除外)”】
房地产经纪机构及其分支机构设立备案
托育机构备案
承印加工境外一般性出版物审批
承印加工境外包装装潢和其他印刷品备案核准
排污许可证核发
放射诊疗许可
旅馆业特种行业许可证核发
机动车维修经营备案
机动车驾驶培训机构备案
校车使用许可
歌舞娱乐场所从事娱乐场所经营活动审批
民办职业培训学校新设立、变更
水路运输辅助业登记备案
港口经营许可
游艺娱乐场所从事娱乐场所经营活动审批
游艺娱乐场所从事娱乐场所经营活动审批,内资娱乐场所变更、延续、补证、注销审批
演出经纪机构延续,演出经纪机构从事营业性演出经营活动审批,演出经纪机构变更,演出经纪机构补证,演出经纪机构注销
烟花爆竹(批发)许可证核发
烟草专卖零售许可
烟草专卖零售许可证核发(电子烟零售)
燃气燃烧器具安装、维修企业资质核准
燃气经营许可证核发
特种设备使用登记
生鲜乳准运证明核发
电视剧制作许可证(乙种)载明内容变更,电视剧制作许可证(乙种)延期,电视剧制作许可证(乙种)申请
社会力量举办非学历教育机构审批
种畜禽生产经营许可
第三类医疗器械经营许可
第二、三类非药品类易制毒化学品生产、经营备案
第二类精神药品零售业务审批
经营性人力资源服务许可
药品经营许可证(零售)
营业执照
营业执照 (必填项)
蜂种生产经营许可证核发
辐射安全许可
道路旅客运输站(场)经营许可
道路货物运输经营许可
金属冶炼建设项目安全设施设计审查
音像制作单位的变更审批,音像制作单位的设立审批
饮用水供水单位卫生许可

View File

@ -45,6 +45,7 @@ from lawrisk.services.licensing_repo import (
filter_permits_advanced,
list_unbound_permits,
list_operation_logs,
_lic_pg_conn,
)
from lawrisk.services.auth_service import (
list_users,
@ -319,6 +320,22 @@ def admin_create_user():
try:
dept_name = (payload.get("display_name") or username).strip() or username
dept_code = username.upper()
# Check if department code already exists
with _lic_pg_conn() as conn:
cur = conn.cursor()
cur.execute("SELECT id, name, code, region_id FROM service_departments WHERE code = %s", (dept_code,))
row = cur.fetchone()
if row:
service_department_id = str(row[0])
created_department = {
"id": str(row[0]),
"name": str(row[1]),
"code": str(row[2]),
"region_id": str(row[3]) if row[3] else None
}
if not service_department_id:
created_department = create_service_department(
name=dept_name,
code=dept_code,
@ -328,6 +345,11 @@ def admin_create_user():
)
service_department_id = created_department.get("id")
except Exception as exc:
err_msg = str(exc)
if "duplicate key value" in err_msg and "service_departments_code_key" in err_msg:
return jsonify({"success": False, "message": f"创建单位失败: 单位代码 {dept_code} 已存在"}), 400
if "violates unique constraint" in err_msg:
return jsonify({"success": False, "message": "创建单位失败: 数据重复"}), 400
return jsonify({"success": False, "message": f"创建单位失败: {exc}"}), 400
try:
@ -350,6 +372,9 @@ def admin_create_user():
except ValueError as exc:
return jsonify({"success": False, "message": str(exc)}), 400
except Exception as exc:
err_msg = str(exc)
if "duplicate key value" in err_msg and "auth_users_username_key" in err_msg:
return jsonify({"success": False, "message": f"创建账号失败: 用户名 {username} 已存在"}), 400
return jsonify({"success": False, "message": str(exc)}), 500
@ -596,6 +621,9 @@ def admin_create_service_department():
except ValueError as exc:
return jsonify({"success": False, "message": str(exc)}), 400
except Exception as exc:
err_msg = str(exc)
if "duplicate key value" in err_msg:
return jsonify({"success": False, "message": "服务部门代码或名称已存在"}), 400
return jsonify({"success": False, "message": str(exc)}), 500

View File

@ -269,16 +269,24 @@ def _row_to_user(row: tuple[Any, ...], columns: tuple[str, ...]) -> Dict[str, An
return {col: row[idx] for idx, col in enumerate(columns)}
def _safe_str(val: Any) -> Optional[str]:
if val is None:
return None
return str(val)
def _public_user_payload(user: Dict[str, Any]) -> Dict[str, Any]:
department = None
if user.get("service_department_id"):
# Ensure IDs are strings as psycopg2 might return UUID objects
dept_id = _safe_str(user.get("service_department_id"))
if dept_id:
department = {
"id": user.get("service_department_id"),
"id": dept_id,
"name": user.get("service_department_name"),
"code": user.get("service_department_code"),
"phone": user.get("service_department_phone"),
"parent_id": user.get("service_department_parent_id"),
"region_id": user.get("service_department_region_id"),
"parent_id": _safe_str(user.get("service_department_parent_id")),
"region_id": _safe_str(user.get("service_department_region_id")),
"region_name": user.get("service_department_region_name"),
"role": user.get("department_role"),
}
@ -295,7 +303,7 @@ def _public_user_payload(user: Dict[str, Any]) -> Dict[str, Any]:
"grade": user.get("grade"),
"is_active": user.get("is_active", True),
"department": department,
"department_id": user.get("service_department_id"),
"department_id": dept_id,
"created_at": created_at_value,
}
@ -437,9 +445,19 @@ def create_user(
# 如果未传入部门,则自动创建同名单位并绑定
dept_token = (service_department_id or "").strip() or None
if not dept_token:
try:
dept_name = (display_name or username_clean).strip() or username_clean
dept_code = username_clean.upper()
# 检查是否已存在同代码的单位
with _auth_conn() as conn:
cur = conn.cursor()
cur.execute("SELECT id FROM service_departments WHERE code = %s", (dept_code,))
row = cur.fetchone()
if row:
dept_token = str(row[0])
if not dept_token:
try:
created = lic_repo.create_service_department(
name=dept_name,
code=dept_code,

View File

@ -2501,18 +2501,40 @@ _THEME_SUMMARY_SELECT = """
t.id,
t.name,
COUNT(DISTINCT rtp.permit_id) AS permit_count,
COUNT(DISTINCT rtp.region_id) AS region_count
COUNT(DISTINCT rtp.region_id) AS region_count,
STRING_AGG(DISTINCT r.name, ',') AS region_names
FROM themes t
LEFT JOIN region_theme_permits rtp ON rtp.theme_id = t.id
LEFT JOIN regions r ON rtp.region_id = r.id
"""
def _serialize_theme_row(record: Dict[str, Any]) -> Dict[str, Any]:
region_names_str = record.get("region_names") or ""
levels = set()
r_names = [n.strip() for n in region_names_str.split(',') if n.strip()]
for r_name in r_names:
if r_name == "市级" or r_name == "佛山市":
levels.add("市级")
else:
# Assuming any other region is District level (e.g. 禅城区, 南海区)
levels.add("区级")
impl_level_list = []
if "市级" in levels:
impl_level_list.append("市级")
if "区级" in levels:
impl_level_list.append("区级")
impl_level = "".join(impl_level_list) if impl_level_list else "-"
return {
"id": _to_optional_str(record.get("id")),
"name": record.get("name"),
"permit_count": int(record.get("permit_count") or 0),
"region_count": int(record.get("region_count") or 0),
"implementation_level": impl_level,
}

View File

@ -1,12 +0,0 @@
Clearing existing data...
Data cleared.
Fetching Region ID for '市级'...
Region ID: 2c29ca08-efc6-4e2c-abc2-d73685e0bdd1
Reading Excel...
Dropped 1 duplicate rows.
Found 61 unique rows.
Loading existing permits...
Loaded 72 existing permits.
Saving report...
File locked, saving to '主题-事项绑定结果_new.xlsx' instead.
Done.

View File

@ -1,68 +0,0 @@
Connecting to postgres@8.138.196.105:5432/licensing_risks
--- Listing Tables ---
Table Found: themes
Table Found: region_theme_permits
Table Found: region_themes
Table Found: risks
Table Found: operation_logs
Table Found: permit_approval_departments
Table Found: business_scopes
Table Found: permit_subitems
Table Found: regions
Table Found: permits
Table Found: region_permit_details
Table Found: permit_risk_snapshots
Table Found: region_permit_risk_vw
Table Found: region_permit_risks
Table Found: region_permit_scopes
Table Found: region_scopes
Table Found: permit_theme_rules
Table Found: region_permit_subitems
Table Found: region_permit_theme_overrides
Table Found: auth_users
Table Found: service_departments
Table Found: permit_files
Table Found: permit_sources
Table Found: permit_file_links
Table Found: service_department_permits
--- Structure of themes ---
id (uuid)
name (text)
--- Structure of theme_permits ---
(Table not found or empty or different schema)
--- Structure of region_theme_permits ---
region_id (uuid)
theme_id (uuid)
permit_id (uuid)
--- Structure of permits ---
id (uuid)
name (text)
--- Structure of permit_theme_rules ---
id (uuid)
theme_id (uuid)
created_at (timestamp with time zone)
region_id (uuid)
permit_name (text)
responsible_department (text)
--- Structure of permit_themes ---
(Table not found or empty or different schema)
--- Excel Analysis ---
0 ... 13
0 佛山市企业开办风险提示涉企许可(备案)\n事项清单 ... NaN
1 序号 ... 是否首次上线
2 1 ... 是
3 2 ... 是
4 3 ... 是
[5 rows x 14 columns]
Detected header row: 0
Columns: ['佛山市企业开办风险提示涉企许可(备案)\n事项清单', 'Unnamed: 1', 'Unnamed: 2', 'Unnamed: 3', 'Unnamed: 4', 'Unnamed: 5', 'Unnamed: 6', 'Unnamed: 7', 'Unnamed: 8', 'Unnamed: 9', 'Unnamed: 10', 'Unnamed: 11', 'Unnamed: 12', 'Unnamed: 13']

View File

@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据库维护页面 - LawRisk</title>
<title>佛山市企业开办风险提示系统</title>
<style>
* {
margin: 0;
@ -32,7 +32,7 @@
position: relative;
text-align: center;
margin-bottom: 20px;
padding-bottom: 20px;
padding-bottom: 50px;
border-bottom: 3px solid #2c5282;
}
@ -2263,8 +2263,8 @@
<div class="container">
<div class="header">
<div class="header-info">
<h1 id="pageTitle">🗃️ 管理员控制台</h1>
<p>法律风险提示系统 - 管理员功能面板</p>
<h1 id="pageTitle">佛山市企业开办风险提示系统</h1>
<p> </p>
</div>
<div class="user-bar" id="userBar">
<div class="user-avatar" id="userAvatar">U</div>
@ -2311,20 +2311,16 @@
<!-- 数据概览标签页 -->
<div id="overview-tab" class="tab-content active">
<h2 style="color: #333; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
<span>📊</span> 数据概览
</h2>
<p style="color: #666; margin-bottom: 20px;">展示系统内的主题分布、事项绑定情况以及待处理的异常数据。</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px;">
<!-- 主题统计面板 -->
<div class="panel" style="margin-top: 0;">
<div
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3
<!-- <h3
style="color: #333; margin: 0; font-size: 18px; display: flex; align-items: center; gap: 8px;">
<span>📂</span> 行业主题统计
</h3>
</h3>-->
<button onclick="loadOverviewThemes()"
style="padding: 4px 8px; font-size: 12px; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 4px; cursor: pointer;">刷新</button>
</div>
@ -2338,10 +2334,10 @@
<div class="panel" style="margin-top: 0;">
<div
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3
<!-- <h3
style="color: #333; margin: 0; font-size: 18px; display: flex; align-items: center; gap: 8px;">
<span>⚠️</span> 待分类事项 (未绑定主题)
</h3>
</h3>-->
<button onclick="loadOverviewUnbound()"
style="padding: 4px 8px; font-size: 12px; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 4px; cursor: pointer;">刷新</button>
</div>
@ -7091,7 +7087,7 @@
<tr style="background: #f8fafc; border-bottom: 2px solid #e2e8f0; text-align: left;">
<th style="padding: 12px;">主题名称</th>
<th style="padding: 12px; text-align: center;">关联事项</th>
<th style="padding: 12px; text-align: center;">涉及地区</th>
<th style="padding: 12px; text-align: center;">实施层级</th>
</tr>
</thead>
<tbody>
@ -7102,7 +7098,7 @@
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 12px; font-weight: 600; color: #1e293b;">${escapeHtml(theme.name)}</td>
<td style="padding: 12px; text-align: center;"><span class="tab-badge">${theme.permit_count}</span></td>
<td style="padding: 12px; text-align: center;"><span class="tab-badge" style="background: #fef3c7; color: #92400e;">${theme.region_count}</span></td>
<td style="padding: 12px; text-align: center;"><span class="tab-badge" style="background: #e0f2fe; color: #0369a1;">${theme.implementation_level || '-'}</span></td>
</tr>
`;
});
@ -7140,22 +7136,34 @@
}
let html = `
<div style="font-size: 13px; color: #ef4444; margin-bottom: 10px; font-weight: 600;">注意:以下事项在对应地区中未关联任何主题,可能无法被 V2 接口检索:</div>
<!--<div style="font-size: 13px; color: #ef4444; margin-bottom: 10px; font-weight: 600;">注意:以下事项在对应地区中未关联任何主题,可能无法被 V2 接口检索:</div>-->
<table class="data-table" style="width: 100%; border-collapse: collapse; font-size: 14px;">
<thead>
<tr style="background: #f8fafc; border-bottom: 2px solid #e2e8f0; text-align: left;">
<th style="padding: 12px;">地区</th>
<th style="padding: 12px;">许可事项</th>
<th style="padding: 12px;">许可(备案)事项名称</th>
<th style="padding: 12px; width: 100px;">实施层级</th>
</tr>
</thead>
<tbody>
`;
permits.forEach(p => {
// Calculate implementation level
let implLevel = "区级";
if (p.region_name === "佛山市" || p.region_name === "市级") {
implLevel = "市级";
}
// Define badge color based on level
let badgeStyle = implLevel === "市级"
? "background: #e0f2fe; color: #0369a1;" // Blue for Municipal
: "background: #fee2e2; color: #b91c1c;"; // Red for District
html += `
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 12px; white-space: nowrap;"><span class="user-role" style="background: #fee2e2; color: #b91c1c;">${escapeHtml(p.region_name)}</span></td>
<td style="padding: 12px; color: #475569;">${escapeHtml(p.permit_name)}</td>
<td style="padding: 12px; white-space: nowrap;"><span class="tab-badge" style="${badgeStyle}">${implLevel}</span></td>
</tr>
`;
});

View File

@ -897,6 +897,81 @@
transition: all 0.2s ease;
}
/* Button Loading State */
.btn-loading {
position: relative;
color: transparent !important;
pointer-events: none;
}
.btn-loading::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
top: 50%;
left: 50%;
margin-top: -8px;
margin-left: -8px;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 50%;
border-top-color: #fff;
animation: spin 0.8s linear infinite;
}
/* Dark spinner for light buttons */
.ghost-btn.btn-loading::after,
.logout-btn.btn-loading::after,
.reset-btn.btn-loading::after {
border-color: rgba(0, 0, 0, 0.2);
border-top-color: #374151;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Drawer Message Bar */
.drawer-message-bar {
margin: 0 24px 16px;
padding: 10px 16px;
border-radius: 8px;
font-size: 14px;
display: none;
align-items: center;
}
.drawer-message-bar.show {
display: flex;
animation: slideDown 0.3s ease;
}
.drawer-message-bar.success {
background: #ecfdf5;
color: #065f46;
border: 1px solid #a7f3d0;
}
.drawer-message-bar.error {
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-actions .cancel-btn {
background: #f3f4f6;
color: #4b5563;
@ -1389,6 +1464,7 @@
</div>
<button class="drawer-close" type="button" id="closeUserDrawer">×</button>
</div>
<div class="drawer-message-bar" id="drawerMessageBar"></div>
<div class="drawer-section" id="userSummary"></div>
<form id="userEditForm" class="drawer-form">
<label>显示名称
@ -1675,24 +1751,46 @@
}, 4000);
}
async function fetchJSON(url, options = {}) {
const resp = await fetch(url, {
async function fetchJSON(url, options = {}, loadingElement = null) {
// Early return if button is already loading
if (loadingElement && loadingElement.classList.contains('btn-loading')) {
throw new Error('操作进行中,请稍候...');
}
if (loadingElement) {
loadingElement.classList.add('btn-loading');
loadingElement.disabled = true;
}
try {
const fetchOptions = {
credentials: 'include',
headers: options.method === 'GET' || options.body instanceof FormData
? options.headers
: { 'Content-Type': 'application/json', ...(options.headers || {}) },
...options
});
};
const resp = await fetch(url, fetchOptions);
let data = {};
try {
data = await resp.json();
} catch (_) {
data = {};
}
if (!resp.ok || data.success === false) {
throw new Error(data.message || '操作失败');
// Prefer server error message, fallback to generic
throw new Error(data.message || `请求失败: ${resp.status}`);
}
return data;
} catch (err) {
throw err; // Re-throw for caller to handle (usually showing message)
} finally {
if (loadingElement) {
loadingElement.classList.remove('btn-loading');
loadingElement.disabled = false;
}
}
}
function formatDate(value) {
@ -2029,21 +2127,26 @@
}
}
async function handleDeleteUser(userId) {
const targetId = userId || activeUserId;
if (!targetId) {
async function handleDeleteUser(userId, btnElement = null) {
const targetId = activeUserId || String(userId); // Fix logic to prefer activeUserId if set (drawer context) or explicit userId
// Correct logic: if userId is passed, use it. If not, fallback to activeUserId (drawer case)
const idToDelete = userId || activeUserId;
if (!idToDelete) {
showMessage('请先选择需要删除的用户', 'error');
return;
}
const user = getUserById(targetId);
const name = user ? (user.display_name || user.username || targetId) : targetId;
const user = getUserById(idToDelete);
const name = user ? (user.display_name || user.username || idToDelete) : idToDelete;
if (!confirm(`确定删除账号「${name}」?`)) {
return;
}
try {
await fetchJSON(`${API_BASE}/admin/users/${targetId}`, { method: 'DELETE' });
await fetchJSON(`${API_BASE}/admin/users/${idToDelete}`, { method: 'DELETE' }, btnElement);
showMessage('用户已删除');
if (String(activeUserId) === String(targetId)) {
if (String(activeUserId) === String(idToDelete)) {
activeUserId = null;
closeUserDrawer();
}
@ -2053,13 +2156,15 @@
}
}
document.getElementById('logoutBtn').addEventListener('click', async () => {
document.getElementById('logoutBtn').addEventListener('click', async (evt) => {
try {
await fetchJSON('/auth/logout', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
await fetchJSON('/auth/logout', { method: 'POST', headers: { 'Content-Type': 'application/json' } }, evt.target);
window.location.href = '/fs-ai-asistant/api/workflow/lawrisk/login';
} catch (err) {
if (err.message !== '操作进行中,请稍候...') {
showMessage(err.message, 'error');
}
}
});
const userCreateForm = document.getElementById('userCreateForm');
@ -2067,6 +2172,8 @@
userCreateForm.addEventListener('submit', async (evt) => {
evt.preventDefault();
const form = evt.target;
const submitBtn = form.querySelector('button[type="submit"]'); // Get submit button
const parentSelect = document.getElementById('userCreateParent');
const regionSelect = document.getElementById('userCreateRegion');
const parentDepartmentId = parentSelect ? (parentSelect.value || '') : '';
@ -2093,7 +2200,8 @@
const resp = await fetchJSON(`${API_BASE}/admin/users`, {
method: 'POST',
body: JSON.stringify(payload)
});
}, submitBtn); // Pass button for loading state
const createdId = resp && resp.data && resp.data.user ? resp.data.user.id : null;
activeUserId = createdId ? String(createdId) : activeUserId;
showMessage('用户创建成功');
@ -2109,8 +2217,10 @@
await refreshDepartments();
await refreshUsers();
} catch (err) {
if (err.message !== '操作进行中,请稍候...') {
showMessage(err.message, 'error');
}
}
});
const parentSelect = document.getElementById('userCreateParent');
@ -2126,11 +2236,23 @@
}
}
function showDrawerMessage(text, type = 'success') {
const bar = document.getElementById('drawerMessageBar');
if (!bar) return;
bar.textContent = text;
bar.className = `drawer-message-bar show ${type}`;
setTimeout(() => {
bar.className = 'drawer-message-bar';
}, 3000);
}
if (userEditForm) {
userEditForm.addEventListener('submit', async (evt) => {
evt.preventDefault();
const submitBtn = userEditForm.querySelector('button[type="submit"]');
if (!activeUserId) {
showMessage('请先在列表中选择用户', 'error');
showDrawerMessage('请先在列表中选择用户', 'error');
return;
}
const payload = {
@ -2149,14 +2271,16 @@
await fetchJSON(`${API_BASE}/admin/users/${activeUserId}`, {
method: 'PATCH',
body: JSON.stringify(payload)
});
showMessage('用户信息已更新');
}, submitBtn);
showDrawerMessage('用户信息已更新', 'success');
if (userEditPassword) {
userEditPassword.value = '';
}
await refreshUsers();
await refreshUsers({ syncDrawer: true });
} catch (err) {
showMessage(err.message, 'error');
if (err.message !== '操作进行中,请稍候...') {
showDrawerMessage(err.message, 'error');
}
}
});
}
@ -2218,7 +2342,7 @@
}
if (deleteUserFromDrawerBtn) {
deleteUserFromDrawerBtn.addEventListener('click', () => handleDeleteUser());
deleteUserFromDrawerBtn.addEventListener('click', (evt) => handleDeleteUser(null, evt.target));
}
document.addEventListener('keydown', (evt) => {
@ -2230,6 +2354,8 @@
document.getElementById('deptCreateForm').addEventListener('submit', async (evt) => {
evt.preventDefault();
const form = evt.target;
const submitBtn = form.querySelector('button[type="submit"]');
const code = (form.code.value || '').trim();
const payload = {
name: form.name.value.trim(),
@ -2242,13 +2368,15 @@
await fetchJSON(`${API_BASE}/admin/service-departments`, {
method: 'POST',
body: JSON.stringify(payload)
});
}, submitBtn);
showMessage('服务部门已创建');
form.reset();
await refreshDepartments();
} catch (err) {
if (err.message !== '操作进行中,请稍候...') {
showMessage(err.message, 'error');
}
}
});
deptTableBody.addEventListener('click', async (evt) => {
@ -2262,30 +2390,36 @@
return;
}
try {
await fetchJSON(`${API_BASE}/admin/service-departments/${deptId}`, { method: 'DELETE' });
await fetchJSON(`${API_BASE}/admin/service-departments/${deptId}`, { method: 'DELETE' }, target);
showMessage('服务部门已删除');
await refreshDepartments();
await refreshUsers();
} catch (err) {
if (err.message !== '操作进行中,请稍候...') {
showMessage(err.message, 'error');
}
}
});
document.getElementById('themeCreateForm').addEventListener('submit', async (evt) => {
evt.preventDefault();
const form = evt.target;
const submitBtn = form.querySelector('button[type="submit"]');
const payload = { name: form.name.value.trim() };
try {
await fetchJSON(`${API_BASE}/admin/themes/catalog`, {
method: 'POST',
body: JSON.stringify(payload)
});
}, submitBtn);
showMessage('主题添加成功');
form.reset();
await refreshThemes();
} catch (err) {
if (err.message !== '操作进行中,请稍候...') {
showMessage(err.message, 'error');
}
}
});
themeTableBody.addEventListener('click', async (evt) => {
@ -2302,22 +2436,26 @@
await fetchJSON(`${API_BASE}/admin/themes/catalog/${themeId}`, {
method: 'PATCH',
body: JSON.stringify({ name: value.trim() })
});
}, target);
showMessage('主题名称已更新');
await refreshThemes();
} catch (err) {
if (err.message !== '操作进行中,请稍候...') {
showMessage(err.message, 'error');
}
}
} else if (action === 'delete-theme') {
if (!confirm(`确定删除主题「${themeName}」及其关联?`)) return;
try {
await fetchJSON(`${API_BASE}/admin/themes/catalog/${themeId}`, { method: 'DELETE' });
await fetchJSON(`${API_BASE}/admin/themes/catalog/${themeId}`, { method: 'DELETE' }, target);
showMessage('主题已删除');
await refreshThemes();
} catch (err) {
if (err.message !== '操作进行中,请稍候...') {
showMessage(err.message, 'error');
}
}
}
});
function switchTab(tabId) {
@ -3027,14 +3165,11 @@
if (target.classList.contains('add-child-btn')) {
showAddChildModal(nodeData);
} else if (target.classList.contains('edit-btn')) {
showEditModal(nodeData);
} else if (target.classList.contains('delete-btn')) {
showDeleteConfirm(nodeData);
showDeleteConfirm(nodeData, target);
} else {
}
@ -3144,7 +3279,7 @@
}
function showDeleteConfirm(nodeData) {
function showDeleteConfirm(nodeData, btnElement = null) {
const childrenCount = orgChartData.allNodes.filter(n => n.id !== nodeData.id && orgChartData.parentMap[n.id] === nodeData.id).length;
let message = `确定要删除部门「${nodeData.name}」吗?`;
@ -3156,22 +3291,30 @@
fetchJSON(`${API_BASE}/admin/service-departments/${nodeData.id}`, {
method: 'DELETE'
}).then(() => {
}, btnElement).then(() => {
showMessage('部门删除成功', 'success');
loadOrgChart();
}).catch(async (err) => {
if (err.message.includes('仍有账号绑定') || (err.message && err.message.includes('HAS_BOUND_USERS'))) {
const force = confirm(`${err.message}\n\n是否强制删除将自动解除所有账号绑定`);
if (force) {
try {
await fetchJSON(`${API_BASE}/admin/service-departments/${nodeData.id}?force=true`, {
method: 'DELETE'
});
}, btnElement);
showMessage('部门强制删除成功', 'success');
loadOrgChart();
} catch (forceErr) {
if (forceErr.message !== '操作进行中,请稍候...') {
showMessage(forceErr.message, 'error');
}
}
}
} else {
if (err.message !== '操作进行中,请稍候...') {
showMessage(err.message, 'error');
}
}
});
}
@ -3212,16 +3355,26 @@
});
form.addEventListener('submit', async (evt) => {
evt.preventDefault();
const btn = submitBtn;
// Manual loading state since createModal doesn't use fetchJSON directly on the button necessarily,
// but usually onSubmit calls something. We can disable the button here.
if (btn.classList.contains('btn-loading')) return;
btn.classList.add('btn-loading');
btn.disabled = true;
try {
const data = getFormData(form);
await onSubmit(data);
overlay.remove();
} catch (err) {
console.error('【DEBUG】提交错误:', err);
showMessage(err.message || '操作失败', 'error');
} finally {
btn.classList.remove('btn-loading');
btn.disabled = false;
}
});

View File

@ -1,34 +0,0 @@
Connecting to DB...
--- Regions ---
市级: 2c29ca08-efc6-4e2c-abc2-d73685e0bdd1
高明区(有意见): 4032e664-9548-4c7b-9e77-82cdb0d0ab85
市级(无意见): d01b8aa3-cb59-4bd1-9721-5319ea745708
禅城区(无意见): 854ec808-340a-4b9d-a3d5-8521225bddfd
南海区(无意见): ea4479ac-2ddf-4e64-8e28-96999defacd9
顺德区(无意见): ccfafc9a-0900-423b-8f6d-d87e3614152d
三水区(无意见): 54cd17c1-19b7-4d73-ae02-182bc6805a1b
Sheet1: 60068d61-992e-4ea5-beb1-cf29227ba135
高明区(样版): b6223a86-f053-40f1-b172-ef7ba28df103
高明区: 9ba7e257-bef7-4579-a124-9c97ec224e8a
营业执照: a2faa968-d8f7-4034-849f-1d20ae8243ce
高明区 (样版): 9183cf5f-cf4e-45e8-b7e4-5b31014f1a0f
高明区(样版): d1ba645d-2095-44b4-801d-bbab99542c50
禅城区: fa078753-974a-4fa7-8240-3b59689dc21d
南海区: e86a675c-2047-418a-a0d7-ee341f8e38fd
顺德区: 058d6257-25cf-420b-a6bc-52cfa32d562b
三水区: bd4dfda9-cbc2-41e3-9f55-5d2608782cab
--- Current Counts ---
Themes: 12
Region Theme Permits: 61
Permit Theme Rules: 61
--- Excel Structure ---
Columns: ['序号', '事项名称', '是否市级实施', '是否区级实施', '备注', '牵头部门', '部门系统简称\n审批服务部门', '市级', '禅城区', '南海区', '顺德区', '高明区', '三水区', '是否首次上线']
Sample Data:
序号 事项名称 是否市级实施 是否区级实施 备注 牵头部门 部门系统简称\n审批服务部门 市级 禅城区 南海区 顺德区 高明区 三水区 是否首次上线
0 1 旅馆业特种行业许可证核发 NaN NaN NaN 市公安局 公安机关 NaN NaN NaN NaN NaN NaN 是
1 2 公章刻制业特种行业许可证核发 NaN NaN NaN 市公安局 公安机关 NaN NaN NaN NaN NaN NaN 是
2 3 互联网上网服务营业场所信息网络安全审核 NaN NaN NaN 市公安局 公安机关 NaN NaN NaN NaN NaN NaN 是