Bundle pending backend/web-admin feature updates, data-migration scripts, and related docs so the V1.5 branch on the Tencent Cloud Gogs matches the current workspace (excluding local secrets). Co-authored-by: Cursor <cursoragent@cursor.com>
183 lines
5.8 KiB
Python
183 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
||
"""Batch apply 运输计划 toolbar pattern across list pages:
|
||
1) Move「已选」from list-toolbar to list-pager-bar (left)
|
||
2) Ensure ListToolbarDateShortcuts uses variant=\"select\"
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[1] / "web-admin" / "src"
|
||
|
||
SKIP_PARTS = {
|
||
"components/bx/template/ListToolbar.vue", # handled separately
|
||
"components/core/list-toolbar-date-shortcuts/index.vue",
|
||
}
|
||
|
||
# Dialog / selector toolbars are not top-level menu pages — still move selected if pager exists
|
||
SELECTED_RE = re.compile(
|
||
r"[ \t]*(?:<div|<span)\s+class=\"list-toolbar-selected\"[^>]*>\s*"
|
||
r"已选\s*<span class=\"sel-num\">[\s\S]*?</span>\s*(?:项|条)\s*"
|
||
r"</(?:div|span)>\s*\n?",
|
||
re.MULTILINE,
|
||
)
|
||
|
||
PAGER_OPEN_RE = re.compile(
|
||
r'(<(?:div|section)\s+class="[^"]*(?:pagination\s+list-pager-bar|list-pager-bar|list-pager)[^"]*"[^>]*>)',
|
||
)
|
||
|
||
LIST_PAGER_SELECTED_RE = re.compile(r'class="[^"]*list-pager-selected')
|
||
|
||
|
||
def extract_selected_blocks(text: str) -> tuple[str, list[str]]:
|
||
blocks = []
|
||
|
||
def _keep(m: re.Match[str]) -> str:
|
||
blocks.append(m.group(0).strip())
|
||
return ""
|
||
|
||
new_text = SELECTED_RE.sub(_keep, text)
|
||
return new_text, blocks
|
||
|
||
|
||
def selected_to_pager_html(block: str) -> str:
|
||
# Normalize to list-pager-selected div
|
||
inner = re.search(
|
||
r"已选\s*<span class=\"sel-num\">([\s\S]*?)</span>\s*(项|条)",
|
||
block,
|
||
)
|
||
if not inner:
|
||
return ""
|
||
expr = inner.group(1).strip()
|
||
unit = inner.group(2)
|
||
return (
|
||
f' <div class="list-pager-selected">\n'
|
||
f' 已选 <span class="sel-num">{expr}</span> {unit}\n'
|
||
f" </div>\n"
|
||
)
|
||
|
||
|
||
def inject_into_pagers(text: str, selected_htmls: list[str]) -> tuple[str, int]:
|
||
if not selected_htmls:
|
||
return text, 0
|
||
# Already has pager selected somewhere — just keep removals (caller already removed toolbar blocks)
|
||
if LIST_PAGER_SELECTED_RE.search(text):
|
||
return text, len(selected_htmls)
|
||
|
||
idx = 0
|
||
injected = 0
|
||
|
||
def _repl(m: re.Match[str]) -> str:
|
||
nonlocal idx, injected
|
||
open_tag = m.group(1)
|
||
html = selected_htmls[min(idx, len(selected_htmls) - 1)]
|
||
idx += 1
|
||
injected += 1
|
||
return open_tag + "\n" + html
|
||
|
||
new_text = PAGER_OPEN_RE.sub(_repl, text, count=len(selected_htmls))
|
||
return new_text, injected
|
||
|
||
|
||
def ensure_date_select_variant_safe(text: str) -> tuple[str, int]:
|
||
changed = 0
|
||
pattern = re.compile(r"<ListToolbarDateShortcuts\b([^>]*)(/?)>", re.DOTALL)
|
||
|
||
def repl(m: re.Match[str]) -> str:
|
||
nonlocal changed
|
||
attrs, slash = m.group(1), m.group(2)
|
||
if 'variant="select"' in attrs or "variant='select'" in attrs:
|
||
return m.group(0)
|
||
if re.search(r"variant\s*=", attrs):
|
||
attrs2 = re.sub(r'''variant\s*=\s*["'][^"']*["']''', 'variant="select"', attrs)
|
||
changed += 1
|
||
return f"<ListToolbarDateShortcuts{attrs2}{slash}>"
|
||
changed += 1
|
||
return f'<ListToolbarDateShortcuts\n variant="select"{attrs}{slash}>'
|
||
|
||
return pattern.sub(repl, text), changed
|
||
|
||
|
||
def process_file(path: Path) -> list[str]:
|
||
rel = str(path.relative_to(ROOT)).replace("\\", "/")
|
||
if rel in SKIP_PARTS:
|
||
return []
|
||
text = path.read_text(encoding="utf-8")
|
||
orig = text
|
||
notes: list[str] = []
|
||
|
||
text_wo, blocks = extract_selected_blocks(text)
|
||
if blocks:
|
||
selected_htmls = [h for h in (selected_to_pager_html(b) for b in blocks) if h]
|
||
has_pager = bool(
|
||
re.search(r"list-pager-bar|class=\"[^\"]*list-pager[^\"]*\"", text)
|
||
)
|
||
already_pager_selected = bool(LIST_PAGER_SELECTED_RE.search(text_wo))
|
||
|
||
if already_pager_selected:
|
||
text = text_wo
|
||
notes.append(f"removed-toolbar-selected×{len(blocks)}")
|
||
elif has_pager:
|
||
text2, injected = inject_into_pagers(text_wo, selected_htmls)
|
||
if injected:
|
||
text = text2
|
||
notes.append(f"moved-selected×{injected}")
|
||
else:
|
||
text = orig
|
||
notes.append("selected-move-FAILED")
|
||
else:
|
||
# Tree / non-pager list: append a footer bar once before closing panel section
|
||
footer = (
|
||
'\n <div class="pagination list-pager-bar">\n'
|
||
+ selected_htmls[0]
|
||
+ " </div>\n"
|
||
)
|
||
# Prefer insert after list-body/list-wrap block
|
||
m = re.search(
|
||
r'(</div>\s*\n)(\s*</section>)',
|
||
text_wo,
|
||
)
|
||
if m and selected_htmls:
|
||
text = text_wo[: m.start(1)] + m.group(1) + footer + m.group(2) + text_wo[m.end() :]
|
||
notes.append("selected-footer-no-pager")
|
||
else:
|
||
text = orig
|
||
notes.append("selected-skip-no-pager")
|
||
|
||
text, n_var = ensure_date_select_variant_safe(text)
|
||
if n_var:
|
||
notes.append(f"date-select×{n_var}")
|
||
|
||
if text != orig:
|
||
path.write_text(text, encoding="utf-8")
|
||
return notes
|
||
|
||
|
||
def main() -> int:
|
||
targets = []
|
||
for p in ROOT.rglob("*.vue"):
|
||
rel = str(p.relative_to(ROOT)).replace("\\", "/")
|
||
if "node_modules" in rel:
|
||
continue
|
||
t = p.read_text(encoding="utf-8")
|
||
if "list-toolbar-selected" in t or "ListToolbarDateShortcuts" in t:
|
||
targets.append(p)
|
||
|
||
report = []
|
||
for p in sorted(targets):
|
||
notes = process_file(p)
|
||
if notes:
|
||
report.append(f"{p.relative_to(ROOT)}: {', '.join(notes)}")
|
||
|
||
print(f"processed candidates: {len(targets)}")
|
||
print(f"changed: {len(report)}")
|
||
for line in report:
|
||
print(line)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|