chore: sync local PL/CRM/BX and migration work to new Gogs host

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>
This commit is contained in:
zlwuandCursor committed 2026-08-01 09:19:21 +08:00
1 parent 5a9abfdd5d
commit 10c3c5470c
269 files changed
+1035435 -92885

No files matched your search

@@ -0,0 +1,182 @@
#!/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())