Files
ulthon_information/view/admin/post/phone_image.html
augushong 34fe255829 feat(phone-image): 增加翻页预览与无封面图排版样式
- 为手机截图生成器添加翻页功能,支持在生成前预览各页内容
- 增加无封面图时的排版样式,使用装饰线条和居中布局
- 改进图片处理逻辑,清除内联样式并展平嵌套包装元素
- 修复 models.dev 同步接口,支持 GET 请求获取缓存数据
- 优化网络请求,添加直连失败后的本地代理重试机制
2026-05-01 16:31:26 +08:00

444 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{$post.title} - 手机图片排版</title>
<link rel="stylesheet" href="/static/lib/layui/css/layui.css">
<link rel="stylesheet" href="/static/css/phone-image-templates.css">
<link rel="stylesheet" href="/static/css/phone-image-fonts.css">
<style>
body {
margin: 0;
padding: 0;
background: #f2f2f2;
}
.page-header {
background: #fff;
padding: 15px 20px;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-header h3 {
margin: 0;
font-size: 18px;
}
.main-layout {
display: flex;
height: calc(100vh - 60px);
}
.toolbar {
width: 260px;
background: #fff;
border-right: 1px solid #e8e8e8;
padding: 15px;
overflow-y: auto;
}
.preview-area {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
justify-content: center;
align-items: flex-start;
}
.toolbar .layui-form-label {
width: 60px;
padding: 6px 8px;
font-size: 13px;
}
.toolbar .layui-input-block {
margin-left: 70px;
}
.toolbar .layui-form-item {
margin-bottom: 12px;
}
.template-btn-group {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.template-btn {
flex: 1;
padding: 8px;
text-align: center;
border: 2px solid #e8e8e8;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
background: #fff;
}
.template-btn.active {
border-color: #1890ff;
color: #1890ff;
}
.template-btn:hover {
border-color: #1890ff;
}
.action-btns {
margin-top: 15px;
}
.action-btns .layui-btn {
width: 100%;
margin-bottom: 8px;
}
.phone-frame {
background: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
}
.preview-nav {
text-align: center;
margin-top: 10px;
}
.preview-nav span {
margin: 0 10px;
cursor: pointer;
color: #1890ff;
}
</style>
</head>
<body>
<!-- 隐藏div存放文章HTML内容供JS读取 -->
<div id="post-content-html" style="display:none;">{$post->content_html|raw}</div>
<div class="page-header">
<div>
<a href="{:url('post/index')}" class="layui-btn layui-btn-sm layui-btn-primary"><i
class="layui-icon layui-icon-return"></i> 返回列表</a>
<a href="{:url('post/postOutputList',['id'=>$post.id])}" class="layui-btn layui-btn-sm">输出管理</a>
</div>
<h3>{$post.title}</h3>
</div>
<div class="main-layout">
<!-- 左侧工具栏 -->
<div class="toolbar">
<div class="layui-form" lay-filter="phoneImageForm">
<div class="layui-form-item">
<label class="layui-form-label">模板</label>
<div class="layui-input-block">
<div class="template-btn-group">
<div class="template-btn active" data-template="minimal">简约</div>
<div class="template-btn" data-template="magazine">杂志</div>
<div class="template-btn" data-template="mixed">图文</div>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">尺寸</label>
<div class="layui-input-block">
<select name="size" lay-filter="size">
<option value="xiaohongshu">小红书 (1080x1440)</option>
<option value="douyin">抖音 (1080x1920)</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">字体</label>
<div class="layui-input-block">
<select name="font" lay-filter="font">
<option value="source-han-sans">思源黑体</option>
<option value="alibaba-puhuiti">阿里巴巴普惠体</option>
<option value="lxgw-wenkai">霞鹜文楷</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">字号</label>
<div class="layui-input-block">
<input type="range" name="fontSize" min="10" max="24" value="14" lay-filter="fontSize"
style="width:100%;">
<span id="fontSizeValue">14px</span>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">水印</label>
<div class="layui-input-block">
<input type="text" name="watermark" placeholder="可选水印文字" class="layui-input">
</div>
</div>
<!-- AI 智能排版 -->
<div style="margin-top: 15px; margin-bottom: 10px; padding-top: 10px; border-top: 1px solid #e8e8e8;">
<label class="layui-form-label" style="font-size: 13px; color: #1890ff;">AI 助手</label>
<div class="layui-input-block" style="margin-top: 5px;">
<button type="button" class="layui-btn layui-btn-sm layui-btn-normal" id="btn-ai-recommend" style="width: 100%; margin-bottom: 5px;">
<i class="layui-icon layui-icon-magic"></i> AI 智能排版
</button>
<button type="button" class="layui-btn layui-btn-sm layui-btn-warm" id="btn-ai-optimize" style="width: 100%;">
<i class="layui-icon layui-icon-edit"></i> AI 优化内容
</button>
</div>
</div>
<div id="ai-reason" style="display:none; margin: 10px 0; padding: 8px 12px; background: #f0f7ff; border-radius: 4px; font-size: 12px; color: #666; line-height: 1.6;"></div>
<div id="ai-content-panel" style="display:none; margin: 10px 0; padding: 10px; background: #fffbe6; border: 1px solid #ffe58f; border-radius: 4px; font-size: 12px;">
<div style="font-weight: bold; margin-bottom: 5px;">AI 内容优化建议</div>
<div id="ai-optimized-title" style="margin-bottom: 5px;"></div>
<div id="ai-summary-points" style="margin-bottom: 8px;"></div>
<button type="button" class="layui-btn layui-btn-xs layui-btn-normal" id="btn-apply-ai">应用优化</button>
<button type="button" class="layui-btn layui-btn-xs layui-btn-primary" id="btn-keep-original">保持原文</button>
</div>
<div class="action-btns">
<button type="button" class="layui-btn" id="btn-preview"><i
class="layui-icon layui-icon-refresh"></i> 预览排版</button>
<button type="button" class="layui-btn layui-btn-normal" id="btn-generate"><i
class="layui-icon layui-icon-picture"></i> 生成并保存</button>
<button type="button" class="layui-btn layui-btn-warm" id="btn-download"><i
class="layui-icon layui-icon-download-circle"></i> 打包下载</button>
</div>
</div>
</div>
<!-- 右侧预览区 -->
<div class="preview-area">
<div>
<div class="phone-frame">
<div id="phone-image-container"
class="phone-image-container tpl-minimal size-xiaohongshu font-source-han-sans">
</div>
</div>
<div class="preview-nav">
<span id="prev-page"><i class="layui-icon layui-icon-left"></i> 上一页</span>
<span id="page-info">第 1 页 / 共 0 页</span>
<span id="next-page">下一页 <i class="layui-icon layui-icon-right"></i></span>
</div>
</div>
</div>
</div>
<script src="/static/lib/jquery/jquery-3.4.1.min.js"></script>
<script src="/static/lib/layui/layui.js"></script>
<script src="/static/lib/html2canvas/html2canvas.js"></script>
<script src="/static/js/phone-image.js"></script>
<script>
layui.use(['form', 'layer'], function () {
var form = layui.form;
var layer = layui.layer;
var lastOutputId = null;
var downloadBaseUrl = '{:url("post/downloadPostOutputZip", ["id" => 0])}';
var postData = {
postId: {$post.id},
title: '{$post.title|raw}',
desc: '{$post.desc|default=""}',
contentHtml: $('#post-content-html').html(),
poster: '{$post.poster|default=""}',
authorName: '{$post.author_name|default=""}',
createTime: '{$post.create_time_text|default=""}',
categoryName: ''
};
// 初始化引擎
PhoneImageEngine.init(postData, {
template: 'minimal',
size: 'xiaohongshu',
font: 'source-han-sans',
fontSize: 14
});
// 模板切换
$('.template-btn').click(function () {
$('.template-btn').removeClass('active');
$(this).addClass('active');
PhoneImageEngine.switchTemplate($(this).data('template'));
doRender();
});
// 尺寸切换
form.on('select(size)', function (data) {
PhoneImageEngine.switchSize(data.value);
doRender();
});
// 字体切换
form.on('select(font)', function (data) {
PhoneImageEngine.switchFont(data.value);
doRender();
});
// 字号调整
$('[name="fontSize"]').on('input', function () {
$('#fontSizeValue').text($(this).val() + 'px');
});
// 预览
$('#btn-preview').click(function () {
doRender();
});
function doRender() {
var fontSize = parseInt($('[name="fontSize"]').val()) || 14;
PhoneImageEngine.init(postData, {
template: $('.template-btn.active').data('template'),
size: $('[name="size"]').val(),
font: $('[name="font"]').val(),
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);
btn.prop('disabled', true).text('生成中...');
layer.msg('正在生成图片,请稍候...');
PhoneImageEngine.saveImages(postData.postId, {
template: $('.template-btn.active').data('template'),
size: $('[name="size"]').val(),
font: $('[name="font"]').val(),
fontSize: parseInt($('[name="fontSize"]').val()) || 14,
watermark: $('[name="watermark"]').val()
}).then(function (data) {
if (data.output_id) {
lastOutputId = data.output_id;
}
layer.msg('保存成功!');
btn.prop('disabled', false).html('<i class="layui-icon layui-icon-picture"></i> 生成并保存');
}).catch(function (err) {
layer.msg('保存失败: ' + err);
btn.prop('disabled', false).html('<i class="layui-icon layui-icon-picture"></i> 生成并保存');
});
});
// 打包下载
$('#btn-download').click(function () {
if (!lastOutputId) {
layer.msg('请先生成并保存图片');
return;
}
var url = downloadBaseUrl.replace('/0/', '/' + lastOutputId + '/');
window.open(url);
});
// ===== AI 智能排版 =====
// AI智能排版推荐
$('#btn-ai-recommend').click(function () {
var btn = $(this);
btn.prop('disabled', true).text('AI 分析中...');
$.post('{:url("post/aiRecommend")}', { post_id: postData.postId }, function (res) {
btn.prop('disabled', false).html('<i class="layui-icon layui-icon-magic"></i> AI 智能排版');
if (res.code === 0) {
PhoneImageEngine.applyAiRecommendation(res.data);
if (res.data.reason) {
$('#ai-reason').html('AI 推荐理由: ' + escapeHtmlSimple(res.data.reason)).show();
}
doRender();
layer.msg('AI 排版推荐已应用');
} else {
layer.msg(res.msg || 'AI 分析失败');
}
}).fail(function () {
btn.prop('disabled', false).html('<i class="layui-icon layui-icon-magic"></i> AI 智能排版');
layer.msg('网络错误');
});
});
// AI优化内容
$('#btn-ai-optimize').click(function () {
var btn = $(this);
btn.prop('disabled', true).text('AI 优化中...');
$.post('{:url("post/aiOptimizeContent")}', { post_id: postData.postId }, function (res) {
btn.prop('disabled', false).html('<i class="layui-icon layui-icon-edit"></i> AI 优化内容');
if (res.code === 0) {
var data = res.data;
var titleHtml = '<b>原标题:</b> ' + escapeHtmlSimple(postData.title);
if (data.optimized_title) {
titleHtml += '<br><b>优化标题:</b> ' + escapeHtmlSimple(data.optimized_title);
}
$('#ai-optimized-title').html(titleHtml);
if (data.summary_points) {
var points = '<b>要点:</b><br>';
for (var i = 0; i < data.summary_points.length; i++) {
points += (i + 1) + '. ' + escapeHtmlSimple(data.summary_points[i]) + '<br>';
}
$('#ai-summary-points').html(points);
}
$('#ai-content-panel').data('aiData', data).show();
} else {
layer.msg(res.msg || 'AI 优化失败');
}
}).fail(function () {
btn.prop('disabled', false).html('<i class="layui-icon layui-icon-edit"></i> AI 优化内容');
layer.msg('网络错误');
});
});
// 应用AI优化
$('#btn-apply-ai').click(function () {
var aiData = $('#ai-content-panel').data('aiData');
if (aiData) {
PhoneImageEngine.applyAiContent(aiData);
doRender();
$('#ai-content-panel').hide();
layer.msg('已应用 AI 优化内容');
}
});
// 保持原文
$('#btn-keep-original').click(function () {
PhoneImageEngine.restoreOriginalContent();
doRender();
$('#ai-content-panel').hide();
layer.msg('已恢复原文');
});
function escapeHtmlSimple(text) {
if (!text) return '';
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
});
</script>
</body>
</html>