mirror of
https://gitee.com/ulthon/ulthon_information.git
synced 2026-07-01 16:22:49 +08:00
feat(phone-image): 增加翻页预览与无封面图排版样式
- 为手机截图生成器添加翻页功能,支持在生成前预览各页内容 - 增加无封面图时的排版样式,使用装饰线条和居中布局 - 改进图片处理逻辑,清除内联样式并展平嵌套包装元素 - 修复 models.dev 同步接口,支持 GET 请求获取缓存数据 - 优化网络请求,添加直连失败后的本地代理重试机制
This commit is contained in:
@@ -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'] ?? []);
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
BIN
phone-image-cover-fixed.png
Normal file
BIN
phone-image-cover-fixed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
phone-image-cover-nocover.png
Normal file
BIN
phone-image-cover-nocover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
phone-image-page2-fixed.png
Normal file
BIN
phone-image-page2-fixed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
BIN
phone-image-page2.png
Normal file
BIN
phone-image-page2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
BIN
phone-image-preview.png
Normal file
BIN
phone-image-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -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);
|
||||
|
||||
@@ -113,6 +113,19 @@ var PhoneImageEngine = (function () {
|
||||
html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
||||
// 清除空段落
|
||||
html = html.replace(/<p[^>]*>\s*<\/p>/gi, '');
|
||||
|
||||
// 处理图片: 清除内联样式, 强制 max-width:100%
|
||||
html = html.replace(/<img([^>]*?)>/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 '<img' + attrs + ' style="max-width:100%;height:auto;display:block;margin:10px 0;">';
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -125,6 +138,29 @@ var PhoneImageEngine = (function () {
|
||||
if (!html) return blocks;
|
||||
|
||||
var $temp = $('<div>').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 = '<div class="phone-image-page page-cover" style="width:' +
|
||||
sizeConfig.width + 'px;height:' + sizeConfig.height + 'px;">';
|
||||
var hasCover = hasValidPoster();
|
||||
var html = '<div class="phone-image-page page-cover';
|
||||
if (hasCover) {
|
||||
html += ' has-cover-image';
|
||||
} else {
|
||||
html += ' no-cover-image';
|
||||
}
|
||||
html += '" style="width:' + sizeConfig.width + 'px;height:' + sizeConfig.height + 'px;">';
|
||||
|
||||
if (postData.poster) {
|
||||
if (hasCover) {
|
||||
html += '<img class="cover-image" src="' + postData.poster + '" alt="">';
|
||||
html += '<div class="cover-title">' + escapeHtml(postData.title) + '</div>';
|
||||
if (postData.desc) {
|
||||
html += '<div class="cover-subtitle">' + escapeHtml(postData.desc) + '</div>';
|
||||
}
|
||||
html += '<div class="cover-meta">';
|
||||
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 += '</div>';
|
||||
} else {
|
||||
// 无封面图: 大标题+摘要+装饰线条排版
|
||||
html += '<div class="cover-decor-line cover-decor-top"></div>';
|
||||
html += '<div class="cover-no-img-content">';
|
||||
html += '<div class="cover-no-img-title">' + escapeHtml(postData.title) + '</div>';
|
||||
html += '<div class="cover-decor-divider"></div>';
|
||||
if (postData.desc) {
|
||||
html += '<div class="cover-no-img-desc">' + escapeHtml(postData.desc) + '</div>';
|
||||
}
|
||||
html += '<div class="cover-no-img-meta">';
|
||||
if (postData.category_name) {
|
||||
html += '<span>' + escapeHtml(postData.category_name) + '</span>';
|
||||
}
|
||||
if (postData.author_name) {
|
||||
html += '<span>' + escapeHtml(postData.author_name) + '</span>';
|
||||
}
|
||||
if (postData.create_time) {
|
||||
html += '<span>' + postData.create_time + '</span>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="cover-decor-line cover-decor-bottom"></div>';
|
||||
}
|
||||
|
||||
html += '<div class="cover-title">' + escapeHtml(postData.title) + '</div>';
|
||||
|
||||
if (postData.desc) {
|
||||
html += '<div class="cover-subtitle">' + escapeHtml(postData.desc) + '</div>';
|
||||
}
|
||||
|
||||
html += '<div class="cover-meta">';
|
||||
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 += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
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
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user