From 518085d49362e1e95dbd7f7a6b61083d53e2c605 Mon Sep 17 00:00:00 2001 From: augushong Date: Mon, 11 May 2026 21:24:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor(phone-image):=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=AF=B9=E9=BD=90=EF=BC=8C=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PHP getConfigFields 移除 template/font,新增 pageAlignments - validateConfig 字段与 getConfigFields 一致 - 架构文档更新数据流、配置体系、已知问题、修复记录章节 --- app/common/tools/PhoneImage.php | 11 +-- docs/phone-image-architecture.md | 165 +++++++++++++++++++++++-------- 2 files changed, 126 insertions(+), 50 deletions(-) diff --git a/app/common/tools/PhoneImage.php b/app/common/tools/PhoneImage.php index a4ffe2c..564b526 100644 --- a/app/common/tools/PhoneImage.php +++ b/app/common/tools/PhoneImage.php @@ -15,11 +15,10 @@ class PhoneImage implements PostOutputManagerInterface public function getConfigFields(): array { return [ - 'template' => ['type' => 'select', 'options' => ['minimal', 'magazine', 'mixed'], 'default' => 'minimal'], 'size' => ['type' => 'select', 'options' => ['xiaohongshu', 'douyin'], 'default' => 'xiaohongshu'], - 'font' => ['type' => 'select', 'options' => ['source-han-sans', 'alibaba-puhuiti', 'lxgw-wenkai'], 'default' => 'source-han-sans'], 'fontSize' => ['type' => 'number', 'default' => 14, 'min' => 10, 'max' => 24], 'watermark' => ['type' => 'text', 'default' => ''], + 'pageAlignments' => ['type' => 'json', 'default' => '{}'], ]; } @@ -30,21 +29,17 @@ class PhoneImage implements PostOutputManagerInterface { $fields = $this->getConfigFields(); - if (isset($config['template']) && !in_array($config['template'], $fields['template']['options'])) { - return false; - } if (isset($config['size']) && !in_array($config['size'], $fields['size']['options'])) { return false; } - if (isset($config['font']) && !in_array($config['font'], $fields['font']['options'])) { - return false; - } if (isset($config['fontSize'])) { $size = intval($config['fontSize']); if ($size < $fields['fontSize']['min'] || $size > $fields['fontSize']['max']) { return false; } } + // watermark 是文本类型,无需特殊验证 + // pageAlignments 是 JSON 类型,存储时由框架自动序列化 return true; } diff --git a/docs/phone-image-architecture.md b/docs/phone-image-architecture.md index 7b27b45..79b8f17 100644 --- a/docs/phone-image-architecture.md +++ b/docs/phone-image-architecture.md @@ -53,7 +53,44 @@ PhoneImageEngine.render() -- L129: 管线入口,带并发 ## 2. 数据流 -### 2.1 HTML内容的三个来源(三源问题) +### 2.1 修复后的单一数据源模型(当前状态) + +修复后,引擎闭包变量 `postData.content_html` 成为唯一的权威数据源,所有读写操作统一通过引擎 API 进行。 + +**权威数据源**:闭包 `postData.content_html`(`phone-image.js:32`),通过以下两个 API 对外暴露: + +| API | 位置 | 用途 | +|-----|------|------| +| `getContentHtml()` | `phone-image.js:1485-1487` | 返回 `postData.content_html`,外部获取内容的唯一标准接口 | +| `updateConfig(newConfig)` | `phone-image.js:1488-1492` | 使用 `$.extend(config, newConfig)` 更新配置,**不重置** `postData` | + +**DOM `#post-content-html` 的角色降级**:仅作为初始化数据源和显示层,不再被保存操作读取。保存后主动同步 `$('#post-content-html').html(PhoneImageEngine.getContentHtml())`(`phone_image.html:271`, `phone_image.html:295`),保持显示层与权威源一致。 + +**各操作的数据流**: + +``` +保存配置 (#btn-save): + content_html 取自 PhoneImageEngine.getContentHtml() -- L268 + 保存后同步: $('#post-content-html').html(getContentHtml()) -- L271 + +生成并保存 (#btn-generate): + content_html 取自 PhoneImageEngine.getContentHtml() -- L290 + 保存后同步: $('#post-content-html').html(getContentHtml()) -- L295 + +重新排版 (doRender): + 用 PhoneImageEngine.updateConfig(newConfig) -- L239 + 不再调用 init(),不重置 postData.content_html + +加载历史 (loadFromHistory): + PhoneImageEngine.init(postData, historyConfig) -- L414 (全量初始化) + PhoneImageEngine.render() -- L416 +``` + +**分页标记操作**仍然直接修改闭包内的 `postData.content_html`(`insertPageBreak` L1223, `removePageBreak` L1261),但保存时通过 `getContentHtml()` 读取,确保标记不会丢失。 + +### 2.2 修复前的三源问题(历史记录) + +> 以下记录修复前的问题状态,保留作为设计决策参考。 文章HTML内容在运行时存在于三个独立位置,彼此之间没有同步机制: @@ -74,9 +111,9 @@ PhoneImageEngine.render() -- L129: 管线入口,带并发 -> 闭包 postData.content_html = options.contentHtml (phone-image.js:85) ``` -**问题**:分页标记操作(`insertPageBreak`/`removePageBreak`)只更新闭包内的 `postData.content_html`(`phone-image.js:1223`, `phone-image.js:1261`),不更新 DOM `#post-content-html` 和模板局部的 `postData.contentHtml`。而保存操作(`#btn-save`, `#btn-generate`)读取的是 `$('#post-content-html').html()`(`phone_image.html:267`, `phone_image.html:288`),这意味着分页标记在保存时会丢失。 +**问题(已修复)**:分页标记操作(`insertPageBreak`/`removePageBreak`)只更新闭包内的 `postData.content_html`(`phone-image.js:1223`, `phone-image.js:1261`),不更新 DOM `#post-content-html` 和模板局部的 `postData.contentHtml`。而保存操作(`#btn-save`, `#btn-generate`)原来读取的是 `$('#post-content-html').html()`,这意味着分页标记在保存时会丢失。此问题已通过将保存操作改为读取 `getContentHtml()` 修复。 -### 2.2 完整数据流图 +### 2.3 完整数据流图(修复后) ``` [数据库 Post.content_html] @@ -98,15 +135,16 @@ phone_image.html 模板渲染 | v 渲染管线 (phone-image.js render()) - - 读取 postData.content_html + - 读取 postData.content_html (唯一权威源) - 生成 pages 数组 - 截图显示缩略图 | v 用户交互 - 分页标记 -> 修改 postData.content_html (闭包内) - - 保存配置 -> POST savePostOutputConfig (content_html 取自 DOM) - - 生成保存 -> POST savePostOutput (content_html 取自 DOM) + - 保存配置 -> POST savePostOutputConfig (content_html 取自 getContentHtml()) + - 生成保存 -> POST savePostOutput (content_html 取自 getContentHtml()) + - 重新排版 -> updateConfig() + render() (不重置 postData) | v 后端存储 @@ -114,18 +152,18 @@ phone_image.html 模板渲染 - PostOutputFile (每页图片文件记录) ``` -### 2.3 历史记录加载流 +### 2.4 历史记录加载流(修复后) ``` -loadFromHistory(outputId) (phone_image.html:371) +loadFromHistory(outputId) (phone_image.html:374) -> GET loadPostOutputConfig?id=XXX -> 更新表单控件 (size/fontSize/watermark) -> 更新 postData.contentHtml (模板局部变量) - -> 更新 DOM #post-content-html - -> doRender(renderConfig) (含 pageAlignments) + -> PhoneImageEngine.init(postData, historyConfig) (全量初始化, L414) + -> PhoneImageEngine.render() (L416) ``` -注意:`loadFromHistory` 更新的是模板局部的 `postData.contentHtml` 和 DOM,但没有直接更新闭包内的 `postData.content_html`。它依赖 `doRender -> init()` 的间接传递来同步。 +修复后 `loadFromHistory` 使用 `init + render` 全量初始化引擎,直接将历史内容传入闭包,不再依赖 `doRender` 的间接传递。 ## 3. 存储模型 @@ -182,17 +220,20 @@ loadFromHistory(outputId) (phone_image.html:371) ## 4. 配置体系 -### 4.1 PHP 端配置字段定义 +### 4.1 PHP 端配置字段定义(对齐后) -`PhoneImage::getConfigFields()` (L15-23) 定义了5个字段: +`PhoneImage::getConfigFields()` (L15-23) 定义了4个字段,已与 JS 端对齐: | 字段 | 类型 | 选项/范围 | 默认值 | |------|------|-----------|--------| -| `template` | select | minimal/magazine/mixed | minimal | | `size` | select | xiaohongshu/douyin | xiaohongshu | -| `font` | select | source-han-sans/alibaba-puhuiti/lxgw-wenkai | source-han-sans | | `fontSize` | number | 10-24 | 14 | | `watermark` | text | - | '' | +| `pageAlignments` | json | - | '{}' | + +**已移除的字段**:`template`(3个模板选项 minimal/magazine/mixed)和 `font`(3个字体选项 source-han-sans/alibaba-puhuiti/lxgw-wenkai)已从 `getConfigFields()` 中移除,因为 JS 端无对应的模板/字体切换逻辑。 + +**新增的字段**:`pageAlignments` 已新增到 `getConfigFields()`,类型为 `json`,默认值 `'{}'`,与 JS 端的逐页对齐功能对齐。 ### 4.2 JS 端实际使用的配置 @@ -209,52 +250,63 @@ config = { } ``` -### 4.3 PHP/JS 字段不对齐 +### 4.3 PHP/JS 字段对齐状态(修复后) -两端存在明显的字段定义差异: +修复后两端字段已基本对齐: -| 问题 | PHP 定义 | JS 实际使用 | -|------|----------|-------------| -| `template` | PHP有定义 (3个模板选项) | JS完全不使用,无任何模板切换逻辑 | -| `font` | PHP有定义 (3个字体选项) | JS不使用,字体通过CSS引入,无切换机制 | -| `pageAlignments` | PHP未定义 | JS核心功能,保存在config中 | -| `contentPadding` | PHP未定义 | JS硬编码为20 (L23) | +| 字段 | PHP 定义 | JS 使用 | 状态 | +|------|----------|---------|------| +| `size` | select, 2个选项 | 核心功能 | 已对齐 | +| `fontSize` | number, 10-24 | 核心功能 | 已对齐 | +| `watermark` | text | 核心功能 | 已对齐 | +| `pageAlignments` | json | 核心功能 | 已对齐 | +| `contentPadding` | 未定义 | 硬编码为20 (L23) | 仍为 JS 内部常量,无需 PHP 定义 | -`validateConfig()` (L29-50) 验证 `template`/`size`/`font`/`fontSize`,但 `pageAlignments` 不在验证范围内。前端发送的配置比PHP定义的字段多,PHP不做字段白名单过滤,全部存入JSON。 +`validateConfig()` (L28-50) 验证 `size`/`fontSize`,`pageAlignments` 为 JSON 类型不做严格校验。前端发送的配置比 PHP 定义的字段多(如 `content_html`、`sizes`、`contentPadding`),PHP 不做字段白名单过滤,全部存入 JSON config 字段。 + +> **修复前的字段不对齐记录**:`template`(PHP 有定义但 JS 不使用)和 `font`(PHP 有定义但 JS 不使用)已从 PHP 端移除;`pageAlignments`(JS 核心功能但 PHP 未定义)已新增到 PHP 端。 ## 5. 已知问题 -### 5.1 分页标记丢失(根因分析) +### 5.1 已修复 + +#### 5.1.1 分页标记丢失(已修复) **现象**:用户通过中间栏的 "+" 按钮插入分页标记后,点击"保存"或"生成并保存",重新加载页面时分页标记消失。 -**根因**:数据源不一致。 +**根因**:数据源不一致。分页标记写入闭包 `postData.content_html`,但保存时读取的是 DOM `#post-content-html`(不含标记)。 -分页标记的插入流程 (`insertPageBreak`, L1198-1227): -1. 从闭包 `postData.content_html` 读取内容 -2. 在目标位置插入 `
` 标签 -3. 写回闭包 `postData.content_html`(L1223) +**修复方案**:保存操作改用 `PhoneImageEngine.getContentHtml()` 读取闭包内的权威数据源(`phone_image.html:268`, `phone_image.html:290`),保存后主动同步 DOM(`phone_image.html:271`, `phone_image.html:295`)。 -保存操作的配置构建 (`phone_image.html:262-268`, `284-289`): -```javascript -content_html: $('#post-content-html').html() // 读取的是DOM元素 -``` +#### 5.1.2 双数据源不一致(已修复) -DOM `#post-content-html` 的内容在页面初始化后从未被 `insertPageBreak` 更新。因此保存时发送的 `content_html` 是初始值,不含 `
` 分页标记。下次加载页面时,服务端用这个无标记的内容渲染,标记丢失。 +**现象**:`doRender` 调用 `init()` 重置 `postData.content_html`,导致分页标记在重新排版时丢失。 -### 5.2 保存数据量限制 +**根因**:`doRender` 通过 `init()` 传递配置,`init()` 会用 DOM 内容覆盖闭包变量。 + +**修复方案**:`doRender` 改用 `PhoneImageEngine.updateConfig(newConfig)`(`phone_image.html:239`),仅更新配置不重置 `postData`。 + +#### 5.1.3 PHP/JS 字段不对齐(已修复) + +**现象**:PHP 定义了 JS 不使用的 `template`/`font` 字段,缺少 JS 核心使用的 `pageAlignments` 字段。 + +**修复方案**:PHP `getConfigFields()` 移除 `template`/`font`,新增 `pageAlignments`(`PhoneImage.php:17-22`)。 + +### 5.2 仍存在的问题 + +#### 5.2.1 保存数据量限制 `saveImages()` 有16MB的客户端检查(`phone-image.js:1317-1320`),但服务端没有对应的分块接收机制。大文章(超过约30页)的base64数据可能超出 PHP `post_max_size` 配置。 -### 5.3 目录权限 0777 +#### 5.2.2 目录权限 0777 `PhoneImage.php:95` 和 `:140` 中 `mkdir` 使用 0777 权限,与项目其他位置的 anti-pattern 一致。 -### 5.4 接口 `PostOutputManagerInterface` 未被充分利用 +#### 5.2.3 接口 `PostOutputManagerInterface` 未被充分利用 `PhoneImage` 实现了 `process()` 和 `getPreview()` 方法,但两者都返回空值(`PhoneImage.php:56-67`)。实际业务完全在控制器中直接调用 `PhoneImage` 的其他方法,绕过了接口定义的流程。接口仅作为类型约束存在。 -### 5.5 `saveConfigOnly` 是静态方法但 `createOutput` 是实例方法 +#### 5.2.4 `saveConfigOnly` 是静态方法但 `createOutput` 是实例方法 `PhoneImage::saveConfigOnly()` 是 static 方法(L223),而同类的 `createOutput()` 是实例方法(L193)。两者的实现几乎相同(都是 `PostOutput::create()`),但调用方式不同。控制器中 `savePostOutput` 通过实例调用 `createOutput`,`savePostOutputConfig` 通过静态调用 `saveConfigOnly`,风格不统一。 @@ -305,11 +357,40 @@ DOM `#post-content-html` 的内容在页面初始化后从未被 `insertPageBrea --- +## 7. 修复记录 + +### 7.1 数据源统一修复 + +**修复日期**:2026-05-11 + +**核心原理**:将三个独立的数据源(DOM、闭包变量、模板局部变量)统一为单一权威数据源——引擎闭包变量 `postData.content_html`,通过新增的引擎 API 对外暴露。 + +**变更清单**: + +| 变更 | 文件 | 说明 | +|------|------|------| +| 新增 `getContentHtml()` | `phone-image.js:1485-1487` | 返回闭包内 `postData.content_html`,外部获取内容的唯一接口 | +| 新增 `updateConfig(newConfig)` | `phone-image.js:1488-1492` | 使用 `$.extend(config, newConfig)` 更新配置,不重置 `postData` | +| 保存按钮改用 `getContentHtml()` | `phone_image.html:268` | `#btn-save` 的 `content_html` 取自引擎 API 而非 DOM | +| 生成按钮改用 `getContentHtml()` | `phone_image.html:290` | `#btn-generate` 的 `content_html` 取自引擎 API 而非 DOM | +| 保存后同步 DOM | `phone_image.html:271,295` | 保存成功后 `$('#post-content-html').html(getContentHtml())` | +| `doRender` 改用 `updateConfig()` | `phone_image.html:239` | 替代 `init()` 调用,避免重置 `postData.content_html` | +| `loadFromHistory` 改用 `init + render` | `phone_image.html:414-416` | 全量初始化引擎,直接传入历史内容 | +| PHP `getConfigFields` 移除 `template`/`font` | `PhoneImage.php:17-22` | 移除 JS 端不使用的字段定义 | +| PHP `getConfigFields` 新增 `pageAlignments` | `PhoneImage.php:21` | 与 JS 端逐页对齐功能对齐 | + +**修复效果**: +- 分页标记在保存和重新排版后不再丢失 +- 历史记录加载正确恢复内容和配置 +- PHP/JS 端配置字段定义一致 + +--- + *文档基于以下源文件的完整阅读:* -- `public/static/js/phone-image.js` (1487行) -- `view/admin/post/phone_image.html` (425行) +- `public/static/js/phone-image.js` (1495行) +- `view/admin/post/phone_image.html` (436行) - `app/admin/controller/Post.php` (L350-579) -- `app/common/tools/PhoneImage.php` (234行) +- `app/common/tools/PhoneImage.php` (229行) - `app/common/tools/PostOutputManagerInterface.php` (28行) - `app/model/PostOutput.php` (62行) - `app/model/PostOutputFile.php` (26行)