| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- #!/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()
|