mirror of
https://gitee.com/ulthon/ulthon_information.git
synced 2026-07-01 16:12:47 +08:00
- 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 持久化
286 lines
8.8 KiB
PHP
286 lines
8.8 KiB
PHP
<?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,
|
||
]);
|
||
}
|
||
}
|