1) 上传结果行:「复制全部 / 清除」原来绝对定位盖在右上角,压住 Markdown with link /
Thumbnail url 两个标签,且只在 hover 时出现(手机没有 hover,等于点不到)。
改为与标签栏同一行的左右两段:标签栏 flex-1 自行横向滚动,按钮固定右侧、常显。
(div 开闭数已核对:与原始一致,只多一层包装)
2) 侧栏品牌块去掉 sm:justify-center → 桌面端 logo + 站点名靠左(手机端本就不居中,无影响)
3) 文案:「设置」→「用户设置」(侧栏导航项、页面大标题「基础设置」、顶栏标题、头像菜单)
4) 桌面端侧栏可折叠(手机端抽屉行为完全不变):
- 折叠状态 = <html class="sidebar-collapsed">,由 stores/sidebar.js 管理并记入 localStorage;
app.blade.php 的首屏内联脚本在样式之前就贴类,不会「先展开再收起」地抖一下
- 折叠宽度 4rem 只留图标;文字、分组标题、容量区整体隐藏;导航项居中;每项带 title 便于悬停辨认
- 折叠规则全部在 @media (min-width: 640px) 内,并用选择器权重(html.sidebar-collapsed #app-sidebar)
压过工具类 sm:w-64 / sm:ml-64 —— 不依赖打包顺序(components 层总在 utilities 之前)
- 顶栏 ☰ 桌面端也显示:桌面=折叠/展开、手机=抽屉,同一语义;toggleSmart() 按视口分流
- 我的图片页:图片墙按容器宽度算布局,侧栏一动必须重排 —— 监听 lsky:sidebar-toggled,
等 300ms 宽度动画结束后 justifiedGallery('norewind') + viewer.update() + ds.setSelectables()
(这三个 API 在翻页加载数据时本来就在用)
- 不做「悬停临时展开」:那会让图片墙每次鼠标划过都重排
验证:
- 新增 test/sidebar.test.mjs(6 例,jsdom):默认展开 / 存过 collapsed 则加载即贴类 /
折叠写存储并派发事件 / 展开还原 / 手机端只开抽屉不动折叠态 / 桌面端走折叠。
与既有 70 例一起由 npm test 串行执行(CI 那步就是 npm test)
- 用真编译产物 + 真实 DOM 结构在 Chromium 里量计算样式:
桌面折叠 → 侧栏 64px、内容左边距 64px、文字/分组/容量隐藏、图标居中偏差 0px、按钮与标签零重叠;
桌面展开 → 256px / 256px;手机端即使贴了折叠类 → 侧栏仍 75% 抽屉、无隐藏、边距 0(确认零影响)
- php -l 全仓库通过;php artisan view:cache 全量编译通过;verify_repo.py 通过;
context-js.js 两份仍字节一致(e114c840…,长按与点击防护未动)
111 lines
4.6 KiB
JavaScript
111 lines
4.6 KiB
JavaScript
/*
|
||
* 侧栏 store 的行为测试(jsdom)
|
||
*
|
||
* 用途:折叠状态有三个必须成立的契约,错了就是"刷新后抖一下"或"手机上侧栏没了":
|
||
* 1. 首屏脚本的契约:localStorage 里存着 collapsed 时,模块加载就要把类贴到 <html> 上
|
||
* 2. 折叠 = 加/去 <html class="sidebar-collapsed"> + 写 localStorage + 派发 lsky:sidebar-toggled
|
||
* 3. 手机端(<640px)点同一个按钮只能开关抽屉,绝不能把桌面折叠状态改掉
|
||
*
|
||
* 运行:npm test(或 node sidebar.test.mjs)
|
||
*/
|
||
import path from 'node:path';
|
||
import { JSDOM } from 'jsdom';
|
||
|
||
const here = path.dirname(decodeURIComponent(new URL(import.meta.url).pathname));
|
||
const STORE = path.join(here, '..', 'src', 'resources', 'js', 'stores', 'sidebar.js');
|
||
|
||
const results = [];
|
||
const check = (name, pass, detail = '') => {
|
||
results.push({ name, pass, detail });
|
||
console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail ? ' → ' + detail : ''}`);
|
||
};
|
||
|
||
// 每个场景一个全新的模块实例(import 加 query 破缓存)+ 全新的 jsdom
|
||
let seq = 0;
|
||
async function boot({ stored = null, wide = true } = {}) {
|
||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { url: 'https://lsky.test/' });
|
||
dom.window.matchMedia = (query) => ({
|
||
matches: wide && query.includes('640px'),
|
||
media: query,
|
||
addEventListener() {}, removeEventListener() {},
|
||
});
|
||
|
||
globalThis.window = dom.window;
|
||
globalThis.document = dom.window.document;
|
||
globalThis.localStorage = dom.window.localStorage;
|
||
globalThis.CustomEvent = dom.window.CustomEvent;
|
||
|
||
if (stored !== null) {
|
||
dom.window.localStorage.setItem('lsky-sidebar', stored);
|
||
}
|
||
|
||
const events = [];
|
||
dom.window.addEventListener('lsky:sidebar-toggled', (e) => events.push(e.detail));
|
||
|
||
const mod = await import(`${new URL('file://' + STORE).href}?v=${++seq}`);
|
||
return { store: mod.default, dom, events };
|
||
}
|
||
|
||
const hasClass = (dom) => dom.window.document.documentElement.classList.contains('sidebar-collapsed');
|
||
|
||
// 1. 默认:没存过 → 展开,不加类
|
||
{
|
||
const { store, dom } = await boot();
|
||
check('默认展开:collapsed=false 且 <html> 没有折叠类', store.collapsed === false && ! hasClass(dom),
|
||
`collapsed=${store.collapsed} class=${hasClass(dom)}`);
|
||
}
|
||
|
||
// 2. 首屏契约:存了 collapsed → 模块加载即贴类(app.blade.php 首屏脚本之外的第二道保险)
|
||
{
|
||
const { store, dom } = await boot({ stored: 'collapsed' });
|
||
check('存过 collapsed:加载即 collapsed=true 且类已贴上', store.collapsed === true && hasClass(dom),
|
||
`collapsed=${store.collapsed} class=${hasClass(dom)}`);
|
||
}
|
||
|
||
// 3. 折叠:加类 + 写存储 + 派发事件
|
||
{
|
||
const { store, dom, events } = await boot();
|
||
store.toggleCollapsed();
|
||
check('折叠:加类、写 localStorage、派发事件',
|
||
hasClass(dom)
|
||
&& dom.window.localStorage.getItem('lsky-sidebar') === 'collapsed'
|
||
&& events.length === 1 && events[0].collapsed === true,
|
||
`class=${hasClass(dom)} stored=${dom.window.localStorage.getItem('lsky-sidebar')} events=${JSON.stringify(events)}`);
|
||
}
|
||
|
||
// 4. 再点一次:还原
|
||
{
|
||
const { store, dom } = await boot({ stored: 'collapsed' });
|
||
store.toggleCollapsed();
|
||
check('展开:去类、存储改为 expanded',
|
||
! hasClass(dom) && dom.window.localStorage.getItem('lsky-sidebar') === 'expanded',
|
||
`class=${hasClass(dom)} stored=${dom.window.localStorage.getItem('lsky-sidebar')}`);
|
||
}
|
||
|
||
// 5. 手机端(<640px):按钮走抽屉,不能碰桌面折叠状态 —— 这是最容易被写错的一条
|
||
{
|
||
const { store, dom, events } = await boot({ wide: false });
|
||
store.toggleSmart();
|
||
check('手机端点按钮:只开抽屉,不动折叠状态',
|
||
store.open === true && store.collapsed === false && ! hasClass(dom) && events.length === 0,
|
||
`open=${store.open} collapsed=${store.collapsed} class=${hasClass(dom)}`);
|
||
}
|
||
|
||
// 6. 桌面端(>=640px):同一个按钮走折叠
|
||
{
|
||
const { store } = await boot({ wide: true });
|
||
store.toggleSmart();
|
||
check('桌面端点按钮:走折叠(open 不受影响)',
|
||
store.collapsed === true && store.open === false,
|
||
`collapsed=${store.collapsed} open=${store.open}`);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 汇总
|
||
const failed = results.filter((r) => !r.pass);
|
||
console.log(`\n${results.length - failed.length}/${results.length} 通过`);
|
||
if (failed.length) {
|
||
console.log('\n失败项:');
|
||
failed.forEach((f) => console.log(` - ${f.name}${f.detail ? ' → ' + f.detail : ''}`));
|
||
}
|
||
process.exit(failed.length ? 1 : 0);
|