Files
ulthon_information/app/common/tools/PhoneImage.php
augushong 10879a8037 feat(output_view): 导出页面重构 - 长图卡片化展示、缩略图增大、预览优化、纯图片页原图保存
- output_view.html: 长图改为固定高度卡片(70px),Blob URL查看,缩略图minmax(280px,1fr),
  竖图预览优先填充视口高度,下载功能完整保留
- phone-image.js: renderPureImageToCanvas()使用naturalWidth/naturalHeight保持原图分辨率,
  新增长图生成和保存功能
- Post.php: 新增outputView()方法提供导出页面渲染数据
- PhoneImage.php: 图片数据改为DB存储,新增saveLongImage()方法
- phone_image.html: 添加导出页面入口按钮
- 新增数据库迁移: post_output_file表添加image_data字段
2026-05-14 23:22:19 +08:00

279 lines
8.5 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' => '{}'],
];
}
/**
* 验证配置合法性
*/
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;
}
}
// 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,
]);
}
}