Cleanup: Organize project structure and resolve merge conflicts
This commit is contained in:
parent
2440a02a2d
commit
762d0c0115
|
|
@ -1,69 +0,0 @@
|
|||
《卫星地面接收设施安装服务许可证》(换发)审批,《卫星地面接收设施安装服务许可证》(注销)审批,《卫星地面接收设施安装服务许可证》(新证)审批
|
||||
《市场准入负面清单》禁止准入类:禁止违规开展金融相关经营活动“非金融机构、不从事金融活动的企业,在注册名称和经营范围中原则上不得使用与金融相关的字样”(设立依据效力层级不足允许暂时保留的禁止或许可措施)
|
||||
一次性内部资料准印证核发
|
||||
互联网上网服务营业场所信息网络安全审核
|
||||
人力资源服务(不含职业中介活动、劳务派遣服务)备案
|
||||
仅销售预包装食品备案
|
||||
从事出版物零售业务许可(含音像制品、电子出版物)
|
||||
从事包装装潢印刷品和其他印刷品(不含商标、票据、保密印刷)印刷经营活动企业(不含外资企业)的设立、变更审批
|
||||
从事县内道路旅客运输包车经营许可
|
||||
公共场所卫生许可
|
||||
养老机构备案
|
||||
农药经营许可
|
||||
出版物发行单位在批准的经营范围内通过互联网等信息网络从事出版物发行业务的备案
|
||||
出版物批发、零售单位设立不具备法人资格的分支机构,或者出版单位设立发行本版出版物的不具备法人资格的发行分支机构的备案
|
||||
出版物批发单位设立、变更审批
|
||||
医疗废物经营许可证核发
|
||||
医疗机构(三级医院、三级妇幼保健院、急救中心、急救站、临床检验中心、中外合资合作医疗机构、港澳台独资医疗机构)设置审批
|
||||
医疗机构(不含诊所)执业许可(执业登记)
|
||||
印章刻制业许可证核发
|
||||
危险化学品建设项目安全条件审查、安全设施设计审查
|
||||
危险化学品经营许可证核发
|
||||
危险废物收集经营许可证核发(广东省厅事项名称) 【国家标准名:危险废物经营许可】
|
||||
巡游出租汽车经营许可
|
||||
巡游出租汽车车辆运营证核发
|
||||
广播电视节目制作经营许可证(载明事项变更)审批,广播电视节目制作经营许可证(新证)审批
|
||||
广播电视视频点播业务许可证(乙种)审批
|
||||
废弃电器电子产品处理企业资格审批
|
||||
建设项目环境影响评价文件审批(广东省厅事项名称) 【国家标准名:“建设项目环境影响评价审批(海洋工程、核与辐射类除外)”】
|
||||
房地产经纪机构及其分支机构设立备案
|
||||
托育机构备案
|
||||
承印加工境外一般性出版物审批
|
||||
承印加工境外包装装潢和其他印刷品备案核准
|
||||
排污许可证核发
|
||||
放射诊疗许可
|
||||
旅馆业特种行业许可证核发
|
||||
机动车维修经营备案
|
||||
机动车驾驶培训机构备案
|
||||
校车使用许可
|
||||
歌舞娱乐场所从事娱乐场所经营活动审批
|
||||
民办职业培训学校新设立、变更
|
||||
水路运输辅助业登记备案
|
||||
港口经营许可
|
||||
游艺娱乐场所从事娱乐场所经营活动审批
|
||||
游艺娱乐场所从事娱乐场所经营活动审批,内资娱乐场所变更、延续、补证、注销审批
|
||||
演出经纪机构延续,演出经纪机构从事营业性演出经营活动审批,演出经纪机构变更,演出经纪机构补证,演出经纪机构注销
|
||||
烟花爆竹(批发)许可证核发
|
||||
烟草专卖零售许可
|
||||
烟草专卖零售许可证核发(电子烟零售)
|
||||
燃气燃烧器具安装、维修企业资质核准
|
||||
燃气经营许可证核发
|
||||
特种设备使用登记
|
||||
生鲜乳准运证明核发
|
||||
电视剧制作许可证(乙种)载明内容变更,电视剧制作许可证(乙种)延期,电视剧制作许可证(乙种)申请
|
||||
社会力量举办非学历教育机构审批
|
||||
种畜禽生产经营许可
|
||||
第三类医疗器械经营许可
|
||||
第二、三类非药品类易制毒化学品生产、经营备案
|
||||
第二类精神药品零售业务审批
|
||||
经营性人力资源服务许可
|
||||
药品经营许可证(零售)
|
||||
营业执照
|
||||
营业执照 (必填项)
|
||||
蜂种生产经营许可证核发
|
||||
辐射安全许可
|
||||
道路旅客运输站(场)经营许可
|
||||
道路货物运输经营许可
|
||||
金属冶炼建设项目安全设施设计审查
|
||||
音像制作单位的变更审批,音像制作单位的设立审批
|
||||
饮用水供水单位卫生许可
|
||||
|
|
@ -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,15 +320,36 @@ def admin_create_user():
|
|||
try:
|
||||
dept_name = (payload.get("display_name") or username).strip() or username
|
||||
dept_code = username.upper()
|
||||
created_department = create_service_department(
|
||||
name=dept_name,
|
||||
code=dept_code,
|
||||
phone=department_phone,
|
||||
parent_id=parent_department_id,
|
||||
region_id=region_id,
|
||||
)
|
||||
service_department_id = created_department.get("id")
|
||||
|
||||
# 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,
|
||||
phone=department_phone,
|
||||
parent_id=parent_department_id,
|
||||
region_id=region_id,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,20 +445,30 @@ 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()
|
||||
created = lic_repo.create_service_department(
|
||||
name=dept_name,
|
||||
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,
|
||||
operator=operator,
|
||||
)
|
||||
dept_token = created.get("id")
|
||||
except Exception as exc:
|
||||
raise ValueError(f"自动创建单位失败: {exc}")
|
||||
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,
|
||||
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,
|
||||
operator=operator,
|
||||
)
|
||||
dept_token = created.get("id")
|
||||
except Exception as exc:
|
||||
raise ValueError(f"自动创建单位失败: {exc}")
|
||||
|
||||
user_id = uuid.uuid4().hex
|
||||
password_hash = pwd_context.hash(password)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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']
|
||||
|
|
@ -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>
|
||||
`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
credentials: 'include',
|
||||
headers: options.method === 'GET' || options.body instanceof FormData
|
||||
? options.headers
|
||||
: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
||||
...options
|
||||
});
|
||||
let data = {};
|
||||
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 {
|
||||
data = await resp.json();
|
||||
} catch (_) {
|
||||
data = {};
|
||||
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) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
if (!resp.ok || data.success === false) {
|
||||
throw new Error(data.message || '操作失败');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
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,12 +2156,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
showMessage(err.message, 'error');
|
||||
if (err.message !== '操作进行中,请稍候...') {
|
||||
showMessage(err.message, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -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,7 +2217,9 @@
|
|||
await refreshDepartments();
|
||||
await refreshUsers();
|
||||
} catch (err) {
|
||||
showMessage(err.message, 'error');
|
||||
if (err.message !== '操作进行中,请稍候...') {
|
||||
showMessage(err.message, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -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,12 +2368,14 @@
|
|||
await fetchJSON(`${API_BASE}/admin/service-departments`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}, submitBtn);
|
||||
showMessage('服务部门已创建');
|
||||
form.reset();
|
||||
await refreshDepartments();
|
||||
} catch (err) {
|
||||
showMessage(err.message, 'error');
|
||||
if (err.message !== '操作进行中,请稍候...') {
|
||||
showMessage(err.message, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -2262,29 +2390,35 @@
|
|||
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) {
|
||||
showMessage(err.message, 'error');
|
||||
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) {
|
||||
showMessage(err.message, 'error');
|
||||
if (err.message !== '操作进行中,请稍候...') {
|
||||
showMessage(err.message, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -2302,20 +2436,24 @@
|
|||
await fetchJSON(`${API_BASE}/admin/themes/catalog/${themeId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ name: value.trim() })
|
||||
});
|
||||
}, target);
|
||||
showMessage('主题名称已更新');
|
||||
await refreshThemes();
|
||||
} catch (err) {
|
||||
showMessage(err.message, 'error');
|
||||
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) {
|
||||
showMessage(err.message, 'error');
|
||||
if (err.message !== '操作进行中,请稍候...') {
|
||||
showMessage(err.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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,21 +3291,29 @@
|
|||
|
||||
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) {
|
||||
await fetchJSON(`${API_BASE}/admin/service-departments/${nodeData.id}?force=true`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
showMessage('部门强制删除成功', 'success');
|
||||
loadOrgChart();
|
||||
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 {
|
||||
showMessage(err.message, 'error');
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 是
|
||||
Loading…
Reference in New Issue