From bcd00e32ea3a66c24867cba203e141a0980fc414 Mon Sep 17 00:00:00 2001 From: augushong Date: Sat, 2 May 2026 09:16:05 +0800 Subject: [PATCH] refactor(phone-image): Wave 2 - form, controller, JS engine, API updates T3: Add cover_text textarea to post edit form T4: Update Post controller - content copy + cover_text passing T5: Refactor JS engine - remove old APIs, add forced breaks, page numbers, per-page alignment T8: Add cover_text to API default_fields, apidoc (4 places), AGENTS.md --- AGENTS.md | 1 + app/admin/controller/Post.php | 21 +++ app/api/controller/Articles.php | 1 + public/static/js/phone-image.js | 259 +++++++++----------------------- view/admin/post/edit.html | 9 ++ view/index/api_doc/index.html | 4 + 6 files changed, 109 insertions(+), 186 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 38f41d6..e0491fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,6 +73,7 @@ AJAX请求 -> JSON; 普通请求 -> 模板页面(common@tpl/success|error) Post --(M:N)--> Category (通过 post_category) Post --(M:N)--> Tag (通过 post_tag) Post --(1:N)--> PostComment, PostVisit +Post 字段含 cover_text: 封面文案,用于手机图片排版封面展示 Admin --(N:1)--> AdminGroup --(权限列表)--> AdminPermission 表前缀: ul_,时间戳: int(10),软删除: delete_time字段 ``` diff --git a/app/admin/controller/Post.php b/app/admin/controller/Post.php index 2f364ca..c41ed71 100644 --- a/app/admin/controller/Post.php +++ b/app/admin/controller/Post.php @@ -358,7 +358,24 @@ class Post extends Common if (empty($model_post)) { $this->error('文章不存在'); } + + // 查询已有的排版输出,获取排版内容副本 + $postOutput = \app\model\PostOutput::where('post_id', $id) + ->where('output_type', 'phone_image') + ->order('id', 'desc') + ->find(); + + // 排版内容副本:优先使用已保存的副本,否则使用原文 + $layoutContentHtml = $model_post->content_html; + $layoutConfig = []; + if ($postOutput && !empty($postOutput->config['content_html'])) { + $layoutContentHtml = $postOutput->config['content_html']; + $layoutConfig = $postOutput->config; + } + View::assign('post', $model_post); + View::assign('layoutContentHtml', $layoutContentHtml); + View::assign('layoutConfig', $layoutConfig); return View::fetch(); } @@ -395,6 +412,10 @@ class Post extends Common } $config = $data['config'] ?? []; + // 确保排版内容副本被保存到 config + if (isset($data['content_html']) && !isset($config['content_html'])) { + $config['content_html'] = $data['content_html']; + } $pages = $data['pages'] ?? []; $admin_id = session('admin_id') ?? 0; diff --git a/app/api/controller/Articles.php b/app/api/controller/Articles.php index 6de3d7e..aab21c4 100644 --- a/app/api/controller/Articles.php +++ b/app/api/controller/Articles.php @@ -154,6 +154,7 @@ class Articles extends BaseController 'jump_to_url_status' => 0, 'poster' => '', 'desc' => '', + 'cover_text' => '', 'author_name' => '', 'hits' => 0, 'is_top' => 0, diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js index 5a7e78c..e0cd02f 100644 --- a/public/static/js/phone-image.js +++ b/public/static/js/phone-image.js @@ -12,23 +12,14 @@ var PhoneImageEngine = (function () { // ===== 配置 ===== var config = { - template: 'minimal', size: 'xiaohongshu', - font: 'source-han-sans', fontSize: 14, watermark: '', - // 尺寸映射 (渲染尺寸, 实际输出是2倍) + pageAlignments: {}, // key=页码(1-based), value='top'|'center' sizes: { xiaohongshu: { width: 540, height: 720 }, douyin: { width: 540, height: 960 } }, - // 字体映射 - fonts: { - 'source-han-sans': "'Source Han Sans', 'SourceHanSans-Normal', sans-serif", - 'alibaba-puhuiti': "'AlibabaPuHuiTi', sans-serif", - 'lxgw-wenkai': "'LXGWWenKai', cursive" - }, - // 页面内容边距 contentPadding: 20 }; @@ -37,6 +28,7 @@ var PhoneImageEngine = (function () { id: 0, title: '', desc: '', + coverText: '', // 封面文案 content_html: '', poster: '', author_name: '', @@ -47,18 +39,16 @@ var PhoneImageEngine = (function () { // ===== 分页结果 ===== var pages = []; - // ===== DOM引用 ===== - var $container = null; - /** * 初始化引擎 - * @param {Object} options - {postId, title, desc, contentHtml, poster, authorName, createTime, categoryName} - * @param {Object} userConfig - {template, size, font, fontSize, watermark} + * @param {Object} options - {postId, title, desc, coverText, contentHtml, poster, authorName, createTime, categoryName} + * @param {Object} userConfig - {size, fontSize, watermark} */ function init(options, userConfig) { postData.id = options.postId || 0; postData.title = options.title || ''; postData.desc = options.desc || ''; + postData.coverText = options.coverText || ''; postData.content_html = options.contentHtml || ''; postData.poster = options.poster || ''; postData.author_name = options.authorName || ''; @@ -68,12 +58,11 @@ var PhoneImageEngine = (function () { if (userConfig) { $.extend(config, userConfig); } - - $container = $('#phone-image-container'); } /** * 渲染排版预览 - 生成分页后的HTML + * 两遍遍历: 先算总页数, 再带页码生成 * @returns {Array} 页面数组,每项是 { type, html, pageNum } */ function render() { @@ -82,22 +71,31 @@ var PhoneImageEngine = (function () { var pageHeight = sizeConfig.height; var contentAreaHeight = pageHeight - (config.contentPadding * 2); - // 内容预处理 var cleanHtml = preprocessContent(postData.content_html); - - // 解析为块级元素 var blocks = parseHtmlToBlocks(cleanHtml); - // 生成封面页 + // 封面页 pages.push(generateCoverPage(sizeConfig)); // 内容分页 var contentPages = paginateContent(blocks, contentAreaHeight, sizeConfig); pages = pages.concat(contentPages); - // 生成尾页 + // 尾页 pages.push(generateSummaryPage(sizeConfig, pages.length)); + // 两遍遍历: 现在知道了总页数, 给每页补上 N/M 页码 + var totalPages = pages.length; + for (var i = 0; i < pages.length; i++) { + if (pages[i].type === 'content') { + // 在页脚中添加 N/M 格式页码 + pages[i].html = pages[i].html.replace( + '' + pages[i].pageNum + '', + '' + (i + 1) + '/' + totalPages + '' + ); + } + } + // 渲染到DOM renderToDOM(sizeConfig); @@ -191,6 +189,11 @@ var PhoneImageEngine = (function () { }; switch (tagName) { + case 'hr': + block.type = 'page-break'; + block.html = ''; + block.estimatedHeight = 0; + break; case 'h1': case 'h2': case 'h3': @@ -296,7 +299,7 @@ var PhoneImageEngine = (function () { /** * 将块级元素按高度累加,超过可用高度时分页 - * 支持超大块拆分(超长文本拆成多段) + * 支持
强制分页和超大块拆分(超长文本拆成多段) */ function paginateContent(blocks, contentAreaHeight, sizeConfig) { var contentPages = []; @@ -307,6 +310,20 @@ var PhoneImageEngine = (function () { for (var i = 0; i < blocks.length; i++) { var block = blocks[i]; + // 强制分页标记 + if (block.type === 'page-break') { + // 结束当前页 + if (currentPageBlocks.length > 0) { + contentPages.push(generateContentPage( + currentPageBlocks, pageNumber, sizeConfig, false + )); + currentPageBlocks = []; + currentHeight = 0; + pageNumber++; + } + continue; // 跳过这个 page-break 块本身 + } + // 超大块处理:单个块超过整页高度时需要拆分 if (block.estimatedHeight > contentAreaHeight) { // 先把当前页已有的内容推出去 @@ -449,6 +466,10 @@ var PhoneImageEngine = (function () { if (postData.desc) { html += '
' + escapeHtml(postData.desc) + '
'; } + // 封面文案(有封面图) + if (postData.coverText) { + html += '
' + escapeHtml(postData.coverText) + '
'; + } html += '
'; if (postData.category_name) { html += escapeHtml(postData.category_name) + ' | '; @@ -469,6 +490,10 @@ var PhoneImageEngine = (function () { if (postData.desc) { html += '
' + escapeHtml(postData.desc) + '
'; } + // 封面文案(无封面图) + if (postData.coverText) { + html += '
' + escapeHtml(postData.coverText) + '
'; + } html += '
'; if (postData.category_name) { html += '' + escapeHtml(postData.category_name) + ''; @@ -490,13 +515,17 @@ var PhoneImageEngine = (function () { } /** - * 生成内容页HTML + * 生成内容页HTML(含逐页对齐支持) */ function generateContentPage(blocks, pageNum, sizeConfig, isLast) { - var html = '
'; - // 页头(仅首页内容页显示标题) + // 页头(仅第一页内容页显示标题) if (pageNum === 1) { html += ''; - // 页脚 + // 页脚 - 占位,render() 中会替换页码 html += '
{/notin} +
+
+ 封面文案 +

用于手机图片排版封面展示

+
+
+ +
+
描述
diff --git a/view/index/api_doc/index.html b/view/index/api_doc/index.html index eeff078..e0ca295 100644 --- a/view/index/api_doc/index.html +++ b/view/index/api_doc/index.html @@ -200,6 +200,7 @@ X-API-Key: {api_key}
"type": "1", "status": 1, "source": "api", + "cover_text": "", "create_time": 1700000000, "update_time": 1700000000, "categorys": [...], @@ -241,6 +242,7 @@ X-API-Key: {api_key}
"content_html": "<p>HTML内容</p>", "content_type": "html", "desc": "摘要", + "cover_text": "封面文案内容示例", "poster": "/uploads/poster.jpg", "type": "1", "status": 1, @@ -267,6 +269,7 @@ X-API-Key: {api_key}
contentstring否文章内容(Markdown) content_htmlstring否文章内容(HTML) descstring否文章摘要 + cover_textstring否封面文案,用于手机图片排版封面展示 posterstring否封面图 URL typestring否文章类型,默认 "1" statusint否状态,默认 0(草稿) @@ -338,6 +341,7 @@ X-API-Key: {api_key}
contentstring否文章内容(Markdown) content_htmlstring否文章内容(HTML) descstring否文章摘要 + cover_textstring否封面文案,用于手机图片排版封面展示 posterstring否封面图 URL statusint否状态 content_typestring否内容类型: "html"(默认) 或 "markdown"
当为 "markdown" 时,系统会自动将 content 转换为 HTML 存储到 content_html 字段