| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- # -*- 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()
|