# -*- 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': ('\u4f01\u5fae\u7ba1\u7406', 'qw'), 'wx': ('\u5fae\u4fe1\u7ba1\u7406', 'wx'), 'gw': ('\u5fae\u4fe1\u7ba1\u7406', 'wx'), 'crm': ('CRM\u5ba2\u6237', 'crm'), 'user': ('\u4f1a\u5458\u7ba1\u7406', 'member'), 'users': ('\u4f1a\u5458\u7ba1\u7406', 'member'), 'member': ('\u4f1a\u5458\u7ba1\u7406', 'member'), 'his': ('\u8bca\u6240\u7ba1\u7406', 'his'), 'store': ('\u5546\u57ce\u7ba1\u7406', 'store'), 'live': ('\u76f4\u64ad\u7ba1\u7406', 'live'), 'liveData': ('\u76f4\u64ad\u7ba1\u7406', 'live'), 'course': ('\u8bfe\u7a0b\u7ba1\u7406', 'course'), 'courseFinishTemp': ('\u8bfe\u7a0b\u7ba1\u7406', 'course'), 'fastGpt': ('AI\u804a\u5929', 'fastGpt'), 'FastGptExtUserTag': ('AI\u804a\u5929', 'fastGpt'), 'chat': ('AI\u804a\u5929', 'fastGpt'), 'aiob': ('AI\u804a\u5929', 'fastGpt'), 'lobster': ('\u9f99\u8679\u5f15\u64ce', 'lobster'), 'adv': ('\u5e7f\u544a\u6295\u653e', 'ad'), 'ad': ('\u5e7f\u544a\u6295\u653e', 'ad'), 'company': ('\u7cfb\u7edf\u7ba1\u7406', 'system'), 'system': ('\u7cfb\u7edf\u7ba1\u7406', 'system'), 'bill': ('\u8d22\u52a1\u7ba1\u7406', 'bill'), 'billing': ('\u8d22\u52a1\u7ba1\u7406', 'bill'), 'calendar': ('\u65e5\u7a0b\u7ba1\u7406', 'calendar'), 'statistics': ('\u6570\u636e\u7edf\u8ba1', 'statistics'), 'taskStatistics': ('\u6570\u636e\u7edf\u8ba1', 'statistics'), 'moduleUsage': ('\u6570\u636e\u7edf\u8ba1', 'statistics'), 'monitor': ('\u76d1\u63a7\u7ba1\u7406', 'watch'), 'watch': ('\u76d1\u63a7\u7ba1\u7406', 'watch'), 'tool': ('\u7cfb\u7edf\u5de5\u5177', 'system'), 'qwExternalContact': ('\u4f01\u5fae\u7ba1\u7406', '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': '\u6d88\u606f\u7ba1\u7406', 'customer': '\u5ba2\u6237\u7ba1\u7406', 'group': '\u7fa4\u804a\u7ba1\u7406', 'moments': '\u670b\u53cb\u5708', 'drainage': '\u5f15\u6d41\u7ba1\u7406', 'tag': '\u6807\u7b7e\u7ba1\u7406', 'setting': '\u4f01\u5fae\u8bbe\u7f6e', 'sop': 'SOP/\u81ea\u52a8\u5316', 'order': '\u8ba2\u5355\u7ba1\u7406', 'product': '\u5546\u54c1\u7ba1\u7406', 'ops': '\u95e8\u5e97\u8fd0\u8425', 'doctor': '\u533b\u751f\u7ba1\u7406', 'inquiry': '\u95ee\u8bca\u7ba1\u7406', 'content': '\u5185\u5bb9\u7ba1\u7406', 'ai': 'AI/\u5de5\u4f5c\u6d41', 'org': '\u7ec4\u7ec7\u7ba1\u7406', 'perm': '\u6743\u9650\u7ba1\u7406', 'config': '\u7cfb\u7edf\u8bbe\u7f6e', 'voice': '\u8bed\u97f3/\u5916\u547c', 'finance': '\u8d22\u52a1', 'comm': '\u901a\u4fe1/\u5de5\u4f5c\u6d41', 'wx': '\u5fae\u4fe1\u8d26\u53f7', 'interact': '\u4e92\u52a8\u8425\u9500', 'study': '\u5b66\u4e60\u8ddf\u8e2a', 'stat': '\u7edf\u8ba1\u5206\u6790', 'general': '\u901a\u7528/\u5f85\u5206\u7ec4', } 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 \u89c6\u56fe\u53cd\u5411\u68b3\u7406 \u2014 \u79df\u6237\u7ba1\u7406\u7aef\u83dc\u5355\u5efa\u8bae\u7ed3\u6784') w('') w('> \u6587\u6863\u7c7b\u578b\uff1a**\u53ea\u8bfb\u68b3\u7406**\uff0c\u6682\u4e0d\u4fee\u6539\u6570\u636e\u5e93\u6216\u4ee3\u7801\u3002') w('> \u626b\u63cf\u6765\u6e90\uff1a`ylrz_saas_his_scrm_adminUI/src/views`') w('> \u751f\u6210\u65e5\u671f\uff1a%s' % date.today().isoformat()) w('> \u626b\u63cf\u7ed3\u679c\uff1a\u5171 **%d** \u4e2a `.vue` \u6587\u4ef6\uff0c\u8bc6\u522b **%d** \u4e2a\u53ef\u72ec\u7acb\u8def\u7531\u9875\u9762' % (all_files, len(pages))) w('') w('---') w('') w('## \u4e00\u3001\u68b3\u7406\u65b9\u6cd5') w('') w('| \u9879\u76ee | \u8bf4\u660e |') w('|------|------|') w('| \u9875\u9762\u8bc6\u522b | `index.vue` / `list.vue` / `myList.vue` \u53ca\u6a21\u5757\u6839\u76ee\u5f55\u5355\u5c42 `.vue` |') w('| \u6392\u9664 | `components/`\u3001\u8be6\u60c5\u9875\u3001\u6388\u6743\u9875\u3001\u5b57\u5178\u5b50\u9875\u3001\u8bbe\u8ba1\u5668\u3001\u65e5\u5fd7\u5b50\u9875\u7b49 |') w('| \u7ec4\u4ef6\u8def\u5ef6 | \u5bf9\u5e94\u540e\u7aef `sys_menu.component`\uff0c\u5982 `qw/externalContact/index` |') w('| \u8def\u7531\u52a0\u8f7d | \u540e\u7aef `getRouters` \u2192 `loadView(@/views/${component})` |') w('| \u53c2\u8003 | `src/views/admin/menu.js` \uff08\u603b\u540e\u53f0 `/admin/*` \u5bf9\u7167\u8868\uff09 |') w('') w('---') w('') w('## \u4e8c\u3001\u5efa\u8bae\u9876\u7ea7\u6a21\u5757\uff08\u79df\u6237 saasadminui \u9876\u680f\uff09') w('') w('| \u5e8f\u53f7 | \u6a21\u5757\u540d | path | \u89c6\u56fe\u6839\u76ee\u5f55 | \u9875\u9762\u6570 | \u5907\u6ce8 |') w('|------|--------|------|------------|--------|------|') tops = [ (1, '\u9996\u9875', 'index', 'index.vue', 1, '\u4eea\u8868\u76d8\uff0c\u53ef hidden'), (2, '\u4f01\u5fae\u7ba1\u7406', 'qw', 'qw/', agg_count['qw'], ''), (3, '\u5fae\u4fe1\u7ba1\u7406', 'wx', 'wx/, gw/', agg_count['wx'], ''), (4, 'CRM\u5ba2\u6237', 'crm', 'crm/', agg_count['crm'], ''), (5, '\u4f1a\u5458\u7ba1\u7406', 'member', 'user/, users/, member/', agg_count['member'], ''), (6, '\u8bca\u6240\u7ba1\u7406', 'his', 'his/', agg_count['his'], ''), (7, '\u5546\u57ce\u7ba1\u7406', 'store', 'store/', agg_count['store'], 'canonical'), (8, '\u76f4\u64ad\u7ba1\u7406', 'live', 'live/, liveData/', agg_count['live'], ''), (9, '\u8bfe\u7a0b\u7ba1\u7406', 'course', 'course/, courseFinishTemp/', agg_count['course'], ''), (10, 'AI\u804a\u5929', 'fastGpt', 'fastGpt/, chat/, aiob/', agg_count['fastGpt'], ''), (11, '\u9f99\u8679\u5f15\u64ce', 'lobster', 'lobster/', agg_count['lobster'], ''), (12, '\u5e7f\u544a\u6295\u653e', 'ad', 'adv/, ad/', agg_count['ad'], ''), (13, '\u7cfb\u7edf\u7ba1\u7406', 'system', 'system/, company/', agg_count['system'], ''), (14, '\u8d22\u52a1\u7ba1\u7406', 'bill', 'bill/, billing/', agg_count['bill'], ''), (15, '\u65e5\u7a0b\u7ba1\u7406', 'calendar', 'calendar/', agg_count['calendar'], ''), (16, '\u6570\u636e\u7edf\u8ba1', 'statistics', 'statistics/, taskStatistics/', agg_count['statistics'], ''), (17, '\u76d1\u63a7\u7ba1\u7406', 'watch', 'watch/, monitor/', agg_count['watch'], ''), (18, '\u5176\u4ed6', 'other', '\u5e73\u53f0/\u9057\u7559', 0, '\u4e0d\u4e0b\u53d1\u79df\u6237\u9ed8\u8ba4\u83dc\u5355'), ] for row in tops: w('| %d | %s | `%s` | %s | %d | %s |' % row) w('') w('---') w('') w('## \u4e09\u3001\u603b\u540e\u53f0\u4e13\u7528\uff08\u5f52\u5165\u300c\u5176\u4ed6\u300d\uff09') w('') w('| \u76ee\u5f55 | \u9875\u9762\u6570 | \u8bf4\u660e |') w('|------|--------|------|') notes = { 'admin': '\u603b\u540e\u53f0\u4e1a\u52a1\uff08\u79df\u6237/\u4ee3\u7406/\u5916\u547c/\u77ed\u4fe1/\u8d22\u52a1\u5ba1\u8ba1\uff09', 'saas': 'SaaS \u8ba1\u8d39/\u79df\u6237\u5b57\u5178/\u79df\u6237\u83dc\u5355\u6a21\u677f', 'sysUser': '\u603b\u540e\u53f0\u5458\u5de5\uff08\u4e0e system/user \u4e0d\u540c\uff09', 'hisStore': '\u65e7\u7248\u5546\u57ce\uff0c\u4e0e store \u91cd\u590d', 'shop': '\u95e8\u5e97\u72ec\u7acb\u83dc\u5355\uff08\u9057\u7559\uff09', } 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, '\u5e73\u53f0\u6216\u9057\u7559'))) w('') w('### 3.1 admin/menu.js \u5bf9\u7167\uff08\u603b\u540e\u53f0 /admin/*\uff09') w('') w('| \u83dc\u5355\u6807\u9898 | path | component |') w('|----------|------|-----------|') admin_rows = [ ('\u6570\u636e\u770b\u677f', 'dashboard', 'admin/dashboard/index'), ('\u79df\u6237\u7ba1\u7406', 'company', 'admin/sysCompany/index'), ('\u79df\u6237\u6a21\u5757\u4f7f\u7528\u7edf\u8ba1', 'moduleUsage', 'admin/moduleUsage/index'), ('\u79df\u6237\u7ba1\u7406\u7aef\u83dc\u5355', 'tenantMenu', 'admin/tenantMenu/index'), ('\u79df\u6237\u9500\u552e\u7aef\u83dc\u5355', 'tenantCompany', 'admin/tenantCompany/index'), ('\u4ee3\u7406\u7ba1\u7406', 'proxy', 'admin/proxy/index'), ('\u6536\u8d39\u914d\u7f6e', 'serviceCost', 'admin/serviceCost/index'), ('\u5458\u5de5\u7ba1\u7406', 'sysUser', 'admin/sysUser/index'), ('\u89d2\u8272\u7ba1\u7406', 'role', 'system/role/index'), ('\u83dc\u5355\u7ba1\u7406', 'menu', 'system/menu/index'), ('\u5916\u547c\u7ba1\u7406', 'voice', 'admin/voice/index'), ('\u77ed\u4fe1\u7ba1\u7406', 'sms', 'admin/sms/index'), ('AI\u6a21\u578b\u914d\u7f6e', 'aiModel', 'admin/aiModel/index'), ('AI\u751f\u6210\u5de5\u4f5c\u6d41', 'workflowGenerate', 'lobster/workflow-generate/index'), ] for title, path, comp in admin_rows: w('| %s | `/admin/%s` | `%s` |' % (title, path, comp)) w('') w('---') w('') w('## \u56db\u3001\u5404\u4e1a\u52a1\u6a21\u5757\u4e8c\u7ea7\u5206\u7ec4\u4e0e\u9875\u9762\u6e05\u5355') w('') detail = [ ('qw', '\u4f01\u5fae\u7ba1\u7406', 'qw'), ('crm', 'CRM', 'crm'), ('store', '\u5546\u57ce', 'store'), ('his', '\u8bca\u6240', 'his'), ('live', '\u76f4\u64ad', 'live'), ('course', '\u8bfe\u7a0b', 'course'), ('fastGpt', 'AI', 'fastGpt'), ('company', '\u4f01\u4e1a/\u7ec4\u7ec7', 'company'), ('system', '\u7cfb\u7edf', 'system'), ('lobster', '\u9f99\u8679', 'lobster'), ('adv', '\u5e7f\u544a', 'ad'), ('wx', '\u5fae\u4fe1', 'wx'), ('monitor', '\u76d1\u63a7', '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 \u9875' % (sec_no, title, top_key, len(items))) sec_no += 1 w('') w('\u5efa\u8bae\u8def\u7531\u524d\u7f00\uff1a`/%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('| \u5efa\u8bae menu_name | component | \u6e90\u6587\u4ef6 |') w('|--------------|-----------|--------|') for p in groups[g]: w('| %s | `%s` | `%s` |' % (p['module'], p['component'], p['file'])) w('') w('---') w('') w('## \u4e94\u3001\u91cd\u590d/\u9057\u7559\u6a21\u5757\u5408\u5e76\u5efa\u8bae') w('') w('| \u7ec4 | \u76ee\u5f55 | \u5efa\u8bae |') w('|------|------|------|') dupes = [ ('\u5546\u57ce', '`store/` vs `hisStore/`', '\u4fdd\u7559 store\uff0chisStore \u653e\u5176\u4ed6'), ('\u5546\u57ce', '`store/` vs `his/store*`', 'his \u5185\u5d4c\u5957\u9875\u7559\u5728\u8bca\u6240\u6a21\u5757'), ('\u4f1a\u5458', '`user/` `users/` `member/`', '\u5408\u5e76\u4e3a member \u9876\u680f'), ('AI', '`fastGpt/` `chat/` `aiob/`', '\u5408\u5e76\u4e3a fastGpt'), ('\u5e7f\u544a', '`adv/` `ad/`', '\u7edf\u4e00 adv'), ('\u5fae\u4fe1', '`wx/` `gw/`', 'gwAccount \u5f52\u5165 wx'), ('\u7edf\u8ba1', '`statistics/` `taskStatistics/`', '\u5408\u5e76 statistics'), ('\u76d1\u63a7', '`monitor/` `watch/`', '\u5408\u5e76 watch'), ('\u7528\u6237', '`system/user` vs `sysUser/`', 'sysUser \u4ec5\u603b\u540e\u53f0'), ] for a, b, c in dupes: w('| %s | %s | %s |' % (a, b, c)) w('') w('---') w('') w('## \u516d\u3001\u5b8c\u6574\u4e00\u7ea7\u76ee\u5f55\u626b\u63cf\u8868') w('') w('| views \u4e00\u7ea7\u76ee\u5f55 | \u53ef\u8def\u7531\u9875\u9762\u6570 | \u5f52\u7c7b |') w('|----------------|-------------|------|') for top in sorted(by_top.keys()): cnt = len(by_top[top]) if top in ADMIN_ONLY_TOP or top == 'admin': cat = '\u5e73\u53f0/\u5176\u4ed6' elif top in TENANT_TOP: cat = '\u79df\u6237 \u2014 ' + TENANT_TOP[top][0] else: cat = '\u5f85\u786e\u8ba4' w('| `%s/` | %d | %s |' % (top, cnt, cat)) w('') w('---') w('') w('## \u4e03\u3001\u672c\u6b21\u4e0d\u6267\u884c\u7684\u843d\u5730\u6b65\u9aa4') w('') w('1. \u8bc4\u5ba1\u672c\u6587\u6863\u4e8c\u7ea7\u5206\u7ec4') w('2. \u5bfc\u51fa component \u4e0e tenant_sys_menu diff') w('3. \u5408\u5e76\u91cd\u590d\u83dc\u5355') w('4. \u66f4\u65b0\u6a21\u677f\u5e76\u540c\u6b65\u79df\u6237\u5e93') w('5. saasadminui \u9a8c\u8bc1\u8def\u7531') 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()