diff --git a/app/admin/controller/System.php b/app/admin/controller/System.php index e1574e0..95e91a9 100644 --- a/app/admin/controller/System.php +++ b/app/admin/controller/System.php @@ -107,10 +107,22 @@ class System extends Common /** * 同步 models.dev 数据. + * + * GET: 返回缓存的供应商列表(不触发远程同步) + * POST: 强制从 models.dev 远程同步 */ public function syncModelsDev() { $sync = new \app\common\tools\ai\ModelsDevSync(); + + if ($this->request->isGet()) { + $result = [ + 'providers' => $sync->getProviders(), + 'models' => [], + ]; + return json(['code' => 0, 'msg' => 'ok', 'data' => $result]); + } + try { $result = $sync->syncProviders(); $count = count($result['providers'] ?? []); diff --git a/app/common/tools/ai/ModelsDevSync.php b/app/common/tools/ai/ModelsDevSync.php index ac769e6..8062c45 100644 --- a/app/common/tools/ai/ModelsDevSync.php +++ b/app/common/tools/ai/ModelsDevSync.php @@ -118,6 +118,46 @@ class ModelsDevSync curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Accept: application/json', + ]); + + // 尝试直连,失败后使用本地代理 + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_errno($ch); + curl_close($ch); + + if ($curlError !== 0 || $httpCode !== 200) { + // 直连失败,尝试通过本地代理 + $response = $this->fetchViaProxy(); + if ($response !== false) { + return $response; + } + + return false; + } + + return $response; + } + + /** + * 通过本地代理获取数据. + * + * @return string|false + */ + protected function fetchViaProxy() + { + $proxyUrl = 'http://127.0.0.1:7005'; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->apiUrl); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_PROXY, $proxyUrl); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Accept: application/json', ]); diff --git a/phone-image-cover-fixed.png b/phone-image-cover-fixed.png new file mode 100644 index 0000000..d7649df Binary files /dev/null and b/phone-image-cover-fixed.png differ diff --git a/phone-image-cover-nocover.png b/phone-image-cover-nocover.png new file mode 100644 index 0000000..5750590 Binary files /dev/null and b/phone-image-cover-nocover.png differ diff --git a/phone-image-page2-fixed.png b/phone-image-page2-fixed.png new file mode 100644 index 0000000..de05915 Binary files /dev/null and b/phone-image-page2-fixed.png differ diff --git a/phone-image-page2.png b/phone-image-page2.png new file mode 100644 index 0000000..de05915 Binary files /dev/null and b/phone-image-page2.png differ diff --git a/phone-image-preview.png b/phone-image-preview.png new file mode 100644 index 0000000..d7649df Binary files /dev/null and b/phone-image-preview.png differ diff --git a/public/static/css/phone-image-templates.css b/public/static/css/phone-image-templates.css index ccc7ca9..18902c8 100644 --- a/public/static/css/phone-image-templates.css +++ b/public/static/css/phone-image-templates.css @@ -116,6 +116,80 @@ margin-top: var(--pi-spacing-sm); } +/* --- 封面页 - 无封面图装饰排版 --- */ +.page-cover.no-cover-image { + justify-content: center; + padding: var(--pi-spacing-lg); +} + +.page-cover.no-cover-image .cover-decor-line { + position: absolute; + left: 30px; + right: 30px; + height: 2px; + background: var(--pi-color-border); +} + +.page-cover.no-cover-image .cover-decor-top { + top: 80px; +} + +.page-cover.no-cover-image .cover-decor-bottom { + bottom: 80px; +} + +.page-cover.no-cover-image .cover-no-img-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: var(--pi-spacing) 0; +} + +.page-cover.no-cover-image .cover-no-img-title { + font-size: 36px; + font-weight: bold; + line-height: 1.4; + color: var(--pi-color-text); + word-break: break-word; + margin-bottom: var(--pi-spacing); + padding: 0 20px; +} + +.page-cover.no-cover-image .cover-decor-divider { + width: 60px; + height: 3px; + background: var(--pi-color-accent); + margin-bottom: var(--pi-spacing); +} + +.page-cover.no-cover-image .cover-no-img-desc { + font-size: var(--pi-font-size-subtitle); + color: var(--pi-color-text-light); + line-height: 1.8; + margin-bottom: var(--pi-spacing); + padding: 0 30px; + word-break: break-word; +} + +.page-cover.no-cover-image .cover-no-img-meta { + display: flex; + gap: 15px; + font-size: var(--pi-font-size-small); + color: var(--pi-color-text-lighter); + flex-wrap: wrap; + justify-content: center; +} + +/* --- 内容页图片强制约束 --- */ +.page-content img { + max-width: 100% !important; + height: auto !important; + display: block; + margin: 10px 0; +} + /* --- 内容页 --- */ .page-body { display: flex; @@ -170,6 +244,28 @@ margin-bottom: var(--pi-spacing-sm); } +.page-content h4 { + font-size: var(--pi-font-size-base); + font-weight: bold; + margin-top: var(--pi-spacing-sm); + margin-bottom: 5px; +} + +.page-content h5 { + font-size: 13px; + font-weight: bold; + margin-top: var(--pi-spacing-sm); + margin-bottom: 5px; +} + +.page-content h6 { + font-size: 12px; + font-weight: bold; + margin-top: var(--pi-spacing-sm); + margin-bottom: 5px; + color: var(--pi-color-text-light); +} + .page-content blockquote { border-left: 3px solid var(--pi-color-accent); padding-left: var(--pi-spacing-sm); diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js index e9e8ff4..5a7e78c 100644 --- a/public/static/js/phone-image.js +++ b/public/static/js/phone-image.js @@ -113,6 +113,19 @@ var PhoneImageEngine = (function () { html = html.replace(/]*>[\s\S]*?<\/style>/gi, ''); // 清除空段落 html = html.replace(/]*>\s*<\/p>/gi, ''); + + // 处理图片: 清除内联样式, 强制 max-width:100% + html = html.replace(/]*?)>/gi, function (match, attrs) { + // 移除 style, width, height 内联属性 + attrs = attrs.replace(/\s*style\s*=\s*"[^"]*"/gi, ''); + attrs = attrs.replace(/\s*style\s*=\s*'[^']*'/gi, ''); + attrs = attrs.replace(/\s*width\s*=\s*"[^"]*"/gi, ''); + attrs = attrs.replace(/\s*width\s*=\s*'[^']*'/gi, ''); + attrs = attrs.replace(/\s*height\s*=\s*"[^"]*"/gi, ''); + attrs = attrs.replace(/\s*height\s*=\s*'[^']*'/gi, ''); + return ''; + }); + return html; } @@ -125,6 +138,29 @@ var PhoneImageEngine = (function () { if (!html) return blocks; var $temp = $('
').html(html); + + // 展平嵌套的包装元素 (div, section, article, main, header, footer) + // 将其内部内容提取为直接子元素 + var wrapperTags = ['div', 'section', 'article', 'main', 'header', 'footer', 'span']; + var changed = true; + var maxIterations = 10; + while (changed && maxIterations > 0) { + changed = false; + maxIterations--; + $temp.children().each(function () { + var $el = $(this); + var tagName = $el.prop('tagName').toLowerCase(); + if ($.inArray(tagName, wrapperTags) !== -1) { + // 将此包装元素的子内容替换自身 + var $children = $el.children(); + if ($children.length > 0) { + $el.replaceWith($children); + changed = true; + } + } + }); + } + var children = $temp.children(); if (children.length === 0) { @@ -161,6 +197,7 @@ var PhoneImageEngine = (function () { case 'h4': case 'h5': case 'h6': + block.type = tagName; block.estimatedHeight = estimateHeadingHeight(tagName, $el.text()); break; case 'img': @@ -380,34 +417,73 @@ var PhoneImageEngine = (function () { // ===== 页面HTML生成 ===== + /** + * 检查是否有有效的封面图(排除默认头像占位图) + */ + function hasValidPoster() { + if (!postData.poster) return false; + // 排除默认头像占位图 + var defaultPatterns = ['/static/images/avatar.png', '/static/images/avatar.jpeg', '/static/images/avatar.jpg']; + for (var i = 0; i < defaultPatterns.length; i++) { + if (postData.poster.indexOf(defaultPatterns[i]) !== -1) return false; + } + return true; + } + /** * 生成封面页HTML */ function generateCoverPage(sizeConfig) { - var html = '
'; + var hasCover = hasValidPoster(); + var html = '
'; - if (postData.poster) { + if (hasCover) { html += ''; + html += '
' + escapeHtml(postData.title) + '
'; + if (postData.desc) { + html += '
' + escapeHtml(postData.desc) + '
'; + } + html += '
'; + if (postData.category_name) { + html += escapeHtml(postData.category_name) + ' | '; + } + if (postData.author_name) { + html += escapeHtml(postData.author_name) + ' | '; + } + if (postData.create_time) { + html += postData.create_time; + } + html += '
'; + } else { + // 无封面图: 大标题+摘要+装饰线条排版 + html += '
'; + html += '
'; + html += '
' + escapeHtml(postData.title) + '
'; + html += '
'; + if (postData.desc) { + html += '
' + escapeHtml(postData.desc) + '
'; + } + html += '
'; + if (postData.category_name) { + html += '' + escapeHtml(postData.category_name) + ''; + } + if (postData.author_name) { + html += '' + escapeHtml(postData.author_name) + ''; + } + if (postData.create_time) { + html += '' + postData.create_time + ''; + } + html += '
'; + html += '
'; + html += '
'; } - html += '
' + escapeHtml(postData.title) + '
'; - - if (postData.desc) { - html += '
' + escapeHtml(postData.desc) + '
'; - } - - html += '
'; - if (postData.category_name) { - html += escapeHtml(postData.category_name) + ' | '; - } - if (postData.author_name) { - html += escapeHtml(postData.author_name) + ' | '; - } - if (postData.create_time) { - html += postData.create_time; - } - html += '
'; html += '
'; return { type: 'cover', html: html }; @@ -470,6 +546,9 @@ var PhoneImageEngine = (function () { // ===== DOM渲染 ===== + // 当前预览页码 (0-based) + var currentPreviewPage = 0; + /** * 渲染分页结果到DOM */ @@ -493,6 +572,55 @@ var PhoneImageEngine = (function () { for (var i = 0; i < pages.length; i++) { $container.append(pages[i].html); } + + // 确保当前页码有效 + if (currentPreviewPage >= pages.length) { + currentPreviewPage = Math.max(0, pages.length - 1); + } + + // 显示当前页, 隐藏其他页 + showPreviewPage(currentPreviewPage); + } + + /** + * 显示指定预览页 + * @param {number} index 页码(0-based) + */ + function showPreviewPage(index) { + if (!$container || !$container.length) return; + var $allPages = $container.find('.phone-image-page'); + $allPages.hide(); + if (index >= 0 && index < $allPages.length) { + $allPages.eq(index).show(); + currentPreviewPage = index; + } + // 更新页码显示 + $container.trigger('pageChange', [currentPreviewPage, pages.length]); + } + + /** + * 下一页 + */ + function nextPage() { + if (currentPreviewPage < pages.length - 1) { + showPreviewPage(currentPreviewPage + 1); + } + } + + /** + * 上一页 + */ + function prevPage() { + if (currentPreviewPage > 0) { + showPreviewPage(currentPreviewPage - 1); + } + } + + /** + * 获取当前预览页码 + */ + function getCurrentPageIndex() { + return currentPreviewPage; } // ===== 图片生成 ===== @@ -512,24 +640,31 @@ var PhoneImageEngine = (function () { return deferred.promise(); } + // 截图时需要所有页面都显示 + $pages.show(); + var index = 0; var total = $pages.length; function captureNext() { if (index >= total) { if (onProgress) onProgress(total, total, null); + // 恢复单页显示 + showPreviewPage(currentPreviewPage); deferred.resolve(canvases); return; } - var $page = $($pages[index]); + // 隐藏其他页, 只显示当前要截图的页 + $pages.hide(); + $pages.eq(index).show(); - html2canvas($page[0], { + html2canvas($pages.eq(index)[0], { scale: 2, useCORS: true, backgroundColor: '#ffffff', - width: $page.outerWidth(), - height: $page.outerHeight(), + width: $pages.eq(index).outerWidth(), + height: $pages.eq(index).outerHeight(), logging: false }).then(function (canvas) { canvases.push(canvas); @@ -537,6 +672,8 @@ var PhoneImageEngine = (function () { if (onProgress) onProgress(index, total, canvas); captureNext(); }).catch(function (err) { + // 恢复单页显示 + showPreviewPage(currentPreviewPage); deferred.reject('截图失败(第' + (index + 1) + '页): ' + err); }); } @@ -731,6 +868,10 @@ var PhoneImageEngine = (function () { getPages: getPages, applyAiRecommendation: applyAiRecommendation, applyAiContent: applyAiContent, - restoreOriginalContent: restoreOriginalContent + restoreOriginalContent: restoreOriginalContent, + nextPage: nextPage, + prevPage: prevPage, + showPreviewPage: showPreviewPage, + getCurrentPageIndex: getCurrentPageIndex }; })(); diff --git a/view/admin/post/phone_image.html b/view/admin/post/phone_image.html index a4f2fd7..9141101 100644 --- a/view/admin/post/phone_image.html +++ b/view/admin/post/phone_image.html @@ -305,9 +305,27 @@ fontSize: fontSize }); var pages = PhoneImageEngine.render(); + updatePageInfo(); layer.msg('排版完成,共 ' + pages.length + ' 页'); } + function updatePageInfo() { + var current = PhoneImageEngine.getCurrentPageIndex() + 1; + var total = PhoneImageEngine.getPages().length; + $('#page-info').text('第 ' + current + ' 页 / 共 ' + total + ' 页'); + } + + // 翻页 + $('#prev-page').click(function () { + PhoneImageEngine.prevPage(); + updatePageInfo(); + }); + + $('#next-page').click(function () { + PhoneImageEngine.nextPage(); + updatePageInfo(); + }); + // 生成并保存 $('#btn-generate').click(function () { var btn = $(this); diff --git a/view/admin/system/ai.html b/view/admin/system/ai.html index 7137784..c784c93 100644 --- a/view/admin/system/ai.html +++ b/view/admin/system/ai.html @@ -283,7 +283,7 @@ var channels = []; // 读取页面内嵌的已配置渠道数据(通过模板变量) - {:php} + {php} $allConfig = get_system_config(); $defaultProvider = get_system_config('ai_default_provider', ''); $configured = []; @@ -494,7 +494,7 @@ // 编辑渠道 window.editChannel = function (providerId) { - {:php} + {php} echo 'var providerConfigs = {};'; if (is_array($allConfig)) { foreach ($configured as $ch) {