# 手机图片排版功能 - 架构文档 > 基于源代码实际阅读编写,记录排版引擎的设计原理与数据流。 ## 1. 渲染管线 排版引擎的核心是 `PhoneImageEngine`(IIFE 闭包,`public/static/js/phone-image.js`),纯前端运行,后端不参与渲染计算。 ### 1.1 管线总览 ``` 用户触发 doRender() 或编辑器 onChange 自动触发 | v PhoneImageEngine.render() -- L82: 管线入口,带并发锁 | +---> editorHtml = window.phoneImageEditor.getHtml() -- L99: 从 wangeditor 读取HTML +---> preprocessContent(editorHtml) -- L154: 清洗HTML(去script/style/iframe,规范化img) +---> parseHtmlToBlocks(cleanHtml) -- L201: HTML -> 块级元素数组,
识别为 page-break | +---> generateCoverPage(sizeConfig) -- L619: 生成封面页HTML | +---> captureEditorBlocks(editorHtml, blocks, areaHeight, sizeConfig) -- L345: DOM测高核心 | | | +---> 按 page-break(
) 将 blocks 分组 -- L349-365 | +---> getEditorChildren() -- L323: 穿透 div 包装层获取编辑器DOM子元素 | +---> 创建 500px 测量容器(fixed, visibility:hidden) -- L376-387 | +---> 串行测高每组:DOM克隆到测量容器 -> getBoundingClientRect -- L392-433 | +---> 按比例分配实际高度给组内各 block -- L421-428 | +---> paginateContent(blocks, contentAreaHeight, sizeConfig) -- L445: 按高度累加分页 | +---> generateSummaryPage(sizeConfig, totalPages) -- L731: 生成尾页HTML +---> 页码补全 N/M -- L122-129: 二次遍历替换占位页码 +---> renderThumbnails(sizeConfig) -- L968: 截图 -> 显示右侧缩略图 | +---> doCapturePages({scale:1}) -- L841: staging隐藏区域渲染 + html2canvas串行截图 | +---> 纯图片页优化: 直接用 Image + Canvas,跳过 html2canvas -- L805-830 ``` ### 1.2 并发控制 `render()` 函数使用 `_locked` 标志防止并发渲染(`phone-image.js:86-91`)。渲染期间如果有新请求,设置 `_pending = true`,当前渲染完成后自动重试(`phone-image.js:134-137`)。 ### 1.3 DOM 测高机制 `captureEditorBlocks`(`phone-image.js:345-437`)是管线中替代旧版 `renderContentFlow` + `measureBlockHeights` 的核心函数。工作流程: 1. 按 `page-break` 类型将 blocks 分为若干组 2. 通过 `getEditorChildren()` 获取 wangeditor 编辑区的真实 DOM 子元素 3. 创建 500px 宽的隐藏测量容器(fixed 定位,`visibility:hidden`),与内容区宽度(540 - 20*2 padding)一致 4. 对每组:将对应的 DOM 子元素克隆到测量容器,通过 `requestAnimationFrame` 后读取 `getBoundingClientRect().height` 获取实际高度 5. 按各 block 估算高度的比例,将测量的实际高度分配给组内各 block 6. 所有组测高完成后,调用 `paginateContent` 进行分页 ### 1.4 截图机制 截图在 `#render-staging` 隐藏区域进行(`phone-image.html:597`)。关键步骤: 1. 将所有页面HTML append到 staging 区域的 `.phone-image-container` 容器中 2. 临时将 `visibility:hidden` 切为 `visible`(html2canvas 会跳过隐藏元素) 3. 串行逐页调用 html2canvas 截图(`runCaptureLoop`, L881) 4. 纯图片页优化:直接用 `Image + Canvas` 绘制,跳过 html2canvas(`phone-image.js:805-830`) 5. 截图完成后恢复 `visibility:hidden` 缩略图用 `scale:1`,保存用 `scale:2`(输出1080px宽度)。 ### 1.5 getEditorChildren 穿透逻辑 `getEditorChildren()`(`phone-image.js:323-334`)处理 wangeditor v5 的 DOM 包装层。wangeditor 的 `[data-slate-editor]` 下可能有一层 `
` 包装,该函数检测到这种情况时穿透到内层子元素,返回实际的内容节点数组。这对 `captureEditorBlocks` 的 DOM 克隆测高和 `exportLongImage` 的长图导出都至关重要。 ## 2. wangeditor 集成 ### 2.1 编辑器初始化 编辑器在 `phone_image.html` 模板脚本中初始化(L192-288): ```javascript var E = window.wangEditor; var phoneImageEditor = E.createEditor({ selector: '#editor-text-area', html: postData.contentHtml, // 初始内容,来自服务端渲染 config: editorConfig }); var toolbar = E.createToolbar({ editor: phoneImageEditor, selector: '#editor-toolbar', config: { excludeKeys: ['fullScreen'], insertKeys: { index: 24, keys: ['divider'] // 分割线按钮,插入位置 index 24 } } }); window.phoneImageEditor = phoneImageEditor; // 全局引用 ``` ### 2.2 工具栏配置 - 排除 `fullScreen` 按钮 - 在 index 24 位置插入 `divider`(分割线)按钮 - 其余使用 wangeditor 默认按钮集 ### 2.3 分割线与分页的关系 wangeditor 的 `divider` 按钮在编辑器中插入 `
` 元素(带 `data-w-e-type="divider"` 属性)。渲染管线中 `parseHtmlToBlocks` 将 `
` 识别为 `page-break` 类型的块(`phone-image.js:259-263`),`paginateContent` 遇到 `page-break` 时强制换页(`phone-image.js:455-466`)。`captureEditorBlocks` 也按 `page-break` 对 blocks 分组测高。 ### 2.4 粘贴处理 `editorConfig.customPaste`(`phone_image.html:220-268`)拦截粘贴操作,将外部图片(`http` 开头的URL图片和 `data:` 开头的 base64 图片)上传到服务器后替换为本地URL,确保编辑器中的图片资源可控。 ### 2.5 DOM 结构 ``` div.content-flow-area (540px, overflow-y:auto) +-- div#editor-toolbar (wangeditor 工具栏) +-- div#editor-text-area (wangeditor 编辑区) +-- div[data-slate-editor] (wangeditor 内部) +-- div (可能的包装层) +-- p / h1 / img / hr(divider) / ... (实际内容节点) ``` ## 3. 数据流 ### 3.1 数据源模型 重构后,wangeditor 编辑器成为运行时的权威内容源。引擎闭包变量 `postData.content_html` 仅作为初始化回退。 **权威数据源**:`window.phoneImageEditor.getHtml()`,通过 `getContentHtml()` API 统一访问(`phone-image.js:1280-1285`): ```javascript getContentHtml: function () { if (window.phoneImageEditor) { return window.phoneImageEditor.getHtml(); } return postData.content_html; // 回退 } ``` **引擎公开 API**: | API | 位置 | 用途 | |-----|------|------| | `getContentHtml()` | `phone-image.js:1280-1285` | 优先从 wangeditor 读取HTML,回退到闭包变量 | | `updateConfig(newConfig)` | `phone-image.js:1286-1290` | 使用 `$.extend(config, newConfig)` 更新配置,不重置内容 | | `saveConfig(postId, saveConfig, url)` | `phone-image.js:1138-1170` | 保存配置到服务端(不生成图片),返回 `{output_id}` | | `saveImages(postId, saveConfig)` | `phone-image.js:1079-1129` | 截图并保存所有页面图片到服务端 | | `exportLongImage()` | `phone-image.js:1192-1257` | 从编辑器DOM导出完整长图 | **DOM `#post-content-html` 的角色**:仅作为初始化数据源(`phone_image.html:105`)和保存后的显示同步层(`phone_image.html:434,459`),保存操作不从中读取内容。 ### 3.2 各操作的数据流 ``` 保存配置 (#btn-save): content_html 取自 PhoneImageEngine.getContentHtml() -- L431 调用 PhoneImageEngine.saveConfig() API -- L428-432 保存后同步: $('#post-content-html').html(getContentHtml()) -- L434 生成并保存 (#btn-generate): content_html 取自 PhoneImageEngine.getContentHtml() -- L454 调用 PhoneImageEngine.saveImages() API -- L451-455 保存后同步: $('#post-content-html').html(getContentHtml()) -- L459 重新排版 (doRender): 用 PhoneImageEngine.updateConfig(newConfig) -- L344 再调 PhoneImageEngine.render() -- L346 自动保存 (onChange): wangeditor onChange -> 2.6s 防抖 -> doAutoSave() -- phone_image.html:209-216 doAutoSave 调用 PhoneImageEngine.saveConfig() -- phone_image.html:403-413 加载历史 (loadFromHistory): window.phoneImageEditor.setHtml(res.data.content_html) -- phone_image.html:561 (加载到编辑器) PhoneImageEngine.updateConfig(historyConfig) -- phone_image.html:578 PhoneImageEngine.render() -- phone_image.html:580 导出长图 (更多菜单 -> exportLong): PhoneImageEngine.exportLongImage() -- phone_image.html:484 内部: getEditorChildren() -> 克隆非divider元素 -> html2canvas scale=2 -> 下载PNG ``` ### 3.3 自动保存机制 编辑器内容变更时触发自动保存(`phone_image.html:206-413`): 1. `editorConfig.onChange` 触发,设置 `autoSaveLock = true`,显示"等待保存..."状态 2. 清除之前的防抖定时器,设置新的 2.6 秒定时器 3. 定时器到期后调用 `doAutoSave()` 4. `doAutoSave()` 检查 `autoSaveLock`,通过 `PhoneImageEngine.saveConfig()` 发送保存请求 5. 保存状态通过 `#save-state` 指示器反馈(waiting/saving/saved/error,`phone_image.html:388-397`) 手动保存按钮点击时会清除自动保存定时器,避免冲突。 ### 3.4 完整数据流图 ``` [数据库 Post.content_html] | v Post.php::phoneImage() (L355) 查询 PostOutput 取已保存的 content_html 副本 | v phone_image.html 模板渲染 - $layoutContentHtml (优先用已保存副本) - $layoutConfig (已保存的配置) | v 前端初始化 (phone_image.html:152-294) 1. 构造 postData 局部对象,contentHtml 取自 #post-content-html (L173) 2. 恢复 savedConfig (L181-186) 3. wangeditor 初始化,html=postData.contentHtml (L270-274) 4. window.phoneImageEditor = phoneImageEditor (L288) 5. PhoneImageEngine.init(postData, initConfig) (L290) 6. doRender() 触发初始渲染 (L594) | v 渲染管线 (phone-image.js render()) - 读取 window.phoneImageEditor.getHtml() (唯一权威源) - parseHtmlToBlocks + captureEditorBlocks(测高) + paginateContent(分页) - staging 渲染 + html2canvas 截图 - 显示缩略图 | v 用户交互 - 编辑内容 -> wangeditor -> onChange 自动保存(2.6s防抖) - 分割线 -> wangeditor divider ->
-> page-break 分页 - 保存配置 -> PhoneImageEngine.saveConfig() (content_html 取自 getContentHtml()) - 生成保存 -> PhoneImageEngine.saveImages() (content_html 取自 getContentHtml()) - 重新排版 -> updateConfig() + render() - 加载历史 -> editor.setHtml() + updateConfig() + render() - 导出长图 -> exportLongImage() (从编辑器DOM直接截图) | v 后端存储 - PostOutput.config (JSON字段,含 content_html 副本) - PostOutputFile (每页图片文件记录) ``` ## 4. 存储模型 ### 4.1 数据库表 **`ul_post_output`** (模型: `app/model/PostOutput.php`) | 字段 | 说明 | |------|------| | `id` | 主键 | | `post_id` | 关联文章ID | | `output_type` | 输出类型,固定为 `phone_image` | | `config` | JSON字段,存储完整配置 + content_html 副本 | | `status` | 0=生成中, 1=已完成, 2=失败 (常量定义 L20-22) | | `page_count` | 页数 | | `admin_id` | 操作管理员ID | | `create_time` | 创建时间 (int) | | `delete_time` | 软删除标记 (默认0,trait: SoftDelete) | 模型使用 `$json = ['config']` 自动序列化/反序列化配置(`PostOutput.php:18`)。 **`ul_post_output_file`** (模型: `app/model/PostOutputFile.php`) | 字段 | 说明 | |------|------| | `id` | 主键 | | `output_id` | 关联 post_output.id | | `page` | 页码 (从1开始) | | `file_path` | 相对路径,如 `/upload/post_output/20260511/5_1.jpg` | | `file_url` | 访问URL (同 file_path) | | `file_size` | 文件字节数 | | `width` | 图片宽度 (px) | | `height` | 图片高度 (px) | 关系: `PostOutput hasMany PostOutputFile`,`PostOutput belongsTo Post`。 ### 4.2 config JSON 结构 ```json { "size": "xiaohongshu", "watermark": "", "pageAlignments": { "1": "top", "2": "center" }, "content_html": "

文章HTML副本


第二页内容

" } ``` `content_html` 作为配置的一部分保存,使每次排版记录都携带当时的内容快照。含 `
` 分页标记的 HTML 被完整保存,`parseHtmlToBlocks` 可以正确解析。 **向后兼容**:旧版 PostOutput 记录中保存的 `
` 分页标记仍可被 `parseHtmlToBlocks` 正确识别为 `page-break`(`phone-image.js:259-263`),不影响历史记录加载和重新排版。 ### 4.3 loadFromHistory 加载机制 ``` loadFromHistory(outputId) -> GET loadPostOutputConfig?id=XXX -> window.phoneImageEditor.setHtml(res.data.content_html) -- 加载到编辑器 -> PhoneImageEngine.updateConfig(historyConfig) -- 更新配置 -> PhoneImageEngine.render() -- 重新渲染 ``` 不再使用 `init()` 全量重置,而是通过 `editor.setHtml()` 直接将历史内容加载到 wangeditor,然后 `updateConfig()` 更新配置后重新渲染。 ### 4.4 文件存储 图片保存到 `public/upload/post_output/{Ymd}/{outputId}_{page}.jpg`(`PhoneImage.php:87-92`)。ZIP 临时文件生成到 `runtime/temp/` 目录,请求结束后通过 `register_shutdown_function` 清理(`Post.php:570-575`)。 ## 5. wangeditor 配置与工具栏 ### 5.1 编辑器配置 ```javascript editorConfig = { MENU_CONF: {}, placeholder: '请输入排版内容,使用分割线标记分页位置', scroll: false, MENU_CONF: { 'uploadImage': { server: '{:url("File/wangEditorSave")}', fieldName: 'file', meta: { type: 'editor' } } }, onChange: function(editor) { /* 自动保存 2.6s 防抖 */ }, customPaste: function(editor, event) { /* 外部图片上传处理 */ } } ``` ### 5.2 工具栏配置 ```javascript toolbar config = { excludeKeys: ['fullScreen'], insertKeys: { index: 24, // 在第24个按钮位置后插入 keys: ['divider'] // 分割线按钮 } } ``` 保留 wangeditor 所有默认按钮(粗体、斜体、标题、列表、引用、图片、链接等),额外添加分割线按钮用于手动标记分页位置。 ## 6. 配置体系 ### 6.1 PHP 端配置字段定义 `PhoneImage::getConfigFields()` 定义了4个字段: | 字段 | 类型 | 选项/范围 | 默认值 | |------|------|-----------|--------| | `size` | select | xiaohongshu/douyin | xiaohongshu | | `fontSize` | number | 10-24 | 14 | | `watermark` | text | - | '' | | `pageAlignments` | json | - | '{}' | ### 6.2 JS 端实际使用的配置 `phone-image.js` 闭包内的 `config` 对象 (L14-23): ```javascript config = { size: 'xiaohongshu', watermark: '', pageAlignments: {}, // 逐页对齐 sizes: { ... }, // 尺寸预设 contentPadding: 20 } ``` ### 6.3 设置弹框 通过 layui layer 弹框实现(`phone_image.html:293-330`),提供: - 尺寸选择:小红书 (1080x1440) / 抖音 (1080x1920) - 水印输入:可选水印文字 确认后自动触发 `doRender()` 重新排版。 ### 6.4 自动保存状态指示器 `` 元素(`phone_image.html:417`)动态显示保存状态: - `waiting`:黄色,"等待保存..." - `saving`:蓝色,"保存中..." - `saved`:绿色,"已保存" - `error`:红色,"保存失败" ## 7. UI 布局 ### 7.1 整体结构 两栏布局,适配宽屏操作: ``` +----------------------------------------------------------+ | page-header (顶部操作栏) | | [返回列表] 标题 [设置] [保存] [生成并保存] [更多v] 已保存 | +------------------------------+---------------------------+ | content-flow-area (540px) | paginated-preview-area | | +--------------------------+ | (flex:1, 横向滚动) | | | #editor-toolbar | | | | | (wangeditor工具栏) | | [缩略图1] [缩略图2] ... | | +--------------------------+ | 每张带页码和对齐按钮 | | | #editor-text-area | | | | | (wangeditor编辑区) | | | | | min-height:300px | | | | +--------------------------+ | | +------------------------------+---------------------------+ ``` ### 7.2 顶部操作栏 按钮直接显示:设置、保存、生成并保存。"更多"下拉菜单(纯 JS 实现,`phone_image.html:358-385`)包含:历史记录、重新生成、打包下载、导出长图。 layui 2.x 没有 dropdown 模块,更多菜单用纯 JS 实现的 `toggle` + `stopPropagation` + 外部点击关闭。 ### 7.3 缩略图区域 右侧缩略图横向排列,每个缩略图带页码标签和对齐切换按钮(仅内容页显示,`phone-image.js:1017-1025`)。对齐按钮切换 `top`/`center` 垂直对齐。 ## 8. 已删除的函数/组件 wangeditor 重构中移除的旧版函数和组件: ### 8.1 渲染管线相关 | 已删除函数 | 说明 | |-----------|------| | `renderContentFlow` | 旧版将块渲染到中间栏 `#content-flow`,已由 wangeditor 编辑器替代 | | `convertFlowBlocksToImages` | 旧版表格/代码块预转换(html2canvas截图替换为img),新版不再需要 | | `measureBlockHeights` | 旧版从中间栏DOM读取实测高度,已由 `captureEditorBlocks` 的测量容器方案替代 | | `insertPageBreak` | 旧版中间栏"+"按钮插入分页标记,已由 wangeditor divider 替代 | | `removePageBreak` | 旧版中间栏"-"按钮移除分页标记,已由编辑器直接删除 divider 替代 | ### 8.2 高度估算相关 | 已删除函数 | 说明 | |-----------|------| | `estimateTextHeight` | 旧版纯计算估算段落高度,已由 DOM 测量替代 | | `estimateHeadingHeight` | 旧版估算标题高度 | | `estimateListHeight` | 旧版估算列表高度 | | `estimateBlockquoteHeight` | 旧版估算引用块高度 | ### 8.3 工具函数 | 已删除函数 | 说明 | |-----------|------| | `simpleHash` | 旧版 djb2 哈希,用于 `convertedBlockCache` 缓存key | | `getContentWidth` | 旧版获取内容区宽度 | ### 8.4 UI 组件 | 已删除组件 | 说明 | |-----------|------| | `#content-flow` 中间栏 | 旧版内容渲染和交互区域,已由 wangeditor 编辑器替代 | | 左侧工具栏 | 旧版编辑操作面板(分页标记按钮等) | | `convertedBlockCache` | 旧版表格/代码块截图缓存,新版不再预转换 | ## 9. 已知限制 ### 9.1 html2canvas 兼容性 html2canvas 截图不支持以下 CSS 特性: - `transform` - `filter` - `clip-path` - `backdrop-filter` - CSS grid - `object-fit` 在 wangeditor 编辑器和自定义样式中应避免使用这些属性,否则截图中可能显示异常。 ### 9.2 wangeditor divider 特性 wangeditor 的 divider 是 void 元素,`::after` 伪元素可能不生效。编辑器中 divider 的视觉样式依赖 wangeditor 内部 CSS。 ### 9.3 layui dropdown 缺失 layui 2.x 没有 dropdown 模块,"更多"菜单用纯 JS 实现(`phone_image.html:358-385`),包括 `toggle` 显示/隐藏、`stopPropagation` 阻止冒泡、全局 `click` 事件关闭。 ### 9.4 保存数据量限制 `saveImages()` 有16MB的客户端检查(`phone-image.js:1096-1100`),但服务端没有对应的分块接收机制。大文章(超过约30页)的base64数据可能超出 PHP `post_max_size` 配置。 ### 9.5 目录权限 0777 `PhoneImage.php:95` 和 `:140` 中 `mkdir` 使用 0777 权限,与项目其他位置的 anti-pattern 一致。 ### 9.6 接口 `PostOutputManagerInterface` 未被充分利用 `PhoneImage` 实现了 `process()` 和 `getPreview()` 方法,但两者都返回空值(`PhoneImage.php:56-67`)。实际业务完全在控制器中直接调用 `PhoneImage` 的其他方法,绕过了接口定义的流程。接口仅作为类型约束存在。 ### 9.7 `saveConfigOnly` 是静态方法但 `createOutput` 是实例方法 `PhoneImage::saveConfigOnly()` 是 static 方法(L223),而同类的 `createOutput()` 是实例方法(L193)。两者的实现几乎相同(都是 `PostOutput::create()`),但调用方式不同。控制器中 `savePostOutput` 通过实例调用 `createOutput`,`savePostOutputConfig` 通过静态调用 `saveConfigOnly`,风格不统一。 ## 10. API 端点清单 所有端点位于 `app/admin/controller/Post.php`,通过 ThinkPHP 自动路由访问,URL前缀为 `/index.php/admin/post/`。 | 端点 | 方法 | 说明 | 前端调用方 | |------|------|------|-----------| | `phoneImage/{id}` | GET | 排版操作页(渲染模板) | 页面跳转 | | `savePostOutput` | POST (JSON) | 保存输出记录+图片 | `PhoneImageEngine.saveImages()` (phone-image.js:1103) | | `savePostOutputConfig` | POST (JSON) | 仅保存配置(不生成图片) | `PhoneImageEngine.saveConfig()` (phone-image.js:1152) | | `loadPostOutputConfig` | GET | 加载历史记录配置 | `loadFromHistory()` (phone_image.html:550) | | `getOutputListJson` | GET | 获取输出记录列表 | 历史弹窗 (phone_image.html:504) | | `deletePostOutput` | POST/GET | 删除输出记录及文件 | 未在前端模板中直接调用 | | `regeneratePostOutput` | POST/GET | 删除旧记录,返回配置 | 未在前端模板中直接调用 | | `downloadPostOutputZip/{id}` | GET | 打包下载ZIP | 更多菜单 -> 打包下载 | | `postOutputList/{id}` | GET | 输出管理列表页 | 独立页面入口 | ### 请求/响应格式 **savePostOutput** 请求体: ```json { "post_id": 123, "output_type": "phone_image", "config": { "size": "...", "watermark": "...", ... }, "content_html": "

...

", "pages": ["data:image/jpeg;base64,..."] } ``` **savePostOutputConfig** 请求体: ```json { "post_id": 123, "output_type": "phone_image", "config": { "size": "...", "watermark": "...", "pageAlignments": {} }, "content_html": "

...

" } ``` **通用响应格式**: ```json { "code": 0, "msg": "", "data": { "output_id": 456 } } ``` 失败时 `code` 为 500。 ## 11. 修复记录 ### 11.1 数据源统一修复(wangeditor 重构前) **修复日期**:2026-05-11 **核心原理**:将三个独立的数据源(DOM、闭包变量、模板局部变量)统一为单一权威数据源,通过新增的引擎 API 对外暴露。 **变更清单**: | 变更 | 文件 | 说明 | |------|------|------| | 新增 `getContentHtml()` | `phone-image.js:1280-1285` | 优先从 wangeditor 读取,回退到闭包变量 | | 新增 `updateConfig(newConfig)` | `phone-image.js:1286-1290` | 使用 `$.extend` 更新配置,不重置内容 | | 新增 `saveConfig()` API | `phone-image.js:1138-1170` | 引擎级保存配置方法 | | 新增 `exportLongImage()` | `phone-image.js:1192-1257` | 从编辑器DOM导出长图 | | 保存按钮改用 `getContentHtml()` | `phone_image.html:431` | content_html 取自引擎 API | | 生成按钮改用 `getContentHtml()` | `phone_image.html:454` | content_html 取自引擎 API | | 自动保存机制 | `phone_image.html:206-413` | onChange 2.6s 防抖自动保存 | | `doRender` 改用 `updateConfig()` | `phone_image.html:344` | 替代 `init()` 调用 | | `loadFromHistory` 用 `editor.setHtml()` | `phone_image.html:561` | 加载到编辑器而非重置引擎 | ### 11.2 wangeditor 重构 **重构日期**:2026-05-11 **核心变更**:将内容编辑从旧版中间栏(`#content-flow` + 手动分页标记)迁移到 wangeditor 富文本编辑器。 **主要变更**: | 变更 | 说明 | |------|------| | 引入 wangeditor v5 | 替代旧版中间栏内容展示和编辑 | | 新增 `captureEditorBlocks` | DOM 测量方案替代旧版高度估算 | | 新增 `getEditorChildren` | 穿透 wangeditor DOM 包装层 | | 新增 `exportLongImage` | 从编辑器DOM导出完整长图 | | 分割线替代分页标记按钮 | wangeditor divider (`
`) = page-break | | 自动保存 | 编辑器 onChange 触发 2.6s 防抖保存 | | 更多菜单纯 JS 实现 | 替代 layui dropdown(不存在) | | 删除旧版渲染管线 | renderContentFlow / convertFlowBlocksToImages / measureBlockHeights 等 | | 删除旧版高度估算 | estimateTextHeight 等,由 DOM 测量替代 | | 删除旧版 UI | 左侧工具栏、中间栏交互 | **重构效果**: - 内容编辑体验从"预览+标记"模式变为"所见即所得"编辑模式 - 分页标记通过分割线直观可见和可编辑 - 自动保存减少手动操作 - DOM 测量替代估算,分页精度提升 --- *文档基于以下源文件的完整阅读:* - `public/static/js/phone-image.js` (1292行) - `view/admin/post/phone_image.html` (600行) - `app/admin/controller/Post.php` (L350-579) - `app/common/tools/PhoneImage.php` (229行) - `app/common/tools/PostOutputManagerInterface.php` (28行) - `app/model/PostOutput.php` (62行) - `app/model/PostOutputFile.php` (26行)