From 2494f0c0cd6a377ca9cf2b4bc6c785660fdc28c8 Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:09:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reader-ui-spec.md | 1 + web/src/pages/reader/ReaderComic.tsx | 47 +++++++++++++++++--- web/src/pages/reader/ReaderViewPage.tsx | 44 ++++++++++++++++-- web/src/stores/readerSettings.ts | 39 +++++++++++++++- web/src/utils/comicImageFit.test.ts | 55 +++++++++++++++++++++++ web/src/utils/comicImageFit.ts | 59 +++++++++++++++++++++++++ 6 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 web/src/utils/comicImageFit.test.ts create mode 100644 web/src/utils/comicImageFit.ts diff --git a/docs/reader-ui-spec.md b/docs/reader-ui-spec.md index fb68601..f963950 100644 --- a/docs/reader-ui-spec.md +++ b/docs/reader-ui-spec.md @@ -97,6 +97,7 @@ - 点击分区:翻页模式渲染一层 `absolute inset-0` 的三分区按钮(左 30% 上一页 / 中 40% 呼出菜单 / 右 30% 下一页);**滚动模式不能渲染这层覆盖层**——它不是滚动容器的子节点,手机上手指落在覆盖层按钮上时浏览器找不到可滚动的祖先,纵向滑动完全失效、只剩点左右能翻屏。滚动模式改为在滚动容器自身的 `onClick` 上按 x 坐标分区(`handleZoneTap`),手指拖动不会产生 click,原生纵向滚动照常。 - 滚动模式的滚轮(`useSmoothWheelScroll`):把滚轮格数累加成一个目标位置,再逐帧向它逼近,滚动连续、松手后自己滑行一段,仿手机上下滑动,而不是浏览器整格跳变。只在文本/漫画的上下滚动模式接管;翻页模式仍走下面的翻页监听器。按真实帧间隔换算逼近比例,120Hz 屏上速度不会翻倍;`ctrl/cmd+滚轮` 保留缩放;手指按下(滑动、拖滚动条、点分区)立刻停掉动画把控制权交还用户。 - 漫画双页铺开(`hooks/useComicSpreads.ts` + `utils/comicSpread.ts`,legado 无对应项,是桌面端补充):窗口宽度 ≥900px 且漫画处于翻页模式时,一屏并排显示两页(`settings.comicDoublePage` 为设备级偏好,默认开,可在菜单里关;窗口不够宽时自动退回单页)。分组规则同 Tachiyomi 的 dual page——宽度明显大于高度(比值 ≥1.1)的图视为「跨页宽图」独占一屏,其余连续的竖版页两两配对;列表接口只给 URL,尺寸靠 `new Image()` 探测(`` 渲染时顺带回填),只探测当前页往后 8 张的窗口,避免整章预载。进度仍按图片序号记,翻页时换算成「第几屏」,所以关掉再开双页进度不会错位。菜单动作行改为 `flex` 等分,容纳「双页/单页」入口(`grid-cols-N` 得写死列数字面量,多一个按钮无法复用)。 +- 漫画图片显示尺寸(`utils/comicImageFit.ts`,仅上下滚动模式):legado 的漫画阅读有「缩放」,Web 版原先只有一种写死的宽度(正文列封顶 900px),桌面端放不大也缩不小,所以补上一组档位,在界面面板的「图片尺寸」一行切换。`default` 就是老样子(900px 居中),`width` 适应宽度(正文列不再封顶、铺满窗口),`height` 适应高度(一屏一页),`long` 适应长边(整页完整可见),`original` 原图(原始像素 1:1,超出部分横向滚动)。`default`/`width` 由外层列宽决定,其余三个由 `` 自身约束,**所以后三种必须放开 900px 列宽上限**,否则会被列再压回去。`height`/`long` 要按「一屏多高」换算,所以量的是滚动容器本身而不是 `window.innerHeight`(正文区是 root 里的 `flex-1`,另挂上下条之后就不等于窗口高度了),并用 `ResizeObserver` 跟随;只在启用了自尺寸档位时才挂观察器,默认档位不白白重渲染整章。`settings.comicImageFit` 是设备级偏好(不同屏幕和源分辨率合适的尺寸不同),和 `comicDoublePage` 一样只存本机、不参与账号同步。翻页模式是整页缩放进视口,本来就不存在「太大/太小」,档位对它不生效、也不显示入口。`ctrl/cmd+滚轮` 依然留给浏览器缩放,本功能只用档位做粗调。 - **文本型漫画源按图片下发**(`internal/service/reader` 的 `imageMarkersOnly`):拷贝漫画这类书源 `bookSourceType=0`(文本),正文规则直接给 ``,服务端把整行标签转成 `[img]<签名地址>` 标记行后类型仍是 `text`,前端就会走文本阅读器——分页把每张图当成一列,一屏只看得到一张,桌面端也谈不上双页铺开。现在 `GetContentForBook` / `LocalChapterContent` 在标记化之后判断「非空行全部是 `[img]` 标记」,是则把 `type` 改成 `image` 并给出 `images`,交由漫画阅读器渲染(整页缩放、双页铺开、进度按图片序号)。判定只认「全是标记」:混了正文的章节一律保持 `text`,孤立的 HTML 标签行(`
`/`
`)与空行当作无内容忽略,绝不吞掉可读文字。 - 鼠标滚轮翻页(仅翻页模式):向上滚=上一页,向下滚=下一页;菜单打开时也不翻页。鼠标滚轮一格一页(间隔至少 220ms,与翻页动画对齐),触控板小步长累计到阈值翻一页且一次手势只翻一页(避免惯性连翻)。`ctrl/cmd+滚轮` 保留浏览器缩放。 - 目录:整屏面板(顶部返回 + 书名 + 章数,Virtuoso 虚拟列表,定位并高亮当前章,点章跳转)。**必须渲染在底部菜单之外**:菜单带 `backdrop-blur`,会成为 `fixed` 后代的包含块,放里面 `h-full` 只能拿到菜单高度;历史上它写的是 `top-0 + bottom-full`,两者同时存在时高度被算成 0,整块目录完全看不见。 diff --git a/web/src/pages/reader/ReaderComic.tsx b/web/src/pages/reader/ReaderComic.tsx index 195ae75..d40fca4 100644 --- a/web/src/pages/reader/ReaderComic.tsx +++ b/web/src/pages/reader/ReaderComic.tsx @@ -3,12 +3,15 @@ import { Loader2 } from 'lucide-react' import { useSmoothWheelScroll } from '../../hooks/useSmoothWheelScroll' import { rememberImageSize } from '../../utils/comicSpread' +import { comicImageSizing, isSelfSizedComicFit } from '../../utils/comicImageFit' +import type { ComicImageFit } from '../../stores/readerSettings' // 漫画/图片阅读器(仿 legado MangaMenu 能力面): // 上下滚动(默认)/ 左右单页 / 左右双页三种呈现;点击分区翻页/呼出菜单;图片懒加载。 // // 双页铺开由外层算好「这一屏显示哪几张」传进来(见 utils/comicSpread.ts), // 这里只负责把它们并排摆好、各自撑满视口高度。 +// 滚动模式的显示尺寸由 imageFit 档位决定(见 utils/comicImageFit.ts)。 interface ReaderComicProps { images: string[] @@ -22,6 +25,8 @@ interface ReaderComicProps { onScrolled: () => void /** 双页模式:本屏要并排显示的图片序号(1 张=独占的宽图/单页,2 张=左右一对)。 */ spread?: number[] | null + /** 滚动模式的图片显示尺寸档位(翻页模式不适用,传 default 即可)。 */ + imageFit?: ComicImageFit } function ComicImage({ @@ -29,13 +34,19 @@ function ComicImage({ theme, fit = false, half = false, + imageFit = 'default', + viewportHeight = 0, }: { src: string theme: { bg: string; text: string; accent: string } - // fit:整页缩放至视口内,长图不再被 overflow-hidden 的容器裁掉。 + // fit:整页缩放至视口内,长图不再被 overflow-hidden 的容器裁掉(翻页模式)。 fit?: boolean // half:双页并排时占位不超过半屏;容器收窄到图片本身宽度,两页之间不留缝。 half?: boolean + // imageFit:滚动模式的显示尺寸档位。 + imageFit?: ComicImageFit + // viewportHeight:滚动容器高度(px),height/long 档位按它换算。 + viewportHeight?: number }) { const [state, setState] = useState<'loading' | 'ok' | 'error'>('loading') @@ -45,6 +56,15 @@ function ComicImage({ setState('loading') }, [src]) + const loaded = state === 'ok' + // 翻页模式:整页缩放进视口。滚动模式:按用户选的尺寸档位摆(外层列宽已放开)。 + const sizing = fit + ? { + className: 'block max-h-full w-auto max-w-full object-contain', + style: loaded ? {} : { minHeight: '10rem' }, + } + : comicImageSizing(imageFit, viewportHeight, loaded) + // 必须始终留在渲染树里(不能 display:none):浏览器不会去拉取 // display:none 的 loading="lazy" 图片,onLoad 就永远不会触发,于是更没机会 // 脱离 loading —— 之前用 hidden 藏图就死在这里,漫画只剩一个转圈。 @@ -71,8 +91,8 @@ function ComicImage({ setState('ok') }} onError={() => setState('error')} - className={fit ? 'block max-h-full w-auto max-w-full object-contain' : 'block w-full'} - style={state === 'ok' ? undefined : { minHeight: '10rem' }} + className={sizing.className} + style={sizing.style} /> {state === 'loading' && (
(null) const imgRefs = useRef<(HTMLDivElement | null)[]>([]) const restoredRef = useRef(false) + // 滚动容器的高度:「一屏多高」以这个容器为准,而不是 window.innerHeight——正文区是 + // root(fixed inset-0)里的 flex-1,将来上下再挂条之类的布局变化就不等于窗口高度了。 + // 用 ResizeObserver 跟着它,改窗口大小时 height/long 档位自动重算。 + const [viewportHeight, setViewportHeight] = useState(0) + + const selfSized = isSelfSizedComicFit(imageFit) + useEffect(() => { + // 只有 height/long/original 用得到,默认档位不挂观察器,免得每次改窗口都重渲染整章。 + if (mode !== 'scroll' || !selfSized) return + const el = scrollRef.current + if (!el) return + const update = () => setViewportHeight(el.clientHeight) + update() + const observer = new ResizeObserver(update) + observer.observe(el) + return () => observer.disconnect() + }, [mode, selfSized]) // 滚动模式:恢复进度(图片序号)并上报当前图 useEffect(() => { @@ -183,7 +220,7 @@ export function ReaderComic({ images, theme, mode, page, onZone, initialImage, o } }} > - +

{i + 1} / {images.length}

diff --git a/web/src/pages/reader/ReaderViewPage.tsx b/web/src/pages/reader/ReaderViewPage.tsx index 94fa7d4..96df606 100644 --- a/web/src/pages/reader/ReaderViewPage.tsx +++ b/web/src/pages/reader/ReaderViewPage.tsx @@ -20,7 +20,7 @@ import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso' import { readerAPI, type ReaderBook, type ReaderChapter, type ReaderChapterContent, type ReaderSearchOrigin } from '../../api/reader' import { useComicSpreads } from '../../hooks/useComicSpreads' import { useSmoothWheelScroll } from '../../hooks/useSmoothWheelScroll' -import { READER_THEMES, getReaderTheme, useReaderSettingsStore } from '../../stores/readerSettings' +import { COMIC_IMAGE_FITS, READER_THEMES, getReaderTheme, useReaderSettingsStore } from '../../stores/readerSettings' import { buildChapterGroups, chapterGroupIndexOf } from '../../utils/chapterGroups' import { ReaderAudioPanel } from './ReaderAudioPanel' import { ReaderComic } from './ReaderComic' @@ -149,6 +149,19 @@ export default function ReaderViewPage() { ? Math.min(comicSpreadIndexOf[comicPage] ?? 0, comicSpreads.length - 1) : 0 + // ── 漫画滚动模式的图片显示尺寸 ── + // 档位只在上下滚动模式生效:翻页模式是整页缩放进视口,没有「太大/太小」的问题, + // 掺进来反而会和双页铺开的排版打架。所以翻页模式一律按 default 处理。 + const comicScrollFit = contentType === 'image' && settings.pageMode === 'scroll' ? settings.comicImageFit : 'default' + /** 图片尺寸档位只在漫画的滚动模式下起作用,别的场景不显示这一行免得点了没反应。 */ + const showComicImageFit = contentType === 'image' && settings.pageMode === 'scroll' + /** + * 正文列是否放开 900px 上限。两种情况:漫画双页铺开要吃满窗口宽度; + * 或者用户把图片尺寸调成了「适应宽度/高度/长边/原图」——这些档位由图片自己定尺寸, + * 900px 的列会把它们再压回去。 + */ + const comicFullWidth = contentType === 'image' && (comicDoublePage || comicScrollFit !== 'default') + // ── 加载书籍与章节 ── useEffect(() => { let cancelled = false @@ -711,7 +724,7 @@ export default function ReaderViewPage() {
{/* 正文视口 */}
-
+
{contentType === 'audio' ? ( media && media.tracks && media.tracks.length > 0 ? ( { if (zone === 'center') setMenuOpen((v) => !v) @@ -975,7 +989,7 @@ export default function ReaderViewPage() { ))}
- {/* 界面设置面板(主题 / 字号 / 行距 / 段距) */} + {/* 界面设置面板(主题 / 字号 / 行距 / 段距 / 漫画图片尺寸) */} {panel === 'style' && (
+ {/* 漫画滚动模式的图片显示尺寸。默认档就是老样子(900px 居中), + 「适应宽度」铺满窗口,「原图」按原始像素 1:1 看细节。 */} + {showComicImageFit && ( +
+ 图片尺寸 + {COMIC_IMAGE_FITS.map((item) => { + const active = settings.comicImageFit === item.id + return ( + + ) + })} +
+ )}
)}
diff --git a/web/src/stores/readerSettings.ts b/web/src/stores/readerSettings.ts index e1de165..018b4bf 100644 --- a/web/src/stores/readerSettings.ts +++ b/web/src/stores/readerSettings.ts @@ -45,6 +45,30 @@ export type ReaderShelfSort = 'recent' | 'update' | 'name' | 'author' | 'mixed' /** 书架网格列数:0 表示按屏幕自适应(legado 是固定 2–6 列)。 */ export const SHELF_GRID_COLUMNS = [0, 2, 3, 4, 5, 6] as const +/** + * 漫画图片的显示尺寸档位(上下滚动模式用)。 + * + * legado 的漫画阅读有「缩放」,Web 版原先只有一种写死的宽度(正文列封顶 900px), + * 桌面端看不到也放不大,所以补上这一组档位。屏幕越宽越该放大,属于设备级偏好, + * 和 comicDoublePage 一样只存本机、不参与账号同步。 + * + * - `default` 现状:正文列封顶 900px 居中,图片铺满该列 + * - `width` 适应宽度:正文列不再封顶,图片铺满窗口宽度 + * - `height` 适应高度:每页高度贴合一屏(一屏一页),宽度按原始比例 + * - `long` 适应长边:整页完整可见,宽高都不超出视口 + * - `original` 原始像素 1:1:既不放大也不缩小,超出部分横向滚动 + */ +export type ComicImageFit = 'default' | 'width' | 'height' | 'long' | 'original' + +/** 漫画图片尺寸档位的展示顺序与标签(界面面板按此渲染)。 */ +export const COMIC_IMAGE_FITS: { id: ComicImageFit; label: string }[] = [ + { id: 'default', label: '默认' }, + { id: 'width', label: '适应宽度' }, + { id: 'height', label: '适应高度' }, + { id: 'long', label: '适应长边' }, + { id: 'original', label: '原图' }, +] + interface ReaderSettingsState { // 首页模式切换(影视 / 阅读) homeMode: ReaderHomeMode @@ -81,6 +105,10 @@ interface ReaderSettingsState { comicDoublePage: boolean setComicDoublePage: (on: boolean) => void + /** 漫画图片显示尺寸档位(设备级偏好,见 ComicImageFit)。 */ + comicImageFit: ComicImageFit + setComicImageFit: (fit: ComicImageFit) => void + // ── 搜索范围(对应 legado 搜索页的 SearchScopeDialog / AppConfig.searchScope)── /** * 已选书源 ID;空数组表示「全部启用书源」(默认)。只影响书籍搜索的并发范围。 @@ -156,6 +184,12 @@ export const useReaderSettingsStore = create()( comicDoublePage: true, setComicDoublePage: (comicDoublePage) => set({ comicDoublePage }), + // 默认保持老样子(正文列 900px 居中):放大到铺满屏幕是「想要更大」时才做的事, + // 不该在升级后突然改变所有人已经习惯的宽度。想放大点「适应宽度」,想 1:1 看细节 + // 点「原图」。 + comicImageFit: 'default', + setComicImageFit: (comicImageFit) => set({ comicImageFit }), + searchScopeIds: [], setSearchScopeIds: (ids) => set({ searchScopeIds: [...new Set(ids)] }), @@ -185,8 +219,9 @@ export const useReaderSettingsStore = create()( * 与账号同步的阅读器偏好(对应后端 GET/PUT /reader/profile 的载荷)。 * * 这些设置原先是设备级的(只存 localStorage);现在按用户落库,换设备也能保持一致。 - * 首页的「影视 / 阅读」模式(homeMode)与漫画双页(comicDoublePage)属于设备偏好, - * 故意不参与同步——前者是入口选择,后者是否合适取决于屏幕宽度。 + * 首页的「影视 / 阅读」模式(homeMode)、漫画双页(comicDoublePage)与漫画图片尺寸 + * (comicImageFit)属于设备偏好,故意不参与同步——前者是入口选择,后两者是否合适 + * 取决于屏幕宽度和分辨率。 */ export interface ReaderSettingsProfile { theme_id: string diff --git a/web/src/utils/comicImageFit.test.ts b/web/src/utils/comicImageFit.test.ts new file mode 100644 index 0000000..42b48ab --- /dev/null +++ b/web/src/utils/comicImageFit.test.ts @@ -0,0 +1,55 @@ +import { comicImageSizing, isSelfSizedComicFit } from './comicImageFit.ts' +import type { ComicImageFit } from '../stores/readerSettings' + +// 漫画图片显示尺寸档位的纯逻辑回归测试(对齐 utils/coverUrl.test.ts 的写法)。 + +function check(name: string, condition: boolean) { + if (!condition) throw new Error(`comicImageFit: ${name}`) +} + +// ── 是否由图片自身约束尺寸 ── +const ALL_FITS: ComicImageFit[] = ['default', 'width', 'height', 'long', 'original'] +check('default 不是自尺寸档位', !isSelfSizedComicFit('default')) +check('width 不是自尺寸档位', !isSelfSizedComicFit('width')) +check('height 是自尺寸档位', isSelfSizedComicFit('height')) +check('long 是自尺寸档位', isSelfSizedComicFit('long')) +check('original 是自尺寸档位', isSelfSizedComicFit('original')) +check( + '自尺寸档位与列宽档位互斥且覆盖全部取值', + ALL_FITS.every((fit) => isSelfSizedComicFit(fit) === (fit === 'height' || fit === 'long' || fit === 'original')), +) + +// ── default / width:宽度交给外层列宽,图片只负责铺满 ── +for (const fit of ['default', 'width'] as ComicImageFit[]) { + const loaded = comicImageSizing(fit, 800, true) + check(`${fit} 加载完成时不留占位高度`, loaded.style.minHeight === undefined) + check(`${fit} 铺满外层列宽`, loaded.className === 'block w-full') + check(`${fit} 不自己定高`, loaded.style.height === undefined && loaded.style.maxHeight === undefined) + check(`${fit} 不改 maxWidth`, loaded.style.maxWidth === undefined) + check(`${fit} 加载中给占位高度`, comicImageSizing(fit, 800, false).style.minHeight === '10rem') +} + +// ── height:一屏一页 ── +check('height 用滚动容器的实测高度', comicImageSizing('height', 592, true).style.height === '592px') +check('height 宽度按原始比例', comicImageSizing('height', 592, true).style.width === 'auto') +check('height 防超宽页横向溢出', comicImageSizing('height', 592, true).style.maxWidth === '100vw') +check('height 不额外设 maxHeight', comicImageSizing('height', 592, true).style.maxHeight === undefined) +check('height 类名不带 w-full(w-full 会顶掉高度约束)', comicImageSizing('height', 592, true).className === 'block') + +// ── long:整页完整可见 ── +check('long 用容器高度封顶', comicImageSizing('long', 592, true).style.maxHeight === '592px') +check('long 不写死高度', comicImageSizing('long', 592, true).style.height === undefined) +check('long 同时约束宽度', comicImageSizing('long', 592, true).style.maxWidth === '100vw') +check('long 加载中给占位高度', comicImageSizing('long', 592, false).style.minHeight === '10rem') + +// ── original:原始像素 1:1 ── +check('original 不限制宽度', comicImageSizing('original', 592, true).style.maxWidth === 'none') +check('original 不限制高度', comicImageSizing('original', 592, true).style.maxHeight === undefined) +check('original 按原始像素渲染', comicImageSizing('original', 592, true).style.width === 'auto') + +// ── 视口高度还没量出来时退回视口单位,首帧不塌 ── +for (const fit of ['height', 'long'] as ComicImageFit[]) { + check(`${fit} 未量到高度时退回 100dvh`, comicImageSizing(fit, 0, true).style[fit === 'height' ? 'height' : 'maxHeight'] === '100dvh') + check(`${fit} 高度为负同样退回 100dvh`, comicImageSizing(fit, -10, true).style[fit === 'height' ? 'height' : 'maxHeight'] === '100dvh') +} +check('default 与视口高度无关', comicImageSizing('default', 0, true).style.height === undefined) diff --git a/web/src/utils/comicImageFit.ts b/web/src/utils/comicImageFit.ts new file mode 100644 index 0000000..608d83b --- /dev/null +++ b/web/src/utils/comicImageFit.ts @@ -0,0 +1,59 @@ +import type { CSSProperties } from 'react' + +import type { ComicImageFit } from '../stores/readerSettings' + +// 漫画图片的显示尺寸:把「档位」翻译成 的 class/style(纯逻辑,便于单测)。 +// +// 只有上下滚动模式用得到:翻页模式是整页缩放进视口(object-contain),本来就没有 +// 「太大/太小」的问题,一个档位反而会和双页铺开的排版打架。 +// +// 两种控制方式: +// - default / width 由外层列宽决定(列封顶 900px 或铺满窗口),图片本身 w-full; +// - height / long / original 由图片自身约束,所以外层必须放开列宽上限 +// (见 ReaderViewPage 的 comicFullWidth),否则 900px 的列会把它们再压回去。 + +/** 图片还没加载出来时的占位高度,避免懒加载期间整屏塌成 0 高。 */ +const PLACEHOLDER_MIN_HEIGHT = '10rem' + +export interface ComicImageSizing { + className: string + style: CSSProperties +} + +/** 该档位是否由 自身约束尺寸(决定外层列宽要不要放开)。 */ +export function isSelfSizedComicFit(fit: ComicImageFit): boolean { + return fit === 'height' || fit === 'long' || fit === 'original' +} + +/** + * 滚动模式下漫画图片的尺寸样式。 + * + * @param fit 显示尺寸档位。 + * @param viewportHeight 滚动容器的高度(px)。height/long 按它换算;还没量出来 + * (0)时退回 `100dvh`,首帧不会因为 0 而塌掉。 + * @param loaded 图片是否已加载完成,未完成时给个占位高度。 + */ +export function comicImageSizing( + fit: ComicImageFit, + viewportHeight: number, + loaded: boolean, +): ComicImageSizing { + const vh = viewportHeight > 0 ? `${viewportHeight}px` : '100dvh' + // height 档位自带高度,占位高度没有意义也不冲突,统一先铺再被档位覆盖。 + const placeholder: CSSProperties = loaded ? {} : { minHeight: PLACEHOLDER_MIN_HEIGHT } + + switch (fit) { + case 'height': + // 一屏一页:高度贴合视口,宽度按原始比例(maxWidth 防止超宽页横向溢出) + return { className: 'block', style: { ...placeholder, height: vh, width: 'auto', maxWidth: '100vw' } } + case 'long': + // 整页完整可见:长边贴合视口,宽高都不超出 + return { className: 'block', style: { ...placeholder, maxHeight: vh, maxWidth: '100vw', width: 'auto' } } + case 'original': + // 原始像素 1:1:不放大也不缩小;比窗口宽时由滚动容器横向滚动 + return { className: 'block', style: { ...placeholder, width: 'auto', maxWidth: 'none' } } + default: + // default / width:宽度交给外层列(900px 封顶或铺满窗口),这里只管铺满 + return { className: 'block w-full', style: placeholder } + } +}