Files
mole404 bc3751b394 feat: 侧栏可折叠 + 上传结果按钮不再压标签 + 文案与对齐调整
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…,长按与点击防护未动)
2026-09-29 02:00:53 +08:00

111 lines
4.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 侧栏 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);