Files
zkxw/backend/scripts/reorder_web_menus.py
T
2026-06-13 19:47:22 +08:00

245 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""重排 Web 端一级菜单顺序,并生成 sys_menu 初始化 SQL。"""
from __future__ import annotations
import ast
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
MENU_PY = ROOT / "backend/app/api/menu.py"
SQXP_TS = ROOT / "web-admin/src/router/modules/sqxp.ts"
SQL_OUT = ROOT / "docs/sql/sys_menu_web_init.sql"
BLOCK_ORDER = [2, 3, 4, 5, 7, 9, 10, 1, 6, 8, 11, 12, 13]
BLOCK_TITLES = {
1: "市场基础信息",
2: "细胞存储",
3: "科研捐赠",
4: "中试服务",
5: "临床应用",
6: "质检基础信息",
7: "质检管理",
8: "生产基础信息",
9: "生产管理",
10: "运营支撑",
11: "系统基础信息",
12: "工作流管理",
13: "系统管理",
}
SQXP_HEADER = """import type { AppRouteRecord } from '@/types/router'
/**
* 前端路由(13 个一级菜单:细胞存储、科研捐赠、中试服务、临床应用、
* 质检管理、生产管理、运营支撑、市场基础信息、质检基础信息、生产基础信息、
* 系统基础信息、工作流管理、系统管理)
* 与后端菜单对齐时请同步 `backend/app/api/menu.py`。
*/
export const sqxpAdminRoutes: AppRouteRecord[] = [
"""
MENU_TAIL = (
"\n]\n\n\n"
'@router.get("/list")\n'
"async def get_menu_list(_user: dict = Depends(require_admin_or_super_admin)):\n"
' """获取菜单列表(与当前前端路由一致)"""\n'
' return success_response({"menuList": STATIC_MENU_TREE})\n'
)
BLOCK_RE_TS = re.compile(
r" // ──+\n // (\d+)\. ([^\n]+)\n // ──+\n",
re.MULTILINE,
)
BLOCK_RE_PY = re.compile(
r" # ──+\n # (\d+)\. ([^\n]+)\n # ──+\n",
re.MULTILINE,
)
def split_menu_blocks(
content: str, array_start_marker: str, block_re: re.Pattern
) -> tuple[str, dict[int, str]]:
start = content.index(array_start_marker) + len(array_start_marker)
depth = 1
i = start
while i < len(content) and depth > 0:
if content[i] == "[":
depth += 1
elif content[i] == "]":
depth -= 1
i += 1
array_body = content[start : i - 1]
prefix = content[:start]
parts = block_re.split(array_body)
blocks: dict[int, str] = {}
idx = 1
while idx < len(parts):
num = int(parts[idx])
block = parts[idx + 2].rstrip()
if block.endswith(","):
block = block[:-1]
blocks[num] = block.rstrip()
idx += 3
return prefix, blocks
def reorder_blocks(blocks: dict[int, str]) -> list[tuple[int, str, str]]:
ordered = []
for new_idx, old_num in enumerate(BLOCK_ORDER, start=1):
ordered.append((new_idx, BLOCK_TITLES[old_num], blocks[old_num]))
return ordered
def assemble_blocks(ordered: list[tuple[int, str, str]], *, ts: bool) -> str:
chunks = []
for num, title, body in ordered:
if ts:
sep = " // " + "─" * 46
chunks.append(f"{sep}\n // {num}. {title}\n{sep}\n{body}")
else:
sep = " # " + "─" * 44
chunks.append(f"{sep}\n # {num}. {title}\n{sep}\n{body}")
return ",\n\n".join(chunks) + "\n"
def load_static_menu_tree() -> list:
content = MENU_PY.read_text(encoding="utf-8")
marker = "STATIC_MENU_TREE = "
start = content.index(marker) + len(marker)
depth = 0
i = start
while i < len(content):
ch = content[i]
if ch == "[":
depth += 1
elif ch == "]":
depth -= 1
if depth == 0:
i += 1
break
i += 1
literal = content[start:i]
return ast.literal_eval(literal)
def sql_escape(val: str | None) -> str:
if val is None:
return "NULL"
return "'" + str(val).replace("'", "''") + "'"
def walk_routes(nodes: list, parent_id: str, order_counter: dict, rows: list) -> None:
for node in nodes:
node_id = node["id"]
meta = node.get("meta") or {}
title = meta.get("title") or node.get("name") or ""
path = node.get("path") or ""
component = node.get("component") or ""
children = node.get("children") or []
is_hide = meta.get("isHide") is True
menu_code = meta.get("menuCode")
perms = menu_code if menu_code else None
if parent_id == "2":
order_counter["web"] += 1
order_num = order_counter["web"]
else:
order_counter.setdefault(parent_id, 0)
order_counter[parent_id] += 1
order_num = order_counter[parent_id]
menu_type = "M" if children else "C"
visible = "0" if is_hide else "1"
rows.append(
(
node_id,
title,
parent_id,
menu_type,
path,
component or None,
perms,
visible,
order_num,
)
)
if children:
walk_routes(children, node_id, order_counter, rows)
def generate_sql(tree: list) -> str:
rows: list = []
order_counter: dict = {"web": 0}
walk_routes(tree, "2", order_counter, rows)
lines = [
"-- Web 端 sys_menu 初始化(根节点 + 5 个一级分组 + 二级/三级业务菜单)",
"-- 生成:backend/scripts/reorder_web_menus.py",
"-- 层级:Web端菜单(id=2) → 市场/生产/质检/运营支撑/系统管理(TOP*) → 业务二级 → 页面三级",
"-- 一级分组顺序:市场 → 生产 → 质检 → 运营支撑 → 系统管理",
"",
"SET NAMES utf8mb4;",
"",
"INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES",
"('1', '移动端菜单', '0', 'M', '/mini', NULL, NULL, 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),",
"('2', 'Web端菜单', '0', 'M', '/web', NULL, NULL, 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0);",
"",
"INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES",
]
value_lines = []
for row in rows:
(
node_id,
title,
parent_id,
menu_type,
path,
component,
perms,
visible,
order_num,
) = row
value_lines.append(
f"({sql_escape(node_id)}, {sql_escape(title)}, {sql_escape(parent_id)}, "
f"{sql_escape(menu_type)}, {sql_escape(path)}, {sql_escape(component)}, "
f"{sql_escape(perms)}, 1, {sql_escape(visible)}, '1', 'zkxw', {order_num}, "
f"NULL, NULL, '', NOW(), NULL, NOW(), 0)"
)
lines.append(",\n".join(value_lines) + ";")
lines.append("")
return "\n".join(lines)
def main() -> None:
menu_content = MENU_PY.read_text(encoding="utf-8")
pre, blocks = split_menu_blocks(menu_content, "STATIC_MENU_TREE = [\n", BLOCK_RE_PY)
body = assemble_blocks(reorder_blocks(blocks), ts=False)
MENU_PY.write_text(pre + body + MENU_TAIL, encoding="utf-8")
print(f"updated {MENU_PY.relative_to(ROOT)}")
ts_content = SQXP_TS.read_text(encoding="utf-8")
_, blocks_ts = split_menu_blocks(
ts_content, "export const sqxpAdminRoutes: AppRouteRecord[] = [\n", BLOCK_RE_TS
)
body_ts = assemble_blocks(reorder_blocks(blocks_ts), ts=True)
SQXP_TS.write_text(SQXP_HEADER + body_ts + "]\n", encoding="utf-8")
print(f"updated {SQXP_TS.relative_to(ROOT)}")
tree = load_static_menu_tree()
sql = generate_sql(tree)
SQL_OUT.write_text(sql, encoding="utf-8")
row_count = sql.count("('") - 2 # minus root inserts
print(f"wrote {SQL_OUT.relative_to(ROOT)} (~{row_count} menu rows, {len(tree)} top-level menus)")
if __name__ == "__main__":
main()