generate_adminUI_menu_doc.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. # -*- coding: utf-8 -*-
  2. """
  3. Scan adminUI src/views and generate reverse-engineered menu structure document.
  4. """
  5. import os
  6. import re
  7. from collections import defaultdict
  8. from datetime import date
  9. ROOT = os.path.normpath(
  10. os.path.join(os.path.dirname(__file__), '..', '..', 'ylrz_saas_his_scrm_adminUI', 'src', 'views')
  11. )
  12. OUT = os.path.join(os.path.dirname(__file__), 'adminUI_views_menu_structure.md')
  13. SKIP_TOP = {'components', 'error', 'dashboard', 'icons', 'login', 'register', 'redirect'}
  14. SKIP_REL = re.compile(
  15. r'(components/|/profile/|authRole|authUser|selectUser|/data\.vue|/design\.vue|'
  16. r'/log\.vue|/editTable|/details\.vue|/my\.vue|sopLogsList|sopUserLogsSchedule|'
  17. r'/transferLog\.vue|/transfer\.vue|/darkRoom\.vue|/blackroom\.vue|config2\.vue|'
  18. r'/sopTempe/|mixins/)'
  19. )
  20. LIST_NAMES = {'index.vue', 'list.vue', 'myList.vue', 'order.vue', 'order1.vue'}
  21. ADMIN_ONLY_TOP = {
  22. 'admin', 'saas', 'sysUser', 'saler', 'shop', 'medical', 'food', 'todo',
  23. 'baidu', 'callRecord', 'aiSipCall', 'storeOrderOfflineItem', 'operation', 'hisStore',
  24. }
  25. TENANT_TOP = {
  26. 'qw': ('企微管理', 'qw'),
  27. 'wx': ('微信管理', 'wx'),
  28. 'gw': ('微信管理', 'wx'),
  29. 'crm': ('CRM客户', 'crm'),
  30. 'user': ('会员管理', 'member'),
  31. 'users': ('会员管理', 'member'),
  32. 'member': ('会员管理', 'member'),
  33. 'his': ('诊所管理', 'his'),
  34. 'store': ('商城管理', 'store'),
  35. 'live': ('直播管理', 'live'),
  36. 'liveData': ('直播管理', 'live'),
  37. 'course': ('课程管理', 'course'),
  38. 'courseFinishTemp': ('课程管理', 'course'),
  39. 'fastGpt': ('AI聊天', 'fastGpt'),
  40. 'FastGptExtUserTag': ('AI聊天', 'fastGpt'),
  41. 'chat': ('AI聊天', 'fastGpt'),
  42. 'aiob': ('AI聊天', 'fastGpt'),
  43. 'lobster': ('龙虹引擎', 'lobster'),
  44. 'adv': ('广告投放', 'ad'),
  45. 'ad': ('广告投放', 'ad'),
  46. 'company': ('系统管理', 'system'),
  47. 'system': ('系统管理', 'system'),
  48. 'bill': ('财务管理', 'bill'),
  49. 'billing': ('财务管理', 'bill'),
  50. 'calendar': ('日程管理', 'calendar'),
  51. 'statistics': ('数据统计', 'statistics'),
  52. 'taskStatistics': ('数据统计', 'statistics'),
  53. 'moduleUsage': ('数据统计', 'statistics'),
  54. 'monitor': ('监控管理', 'watch'),
  55. 'watch': ('监控管理', 'watch'),
  56. 'tool': ('系统工具', 'system'),
  57. 'qwExternalContact': ('企微管理', 'qw'),
  58. }
  59. SECONDARY_HINTS = {
  60. 'qw': {
  61. 'msg': ['QwWorkTask', 'groupMsg', 'record', 'qwPushCount'],
  62. 'customer': ['externalContact', 'contactWay', 'drainageLink', 'assignRule', 'customerLink'],
  63. 'group': ['groupChat', 'groupLiveCode', 'groupActual'],
  64. 'moments': ['friendCircle', 'friendMaterial', 'friendWelcome'],
  65. 'drainage': ['appAdvertisingReport', 'appContactWay'],
  66. 'tag': ['tag', 'tagGroup', 'autoTags'],
  67. 'setting': ['qwDept', 'companyUser', 'material', 'welcome', 'applyIpad', 'userBehaviorData'],
  68. 'sop': ['sop', 'sopTemp', 'sopLogs', 'aiSop'],
  69. },
  70. 'store': {
  71. 'order': ['storeOrder', 'storeAfterSales', 'inquiryOrder', 'PromotionOrder', 'healthStoreOrder'],
  72. 'product': ['storeProduct', 'prescribe', 'shippingTemplates', 'package'],
  73. 'ops': ['storeShop', 'storeCoupon', 'homeArticle', 'exportTask', 'userPromoterApply', 'adv'],
  74. },
  75. 'his': {
  76. 'doctor': ['doctor', 'doctorBill', 'doctorExtract', 'doctorOperLog'],
  77. 'inquiry': ['inquiryOrder', 'inquiryOrderReport', 'patient'],
  78. 'store': ['storeOrder', 'storeProduct', 'storeBill'],
  79. 'content': ['article', 'articleCate', 'answer'],
  80. 'ai': ['aiWorkflow', 'aiDoctorRole', 'aiDoctorChatSession'],
  81. },
  82. 'system': {
  83. 'org': ['dept', 'post', 'user'],
  84. 'perm': ['role', 'menu'],
  85. 'config': ['dict', 'config', 'notice', 'keyword', 'set'],
  86. 'voice': ['companyVoiceDialog', 'companyVoiceRobotic'],
  87. },
  88. 'company': {
  89. 'org': ['companyDept', 'companyPost', 'companyUser'],
  90. 'perm': ['companyRole', 'companyMenu'],
  91. 'finance': ['companyRecharge', 'companyProfit', 'companyMoneyLogs', 'companyDeduct'],
  92. 'comm': ['companySms', 'companyVoice', 'companyWorkflow'],
  93. 'wx': ['wxAccount', 'wxUser', 'wxDialog'],
  94. },
  95. 'live': {
  96. 'ops': ['liveConsole', 'liveConfig', 'liveData', 'liveWatch'],
  97. 'order': ['liveOrder', 'liveAfterSales', 'healthLiveOrder'],
  98. 'interact': ['liveCoupon', 'liveQuestion', 'liveReward', 'comment'],
  99. },
  100. 'course': {
  101. 'content': ['videoResource', 'Material', 'period', 'courseFinishTemp'],
  102. 'study': ['userCourse', 'courseWatchLog', 'userWatch'],
  103. 'stat': ['statistics', 'courseUserStatistics', 'huaweiCloudStatistics'],
  104. },
  105. }
  106. GROUP_TITLES = {
  107. 'msg': '消息管理', 'customer': '客户管理',
  108. 'group': '群聊管理', 'moments': '朋友圈',
  109. 'drainage': '引流管理', 'tag': '标签管理',
  110. 'setting': '企微设置', 'sop': 'SOP/自动化',
  111. 'order': '订单管理', 'product': '商品管理',
  112. 'ops': '门店运营', 'doctor': '医生管理',
  113. 'inquiry': '问诊管理', 'content': '内容管理',
  114. 'ai': 'AI/工作流', 'org': '组织管理',
  115. 'perm': '权限管理', 'config': '系统设置',
  116. 'voice': '语音/外呼', 'finance': '财务',
  117. 'comm': '通信/工作流', 'wx': '微信账号',
  118. 'interact': '互动营销', 'study': '学习跟踪',
  119. 'stat': '统计分析', 'general': '通用/待分组',
  120. }
  121. def to_component(rel_path):
  122. comp = rel_path.replace('\\', '/').replace('.vue', '')
  123. if comp.endswith('/index/index'):
  124. comp = comp[:-6]
  125. return comp
  126. def is_menu_page(rel_path, filename):
  127. if SKIP_REL.search(rel_path):
  128. return False
  129. parts = rel_path.replace('\\', '/').split('/')
  130. if parts[0] in SKIP_TOP:
  131. return False
  132. if filename in LIST_NAMES:
  133. return True
  134. if len(parts) == 2 and filename.endswith('.vue'):
  135. return True
  136. return False
  137. def scan_views():
  138. pages = []
  139. all_files = 0
  140. for dirpath, dirnames, filenames in os.walk(ROOT):
  141. dirnames[:] = [d for d in dirnames if d not in SKIP_TOP and d != 'mixins']
  142. for fn in filenames:
  143. if not fn.endswith('.vue'):
  144. continue
  145. all_files += 1
  146. full = os.path.join(dirpath, fn)
  147. rel = full[len(ROOT) + 1:]
  148. if is_menu_page(rel, fn):
  149. comp = to_component(rel)
  150. module = rel.replace('\\', '/').split('/')[1] if '/' in rel else fn.replace('.vue', '')
  151. pages.append({
  152. 'top': rel.replace('\\', '/').split('/')[0],
  153. 'module': module,
  154. 'component': comp,
  155. 'file': rel.replace('\\', '/'),
  156. })
  157. return pages, all_files
  158. def guess_secondary(top, module):
  159. hints = SECONDARY_HINTS.get(top, {})
  160. for group, keys in hints.items():
  161. for k in keys:
  162. if k.lower() in module.lower():
  163. return group
  164. return 'general'
  165. def build_markdown(pages, all_files):
  166. by_top = defaultdict(list)
  167. for p in pages:
  168. by_top[p['top']].append(p)
  169. agg_count = defaultdict(int)
  170. for p in pages:
  171. info = TENANT_TOP.get(p['top'])
  172. key = info[1] if info else ('other' if p['top'] in ADMIN_ONLY_TOP else 'other')
  173. agg_count[key] += 1
  174. L = []
  175. w = L.append
  176. w('# adminUI 视图反向梳理 \u2014 租户管理端菜单建议结构')
  177. w('')
  178. w('> 文档类型:**只读梳理**,暂不修改数据库或代码。')
  179. w('> 扫描来源:`ylrz_saas_his_scrm_adminUI/src/views`')
  180. w('> 生成日期:%s' % date.today().isoformat())
  181. w('> 扫描结果:共 **%d** 个 `.vue` 文件,识别 **%d** 个可独立路由页面' % (all_files, len(pages)))
  182. w('')
  183. w('---')
  184. w('')
  185. w('## 一、梳理方法')
  186. w('')
  187. w('| 项目 | 说明 |')
  188. w('|------|------|')
  189. w('| 页面识别 | `index.vue` / `list.vue` / `myList.vue` 及模块根目录单层 `.vue` |')
  190. w('| 排除 | `components/`、详情页、授权页、字典子页、设计器、日志子页等 |')
  191. w('| 组件路延 | 对应后端 `sys_menu.component`,如 `qw/externalContact/index` |')
  192. w('| 路由加载 | 后端 `getRouters` \u2192 `loadView(@/views/${component})` |')
  193. w('| 参考 | `src/views/admin/menu.js` (总后台 `/admin/*` 对照表) |')
  194. w('')
  195. w('---')
  196. w('')
  197. w('## 二、建议顶级模块(租户 saasadminui 顶栏)')
  198. w('')
  199. w('| 序号 | 模块名 | path | 视图根目录 | 页面数 | 备注 |')
  200. w('|------|--------|------|------------|--------|------|')
  201. tops = [
  202. (1, '首页', 'index', 'index.vue', 1, '仪表盘,可 hidden'),
  203. (2, '企微管理', 'qw', 'qw/', agg_count['qw'], ''),
  204. (3, '微信管理', 'wx', 'wx/, gw/', agg_count['wx'], ''),
  205. (4, 'CRM客户', 'crm', 'crm/', agg_count['crm'], ''),
  206. (5, '会员管理', 'member', 'user/, users/, member/', agg_count['member'], ''),
  207. (6, '诊所管理', 'his', 'his/', agg_count['his'], ''),
  208. (7, '商城管理', 'store', 'store/', agg_count['store'], 'canonical'),
  209. (8, '直播管理', 'live', 'live/, liveData/', agg_count['live'], ''),
  210. (9, '课程管理', 'course', 'course/, courseFinishTemp/', agg_count['course'], ''),
  211. (10, 'AI聊天', 'fastGpt', 'fastGpt/, chat/, aiob/', agg_count['fastGpt'], ''),
  212. (11, '龙虹引擎', 'lobster', 'lobster/', agg_count['lobster'], ''),
  213. (12, '广告投放', 'ad', 'adv/, ad/', agg_count['ad'], ''),
  214. (13, '系统管理', 'system', 'system/, company/', agg_count['system'], ''),
  215. (14, '财务管理', 'bill', 'bill/, billing/', agg_count['bill'], ''),
  216. (15, '日程管理', 'calendar', 'calendar/', agg_count['calendar'], ''),
  217. (16, '数据统计', 'statistics', 'statistics/, taskStatistics/', agg_count['statistics'], ''),
  218. (17, '监控管理', 'watch', 'watch/, monitor/', agg_count['watch'], ''),
  219. (18, '其他', 'other', '平台/遗留', 0, '不下发租户默认菜单'),
  220. ]
  221. for row in tops:
  222. w('| %d | %s | `%s` | %s | %d | %s |' % row)
  223. w('')
  224. w('---')
  225. w('')
  226. w('## 三、总后台专用(归入「其他」)')
  227. w('')
  228. w('| 目录 | 页面数 | 说明 |')
  229. w('|------|--------|------|')
  230. notes = {
  231. 'admin': '总后台业务(租户/代理/外呼/短信/财务审计)',
  232. 'saas': 'SaaS 计费/租户字典/租户菜单模板',
  233. 'sysUser': '总后台员工(与 system/user 不同)',
  234. 'hisStore': '旧版商城,与 store 重复',
  235. 'shop': '门店独立菜单(遗留)',
  236. }
  237. for t in sorted(set(ADMIN_ONLY_TOP) | {'admin', 'saas'}):
  238. cnt = len(by_top.get(t, []))
  239. if cnt:
  240. w('| `%s/` | %d | %s |' % (t, cnt, notes.get(t, '平台或遗留')))
  241. w('')
  242. w('### 3.1 admin/menu.js 对照(总后台 /admin/*)')
  243. w('')
  244. w('| 菜单标题 | path | component |')
  245. w('|----------|------|-----------|')
  246. admin_rows = [
  247. ('数据看板', 'dashboard', 'admin/dashboard/index'),
  248. ('租户管理', 'company', 'admin/sysCompany/index'),
  249. ('租户模块使用统计', 'moduleUsage', 'admin/moduleUsage/index'),
  250. ('租户管理端菜单', 'tenantMenu', 'admin/tenantMenu/index'),
  251. ('租户销售端菜单', 'tenantCompany', 'admin/tenantCompany/index'),
  252. ('代理管理', 'proxy', 'admin/proxy/index'),
  253. ('收费配置', 'serviceCost', 'admin/serviceCost/index'),
  254. ('员工管理', 'sysUser', 'admin/sysUser/index'),
  255. ('角色管理', 'role', 'system/role/index'),
  256. ('菜单管理', 'menu', 'system/menu/index'),
  257. ('外呼管理', 'voice', 'admin/voice/index'),
  258. ('短信管理', 'sms', 'admin/sms/index'),
  259. ('AI模型配置', 'aiModel', 'admin/aiModel/index'),
  260. ('AI生成工作流', 'workflowGenerate', 'lobster/workflow-generate/index'),
  261. ]
  262. for title, path, comp in admin_rows:
  263. w('| %s | `/admin/%s` | `%s` |' % (title, path, comp))
  264. w('')
  265. w('---')
  266. w('')
  267. w('## 四、各业务模块二级分组与页面清单')
  268. w('')
  269. detail = [
  270. ('qw', '企微管理', 'qw'),
  271. ('crm', 'CRM', 'crm'),
  272. ('store', '商城', 'store'),
  273. ('his', '诊所', 'his'),
  274. ('live', '直播', 'live'),
  275. ('course', '课程', 'course'),
  276. ('fastGpt', 'AI', 'fastGpt'),
  277. ('company', '企业/组织', 'company'),
  278. ('system', '系统', 'system'),
  279. ('lobster', '龙虹', 'lobster'),
  280. ('adv', '广告', 'ad'),
  281. ('wx', '微信', 'wx'),
  282. ('monitor', '监控', 'monitor'),
  283. ]
  284. sec_no = 1
  285. for top_key, title, path_prefix in detail:
  286. items = list(by_top.get(top_key, []))
  287. if top_key == 'fastGpt':
  288. for extra in ('chat', 'aiob', 'FastGptExtUserTag'):
  289. items.extend(by_top.get(extra, []))
  290. if not items:
  291. continue
  292. w('### 4.%d %s (`%s/`) \u2014 %d 页' % (sec_no, title, top_key, len(items)))
  293. sec_no += 1
  294. w('')
  295. w('建议路由前缀:`/%s`' % path_prefix)
  296. w('')
  297. groups = defaultdict(list)
  298. for p in sorted(items, key=lambda x: x['component']):
  299. groups[guess_secondary(top_key, p['module'])].append(p)
  300. order = list(SECONDARY_HINTS.get(top_key, {}).keys()) + ['general']
  301. for g in order:
  302. if g not in groups:
  303. continue
  304. w('#### %s' % GROUP_TITLES.get(g, g))
  305. w('')
  306. w('| 建议 menu_name | component | 源文件 |')
  307. w('|--------------|-----------|--------|')
  308. for p in groups[g]:
  309. w('| %s | `%s` | `%s` |' % (p['module'], p['component'], p['file']))
  310. w('')
  311. w('---')
  312. w('')
  313. w('## 五、重复/遗留模块合并建议')
  314. w('')
  315. w('| 组 | 目录 | 建议 |')
  316. w('|------|------|------|')
  317. dupes = [
  318. ('商城', '`store/` vs `hisStore/`', '保留 store,hisStore 放其他'),
  319. ('商城', '`store/` vs `his/store*`', 'his 内嵌套页留在诊所模块'),
  320. ('会员', '`user/` `users/` `member/`', '合并为 member 顶栏'),
  321. ('AI', '`fastGpt/` `chat/` `aiob/`', '合并为 fastGpt'),
  322. ('广告', '`adv/` `ad/`', '统一 adv'),
  323. ('微信', '`wx/` `gw/`', 'gwAccount 归入 wx'),
  324. ('统计', '`statistics/` `taskStatistics/`', '合并 statistics'),
  325. ('监控', '`monitor/` `watch/`', '合并 watch'),
  326. ('用户', '`system/user` vs `sysUser/`', 'sysUser 仅总后台'),
  327. ]
  328. for a, b, c in dupes:
  329. w('| %s | %s | %s |' % (a, b, c))
  330. w('')
  331. w('---')
  332. w('')
  333. w('## 六、完整一级目录扫描表')
  334. w('')
  335. w('| views 一级目录 | 可路由页面数 | 归类 |')
  336. w('|----------------|-------------|------|')
  337. for top in sorted(by_top.keys()):
  338. cnt = len(by_top[top])
  339. if top in ADMIN_ONLY_TOP or top == 'admin':
  340. cat = '平台/其他'
  341. elif top in TENANT_TOP:
  342. cat = '租户 \u2014 ' + TENANT_TOP[top][0]
  343. else:
  344. cat = '待确认'
  345. w('| `%s/` | %d | %s |' % (top, cnt, cat))
  346. w('')
  347. w('---')
  348. w('')
  349. w('## 七、本次不执行的落地步骤')
  350. w('')
  351. w('1. 评审本文档二级分组')
  352. w('2. 导出 component 与 tenant_sys_menu diff')
  353. w('3. 合并重复菜单')
  354. w('4. 更新模板并同步租户库')
  355. w('5. saasadminui 验证路由')
  356. w('')
  357. w('---')
  358. w('')
  359. w('*Generated by `sql/generate_adminUI_menu_doc.py`*')
  360. return '\n'.join(L)
  361. def main():
  362. pages, all_files = scan_views()
  363. md = build_markdown(pages, all_files)
  364. with open(OUT, 'w', encoding='utf-8') as f:
  365. f.write(md)
  366. print('Wrote', OUT)
  367. print('pages', len(pages), 'all vue', all_files)
  368. if __name__ == '__main__':
  369. main()