mirror of
https://gitee.com/ulthon/ulthon_information.git
synced 2026-07-08 17:52:48 +08:00
feat(post): 新增手机图片排版与AI智能排版功能
- 新增手机图片排版功能,支持小红书/抖音尺寸输出 - 新增AI智能排版顾问,支持内容分析与优化推荐 - 新增AI供应商管理,支持多渠道配置与同步 - 新增文章输出管理页面,支持图片预览与批量下载 - 新增字体文件与排版样式配置
This commit is contained in:
23
public/static/css/phone-image-fonts.css
Normal file
23
public/static/css/phone-image-fonts.css
Normal file
@@ -0,0 +1,23 @@
|
||||
@font-face {
|
||||
font-family: 'AlibabaPuHuiTi';
|
||||
src: url('/source/font/AlibabaPuHuiTi-3-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'AlibabaPuHuiTi';
|
||||
src: url('/source/font/AlibabaPuHuiTi-3-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'LXGWWenKai';
|
||||
src: url('/source/font/LXGWWenKai-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
694
public/static/css/phone-image-templates.css
Normal file
694
public/static/css/phone-image-templates.css
Normal file
@@ -0,0 +1,694 @@
|
||||
/* ============================================
|
||||
Phone Image Template System
|
||||
用于生成手机端分享图片的CSS框架
|
||||
渲染宽度540px, html2canvas scale=2 输出1080px
|
||||
注意: 仅使用html2canvas兼容的CSS属性
|
||||
支持: flexbox, border-radius, box-shadow, background-color,
|
||||
font, padding, margin, border, position(relative/absolute/fixed)
|
||||
不支持: transform, filter, clip-path, backdrop-filter,
|
||||
CSS grid, object-fit, position:sticky, CSS animations
|
||||
============================================ */
|
||||
|
||||
/* --- CSS Custom Properties --- */
|
||||
:root {
|
||||
/* 字号 */
|
||||
--pi-font-size-base: 14px;
|
||||
--pi-font-size-title: 24px;
|
||||
--pi-font-size-subtitle: 16px;
|
||||
--pi-font-size-small: 12px;
|
||||
/* 行高 */
|
||||
--pi-line-height: 1.8;
|
||||
/* 间距 */
|
||||
--pi-spacing: 20px;
|
||||
--pi-spacing-sm: 10px;
|
||||
--pi-spacing-lg: 30px;
|
||||
/* 颜色 */
|
||||
--pi-color-text: #333333;
|
||||
--pi-color-text-light: #666666;
|
||||
--pi-color-text-lighter: #999999;
|
||||
--pi-color-bg: #ffffff;
|
||||
--pi-color-accent: #1890ff;
|
||||
--pi-color-border: #eeeeee;
|
||||
/* 字体 */
|
||||
--pi-font-family: 'Source Han Sans', 'SourceHanSans-Normal', sans-serif;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Base Container
|
||||
固定540px宽度的最外层容器
|
||||
============================================ */
|
||||
.phone-image-container {
|
||||
width: 540px;
|
||||
background: var(--pi-color-bg);
|
||||
color: var(--pi-color-text);
|
||||
font-family: var(--pi-font-family);
|
||||
font-size: var(--pi-font-size-base);
|
||||
line-height: var(--pi-line-height);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Page Container
|
||||
每一页的容器, 尺寸由 size class 控制
|
||||
============================================ */
|
||||
.phone-image-page {
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Size Variants
|
||||
两种主流手机图片尺寸
|
||||
============================================ */
|
||||
|
||||
/* 小红书尺寸 540x720 (输出1080x1440) */
|
||||
.size-xiaohongshu .phone-image-page {
|
||||
width: 540px;
|
||||
height: 720px;
|
||||
}
|
||||
|
||||
/* 抖音尺寸 540x960 (输出1080x1920) */
|
||||
.size-douyin .phone-image-page {
|
||||
width: 540px;
|
||||
height: 960px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Page Type Variants
|
||||
三种页面类型: 封面页/内容页/总结页
|
||||
============================================ */
|
||||
|
||||
/* --- 封面页/首页 --- */
|
||||
.page-cover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--pi-spacing-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-cover .cover-title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
margin-bottom: var(--pi-spacing);
|
||||
color: var(--pi-color-text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.page-cover .cover-subtitle {
|
||||
font-size: var(--pi-font-size-subtitle);
|
||||
color: var(--pi-color-text-light);
|
||||
margin-bottom: var(--pi-spacing);
|
||||
}
|
||||
|
||||
.page-cover .cover-image {
|
||||
width: 100%;
|
||||
margin-bottom: var(--pi-spacing);
|
||||
}
|
||||
|
||||
.page-cover .cover-meta {
|
||||
font-size: var(--pi-font-size-small);
|
||||
color: var(--pi-color-text-lighter);
|
||||
margin-top: var(--pi-spacing-sm);
|
||||
}
|
||||
|
||||
/* --- 内容页 --- */
|
||||
.page-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--pi-spacing);
|
||||
}
|
||||
|
||||
/* 内容页 - 页头区域 */
|
||||
.page-header {
|
||||
padding-bottom: var(--pi-spacing-sm);
|
||||
margin-bottom: var(--pi-spacing);
|
||||
border-bottom: 1px solid var(--pi-color-border);
|
||||
}
|
||||
|
||||
.page-header .page-title {
|
||||
font-size: var(--pi-font-size-title);
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
color: var(--pi-color-text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.page-header .page-subtitle {
|
||||
font-size: var(--pi-font-size-subtitle);
|
||||
color: var(--pi-color-text-light);
|
||||
margin-top: var(--pi-spacing-sm);
|
||||
}
|
||||
|
||||
/* 内容页 - 正文区域 */
|
||||
.page-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.page-content p {
|
||||
margin-bottom: var(--pi-spacing-sm);
|
||||
text-indent: 2em;
|
||||
}
|
||||
|
||||
.page-content h2 {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-top: var(--pi-spacing);
|
||||
margin-bottom: var(--pi-spacing-sm);
|
||||
}
|
||||
|
||||
.page-content h3 {
|
||||
font-size: var(--pi-font-size-subtitle);
|
||||
font-weight: bold;
|
||||
margin-top: var(--pi-spacing-sm);
|
||||
margin-bottom: var(--pi-spacing-sm);
|
||||
}
|
||||
|
||||
.page-content blockquote {
|
||||
border-left: 3px solid var(--pi-color-accent);
|
||||
padding-left: var(--pi-spacing-sm);
|
||||
margin: var(--pi-spacing-sm) 0;
|
||||
color: var(--pi-color-text-light);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.page-content ul,
|
||||
.page-content ol {
|
||||
padding-left: var(--pi-spacing);
|
||||
margin-bottom: var(--pi-spacing-sm);
|
||||
}
|
||||
|
||||
.page-content li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* 内容页 - 页脚区域 */
|
||||
.page-footer {
|
||||
padding-top: var(--pi-spacing-sm);
|
||||
margin-top: var(--pi-spacing-sm);
|
||||
border-top: 1px solid var(--pi-color-border);
|
||||
font-size: var(--pi-font-size-small);
|
||||
color: var(--pi-color-text-lighter);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* --- 总结页/尾页 --- */
|
||||
.page-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--pi-spacing-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-summary .summary-title {
|
||||
font-size: var(--pi-font-size-title);
|
||||
font-weight: bold;
|
||||
margin-bottom: var(--pi-spacing);
|
||||
color: var(--pi-color-text);
|
||||
}
|
||||
|
||||
.page-summary .summary-text {
|
||||
font-size: var(--pi-font-size-subtitle);
|
||||
color: var(--pi-color-text-light);
|
||||
margin-bottom: var(--pi-spacing);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.page-summary .summary-qr {
|
||||
margin-bottom: var(--pi-spacing-sm);
|
||||
}
|
||||
|
||||
.page-summary .summary-footer {
|
||||
font-size: var(--pi-font-size-small);
|
||||
color: var(--pi-color-text-lighter);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Font Variants
|
||||
三种可选字体
|
||||
============================================ */
|
||||
.font-source-han-sans {
|
||||
--pi-font-family: 'Source Han Sans', 'SourceHanSans-Normal', sans-serif;
|
||||
}
|
||||
|
||||
.font-alibaba-puhuiti {
|
||||
--pi-font-family: 'AlibabaPuHuiTi', sans-serif;
|
||||
}
|
||||
|
||||
.font-lxgw-wenkai {
|
||||
--pi-font-family: 'LXGWWenKai', cursive;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Article Images
|
||||
文章内图片样式
|
||||
============================================ */
|
||||
|
||||
/* 行内图片, 有边距 */
|
||||
.article-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: var(--pi-spacing) 0;
|
||||
}
|
||||
|
||||
/* 全宽图片, 无边距 */
|
||||
.article-image-full {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Page Number
|
||||
页码指示器
|
||||
============================================ */
|
||||
.page-number {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 15px;
|
||||
font-size: var(--pi-font-size-small);
|
||||
color: var(--pi-color-text-lighter);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Template Namespaces
|
||||
三个模板命名空间, 基础结构样式
|
||||
后续Task 8-10会填充详细样式
|
||||
============================================ */
|
||||
|
||||
/* --- 简约文字卡片模板 --- */
|
||||
.tpl-minimal {
|
||||
/* 基础结构: 白底黑字, 大量留白, 现代简约风格 */
|
||||
}
|
||||
|
||||
/* 封面页 - 居中大标题 + 副标题 + 日期 */
|
||||
.tpl-minimal .page-cover .cover-title {
|
||||
font-size: 28px;
|
||||
font-weight: normal;
|
||||
letter-spacing: 2px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-cover .cover-subtitle {
|
||||
font-weight: normal;
|
||||
letter-spacing: 1px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-cover .cover-meta {
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* 内容页 - 页头区域 */
|
||||
.tpl-minimal .page-header {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-header .page-title {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
letter-spacing: 1px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 内容页 - 正文区域: 宽行高(2.0), 大段落间距, 首行缩进2em */
|
||||
.tpl-minimal .page-content {
|
||||
line-height: 2.0;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-content p {
|
||||
text-indent: 2em;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-content h2 {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-content h3 {
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-content blockquote {
|
||||
border-left: 1px solid #ccc;
|
||||
font-style: normal;
|
||||
color: #888;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-content .article-image {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-content .article-image-full {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* 内容页 - 页脚区域: 小号版权 + 页码, 细线分隔 */
|
||||
.tpl-minimal .page-footer {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
font-size: 11px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* 总结页/尾页 */
|
||||
.tpl-minimal .page-summary .summary-title {
|
||||
font-weight: normal;
|
||||
letter-spacing: 2px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-summary .summary-text {
|
||||
color: #aaa;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.tpl-minimal .page-summary .summary-footer {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* --- 杂志模板 - 完整样式 --- */
|
||||
.tpl-magazine .page-cover {
|
||||
padding: 0;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-cover::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: #1890ff;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-cover .cover-title {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 1px;
|
||||
position: relative;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-cover .cover-subtitle {
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-cover .cover-meta {
|
||||
position: relative;
|
||||
padding: 8px 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-header {
|
||||
border-bottom: 2px solid #1890ff;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-header .page-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-content {
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-content p {
|
||||
margin-bottom: 14px;
|
||||
text-indent: 2em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-content p:first-child {
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-content p:first-child::first-letter {
|
||||
font-size: 3.2em;
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
line-height: 1;
|
||||
margin-right: 8px;
|
||||
margin-top: 4px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-content h2 {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-top: 22px;
|
||||
margin-bottom: 10px;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid #1890ff;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-content h3 {
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-content blockquote {
|
||||
border-left: 4px solid #1890ff;
|
||||
background: #f8f9fa;
|
||||
padding: 12px 15px;
|
||||
margin: 12px 0;
|
||||
font-style: normal;
|
||||
color: #555;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-content .article-image {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-footer {
|
||||
border-top: 2px solid #1890ff;
|
||||
padding-top: 8px;
|
||||
color: #1890ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-summary {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-summary .summary-title {
|
||||
color: #1890ff;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-summary .summary-text {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tpl-magazine .page-summary .summary-footer {
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 15px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* --- 图文混排模板 - 完整样式 --- */
|
||||
.tpl-mixed {
|
||||
/* 基础结构: 图文混排 */
|
||||
}
|
||||
|
||||
/* 封面页: 图片背景 + 底部文字叠加 */
|
||||
.tpl-mixed .page-cover {
|
||||
justify-content: flex-end;
|
||||
padding: 0;
|
||||
padding-bottom: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-cover .cover-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-cover .cover-title {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 15px 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-cover .cover-subtitle {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
color: #eee;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 8px 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-cover .cover-meta {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
color: #ccc;
|
||||
padding: 5px 20px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 无封面图时的降级样式 */
|
||||
.tpl-mixed .page-cover.no-cover-image {
|
||||
justify-content: center;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-cover.no-cover-image .cover-title {
|
||||
text-align: center;
|
||||
background: none;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-cover.no-cover-image .cover-subtitle {
|
||||
text-align: center;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-cover.no-cover-image .cover-meta {
|
||||
text-align: center;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* 内容页头部 */
|
||||
.tpl-mixed .page-header {
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-header .page-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 内容页正文 */
|
||||
.tpl-mixed .page-content {
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-content p {
|
||||
margin-bottom: 12px;
|
||||
text-indent: 2em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-content h2 {
|
||||
font-size: 19px;
|
||||
font-weight: bold;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 12px;
|
||||
background: #f0f0f0;
|
||||
border-left: 4px solid #333;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-content h3 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 6px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-content blockquote {
|
||||
border-left: 3px solid #333;
|
||||
background: #f8f8f8;
|
||||
padding: 10px 15px;
|
||||
font-style: normal;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-content .article-image {
|
||||
border-radius: 0;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-content .article-image-full {
|
||||
margin: 0 -20px;
|
||||
max-width: none;
|
||||
width: 540px;
|
||||
}
|
||||
|
||||
/* 内容页底部 */
|
||||
.tpl-mixed .page-footer {
|
||||
border-top: 1px solid #333;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 总结页 */
|
||||
.tpl-mixed .page-summary {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-summary .summary-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-summary .summary-text {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tpl-mixed .page-summary .summary-footer {
|
||||
color: #999;
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 15px;
|
||||
}
|
||||
736
public/static/js/phone-image.js
Normal file
736
public/static/js/phone-image.js
Normal file
@@ -0,0 +1,736 @@
|
||||
/**
|
||||
* PhoneImageEngine - 手机图片排版引擎
|
||||
*
|
||||
* 将文章HTML内容自动分页并渲染为手机尺寸图片
|
||||
* 依赖: jQuery, html2canvas (项目已有)
|
||||
*
|
||||
* 渲染宽度540px, html2canvas scale=2 输出1080px
|
||||
* 小红书: 540x720 (输出1080x1440)
|
||||
* 抖音: 540x960 (输出1080x1920)
|
||||
*/
|
||||
var PhoneImageEngine = (function () {
|
||||
|
||||
// ===== 配置 =====
|
||||
var config = {
|
||||
template: 'minimal',
|
||||
size: 'xiaohongshu',
|
||||
font: 'source-han-sans',
|
||||
fontSize: 14,
|
||||
watermark: '',
|
||||
// 尺寸映射 (渲染尺寸, 实际输出是2倍)
|
||||
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
|
||||
};
|
||||
|
||||
// ===== 文章数据 =====
|
||||
var postData = {
|
||||
id: 0,
|
||||
title: '',
|
||||
desc: '',
|
||||
content_html: '',
|
||||
poster: '',
|
||||
author_name: '',
|
||||
create_time: '',
|
||||
category_name: ''
|
||||
};
|
||||
|
||||
// ===== 分页结果 =====
|
||||
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}
|
||||
*/
|
||||
function init(options, userConfig) {
|
||||
postData.id = options.postId || 0;
|
||||
postData.title = options.title || '';
|
||||
postData.desc = options.desc || '';
|
||||
postData.content_html = options.contentHtml || '';
|
||||
postData.poster = options.poster || '';
|
||||
postData.author_name = options.authorName || '';
|
||||
postData.create_time = options.createTime || '';
|
||||
postData.category_name = options.categoryName || '';
|
||||
|
||||
if (userConfig) {
|
||||
$.extend(config, userConfig);
|
||||
}
|
||||
|
||||
$container = $('#phone-image-container');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染排版预览 - 生成分页后的HTML
|
||||
* @returns {Array} 页面数组,每项是 { type, html, pageNum }
|
||||
*/
|
||||
function render() {
|
||||
pages = [];
|
||||
var sizeConfig = config.sizes[config.size] || config.sizes.xiaohongshu;
|
||||
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));
|
||||
|
||||
// 渲染到DOM
|
||||
renderToDOM(sizeConfig);
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预处理HTML内容
|
||||
*/
|
||||
function preprocessContent(html) {
|
||||
if (!html) return '';
|
||||
html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
||||
html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
||||
// 清除空段落
|
||||
html = html.replace(/<p[^>]*>\s*<\/p>/gi, '');
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将HTML解析为块级元素数组
|
||||
* 每个块: { type, html, estimatedHeight }
|
||||
*/
|
||||
function parseHtmlToBlocks(html) {
|
||||
var blocks = [];
|
||||
if (!html) return blocks;
|
||||
|
||||
var $temp = $('<div>').html(html);
|
||||
var children = $temp.children();
|
||||
|
||||
if (children.length === 0) {
|
||||
// 纯文本内容,按段落分割
|
||||
var text = $temp.text().trim();
|
||||
if (text) {
|
||||
var paragraphs = text.split(/\n\s*\n|\n/);
|
||||
for (var i = 0; i < paragraphs.length; i++) {
|
||||
var p = paragraphs[i].trim();
|
||||
if (p) {
|
||||
blocks.push({
|
||||
type: 'p',
|
||||
html: '<p>' + p + '</p>',
|
||||
estimatedHeight: estimateTextHeight(p, config.fontSize)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
children.each(function () {
|
||||
var $el = $(this);
|
||||
var tagName = $el.prop('tagName').toLowerCase();
|
||||
var block = {
|
||||
type: tagName,
|
||||
html: $el[0].outerHTML
|
||||
};
|
||||
|
||||
switch (tagName) {
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
block.estimatedHeight = estimateHeadingHeight(tagName, $el.text());
|
||||
break;
|
||||
case 'img':
|
||||
block.estimatedHeight = estimateImageHeight($el);
|
||||
block.$img = $el;
|
||||
break;
|
||||
case 'ul':
|
||||
case 'ol':
|
||||
block.estimatedHeight = estimateListHeight($el);
|
||||
break;
|
||||
case 'blockquote':
|
||||
block.estimatedHeight = estimateBlockquoteHeight($el);
|
||||
break;
|
||||
case 'p':
|
||||
default:
|
||||
block.type = 'p';
|
||||
block.estimatedHeight = estimateTextHeight($el.text(), config.fontSize);
|
||||
break;
|
||||
}
|
||||
|
||||
blocks.push(block);
|
||||
});
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// ===== 高度估算 =====
|
||||
|
||||
/**
|
||||
* 获取内容区可用宽度 (540 - 40px padding)
|
||||
*/
|
||||
function getContentWidth() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算文本高度
|
||||
* @param {string} text
|
||||
* @param {number} fontSize
|
||||
* @returns {number} px
|
||||
*/
|
||||
function estimateTextHeight(text, fontSize) {
|
||||
var charCount = text.length;
|
||||
if (charCount === 0) return 0;
|
||||
var charsPerLine = Math.floor(getContentWidth() / (fontSize * 1.0));
|
||||
if (charsPerLine < 1) charsPerLine = 1;
|
||||
var lines = Math.ceil(charCount / charsPerLine);
|
||||
return lines * fontSize * 1.8 + 10; // 1.8行高 + 10px段落间距
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算标题高度
|
||||
*/
|
||||
function estimateHeadingHeight(tag, text) {
|
||||
var sizeMap = { h1: 28, h2: 24, h3: 20, h4: 18, h5: 16, h6: 14 };
|
||||
var hSize = sizeMap[tag] || 20;
|
||||
var charsPerLine = Math.floor(getContentWidth() / (hSize * 1.0));
|
||||
if (charsPerLine < 1) charsPerLine = 1;
|
||||
var lines = Math.ceil(text.length / charsPerLine);
|
||||
return lines * hSize * 1.4 + 20; // heading行高1.4 + 20px上下间距
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算图片高度
|
||||
* 默认按内容区宽度的60%估算
|
||||
*/
|
||||
function estimateImageHeight($img) {
|
||||
var w = parseInt($img.attr('width'), 10);
|
||||
var h = parseInt($img.attr('height'), 10);
|
||||
if (w > 0 && h > 0) {
|
||||
// 按比例缩放到内容宽度
|
||||
var ratio = getContentWidth() / w;
|
||||
return Math.round(h * ratio) + 10;
|
||||
}
|
||||
// 无尺寸信息时默认300px
|
||||
return 310;
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算列表高度
|
||||
*/
|
||||
function estimateListHeight($list) {
|
||||
var items = $list.find('li').length;
|
||||
return items * (config.fontSize * 1.8 + 5) + 15;
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算引用块高度
|
||||
*/
|
||||
function estimateBlockquoteHeight($bq) {
|
||||
var text = $bq.text();
|
||||
return estimateTextHeight(text, config.fontSize) + 10;
|
||||
}
|
||||
|
||||
// ===== 分页核心算法 =====
|
||||
|
||||
/**
|
||||
* 将块级元素按高度累加,超过可用高度时分页
|
||||
* 支持超大块拆分(超长文本拆成多段)
|
||||
*/
|
||||
function paginateContent(blocks, contentAreaHeight, sizeConfig) {
|
||||
var contentPages = [];
|
||||
var currentPageBlocks = [];
|
||||
var currentHeight = 0;
|
||||
var pageNumber = 1;
|
||||
|
||||
for (var i = 0; i < blocks.length; i++) {
|
||||
var block = blocks[i];
|
||||
|
||||
// 超大块处理:单个块超过整页高度时需要拆分
|
||||
if (block.estimatedHeight > contentAreaHeight) {
|
||||
// 先把当前页已有的内容推出去
|
||||
if (currentPageBlocks.length > 0) {
|
||||
contentPages.push(generateContentPage(
|
||||
currentPageBlocks, pageNumber, sizeConfig, false
|
||||
));
|
||||
currentPageBlocks = [];
|
||||
currentHeight = 0;
|
||||
pageNumber++;
|
||||
}
|
||||
|
||||
// 尝试拆分超大块
|
||||
var splitBlocks = splitOversizedBlock(block, contentAreaHeight);
|
||||
for (var s = 0; s < splitBlocks.length; s++) {
|
||||
var sb = splitBlocks[s];
|
||||
if (currentHeight + sb.estimatedHeight > contentAreaHeight && currentPageBlocks.length > 0) {
|
||||
contentPages.push(generateContentPage(
|
||||
currentPageBlocks, pageNumber, sizeConfig, false
|
||||
));
|
||||
currentPageBlocks = [];
|
||||
currentHeight = 0;
|
||||
pageNumber++;
|
||||
}
|
||||
currentPageBlocks.push(sb);
|
||||
currentHeight += sb.estimatedHeight;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 正常块:判断是否需要换页
|
||||
if (currentHeight + block.estimatedHeight > contentAreaHeight && currentPageBlocks.length > 0) {
|
||||
contentPages.push(generateContentPage(
|
||||
currentPageBlocks, pageNumber, sizeConfig, false
|
||||
));
|
||||
currentPageBlocks = [];
|
||||
currentHeight = 0;
|
||||
pageNumber++;
|
||||
}
|
||||
|
||||
currentPageBlocks.push(block);
|
||||
currentHeight += block.estimatedHeight;
|
||||
}
|
||||
|
||||
// 最后一页
|
||||
if (currentPageBlocks.length > 0) {
|
||||
contentPages.push(generateContentPage(
|
||||
currentPageBlocks, pageNumber, sizeConfig, true
|
||||
));
|
||||
}
|
||||
|
||||
return contentPages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆分超大块(超长段落文本拆成多段)
|
||||
* @param {Object} block - 块级元素
|
||||
* @param {number} pageHeight - 可用内容高度
|
||||
* @returns {Array} 拆分后的块数组
|
||||
*/
|
||||
function splitOversizedBlock(block, pageHeight) {
|
||||
// 图片块不拆分,保留原样(会被截断但保持完整)
|
||||
if (block.type === 'img') {
|
||||
return [block];
|
||||
}
|
||||
|
||||
// 文本类块:按句子拆分
|
||||
var text = '';
|
||||
var wrapperTag = 'p';
|
||||
if (block.type === 'blockquote') {
|
||||
text = $(block.html).text();
|
||||
wrapperTag = 'blockquote';
|
||||
} else {
|
||||
text = $(block.html).text();
|
||||
}
|
||||
|
||||
if (!text || text.length === 0) {
|
||||
return [block];
|
||||
}
|
||||
|
||||
// 按句子拆分:句号、问号、叹号、换行
|
||||
var sentences = text.split(/(?<=[。!?\n])/);
|
||||
if (sentences.length <= 1) {
|
||||
// 无法按句子拆分,按固定字符数拆分
|
||||
sentences = [];
|
||||
var chunkSize = Math.floor(getContentWidth() / config.fontSize) *
|
||||
Math.floor(pageHeight / (config.fontSize * 1.8));
|
||||
if (chunkSize < 10) chunkSize = 10;
|
||||
for (var ci = 0; ci < text.length; ci += chunkSize) {
|
||||
sentences.push(text.substring(ci, ci + chunkSize));
|
||||
}
|
||||
}
|
||||
|
||||
var result = [];
|
||||
for (var j = 0; j < sentences.length; j++) {
|
||||
var s = sentences[j].trim();
|
||||
if (!s) continue;
|
||||
var h = estimateTextHeight(s, config.fontSize);
|
||||
result.push({
|
||||
type: wrapperTag === 'blockquote' ? 'blockquote' : 'p',
|
||||
html: '<' + wrapperTag + '>' + s + '</' + wrapperTag + '>',
|
||||
estimatedHeight: h
|
||||
});
|
||||
}
|
||||
|
||||
return result.length > 0 ? result : [block];
|
||||
}
|
||||
|
||||
// ===== 页面HTML生成 =====
|
||||
|
||||
/**
|
||||
* 生成封面页HTML
|
||||
*/
|
||||
function generateCoverPage(sizeConfig) {
|
||||
var html = '<div class="phone-image-page page-cover" style="width:' +
|
||||
sizeConfig.width + 'px;height:' + sizeConfig.height + 'px;">';
|
||||
|
||||
if (postData.poster) {
|
||||
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>';
|
||||
html += '</div>';
|
||||
|
||||
return { type: 'cover', html: html };
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成内容页HTML
|
||||
*/
|
||||
function generateContentPage(blocks, pageNum, sizeConfig, isLast) {
|
||||
var html = '<div class="phone-image-page page-body" style="width:' +
|
||||
sizeConfig.width + 'px;height:' + sizeConfig.height + 'px;">';
|
||||
|
||||
// 页头(仅首页内容页显示标题)
|
||||
if (pageNum === 1) {
|
||||
html += '<div class="page-header">';
|
||||
html += '<div class="page-title">' + escapeHtml(postData.title) + '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// 正文内容区
|
||||
html += '<div class="page-content">';
|
||||
for (var i = 0; i < blocks.length; i++) {
|
||||
html += blocks[i].html;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// 页脚
|
||||
html += '<div class="page-footer">';
|
||||
html += '<span>' + escapeHtml(postData.author_name || '') + '</span>';
|
||||
html += '<span>' + pageNum + '</span>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>';
|
||||
return { type: 'content', html: html, pageNum: pageNum };
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成尾页/总结页HTML
|
||||
*/
|
||||
function generateSummaryPage(sizeConfig, totalPages) {
|
||||
var html = '<div class="phone-image-page page-summary" style="width:' +
|
||||
sizeConfig.width + 'px;height:' + sizeConfig.height + 'px;">';
|
||||
html += '<div class="summary-title">感谢阅读</div>';
|
||||
html += '<div class="summary-text">' + escapeHtml(postData.title) + '</div>';
|
||||
|
||||
if (postData.desc) {
|
||||
html += '<div class="summary-text" style="font-size:12px;">' + escapeHtml(postData.desc) + '</div>';
|
||||
}
|
||||
|
||||
html += '<div class="summary-footer">';
|
||||
html += '共 ' + totalPages + ' 页';
|
||||
if (postData.author_name) {
|
||||
html += ' | ' + escapeHtml(postData.author_name);
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
return { type: 'summary', html: html };
|
||||
}
|
||||
|
||||
// ===== DOM渲染 =====
|
||||
|
||||
/**
|
||||
* 渲染分页结果到DOM
|
||||
*/
|
||||
function renderToDOM(sizeConfig) {
|
||||
if (!$container || !$container.length) return;
|
||||
|
||||
var containerClasses = [
|
||||
'phone-image-container',
|
||||
'tpl-' + config.template,
|
||||
'size-' + config.size,
|
||||
'font-' + config.font
|
||||
];
|
||||
|
||||
$container.attr('class', containerClasses.join(' '));
|
||||
$container.css({
|
||||
'font-family': config.fonts[config.font] || config.fonts['source-han-sans'],
|
||||
'font-size': config.fontSize + 'px'
|
||||
});
|
||||
|
||||
$container.empty();
|
||||
for (var i = 0; i < pages.length; i++) {
|
||||
$container.append(pages[i].html);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 图片生成 =====
|
||||
|
||||
/**
|
||||
* 逐页截图生成图片
|
||||
* @param {Function} [onProgress] 进度回调 function(currentIndex, totalPages, canvas)
|
||||
* @returns {Promise} jQuery Deferred, resolves with array of canvas objects
|
||||
*/
|
||||
function generateImages(onProgress) {
|
||||
var deferred = $.Deferred();
|
||||
var canvases = [];
|
||||
var $pages = $container.find('.phone-image-page');
|
||||
|
||||
if ($pages.length === 0) {
|
||||
deferred.reject('没有可渲染的页面');
|
||||
return deferred.promise();
|
||||
}
|
||||
|
||||
var index = 0;
|
||||
var total = $pages.length;
|
||||
|
||||
function captureNext() {
|
||||
if (index >= total) {
|
||||
if (onProgress) onProgress(total, total, null);
|
||||
deferred.resolve(canvases);
|
||||
return;
|
||||
}
|
||||
|
||||
var $page = $($pages[index]);
|
||||
|
||||
html2canvas($page[0], {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
backgroundColor: '#ffffff',
|
||||
width: $page.outerWidth(),
|
||||
height: $page.outerHeight(),
|
||||
logging: false
|
||||
}).then(function (canvas) {
|
||||
canvases.push(canvas);
|
||||
index++;
|
||||
if (onProgress) onProgress(index, total, canvas);
|
||||
captureNext();
|
||||
}).catch(function (err) {
|
||||
deferred.reject('截图失败(第' + (index + 1) + '页): ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
if (onProgress) onProgress(0, total, null);
|
||||
captureNext();
|
||||
return deferred.promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将canvas转为base64并保存到服务端
|
||||
* @param {number} postId 文章ID
|
||||
* @param {Object} saveConfig 配置信息
|
||||
* @param {Function} [onProgress] 生成进度回调 function(current, total, canvas)
|
||||
* @returns {Promise} jQuery Deferred
|
||||
*/
|
||||
function saveImages(postId, saveConfig, onProgress) {
|
||||
var deferred = $.Deferred();
|
||||
|
||||
generateImages(onProgress).then(function (canvases) {
|
||||
var pagesData = [];
|
||||
for (var i = 0; i < canvases.length; i++) {
|
||||
pagesData.push(canvases[i].toDataURL('image/jpeg', 0.92));
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/index.php/admin/post/savePostOutput',
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
post_id: postId,
|
||||
output_type: 'phone_image',
|
||||
config: saveConfig || config,
|
||||
pages: pagesData
|
||||
}),
|
||||
contentType: 'application/json',
|
||||
success: function (result) {
|
||||
if (result.code === 0) {
|
||||
deferred.resolve(result.data);
|
||||
} else {
|
||||
deferred.reject(result.msg || '保存失败');
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
deferred.reject('网络错误: ' + xhr.statusText);
|
||||
}
|
||||
});
|
||||
}).catch(function (err) {
|
||||
deferred.reject(err);
|
||||
});
|
||||
|
||||
return deferred.promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将生成的canvas数组显示为缩略图
|
||||
* @param {Array} canvases canvas对象数组
|
||||
* @param {string} containerSelector 容器选择器
|
||||
*/
|
||||
function showGeneratedThumbnails(canvases, containerSelector) {
|
||||
var $container = $(containerSelector);
|
||||
$container.empty();
|
||||
|
||||
for (var i = 0; i < canvases.length; i++) {
|
||||
var thumb = canvases[i].toDataURL('image/jpeg', 0.5);
|
||||
var $item = $('<div class="phone-thumb-item" data-index="' + i + '">' +
|
||||
'<img src="' + thumb + '" alt="第' + (i + 1) + '页">' +
|
||||
'<span class="phone-thumb-page">' + (i + 1) + '</span>' +
|
||||
'</div>');
|
||||
$container.append($item);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 配置切换 =====
|
||||
|
||||
/**
|
||||
* 切换模板
|
||||
*/
|
||||
function switchTemplate(name) {
|
||||
config.template = name;
|
||||
if ($container) {
|
||||
$container.removeClass('tpl-minimal tpl-magazine tpl-mixed').addClass('tpl-' + name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换尺寸
|
||||
*/
|
||||
function switchSize(name) {
|
||||
config.size = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换字体
|
||||
*/
|
||||
function switchFont(name) {
|
||||
config.font = name;
|
||||
if ($container) {
|
||||
$container.removeClass('font-source-han-sans font-alibaba-puhuiti font-lxgw-wenkai').addClass('font-' + name);
|
||||
$container.css('font-family', config.fonts[name] || config.fonts['source-han-sans']);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 工具方法 =====
|
||||
|
||||
/**
|
||||
* HTML转义
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(text));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
*/
|
||||
function getConfig() {
|
||||
return $.extend({}, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分页结果
|
||||
*/
|
||||
function getPages() {
|
||||
return pages.slice();
|
||||
}
|
||||
|
||||
// ===== AI 智能排版 =====
|
||||
|
||||
/**
|
||||
* 应用AI推荐配置
|
||||
*/
|
||||
function applyAiRecommendation(recommendation) {
|
||||
if (recommendation.template) {
|
||||
switchTemplate(recommendation.template);
|
||||
$('.template-btn').removeClass('active');
|
||||
$('.template-btn[data-template="' + recommendation.template + '"]').addClass('active');
|
||||
$('[name="size"]').val(config.size);
|
||||
}
|
||||
if (recommendation.font) {
|
||||
switchFont(recommendation.font);
|
||||
$('[name="font"]').val(recommendation.font);
|
||||
}
|
||||
if (recommendation.font_size) {
|
||||
config.fontSize = parseInt(recommendation.font_size);
|
||||
$('[name="fontSize"]').val(config.fontSize);
|
||||
$('#fontSizeValue').text(config.fontSize + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用AI优化内容(替换预览用的内容)
|
||||
*/
|
||||
var originalContentHtml = '';
|
||||
function applyAiContent(optimizedContent) {
|
||||
if (!originalContentHtml) {
|
||||
originalContentHtml = postData.content_html;
|
||||
}
|
||||
if (optimizedContent.optimized_paragraphs) {
|
||||
var newHtml = '';
|
||||
for (var i = 0; i < optimizedContent.optimized_paragraphs.length; i++) {
|
||||
newHtml += '<p>' + optimizedContent.optimized_paragraphs[i] + '</p>';
|
||||
}
|
||||
postData.content_html = newHtml;
|
||||
postData.title = optimizedContent.optimized_title || postData.title;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复原始内容
|
||||
*/
|
||||
function restoreOriginalContent() {
|
||||
if (originalContentHtml) {
|
||||
postData.content_html = originalContentHtml;
|
||||
originalContentHtml = '';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 公开API =====
|
||||
return {
|
||||
init: init,
|
||||
render: render,
|
||||
paginate: render,
|
||||
generateImages: generateImages,
|
||||
saveImages: saveImages,
|
||||
showGeneratedThumbnails: showGeneratedThumbnails,
|
||||
switchTemplate: switchTemplate,
|
||||
switchSize: switchSize,
|
||||
switchFont: switchFont,
|
||||
getConfig: getConfig,
|
||||
getPages: getPages,
|
||||
applyAiRecommendation: applyAiRecommendation,
|
||||
applyAiContent: applyAiContent,
|
||||
restoreOriginalContent: restoreOriginalContent
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user