reorder_web_menus.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #!/usr/bin/env python3
  2. """重排 Web 端一级菜单顺序,并生成 sys_menu 初始化 SQL。"""
  3. from __future__ import annotations
  4. import ast
  5. import re
  6. import sys
  7. from pathlib import Path
  8. ROOT = Path(__file__).resolve().parents[2]
  9. MENU_PY = ROOT / "backend/app/api/menu.py"
  10. SQXP_TS = ROOT / "web-admin/src/router/modules/sqxp.ts"
  11. SQL_OUT = ROOT / "docs/sql/sys_menu_web_init.sql"
  12. BLOCK_ORDER = [2, 3, 4, 5, 7, 9, 10, 1, 6, 8, 11, 12, 13]
  13. BLOCK_TITLES = {
  14. 1: "市场基础信息",
  15. 2: "细胞存储",
  16. 3: "科研捐赠",
  17. 4: "中试服务",
  18. 5: "临床应用",
  19. 6: "质检基础信息",
  20. 7: "质检管理",
  21. 8: "生产基础信息",
  22. 9: "生产管理",
  23. 10: "运营支撑",
  24. 11: "系统基础信息",
  25. 12: "工作流管理",
  26. 13: "系统管理",
  27. }
  28. SQXP_HEADER = """import type { AppRouteRecord } from '@/types/router'
  29. /**
  30. * 前端路由(13 个一级菜单:细胞存储、科研捐赠、中试服务、临床应用、
  31. * 质检管理、生产管理、运营支撑、市场基础信息、质检基础信息、生产基础信息、
  32. * 系统基础信息、工作流管理、系统管理)
  33. * 与后端菜单对齐时请同步 `backend/app/api/menu.py`。
  34. */
  35. export const sqxpAdminRoutes: AppRouteRecord[] = [
  36. """
  37. MENU_TAIL = (
  38. "\n]\n\n\n"
  39. '@router.get("/list")\n'
  40. "async def get_menu_list(_user: dict = Depends(require_admin_or_super_admin)):\n"
  41. ' """获取菜单列表(与当前前端路由一致)"""\n'
  42. ' return success_response({"menuList": STATIC_MENU_TREE})\n'
  43. )
  44. BLOCK_RE_TS = re.compile(
  45. r" // ──+\n // (\d+)\. ([^\n]+)\n // ──+\n",
  46. re.MULTILINE,
  47. )
  48. BLOCK_RE_PY = re.compile(
  49. r" # ──+\n # (\d+)\. ([^\n]+)\n # ──+\n",
  50. re.MULTILINE,
  51. )
  52. def split_menu_blocks(
  53. content: str, array_start_marker: str, block_re: re.Pattern
  54. ) -> tuple[str, dict[int, str]]:
  55. start = content.index(array_start_marker) + len(array_start_marker)
  56. depth = 1
  57. i = start
  58. while i < len(content) and depth > 0:
  59. if content[i] == "[":
  60. depth += 1
  61. elif content[i] == "]":
  62. depth -= 1
  63. i += 1
  64. array_body = content[start : i - 1]
  65. prefix = content[:start]
  66. parts = block_re.split(array_body)
  67. blocks: dict[int, str] = {}
  68. idx = 1
  69. while idx < len(parts):
  70. num = int(parts[idx])
  71. block = parts[idx + 2].rstrip()
  72. if block.endswith(","):
  73. block = block[:-1]
  74. blocks[num] = block.rstrip()
  75. idx += 3
  76. return prefix, blocks
  77. def reorder_blocks(blocks: dict[int, str]) -> list[tuple[int, str, str]]:
  78. ordered = []
  79. for new_idx, old_num in enumerate(BLOCK_ORDER, start=1):
  80. ordered.append((new_idx, BLOCK_TITLES[old_num], blocks[old_num]))
  81. return ordered
  82. def assemble_blocks(ordered: list[tuple[int, str, str]], *, ts: bool) -> str:
  83. chunks = []
  84. for num, title, body in ordered:
  85. if ts:
  86. sep = " // " + "─" * 46
  87. chunks.append(f"{sep}\n // {num}. {title}\n{sep}\n{body}")
  88. else:
  89. sep = " # " + "─" * 44
  90. chunks.append(f"{sep}\n # {num}. {title}\n{sep}\n{body}")
  91. return ",\n\n".join(chunks) + "\n"
  92. def load_static_menu_tree() -> list:
  93. content = MENU_PY.read_text(encoding="utf-8")
  94. marker = "STATIC_MENU_TREE = "
  95. start = content.index(marker) + len(marker)
  96. depth = 0
  97. i = start
  98. while i < len(content):
  99. ch = content[i]
  100. if ch == "[":
  101. depth += 1
  102. elif ch == "]":
  103. depth -= 1
  104. if depth == 0:
  105. i += 1
  106. break
  107. i += 1
  108. literal = content[start:i]
  109. return ast.literal_eval(literal)
  110. def sql_escape(val: str | None) -> str:
  111. if val is None:
  112. return "NULL"
  113. return "'" + str(val).replace("'", "''") + "'"
  114. def walk_routes(nodes: list, parent_id: str, order_counter: dict, rows: list) -> None:
  115. for node in nodes:
  116. node_id = node["id"]
  117. meta = node.get("meta") or {}
  118. title = meta.get("title") or node.get("name") or ""
  119. path = node.get("path") or ""
  120. component = node.get("component") or ""
  121. children = node.get("children") or []
  122. is_hide = meta.get("isHide") is True
  123. menu_code = meta.get("menuCode")
  124. perms = menu_code if menu_code else None
  125. if parent_id == "2":
  126. order_counter["web"] += 1
  127. order_num = order_counter["web"]
  128. else:
  129. order_counter.setdefault(parent_id, 0)
  130. order_counter[parent_id] += 1
  131. order_num = order_counter[parent_id]
  132. menu_type = "M" if children else "C"
  133. visible = "0" if is_hide else "1"
  134. rows.append(
  135. (
  136. node_id,
  137. title,
  138. parent_id,
  139. menu_type,
  140. path,
  141. component or None,
  142. perms,
  143. visible,
  144. order_num,
  145. )
  146. )
  147. if children:
  148. walk_routes(children, node_id, order_counter, rows)
  149. def generate_sql(tree: list) -> str:
  150. rows: list = []
  151. order_counter: dict = {"web": 0}
  152. walk_routes(tree, "2", order_counter, rows)
  153. lines = [
  154. "-- Web 端 sys_menu 初始化(根节点 + 5 个一级分组 + 二级/三级业务菜单)",
  155. "-- 生成:backend/scripts/reorder_web_menus.py",
  156. "-- 层级:Web端菜单(id=2) → 市场/生产/质检/运营支撑/系统管理(TOP*) → 业务二级 → 页面三级",
  157. "-- 一级分组顺序:市场 → 生产 → 质检 → 运营支撑 → 系统管理",
  158. "",
  159. "SET NAMES utf8mb4;",
  160. "",
  161. "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",
  162. "('1', '移动端菜单', '0', 'M', '/mini', NULL, NULL, 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),",
  163. "('2', 'Web端菜单', '0', 'M', '/web', NULL, NULL, 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0);",
  164. "",
  165. "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",
  166. ]
  167. value_lines = []
  168. for row in rows:
  169. (
  170. node_id,
  171. title,
  172. parent_id,
  173. menu_type,
  174. path,
  175. component,
  176. perms,
  177. visible,
  178. order_num,
  179. ) = row
  180. value_lines.append(
  181. f"({sql_escape(node_id)}, {sql_escape(title)}, {sql_escape(parent_id)}, "
  182. f"{sql_escape(menu_type)}, {sql_escape(path)}, {sql_escape(component)}, "
  183. f"{sql_escape(perms)}, 1, {sql_escape(visible)}, '1', 'zkxw', {order_num}, "
  184. f"NULL, NULL, '', NOW(), NULL, NOW(), 0)"
  185. )
  186. lines.append(",\n".join(value_lines) + ";")
  187. lines.append("")
  188. return "\n".join(lines)
  189. def main() -> None:
  190. menu_content = MENU_PY.read_text(encoding="utf-8")
  191. pre, blocks = split_menu_blocks(menu_content, "STATIC_MENU_TREE = [\n", BLOCK_RE_PY)
  192. body = assemble_blocks(reorder_blocks(blocks), ts=False)
  193. MENU_PY.write_text(pre + body + MENU_TAIL, encoding="utf-8")
  194. print(f"updated {MENU_PY.relative_to(ROOT)}")
  195. ts_content = SQXP_TS.read_text(encoding="utf-8")
  196. _, blocks_ts = split_menu_blocks(
  197. ts_content, "export const sqxpAdminRoutes: AppRouteRecord[] = [\n", BLOCK_RE_TS
  198. )
  199. body_ts = assemble_blocks(reorder_blocks(blocks_ts), ts=True)
  200. SQXP_TS.write_text(SQXP_HEADER + body_ts + "]\n", encoding="utf-8")
  201. print(f"updated {SQXP_TS.relative_to(ROOT)}")
  202. tree = load_static_menu_tree()
  203. sql = generate_sql(tree)
  204. SQL_OUT.write_text(sql, encoding="utf-8")
  205. row_count = sql.count("('") - 2 # minus root inserts
  206. print(f"wrote {SQL_OUT.relative_to(ROOT)} (~{row_count} menu rows, {len(tree)} top-level menus)")
  207. if __name__ == "__main__":
  208. main()