# -*- coding: utf-8 -*- """ Scan adminUI src/views and generate reverse-engineered menu structure document. """ import os import re from collections import defaultdict from datetime import date ROOT = os.path.normpath( os.path.join(os.path.dirname(__file__), '..', '..', 'ylrz_saas_his_scrm_adminUI', 'src', 'views') ) OUT = os.path.join(os.path.dirname(__file__), 'adminUI_views_menu_structure.md') SKIP_TOP = {'components', 'error', 'dashboard', 'icons', 'login', 'register', 'redirect'} SKIP_REL = re.compile( r'(components/|/profile/|authRole|authUser|selectUser|/data\.vue|/design\.vue|' r'/log\.vue|/editTable|/details\.vue|/my\.vue|sopLogsList|sopUserLogsSchedule|' r'/transferLog\.vue|/transfer\.vue|/darkRoom\.vue|/blackroom\.vue|config2\.vue|' r'/sopTempe/|mixins/)' ) LIST_NAMES = {'index.vue', 'list.vue', 'myList.vue', 'order.vue', 'order1.vue'} ADMIN_ONLY_TOP = { 'admin', 'saas', 'sysUser', 'saler', 'shop', 'medical', 'food', 'todo', 'baidu', 'callRecord', 'aiSipCall', 'storeOrderOfflineItem', 'operation', 'hisStore', } TENANT_TOP = { 'qw': ('企微管理', 'qw'), 'wx': ('微信管理', 'wx'), 'gw': ('微信管理', 'wx'), 'crm': ('CRM客户', 'crm'), 'user': ('会员管理', 'member'), 'users': ('会员管理', 'member'), 'member': ('会员管理', 'member'), 'his': ('诊所管理', 'his'), 'store': ('商城管理', 'store'), 'live': ('直播管理', 'live'), 'liveData': ('直播管理', 'live'), 'course': ('课程管理', 'course'), 'courseFinishTemp': ('课程管理', 'course'), 'fastGpt': ('AI聊天', 'fastGpt'), 'FastGptExtUserTag': ('AI聊天', 'fastGpt'), 'chat': ('AI聊天', 'fastGpt'), 'aiob': ('AI聊天', 'fastGpt'), 'lobster': ('龙虹引擎', 'lobster'), 'adv': ('广告投放', 'ad'), 'ad': ('广告投放', 'ad'), 'company': ('系统管理', 'system'), 'system': ('系统管理', 'system'), 'bill': ('财务管理', 'bill'), 'billing': ('财务管理', 'bill'), 'calendar': ('日程管理', 'calendar'), 'statistics': ('数据统计', 'statistics'), 'taskStatistics': ('数据统计', 'statistics'), 'moduleUsage': ('数据统计', 'statistics'), 'monitor': ('监控管理', 'watch'), 'watch': ('监控管理', 'watch'), 'tool': ('系统工具', 'system'), 'qwExternalContact': ('企微管理', 'qw'), } SECONDARY_HINTS = { 'qw': { 'msg': ['QwWorkTask', 'groupMsg', 'record', 'qwPushCount'], 'customer': ['externalContact', 'contactWay', 'drainageLink', 'assignRule', 'customerLink'], 'group': ['groupChat', 'groupLiveCode', 'groupActual'], 'moments': ['friendCircle', 'friendMaterial', 'friendWelcome'], 'drainage': ['appAdvertisingReport', 'appContactWay'], 'tag': ['tag', 'tagGroup', 'autoTags'], 'setting': ['qwDept', 'companyUser', 'material', 'welcome', 'applyIpad', 'userBehaviorData'], 'sop': ['sop', 'sopTemp', 'sopLogs', 'aiSop'], }, 'store': { 'order': ['storeOrder', 'storeAfterSales', 'inquiryOrder', 'PromotionOrder', 'healthStoreOrder'], 'product': ['storeProduct', 'prescribe', 'shippingTemplates', 'package'], 'ops': ['storeShop', 'storeCoupon', 'homeArticle', 'exportTask', 'userPromoterApply', 'adv'], }, 'his': { 'doctor': ['doctor', 'doctorBill', 'doctorExtract', 'doctorOperLog'], 'inquiry': ['inquiryOrder', 'inquiryOrderReport', 'patient'], 'store': ['storeOrder', 'storeProduct', 'storeBill'], 'content': ['article', 'articleCate', 'answer'], 'ai': ['aiWorkflow', 'aiDoctorRole', 'aiDoctorChatSession'], }, 'system': { 'org': ['dept', 'post', 'user'], 'perm': ['role', 'menu'], 'config': ['dict', 'config', 'notice', 'keyword', 'set'], 'voice': ['companyVoiceDialog', 'companyVoiceRobotic'], }, 'company': { 'org': ['companyDept', 'companyPost', 'companyUser'], 'perm': ['companyRole', 'companyMenu'], 'finance': ['companyRecharge', 'companyProfit', 'companyMoneyLogs', 'companyDeduct'], 'comm': ['companySms', 'companyVoice', 'companyWorkflow'], 'wx': ['wxAccount', 'wxUser', 'wxDialog'], }, 'live': { 'ops': ['liveConsole', 'liveConfig', 'liveData', 'liveWatch'], 'order': ['liveOrder', 'liveAfterSales', 'healthLiveOrder'], 'interact': ['liveCoupon', 'liveQuestion', 'liveReward', 'comment'], }, 'course': { 'content': ['videoResource', 'Material', 'period', 'courseFinishTemp'], 'study': ['userCourse', 'courseWatchLog', 'userWatch'], 'stat': ['statistics', 'courseUserStatistics', 'huaweiCloudStatistics'], }, } GROUP_TITLES = { 'msg': '消息管理', 'customer': '客户管理', 'group': '群聊管理', 'moments': '朋友圈', 'drainage': '引流管理', 'tag': '标签管理', 'setting': '企微设置', 'sop': 'SOP/自动化', 'order': '订单管理', 'product': '商品管理', 'ops': '门店运营', 'doctor': '医生管理', 'inquiry': '问诊管理', 'content': '内容管理', 'ai': 'AI/工作流', 'org': '组织管理', 'perm': '权限管理', 'config': '系统设置', 'voice': '语音/外呼', 'finance': '财务', 'comm': '通信/工作流', 'wx': '微信账号', 'interact': '互动营销', 'study': '学习跟踪', 'stat': '统计分析', 'general': '通用/待分组', } def to_component(rel_path): comp = rel_path.replace('\\', '/').replace('.vue', '') if comp.endswith('/index/index'): comp = comp[:-6] return comp def is_menu_page(rel_path, filename): if SKIP_REL.search(rel_path): return False parts = rel_path.replace('\\', '/').split('/') if parts[0] in SKIP_TOP: return False if filename in LIST_NAMES: return True if len(parts) == 2 and filename.endswith('.vue'): return True return False def scan_views(): pages = [] all_files = 0 for dirpath, dirnames, filenames in os.walk(ROOT): dirnames[:] = [d for d in dirnames if d not in SKIP_TOP and d != 'mixins'] for fn in filenames: if not fn.endswith('.vue'): continue all_files += 1 full = os.path.join(dirpath, fn) rel = full[len(ROOT) + 1:] if is_menu_page(rel, fn): comp = to_component(rel) module = rel.replace('\\', '/').split('/')[1] if '/' in rel else fn.replace('.vue', '') pages.append({ 'top': rel.replace('\\', '/').split('/')[0], 'module': module, 'component': comp, 'file': rel.replace('\\', '/'), }) return pages, all_files def guess_secondary(top, module): hints = SECONDARY_HINTS.get(top, {}) for group, keys in hints.items(): for k in keys: if k.lower() in module.lower(): return group return 'general' def build_markdown(pages, all_files): by_top = defaultdict(list) for p in pages: by_top[p['top']].append(p) agg_count = defaultdict(int) for p in pages: info = TENANT_TOP.get(p['top']) key = info[1] if info else ('other' if p['top'] in ADMIN_ONLY_TOP else 'other') agg_count[key] += 1 L = [] w = L.append w('# adminUI 视图反向梳理 \u2014 租户管理端菜单建议结构') w('') w('> 文档类型:**只读梳理**,暂不修改数据库或代码。') w('> 扫描来源:`ylrz_saas_his_scrm_adminUI/src/views`') w('> 生成日期:%s' % date.today().isoformat()) w('> 扫描结果:共 **%d** 个 `.vue` 文件,识别 **%d** 个可独立路由页面' % (all_files, len(pages))) w('') w('---') w('') w('## 一、梳理方法') w('') w('| 项目 | 说明 |') w('|------|------|') w('| 页面识别 | `index.vue` / `list.vue` / `myList.vue` 及模块根目录单层 `.vue` |') w('| 排除 | `components/`、详情页、授权页、字典子页、设计器、日志子页等 |') w('| 组件路延 | 对应后端 `sys_menu.component`,如 `qw/externalContact/index` |') w('| 路由加载 | 后端 `getRouters` \u2192 `loadView(@/views/${component})` |') w('| 参考 | `src/views/admin/menu.js` (总后台 `/admin/*` 对照表) |') w('') w('---') w('') w('## 二、建议顶级模块(租户 saasadminui 顶栏)') w('') w('| 序号 | 模块名 | path | 视图根目录 | 页面数 | 备注 |') w('|------|--------|------|------------|--------|------|') tops = [ (1, '首页', 'index', 'index.vue', 1, '仪表盘,可 hidden'), (2, '企微管理', 'qw', 'qw/', agg_count['qw'], ''), (3, '微信管理', 'wx', 'wx/, gw/', agg_count['wx'], ''), (4, 'CRM客户', 'crm', 'crm/', agg_count['crm'], ''), (5, '会员管理', 'member', 'user/, users/, member/', agg_count['member'], ''), (6, '诊所管理', 'his', 'his/', agg_count['his'], ''), (7, '商城管理', 'store', 'store/', agg_count['store'], 'canonical'), (8, '直播管理', 'live', 'live/, liveData/', agg_count['live'], ''), (9, '课程管理', 'course', 'course/, courseFinishTemp/', agg_count['course'], ''), (10, 'AI聊天', 'fastGpt', 'fastGpt/, chat/, aiob/', agg_count['fastGpt'], ''), (11, '龙虹引擎', 'lobster', 'lobster/', agg_count['lobster'], ''), (12, '广告投放', 'ad', 'adv/, ad/', agg_count['ad'], ''), (13, '系统管理', 'system', 'system/, company/', agg_count['system'], ''), (14, '财务管理', 'bill', 'bill/, billing/', agg_count['bill'], ''), (15, '日程管理', 'calendar', 'calendar/', agg_count['calendar'], ''), (16, '数据统计', 'statistics', 'statistics/, taskStatistics/', agg_count['statistics'], ''), (17, '监控管理', 'watch', 'watch/, monitor/', agg_count['watch'], ''), (18, '其他', 'other', '平台/遗留', 0, '不下发租户默认菜单'), ] for row in tops: w('| %d | %s | `%s` | %s | %d | %s |' % row) w('') w('---') w('') w('## 三、总后台专用(归入「其他」)') w('') w('| 目录 | 页面数 | 说明 |') w('|------|--------|------|') notes = { 'admin': '总后台业务(租户/代理/外呼/短信/财务审计)', 'saas': 'SaaS 计费/租户字典/租户菜单模板', 'sysUser': '总后台员工(与 system/user 不同)', 'hisStore': '旧版商城,与 store 重复', 'shop': '门店独立菜单(遗留)', } for t in sorted(set(ADMIN_ONLY_TOP) | {'admin', 'saas'}): cnt = len(by_top.get(t, [])) if cnt: w('| `%s/` | %d | %s |' % (t, cnt, notes.get(t, '平台或遗留'))) w('') w('### 3.1 admin/menu.js 对照(总后台 /admin/*)') w('') w('| 菜单标题 | path | component |') w('|----------|------|-----------|') admin_rows = [ ('数据看板', 'dashboard', 'admin/dashboard/index'), ('租户管理', 'company', 'admin/sysCompany/index'), ('租户模块使用统计', 'moduleUsage', 'admin/moduleUsage/index'), ('租户管理端菜单', 'tenantMenu', 'admin/tenantMenu/index'), ('租户销售端菜单', 'tenantCompany', 'admin/tenantCompany/index'), ('代理管理', 'proxy', 'admin/proxy/index'), ('收费配置', 'serviceCost', 'admin/serviceCost/index'), ('员工管理', 'sysUser', 'admin/sysUser/index'), ('角色管理', 'role', 'system/role/index'), ('菜单管理', 'menu', 'system/menu/index'), ('外呼管理', 'voice', 'admin/voice/index'), ('短信管理', 'sms', 'admin/sms/index'), ('AI模型配置', 'aiModel', 'admin/aiModel/index'), ('AI生成工作流', 'workflowGenerate', 'lobster/workflow-generate/index'), ] for title, path, comp in admin_rows: w('| %s | `/admin/%s` | `%s` |' % (title, path, comp)) w('') w('---') w('') w('## 四、各业务模块二级分组与页面清单') w('') detail = [ ('qw', '企微管理', 'qw'), ('crm', 'CRM', 'crm'), ('store', '商城', 'store'), ('his', '诊所', 'his'), ('live', '直播', 'live'), ('course', '课程', 'course'), ('fastGpt', 'AI', 'fastGpt'), ('company', '企业/组织', 'company'), ('system', '系统', 'system'), ('lobster', '龙虹', 'lobster'), ('adv', '广告', 'ad'), ('wx', '微信', 'wx'), ('monitor', '监控', 'monitor'), ] sec_no = 1 for top_key, title, path_prefix in detail: items = list(by_top.get(top_key, [])) if top_key == 'fastGpt': for extra in ('chat', 'aiob', 'FastGptExtUserTag'): items.extend(by_top.get(extra, [])) if not items: continue w('### 4.%d %s (`%s/`) \u2014 %d 页' % (sec_no, title, top_key, len(items))) sec_no += 1 w('') w('建议路由前缀:`/%s`' % path_prefix) w('') groups = defaultdict(list) for p in sorted(items, key=lambda x: x['component']): groups[guess_secondary(top_key, p['module'])].append(p) order = list(SECONDARY_HINTS.get(top_key, {}).keys()) + ['general'] for g in order: if g not in groups: continue w('#### %s' % GROUP_TITLES.get(g, g)) w('') w('| 建议 menu_name | component | 源文件 |') w('|--------------|-----------|--------|') for p in groups[g]: w('| %s | `%s` | `%s` |' % (p['module'], p['component'], p['file'])) w('') w('---') w('') w('## 五、重复/遗留模块合并建议') w('') w('| 组 | 目录 | 建议 |') w('|------|------|------|') dupes = [ ('商城', '`store/` vs `hisStore/`', '保留 store,hisStore 放其他'), ('商城', '`store/` vs `his/store*`', 'his 内嵌套页留在诊所模块'), ('会员', '`user/` `users/` `member/`', '合并为 member 顶栏'), ('AI', '`fastGpt/` `chat/` `aiob/`', '合并为 fastGpt'), ('广告', '`adv/` `ad/`', '统一 adv'), ('微信', '`wx/` `gw/`', 'gwAccount 归入 wx'), ('统计', '`statistics/` `taskStatistics/`', '合并 statistics'), ('监控', '`monitor/` `watch/`', '合并 watch'), ('用户', '`system/user` vs `sysUser/`', 'sysUser 仅总后台'), ] for a, b, c in dupes: w('| %s | %s | %s |' % (a, b, c)) w('') w('---') w('') w('## 六、完整一级目录扫描表') w('') w('| views 一级目录 | 可路由页面数 | 归类 |') w('|----------------|-------------|------|') for top in sorted(by_top.keys()): cnt = len(by_top[top]) if top in ADMIN_ONLY_TOP or top == 'admin': cat = '平台/其他' elif top in TENANT_TOP: cat = '租户 \u2014 ' + TENANT_TOP[top][0] else: cat = '待确认' w('| `%s/` | %d | %s |' % (top, cnt, cat)) w('') w('---') w('') w('## 七、本次不执行的落地步骤') w('') w('1. 评审本文档二级分组') w('2. 导出 component 与 tenant_sys_menu diff') w('3. 合并重复菜单') w('4. 更新模板并同步租户库') w('5. saasadminui 验证路由') w('') w('---') w('') w('*Generated by `sql/generate_adminUI_menu_doc.py`*') return '\n'.join(L) def main(): pages, all_files = scan_views() md = build_markdown(pages, all_files) with open(OUT, 'w', encoding='utf-8') as f: f.write(md) print('Wrote', OUT) print('pages', len(pages), 'all vue', all_files) if __name__ == '__main__': main()