check_navigation_consistency.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 导航一致性检查工具
  5. 版本: 1.0
  6. 描述: 检查所有前端页面的导航元素一致性,确保导航结构统一
  7. """
  8. import os
  9. import sys
  10. import json
  11. import re
  12. from pathlib import Path
  13. import html.parser
  14. import difflib
  15. class SimpleHTMLParser(html.parser.HTMLParser):
  16. """简单的HTML解析器,用于提取导航结构"""
  17. def __init__(self):
  18. super().__init__()
  19. self.current_tag = None
  20. self.nav_elements = []
  21. self.current_element = None
  22. self.nav_tags = {'header', 'nav', 'aside', 'div'}
  23. self.nav_classes = {'header', 'navigation', 'nav', 'sidebar', 'breadcrumb', 'menu'}
  24. def handle_starttag(self, tag, attrs):
  25. attrs_dict = dict(attrs)
  26. classes = attrs_dict.get('class', '').split()
  27. element_id = attrs_dict.get('id', '')
  28. # 检查是否是导航相关元素
  29. if (tag in self.nav_tags or
  30. any(nav_class in classes for nav_class in self.nav_classes) or
  31. any(nav_class in element_id.lower() for nav_class in self.nav_classes)):
  32. self.current_element = {
  33. 'tag': tag,
  34. 'classes': classes,
  35. 'id': element_id,
  36. 'attrs': attrs_dict
  37. }
  38. def handle_endtag(self, tag):
  39. if self.current_element and self.current_element['tag'] == tag:
  40. self.nav_elements.append(self.current_element)
  41. self.current_element = None
  42. # 添加项目根目录到Python路径
  43. project_root = Path(__file__).parent.parent.parent
  44. sys.path.append(str(project_root))
  45. class Colors:
  46. """终端颜色定义"""
  47. RED = '\033[91m'
  48. GREEN = '\033[92m'
  49. YELLOW = '\033[93m'
  50. BLUE = '\033[94m'
  51. MAGENTA = '\033[95m'
  52. CYAN = '\033[96m'
  53. WHITE = '\033[97m'
  54. BOLD = '\033[1m'
  55. UNDERLINE = '\033[4m'
  56. RESET = '\033[0m'
  57. def print_message(color, message):
  58. """打印带颜色的消息"""
  59. print(f"{color}{message}{Colors.RESET}")
  60. def print_success(message):
  61. print_message(Colors.GREEN, f"✓ {message}")
  62. def print_info(message):
  63. print_message(Colors.BLUE, f"ℹ {message}")
  64. def print_warning(message):
  65. print_message(Colors.YELLOW, f"⚠ {message}")
  66. def print_error(message):
  67. print_message(Colors.RED, f"✗ {message}")
  68. class NavigationConsistencyChecker:
  69. """导航一致性检查器"""
  70. def __init__(self):
  71. self.project_root = project_root
  72. self.front_dir = self.project_root / "output_sourcecode" / "front"
  73. self.navigation_issues = []
  74. self.pages_data = {}
  75. def check_all(self):
  76. """执行所有导航一致性检查"""
  77. print_message(Colors.BOLD + Colors.CYAN, "🔍 导航一致性检查工具")
  78. print_message(Colors.CYAN, "=" * 60)
  79. if not self.front_dir.exists():
  80. print_error(f"前端页面目录不存在: {self.front_dir}")
  81. return False
  82. # 获取所有HTML文件
  83. html_files = list(self.front_dir.glob("*.html"))
  84. if not html_files:
  85. print_error("未找到任何HTML页面文件")
  86. return False
  87. print_info(f"找到 {len(html_files)} 个HTML页面文件")
  88. # 解析所有页面
  89. self.parse_all_pages(html_files)
  90. # 执行各项检查
  91. all_passed = True
  92. all_passed &= self.check_header_consistency()
  93. all_passed &= self.check_sidebar_consistency()
  94. all_passed &= self.check_breadcrumb_consistency()
  95. all_passed &= self.check_navigation_links()
  96. all_passed &= self.check_css_class_consistency()
  97. all_passed &= self.check_javascript_consistency()
  98. # 输出检查结果
  99. self.print_summary(all_passed)
  100. return all_passed
  101. def parse_all_pages(self, html_files):
  102. """解析所有页面的导航结构"""
  103. print_info("解析页面导航结构...")
  104. for html_file in html_files:
  105. try:
  106. with open(html_file, 'r', encoding='utf-8') as f:
  107. content = f.read()
  108. # 使用简单的正则表达式和字符串匹配来提取导航结构
  109. page_data = {
  110. 'file': html_file.name,
  111. 'path': str(html_file),
  112. 'header': self.extract_header_structure(content),
  113. 'sidebar': self.extract_sidebar_structure(content),
  114. 'breadcrumb': self.extract_breadcrumb_structure(content),
  115. 'navigation_links': self.extract_navigation_links(content),
  116. 'css_classes': self.extract_navigation_css_classes(content),
  117. 'javascript': self.extract_navigation_javascript(content)
  118. }
  119. self.pages_data[html_file.name] = page_data
  120. except Exception as e:
  121. print_error(f"解析页面失败 {html_file.name}: {e}")
  122. def extract_header_structure(self, soup):
  123. """提取头部导航结构"""
  124. header_selectors = ['header', '.header', '.app-header', '#header', '#app-header']
  125. for selector in header_selectors:
  126. header = soup.select_one(selector)
  127. if header:
  128. return {
  129. 'tag': header.name,
  130. 'classes': header.get('class', []),
  131. 'id': header.get('id', ''),
  132. 'structure': self.get_element_structure(header),
  133. 'menu_items': self.extract_menu_items(header)
  134. }
  135. return None
  136. def extract_sidebar_structure(self, soup):
  137. """提取侧边栏导航结构"""
  138. sidebar_selectors = ['.sidebar', '.app-sidebar', '#sidebar', '#app-sidebar', 'aside']
  139. for selector in sidebar_selectors:
  140. sidebar = soup.select_one(selector)
  141. if sidebar:
  142. return {
  143. 'tag': sidebar.name,
  144. 'classes': sidebar.get('class', []),
  145. 'id': sidebar.get('id', ''),
  146. 'structure': self.get_element_structure(sidebar),
  147. 'menu_items': self.extract_menu_items(sidebar)
  148. }
  149. return None
  150. def extract_breadcrumb_structure(self, soup):
  151. """提取面包屑导航结构"""
  152. breadcrumb_selectors = ['.breadcrumb', '.breadcrumb-nav', '#breadcrumb', 'nav[aria-label*="breadcrumb"]']
  153. for selector in breadcrumb_selectors:
  154. breadcrumb = soup.select_one(selector)
  155. if breadcrumb:
  156. return {
  157. 'tag': breadcrumb.name,
  158. 'classes': breadcrumb.get('class', []),
  159. 'id': breadcrumb.get('id', ''),
  160. 'structure': self.get_element_structure(breadcrumb),
  161. 'items': self.extract_breadcrumb_items(breadcrumb)
  162. }
  163. return None
  164. def extract_navigation_links(self, soup):
  165. """提取导航链接"""
  166. links = []
  167. nav_areas = soup.select('header a, .sidebar a, .breadcrumb a, nav a')
  168. for link in nav_areas:
  169. href = link.get('href', '')
  170. if href and not href.startswith('#') and not href.startswith('http'):
  171. links.append({
  172. 'href': href,
  173. 'text': link.get_text(strip=True),
  174. 'classes': link.get('class', [])
  175. })
  176. return links
  177. def extract_navigation_css_classes(self, soup):
  178. """提取导航相关的CSS类名"""
  179. nav_classes = set()
  180. nav_elements = soup.select('header, header *, .sidebar, .sidebar *, .breadcrumb, .breadcrumb *, nav, nav *')
  181. for element in nav_elements:
  182. classes = element.get('class', [])
  183. nav_classes.update(classes)
  184. return sorted(list(nav_classes))
  185. def extract_navigation_javascript(self, content):
  186. """提取导航相关的JavaScript"""
  187. # 查找导航相关的JavaScript代码
  188. js_patterns = [
  189. r'navigation|Navigation',
  190. r'sidebar|Sidebar',
  191. r'menu|Menu',
  192. r'breadcrumb|Breadcrumb'
  193. ]
  194. js_found = []
  195. for pattern in js_patterns:
  196. matches = re.findall(rf'{pattern}[^;]*', content, re.IGNORECASE)
  197. js_found.extend(matches)
  198. return js_found
  199. def get_element_structure(self, element):
  200. """获取元素的结构特征"""
  201. if not element:
  202. return None
  203. return {
  204. 'tag': element.name,
  205. 'classes': element.get('class', []),
  206. 'id': element.get('id', ''),
  207. 'children_count': len(element.find_all()),
  208. 'children_tags': [child.name for child in element.children if hasattr(child, 'name') and child.name]
  209. }
  210. def extract_menu_items(self, nav_element):
  211. """提取菜单项"""
  212. if not nav_element:
  213. return []
  214. menu_items = []
  215. links = nav_element.find_all('a')
  216. for link in links:
  217. menu_items.append({
  218. 'href': link.get('href', ''),
  219. 'text': link.get_text(strip=True),
  220. 'classes': link.get('class', [])
  221. })
  222. return menu_items
  223. def extract_breadcrumb_items(self, breadcrumb_element):
  224. """提取面包屑项"""
  225. if not breadcrumb_element:
  226. return []
  227. items = []
  228. links = breadcrumb_element.find_all(['a', 'span', 'li'])
  229. for item in links:
  230. items.append({
  231. 'tag': item.name,
  232. 'href': item.get('href', '') if item.name == 'a' else '',
  233. 'text': item.get_text(strip=True),
  234. 'classes': item.get('class', [])
  235. })
  236. return items
  237. def check_header_consistency(self):
  238. """检查头部导航一致性"""
  239. print_info("检查头部导航一致性...")
  240. headers = [page['header'] for page in self.pages_data.values() if page['header']]
  241. if not headers:
  242. print_error("未找到任何头部导航结构")
  243. return False
  244. # 检查结构一致性
  245. reference_header = headers[0]
  246. issues = []
  247. for page_name, page_data in self.pages_data.items():
  248. header = page_data['header']
  249. if not header:
  250. issues.append(f"{page_name}: 缺少头部导航")
  251. continue
  252. # 检查基本结构
  253. if header['tag'] != reference_header['tag']:
  254. issues.append(f"{page_name}: 头部标签不一致 ({header['tag']} vs {reference_header['tag']})")
  255. if set(header['classes']) != set(reference_header['classes']):
  256. issues.append(f"{page_name}: 头部CSS类不一致")
  257. # 检查菜单项数量
  258. if len(header['menu_items']) != len(reference_header['menu_items']):
  259. issues.append(f"{page_name}: 头部菜单项数量不一致")
  260. if issues:
  261. for issue in issues:
  262. print_error(f" {issue}")
  263. return False
  264. else:
  265. print_success("头部导航结构一致")
  266. return True
  267. def check_sidebar_consistency(self):
  268. """检查侧边栏一致性"""
  269. print_info("检查侧边栏导航一致性...")
  270. sidebars = [page['sidebar'] for page in self.pages_data.values() if page['sidebar']]
  271. if not sidebars:
  272. print_warning("未找到侧边栏导航结构")
  273. return True # 如果没有侧边栏,不算错误
  274. # 检查结构一致性
  275. reference_sidebar = sidebars[0]
  276. issues = []
  277. for page_name, page_data in self.pages_data.items():
  278. sidebar = page_data['sidebar']
  279. if not sidebar and sidebars:
  280. issues.append(f"{page_name}: 缺少侧边栏导航")
  281. continue
  282. if sidebar:
  283. # 检查基本结构
  284. if sidebar['tag'] != reference_sidebar['tag']:
  285. issues.append(f"{page_name}: 侧边栏标签不一致")
  286. if set(sidebar['classes']) != set(reference_sidebar['classes']):
  287. issues.append(f"{page_name}: 侧边栏CSS类不一致")
  288. if issues:
  289. for issue in issues:
  290. print_error(f" {issue}")
  291. return False
  292. else:
  293. print_success("侧边栏导航结构一致")
  294. return True
  295. def check_breadcrumb_consistency(self):
  296. """检查面包屑一致性"""
  297. print_info("检查面包屑导航一致性...")
  298. breadcrumbs = [page['breadcrumb'] for page in self.pages_data.values() if page['breadcrumb']]
  299. if not breadcrumbs:
  300. print_warning("未找到面包屑导航结构")
  301. return True # 如果没有面包屑,不算错误
  302. # 检查结构一致性
  303. reference_breadcrumb = breadcrumbs[0]
  304. issues = []
  305. for page_name, page_data in self.pages_data.items():
  306. breadcrumb = page_data['breadcrumb']
  307. if not breadcrumb and breadcrumbs:
  308. issues.append(f"{page_name}: 缺少面包屑导航")
  309. continue
  310. if breadcrumb:
  311. # 检查基本结构
  312. if breadcrumb['tag'] != reference_breadcrumb['tag']:
  313. issues.append(f"{page_name}: 面包屑标签不一致")
  314. if set(breadcrumb['classes']) != set(reference_breadcrumb['classes']):
  315. issues.append(f"{page_name}: 面包屑CSS类不一致")
  316. if issues:
  317. for issue in issues:
  318. print_error(f" {issue}")
  319. return False
  320. else:
  321. print_success("面包屑导航结构一致")
  322. return True
  323. def check_navigation_links(self):
  324. """检查导航链接有效性"""
  325. print_info("检查导航链接有效性...")
  326. issues = []
  327. all_files = set(page_data['file'] for page_data in self.pages_data.values())
  328. for page_name, page_data in self.pages_data.items():
  329. for link in page_data['navigation_links']:
  330. href = link['href']
  331. if href.endswith('.html'):
  332. # 检查相对路径
  333. target_file = href.split('/')[-1] # 获取文件名
  334. if target_file not in all_files:
  335. issues.append(f"{page_name}: 导航链接指向不存在的文件 '{href}'")
  336. if issues:
  337. for issue in issues:
  338. print_error(f" {issue}")
  339. return False
  340. else:
  341. print_success("所有导航链接有效")
  342. return True
  343. def check_css_class_consistency(self):
  344. """检查CSS类名一致性"""
  345. print_info("检查导航CSS类名一致性...")
  346. # 收集所有页面的导航CSS类
  347. all_nav_classes = set()
  348. for page_data in self.pages_data.values():
  349. all_nav_classes.update(page_data['css_classes'])
  350. # 检查每个页面是否包含核心导航类
  351. core_nav_classes = {
  352. 'header', 'app-header', 'nav', 'navigation',
  353. 'sidebar', 'app-sidebar', 'menu',
  354. 'breadcrumb', 'breadcrumb-nav'
  355. }
  356. issues = []
  357. for page_name, page_data in self.pages_data.items():
  358. page_classes = set(page_data['css_classes'])
  359. missing_core_classes = core_nav_classes - page_classes
  360. # 如果页面缺少太多核心导航类,可能存在问题
  361. if len(missing_core_classes) > len(core_nav_classes) * 0.7:
  362. issues.append(f"{page_name}: 可能缺少导航相关的CSS类")
  363. if issues:
  364. for issue in issues:
  365. print_warning(f" {issue}")
  366. return True # 这是警告,不是错误
  367. else:
  368. print_success("导航CSS类名使用合理")
  369. return True
  370. def check_javascript_consistency(self):
  371. """检查JavaScript一致性"""
  372. print_info("检查导航JavaScript一致性...")
  373. # 这是一个简化的检查,主要确保页面包含导航相关的JavaScript
  374. issues = []
  375. for page_name, page_data in self.pages_data.items():
  376. js_code = page_data['javascript']
  377. if not js_code:
  378. issues.append(f"{page_name}: 可能缺少导航相关的JavaScript代码")
  379. if issues:
  380. for issue in issues:
  381. print_warning(f" {issue}")
  382. return True # 这是警告,不是错误
  383. else:
  384. print_success("导航JavaScript代码存在")
  385. return True
  386. def print_summary(self, all_passed):
  387. """打印检查结果摘要"""
  388. print_message(Colors.CYAN, "=" * 60)
  389. if all_passed:
  390. print_success("🎉 所有导航一致性检查通过!")
  391. print_info("页面导航结构完全一致,符合软著申请质量要求")
  392. else:
  393. print_error("❌ 发现导航一致性问题")
  394. print_warning("请修复上述问题以确保导航的一致性")
  395. print_info("建议:")
  396. print(" 1. 确保所有页面使用相同的导航组件模板")
  397. print(" 2. 检查CSS类名和ID的一致性")
  398. print(" 3. 验证所有导航链接的有效性")
  399. print(" 4. 确认当前页面在导航中的正确标识")
  400. print_message(Colors.CYAN, "=" * 60)
  401. def main():
  402. """主函数"""
  403. try:
  404. checker = NavigationConsistencyChecker()
  405. success = checker.check_all()
  406. # 返回适当的退出码
  407. sys.exit(0 if success else 1)
  408. except Exception as e:
  409. print_error(f"检查过程中发生错误: {e}")
  410. import traceback
  411. traceback.print_exc()
  412. sys.exit(1)
  413. if __name__ == "__main__":
  414. main()