apply-list-toolbar-selected-to-pager.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. #!/usr/bin/env python3
  2. """Batch apply 运输计划 toolbar pattern across list pages:
  3. 1) Move「已选」from list-toolbar to list-pager-bar (left)
  4. 2) Ensure ListToolbarDateShortcuts uses variant=\"select\"
  5. """
  6. from __future__ import annotations
  7. import re
  8. import sys
  9. from pathlib import Path
  10. ROOT = Path(__file__).resolve().parents[1] / "web-admin" / "src"
  11. SKIP_PARTS = {
  12. "components/bx/template/ListToolbar.vue", # handled separately
  13. "components/core/list-toolbar-date-shortcuts/index.vue",
  14. }
  15. # Dialog / selector toolbars are not top-level menu pages — still move selected if pager exists
  16. SELECTED_RE = re.compile(
  17. r"[ \t]*(?:<div|<span)\s+class=\"list-toolbar-selected\"[^>]*>\s*"
  18. r"已选\s*<span class=\"sel-num\">[\s\S]*?</span>\s*(?:项|条)\s*"
  19. r"</(?:div|span)>\s*\n?",
  20. re.MULTILINE,
  21. )
  22. PAGER_OPEN_RE = re.compile(
  23. r'(<(?:div|section)\s+class="[^"]*(?:pagination\s+list-pager-bar|list-pager-bar|list-pager)[^"]*"[^>]*>)',
  24. )
  25. LIST_PAGER_SELECTED_RE = re.compile(r'class="[^"]*list-pager-selected')
  26. def extract_selected_blocks(text: str) -> tuple[str, list[str]]:
  27. blocks = []
  28. def _keep(m: re.Match[str]) -> str:
  29. blocks.append(m.group(0).strip())
  30. return ""
  31. new_text = SELECTED_RE.sub(_keep, text)
  32. return new_text, blocks
  33. def selected_to_pager_html(block: str) -> str:
  34. # Normalize to list-pager-selected div
  35. inner = re.search(
  36. r"已选\s*<span class=\"sel-num\">([\s\S]*?)</span>\s*(项|条)",
  37. block,
  38. )
  39. if not inner:
  40. return ""
  41. expr = inner.group(1).strip()
  42. unit = inner.group(2)
  43. return (
  44. f' <div class="list-pager-selected">\n'
  45. f' 已选 <span class="sel-num">{expr}</span> {unit}\n'
  46. f" </div>\n"
  47. )
  48. def inject_into_pagers(text: str, selected_htmls: list[str]) -> tuple[str, int]:
  49. if not selected_htmls:
  50. return text, 0
  51. # Already has pager selected somewhere — just keep removals (caller already removed toolbar blocks)
  52. if LIST_PAGER_SELECTED_RE.search(text):
  53. return text, len(selected_htmls)
  54. idx = 0
  55. injected = 0
  56. def _repl(m: re.Match[str]) -> str:
  57. nonlocal idx, injected
  58. open_tag = m.group(1)
  59. html = selected_htmls[min(idx, len(selected_htmls) - 1)]
  60. idx += 1
  61. injected += 1
  62. return open_tag + "\n" + html
  63. new_text = PAGER_OPEN_RE.sub(_repl, text, count=len(selected_htmls))
  64. return new_text, injected
  65. def ensure_date_select_variant_safe(text: str) -> tuple[str, int]:
  66. changed = 0
  67. pattern = re.compile(r"<ListToolbarDateShortcuts\b([^>]*)(/?)>", re.DOTALL)
  68. def repl(m: re.Match[str]) -> str:
  69. nonlocal changed
  70. attrs, slash = m.group(1), m.group(2)
  71. if 'variant="select"' in attrs or "variant='select'" in attrs:
  72. return m.group(0)
  73. if re.search(r"variant\s*=", attrs):
  74. attrs2 = re.sub(r'''variant\s*=\s*["'][^"']*["']''', 'variant="select"', attrs)
  75. changed += 1
  76. return f"<ListToolbarDateShortcuts{attrs2}{slash}>"
  77. changed += 1
  78. return f'<ListToolbarDateShortcuts\n variant="select"{attrs}{slash}>'
  79. return pattern.sub(repl, text), changed
  80. def process_file(path: Path) -> list[str]:
  81. rel = str(path.relative_to(ROOT)).replace("\\", "/")
  82. if rel in SKIP_PARTS:
  83. return []
  84. text = path.read_text(encoding="utf-8")
  85. orig = text
  86. notes: list[str] = []
  87. text_wo, blocks = extract_selected_blocks(text)
  88. if blocks:
  89. selected_htmls = [h for h in (selected_to_pager_html(b) for b in blocks) if h]
  90. has_pager = bool(
  91. re.search(r"list-pager-bar|class=\"[^\"]*list-pager[^\"]*\"", text)
  92. )
  93. already_pager_selected = bool(LIST_PAGER_SELECTED_RE.search(text_wo))
  94. if already_pager_selected:
  95. text = text_wo
  96. notes.append(f"removed-toolbar-selected×{len(blocks)}")
  97. elif has_pager:
  98. text2, injected = inject_into_pagers(text_wo, selected_htmls)
  99. if injected:
  100. text = text2
  101. notes.append(f"moved-selected×{injected}")
  102. else:
  103. text = orig
  104. notes.append("selected-move-FAILED")
  105. else:
  106. # Tree / non-pager list: append a footer bar once before closing panel section
  107. footer = (
  108. '\n <div class="pagination list-pager-bar">\n'
  109. + selected_htmls[0]
  110. + " </div>\n"
  111. )
  112. # Prefer insert after list-body/list-wrap block
  113. m = re.search(
  114. r'(</div>\s*\n)(\s*</section>)',
  115. text_wo,
  116. )
  117. if m and selected_htmls:
  118. text = text_wo[: m.start(1)] + m.group(1) + footer + m.group(2) + text_wo[m.end() :]
  119. notes.append("selected-footer-no-pager")
  120. else:
  121. text = orig
  122. notes.append("selected-skip-no-pager")
  123. text, n_var = ensure_date_select_variant_safe(text)
  124. if n_var:
  125. notes.append(f"date-select×{n_var}")
  126. if text != orig:
  127. path.write_text(text, encoding="utf-8")
  128. return notes
  129. def main() -> int:
  130. targets = []
  131. for p in ROOT.rglob("*.vue"):
  132. rel = str(p.relative_to(ROOT)).replace("\\", "/")
  133. if "node_modules" in rel:
  134. continue
  135. t = p.read_text(encoding="utf-8")
  136. if "list-toolbar-selected" in t or "ListToolbarDateShortcuts" in t:
  137. targets.append(p)
  138. report = []
  139. for p in sorted(targets):
  140. notes = process_file(p)
  141. if notes:
  142. report.append(f"{p.relative_to(ROOT)}: {', '.join(notes)}")
  143. print(f"processed candidates: {len(targets)}")
  144. print(f"changed: {len(report)}")
  145. for line in report:
  146. print(line)
  147. return 0
  148. if __name__ == "__main__":
  149. sys.exit(main())