Files
ulthon_information/app/common/tools/PhoneImage.php
augushong 5a81385448 feat(phone-image): 添加字号倍数控制功能
- CSS: 新增 --pi-font-scale 变量,全量 font-size 支持 calc 缩放
- JS: config.fontScale 影响分页计算,applyFontScale() 同步CSS变量
- HTML: 渲染预览区 Slider 控件(0.5x~2.0x),拖动即时预览,松手完整渲染
- 后端: PhoneImage.php 新增 fontScale 配置字段和校验
- 所有保存路径(autoSave/save/generate)包含 fontScale 持久化
2026-05-15 00:50:57 +08:00

286 lines
8.8 KiB
PHP
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.

<?php
namespace app\common\tools;
use app\model\Post;
use app\model\PostOutput;
use app\model\PostOutputFile;
use think\facade\App;
class PhoneImage implements PostOutputManagerInterface
{
/**
* 返回配置字段定义
*/
public function getConfigFields(): array
{
return [
'size' => ['type' => 'select', 'options' => ['xiaohongshu', 'douyin'], 'default' => 'xiaohongshu'],
'fontSize' => ['type' => 'number', 'default' => 14, 'min' => 10, 'max' => 24],
'watermark' => ['type' => 'text', 'default' => ''],
'pageAlignments' => ['type' => 'json', 'default' => '{}'],
'fontScale' => ['type' => 'number', 'default' => 1, 'min' => 0.5, 'max' => 2.0],
];
}
/**
* 验证配置合法性
*/
public function validateConfig(array $config): bool
{
$fields = $this->getConfigFields();
if (isset($config['size']) && !in_array($config['size'], $fields['size']['options'])) {
return false;
}
if (isset($config['fontSize'])) {
$size = intval($config['fontSize']);
if ($size < $fields['fontSize']['min'] || $size > $fields['fontSize']['max']) {
return false;
}
}
if (isset($config['fontScale'])) {
$scale = floatval($config['fontScale']);
if ($scale < 0.5 || $scale > 2.0) {
return false;
}
}
// watermark 是文本类型,无需特殊验证
// pageAlignments 是 JSON 类型,存储时由框架自动序列化
return true;
}
/**
* 执行输出处理 - 服务端接收base64数据保存文件创建记录
* 注意HTML渲染在前端完成(html2canvas),服务端只做文件保存和记录创建
*/
public function process(Post $post, array $config): array
{
return [];
}
/**
* 返回预览HTML - 预览在前端完成,服务端返回空
*/
public function getPreview(Post $post, array $config): string
{
return '';
}
/**
* 保存单页图片
* @param int $outputId 输出记录ID
* @param int $page 页码(从1开始)
* @param string $imageData base64编码的图片数据
* @return PostOutputFile|false
*/
public function savePageImage(int $outputId, int $page, string $imageData)
{
// 保留原始base64用于存储
$rawBase64 = str_replace('data:image/jpeg;base64,', '', $imageData);
$rawBase64 = str_replace('data:image/png;base64,', '', $rawBase64);
$rawBase64 = str_replace(' ', '+', $rawBase64);
$decoded = base64_decode($rawBase64);
if ($decoded === false) {
return false;
}
$imageInfo = @getimagesizefromstring($decoded);
$width = is_array($imageInfo) ? $imageInfo[0] : 0;
$height = is_array($imageInfo) ? $imageInfo[1] : 0;
$fileRecord = PostOutputFile::create([
'output_id' => $outputId,
'page' => $page,
'file_path' => '',
'file_url' => '',
'file_size' => strlen($decoded),
'width' => $width,
'height' => $height,
'image_data' => $rawBase64,
]);
return $fileRecord;
}
/**
* 保存长图
* @param int $outputId 输出记录ID
* @param string $imageData base64编码的长图数据
* @return PostOutputFile|false
*/
public function saveLongImage(int $outputId, string $imageData)
{
$rawBase64 = str_replace('data:image/jpeg;base64,', '', $imageData);
$rawBase64 = str_replace('data:image/png;base64,', '', $rawBase64);
$rawBase64 = str_replace(' ', '+', $rawBase64);
$decoded = base64_decode($rawBase64);
if ($decoded === false) {
return false;
}
$imageInfo = @getimagesizefromstring($decoded);
$width = is_array($imageInfo) ? $imageInfo[0] : 0;
$height = is_array($imageInfo) ? $imageInfo[1] : 0;
// 先删除该output_id下已有的长图(page=0)
PostOutputFile::where('output_id', $outputId)
->where('page', 0)
->delete();
$fileRecord = PostOutputFile::create([
'output_id' => $outputId,
'page' => 0,
'file_path' => '',
'file_url' => '',
'file_size' => strlen($decoded),
'width' => $width,
'height' => $height,
'image_data' => $rawBase64,
]);
return $fileRecord;
}
/**
* 打包某条输出记录的所有图片为ZIP
* @param int $outputId
* @return string ZIP文件路径
* @throws \Exception
*/
public function createZip(int $outputId): string
{
$output = PostOutput::find($outputId);
if (!$output) {
throw new \Exception('输出记录不存在');
}
$files = PostOutputFile::where('output_id', $outputId)
->order('page', 'asc')
->select();
if ($files->isEmpty()) {
throw new \Exception('没有可下载的文件');
}
$tempDir = App::getRuntimePath() . 'temp';
if (!is_dir($tempDir)) {
mkdir($tempDir, 0777, true);
}
$zipFileName = 'post_output_' . $outputId . '.zip';
$zipPath = $tempDir . '/' . $zipFileName;
$tempFiles = [];
try {
$zip = new \ZipArchive();
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
throw new \Exception('无法创建ZIP文件');
}
foreach ($files as $file) {
$pageName = str_pad((string) $file->page, 2, '0', STR_PAD_LEFT) . '.jpg';
if (!empty($file->image_data)) {
$decoded = base64_decode($file->image_data);
if ($decoded !== false) {
$tempFilePath = $tempDir . '/page_' . $file->page . '_' . $outputId . '.jpg';
file_put_contents($tempFilePath, $decoded);
$tempFiles[] = $tempFilePath;
$zip->addFile($tempFilePath, $pageName);
}
} else {
$filePath = App::getRootPath() . '/public' . $file->file_path;
if (file_exists($filePath)) {
$zip->addFile($filePath, $pageName);
}
}
}
$zip->close();
} finally {
foreach ($tempFiles as $tempFile) {
if (file_exists($tempFile)) {
@unlink($tempFile);
}
}
}
return $zipPath;
}
/**
* 删除输出记录及所有关联文件
* @param int $outputId
* @return bool
*/
public function deleteOutput(int $outputId): bool
{
$files = PostOutputFile::where('output_id', $outputId)->select();
foreach ($files as $file) {
$filePath = App::getRootPath() . '/public' . $file->file_path;
if (file_exists($filePath)) {
@unlink($filePath);
}
}
PostOutputFile::where('output_id', $outputId)->delete();
PostOutput::destroy($outputId);
return true;
}
/**
* 创建输出记录
* @param int $postId 文章ID
* @param array $config 配置
* @param int $adminId 管理员ID
* @return PostOutput
*/
public function createOutput(int $postId, array $config, int $adminId): PostOutput
{
return PostOutput::create([
'post_id' => $postId,
'output_type' => 'phone_image',
'config' => $config,
'status' => PostOutput::STATUS_PROCESSING,
'page_count' => 0,
'admin_id' => $adminId,
]);
}
/**
* 完成输出记录 - 更新状态和页数
*/
public function completeOutput(int $outputId, int $pageCount): bool
{
return PostOutput::where('id', $outputId)->update([
'status' => PostOutput::STATUS_COMPLETED,
'page_count' => $pageCount,
]) > 0;
}
/**
* 仅保存配置(不生成图片),创建新的历史记录
* @param int $postId 文章ID
* @param array $config 配置(含 size/fontSize/watermark/pageAlignments/content_html)
* @param int $adminId 管理员ID
* @return PostOutput
*/
public static function saveConfigOnly(int $postId, array $config, int $adminId): PostOutput
{
return PostOutput::create([
'post_id' => $postId,
'output_type' => 'phone_image',
'config' => $config,
'status' => PostOutput::STATUS_PROCESSING,
'page_count' => 0,
'admin_id' => $adminId,
]);
}
}