feat(plugins): 4 个业务插件代码 + 前台公开页面改造

plugins/navManager:导航 CRUD + 导航分类 CRUD(独立 blueprint)
plugins/friendManager:友情链接 CRUD
plugins/aboutManager:关于本站 CRUD
plugins/siteStats:访问统计主页 + JSON 接口 + /site/* GET 埋点 + nav 卡片点击埋点

applications/view/public/nav.py:导航聚合页 / 分类详情 / JSON 接口,支持 visit_count 注入
applications/view/public/about.py:关于本站公开页
applications/view/public/friend.py:友情链接公开页

templates/public/base.html:响应式首页(搜索框 / 分类抽屉 / 5 主题切换 / footer 隐私小字)
templates/public/index.html / category.html:卡片右下角访问次数
templates/public/about.html / friend.html:关于 + 友链前台页

docs/plugins-development.md:插件开发完整指南(生命周期 + 4 个示例 + FAQ + framework 迁移)

scripts/:探针与端到端验证脚本(probe_*/verify_*/test_*)
This commit is contained in:
bwstudio
2026-09-06 18:18:06 +08:00
parent 1ea69c2c34
commit 0ffc103e46
53 changed files with 4705 additions and 89 deletions
+75
View File
@@ -0,0 +1,75 @@
"""A+E 主题自适应验证(每个场景前清 localStorage"""
from playwright.sync_api import sync_playwright
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
ctx = browser.new_context(viewport={'width':1440,'height':900})
page = ctx.new_page()
msgs = []
page.on('pageerror', lambda e: msgs.append(f'[ERR] {e}'))
# 场景 1saved=auto + system=light → 应 default
page.goto('http://127.0.0.1:5000/', wait_until='networkidle')
page.evaluate('localStorage.setItem("bw-theme", "auto")')
page.reload(wait_until='networkidle')
page.wait_for_timeout(500)
theme = page.evaluate('document.documentElement.getAttribute("data-theme")')
label = page.locator('#theme-toggle-label').inner_text()
sys_dark = page.evaluate('window.matchMedia("(prefers-color-scheme: dark)").matches')
print(f'[1] saved=auto, system_dark={sys_dark} -> data-theme={theme!r}, label={label!r} (expect default, "跟随系统")')
# 场景 2saved=dark + system=light → 应 dark
page.evaluate('localStorage.setItem("bw-theme", "dark")')
page.reload(wait_until='networkidle')
page.wait_for_timeout(500)
theme = page.evaluate('document.documentElement.getAttribute("data-theme")')
label = page.locator('#theme-toggle-label').inner_text()
print(f'[2] saved=dark, system_dark={sys_dark} -> data-theme={theme!r}, label={label!r} (expect dark, "暗色")')
# 场景 3saved=default → 应 default
page.evaluate('localStorage.setItem("bw-theme", "default")')
page.reload(wait_until='networkidle')
page.wait_for_timeout(500)
theme = page.evaluate('document.documentElement.getAttribute("data-theme")')
label = page.locator('#theme-toggle-label').inner_text()
print(f'[3] saved=default -> data-theme={theme!r}, label={label!r} (expect default, "蓝绿")')
# 场景 4:无 localStorage + system=light → 应 default(不持久化为 auto
page.evaluate('localStorage.removeItem("bw-theme")')
page.reload(wait_until='networkidle')
page.wait_for_timeout(500)
theme = page.evaluate('document.documentElement.getAttribute("data-theme")')
label = page.locator('#theme-toggle-label').inner_text()
saved_after = page.evaluate('localStorage.getItem("bw-theme")')
print(f'[4] no saved, system_dark={sys_dark} -> data-theme={theme!r}, label={label!r}, after-save={saved_after!r}')
print(f' (system=light 时应 default,不写 auto;只有 system=dark 才写 auto)')
# 场景 5:从 sunset 点 auto → 应 default + label="跟随系统" + saved=auto
page.evaluate('localStorage.setItem("bw-theme", "sunset")')
page.reload(wait_until='networkidle')
page.wait_for_timeout(500)
page.click('#theme-toggle')
page.wait_for_timeout(300)
page.click('.theme-menu li[data-theme="auto"]')
page.wait_for_timeout(500)
theme = page.evaluate('document.documentElement.getAttribute("data-theme")')
label = page.locator('#theme-toggle-label').inner_text()
saved = page.evaluate('localStorage.getItem("bw-theme")')
print(f'[5] click auto -> data-theme={theme!r}, label={label!r}, saved={saved!r} (expect default, "跟随系统", auto)')
# 场景 6:早期脚本无闪烁(head 检查 data-theme 在脚本运行前已设)
page.evaluate('localStorage.setItem("bw-theme", "dark")')
early_attr = page.evaluate('''
() => {
return new Promise(resolve => {
const iframe = document.createElement('iframe');
// 没法直接测 early-script 在主页面执行前的状态,改用:再次模拟 reload 看 meta
resolve(null);
});
}
''')
print()
print('JS errors:', msgs[-5:])
browser.close()