AI一键生成文章封面deepseek

自动生成文章封面图片功能实现

最近给博客后台加了个功能:写文章时,可以根据文章标题自动生成封面图片。支持 4 种风格(渐变、科技、简约、暗黑),生成后自动上传到服务器。不用自己找图片了,AI 帮你生成。

一、功能需求

  • 后台写文章时,在缩略图上传区域添加「AI 生成封面」按钮
  • 支持 4 种风格选择:渐变、科技、简约、暗黑
  • 根据文章标题自动绘制 800x450 的封面图
  • 支持中文自动换行
  • 生成后自动上传到服务器

二、技术选型

  • 前端:Canvas API + Blob API(纯前端实现,不需要后端 API)
  • 上传:通过现有的封面上传接口上传

三、实现思路

这个功能是纯前端实现,不需要后端 API。用 Canvas 绘制封面图,然后转为 Blob 上传。

四、前端实现

1. 页面添加按钮和风格选择

views/blog/article/write.vue 中:

vue 复制代码
<template>
    <div class="app-container">
        <!-- ... 文章编辑区域 ... -->

        <!-- 发布或修改对话框 -->
        <el-dialog title="发布文章" v-model="addOrUpdate" width="600px" top="0.5vh" append-to-body>
            <el-form ref="articleFormRef" label-width="80px" :model="articleForm" :rules="rules">
                <!-- 缩略图 -->
                <el-form-item label="缩略图" prop="articleCover">
                    <div style="display: flex; flex-direction: column; gap: 8px">
                        <el-upload drag :show-file-list="false" :headers="authorization"
                                   action="/api/admin/article/upload" accept="image/*"
                                   :before-upload="beforeUpload" :on-success="handleSuccess">
                            <el-icon class="el-icon--upload" v-if="articleForm.articleCover === ''">
                                <upload-filled />
                            </el-icon>
                            <div class="el-upload__text" v-if="articleForm.articleCover === ''">
                                将文件拖到此处,或<em>点击上传</em>
                            </div>
                            <img v-else :src="articleForm.articleCover" width="360" />
                        </el-upload>
                        <!-- AI 生成封面 -->
                        <div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap">
                            <el-button type="primary" :loading="coverLoading" :icon="MagicStick" 
                                       @click="handleGenerateCover">
                                {{ coverLoading ? "生成中..." : "AI 生成封面" }}
                            </el-button>
                            <el-radio-group v-model="coverStyle" size="small">
                                <el-radio-button label="gradient">渐变</el-radio-button>
                                <el-radio-button label="tech">科技</el-radio-button>
                                <el-radio-button label="minimal">简约</el-radio-button>
                                <el-radio-button label="dark">暗黑</el-radio-button>
                            </el-radio-group>
                        </div>
                    </div>
                </el-form-item>

                <!-- ... 其他表单项 ... -->
            </el-form>
        </el-dialog>

        <!-- 隐藏的 Canvas 用于生成封面 -->
        <canvas ref="coverCanvas" width="800" height="450" style="display: none"></canvas>
    </div>
</template>

<script setup lang="ts">
import { uploadArticleCover } from "@/api/article";
import { MagicStick } from '@element-plus/icons-vue';
import { ElMessage } from "element-plus";
import { UploadFilled } from '@element-plus/icons-vue';

// 文章表单数据
const articleForm = ref({
    articleTitle: "",
    articleCover: "",
    // ... 其他字段
});

// AI 生成封面相关
const coverCanvas = ref<HTMLCanvasElement>();
const coverStyle = ref("gradient");
const coverLoading = ref(false);

// 封面风格配置
const COVER_STYLES: Record<string, { bg: string[]; textColor: string; subColor: string }> = {
    gradient: {
        bg: ["#667eea", "#764ba2"],
        textColor: "#ffffff",
        subColor: "rgba(255,255,255,0.7)",
    },
    tech: {
        bg: ["#0f2027", "#2c5364"],
        textColor: "#00d2ff",
        subColor: "rgba(0,210,255,0.5)",
    },
    minimal: {
        bg: ["#ffecd2", "#fcb69f"],
        textColor: "#2d3436",
        subColor: "rgba(45,52,54,0.5)",
    },
    dark: {
        bg: ["#232526", "#414345"],
        textColor: "#f5f6fa",
        subColor: "rgba(245,246,250,0.5)",
    },
};

// 生成封面
const handleGenerateCover = async () => {
    const title = articleForm.value.articleTitle;
    if (!title || title.trim() === "") {
        ElMessage.warning("请先输入文章标题");
        return;
    }
    const canvas = coverCanvas.value;
    if (!canvas) return;

    coverLoading.value = true;
    try {
        const ctx = canvas.getContext("2d")!;
        const style = COVER_STYLES[coverStyle.value] || COVER_STYLES.gradient;
        const w = canvas.width;
        const h = canvas.height;

        // 绘制渐变背景
        const gradient = ctx.createLinearGradient(0, 0, w, h);
        gradient.addColorStop(0, style.bg[0]);
        gradient.addColorStop(1, style.bg[1]);
        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, w, h);

        // 绘制装饰元素(半透明圆形)
        ctx.globalAlpha = 0.1;
        for (let i = 0; i < 5; i++) {
            ctx.beginPath();
            ctx.arc(
                Math.random() * w,
                Math.random() * h,
                Math.random() * 120 + 40,
                0,
                Math.PI * 2
            );
            ctx.fillStyle = "#ffffff";
            ctx.fill();
        }
        ctx.globalAlpha = 1;

        // 绘制标题文字(自动换行)
        const maxWidth = w - 100;
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";

        let fontSize = 42;
        ctx.font = `bold ${fontSize}px "Microsoft YaHei", "PingFang SC", sans-serif`;
        const lines = wrapText(ctx, title, maxWidth);

        ctx.fillStyle = style.textColor;
        const lineHeight = fontSize * 1.4;
        const startY = h / 2 - ((lines.length - 1) * lineHeight) / 2;
        lines.forEach((line, i) => {
            ctx.fillText(line, w / 2, startY + i * lineHeight);
        });

        // 绘制底部日期
        ctx.font = `16px "Microsoft YaHei", "PingFang SC", sans-serif`;
        ctx.fillStyle = style.subColor;
        const date = new Date().toISOString().slice(0, 10);
        ctx.fillText(date, w / 2, h - 40);

        // 转为 Blob 并上传
        canvas.toBlob(async (blob) => {
            if (!blob) {
                ElMessage.error("封面生成失败");
                coverLoading.value = false;
                return;
            }
            const file = new File([blob], `cover-${Date.now()}.png`, { type: "image/png" });
            const formData = new FormData();
            formData.append("file", file);
            try {
                const { data } = await uploadArticleCover(formData);
                if (data.flag) {
                    articleForm.value.articleCover = data.data;
                    ElMessage.success("封面生成并上传成功");
                } else {
                    ElMessage.error("封面上传失败");
                }
            } catch {
                ElMessage.error("封面上传失败");
            } finally {
                coverLoading.value = false;
            }
        }, "image/png");
    } catch {
        ElMessage.error("封面生成异常");
        coverLoading.value = false;
    }
};

// 文字自动换行
const wrapText = (ctx: CanvasRenderingContext2D, text: string, maxWidth: number): string[] => {
    const lines: string[] = [];
    let currentLine = "";
    for (const char of text) {
        const testLine = currentLine + char;
        if (ctx.measureText(testLine).width > maxWidth) {
            lines.push(currentLine);
            currentLine = char;
        } else {
            currentLine = testLine;
        }
    }
    if (currentLine) lines.push(currentLine);
    return lines.length > 3 ? lines.slice(0, 3) : lines; // 最多 3 行
};

// 上传成功回调
const handleSuccess = (response: any) => {
    articleForm.value.articleCover = response.data;
};
</script>

2. API 函数

确保 api/article/index.ts 中有封面上传函数:

typescript 复制代码
/**
 * 上传文章图片
 * @returns 图片链接
 */
export function uploadArticleCover(data: FormData): AxiosPromise<Result<string>> {
  return request({
    url: "/admin/article/upload",
    headers: { "content-type": "multipart/form-data" },
    method: "post",
    data,
  });
}

五、使用效果

  1. 在后台写文章页面,输入文章标题
  2. 选择封面风格(渐变、科技、简约、暗黑)
  3. 点击「AI 生成封面」按钮
  4. 等待几秒(按钮显示「生成中...」)
  5. Canvas 自动绘制 800x450 的封面图
  6. 生成后自动上传到服务器
  7. 封面 URL 自动填入表单

六、遇到的问题

1. 中文换行问题

问题 :Canvas 的 measureText 对中文字符宽度计算可能不准确,导致换行位置不对。

解决 :逐字符判断宽度,超过 maxWidth 就换行。限制最多 3 行,超出部分截断。

2. Canvas 字体问题

问题:不同系统可能没有指定的字体,导致渲染效果不一致。

解决 :使用通用字体栈:"Microsoft YaHei", "PingFang SC", sans-serif,Windows 用微软雅黑,Mac 用苹方,都没有就用系统默认无衬线字体。

3. Blob 上传问题

问题:Canvas 转为 Blob 后,需要通过 FormData 上传,但可能格式不对。

解决 :用 canvas.toBlob() 转为 Blob,再创建 File 对象,通过 FormData 上传。确保 Content-Typeimage/png

4. 图片质量

问题:Canvas 绘制的图片可能质量不够高。

解决:Canvas 尺寸设置为 800x450(足够清晰),绘制时使用合适的字体大小和行高,确保文字清晰可读。

七、总结

这个功能实现起来不算复杂:

  1. 前端:用 Canvas API 绘制封面图,支持渐变背景、文字、装饰元素
  2. 上传:Canvas 转为 Blob,通过现有的封面上传接口上传

关键是:

  • 定义好 4 种风格的配置(背景色、文字色、辅助色)
  • 实现文字自动换行(逐字符判断宽度)
  • Canvas 转为 Blob 并上传
  • 处理错误情况,给用户友好提示

现在写文章时,封面可以一键生成,省了不少时间。4 种风格可以满足不同需求,如果生成的不满意,也可以手动上传其他图片。

八、完整代码文件清单

前端

  • views/blog/article/write.vue - 添加封面生成按钮、Canvas 绘制逻辑和上传逻辑
  • api/article/index.ts - 确保有 uploadArticleCover 函数

后端

  • 不需要新增代码,使用现有的封面上传接口即可

按照上面的步骤,就可以在自己的项目中实现这个功能了。

相关推荐
mCell5 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell6 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
九.九6 小时前
ops-transformer:AI 处理器上的高性能 Transformer 算子库
人工智能·深度学习·transformer
春日见6 小时前
拉取与合并:如何让个人分支既包含你昨天的修改,也包含 develop 最新更新
大数据·人工智能·深度学习·elasticsearch·搜索引擎
恋猫de小郭6 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清6 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
deephub6 小时前
Agent Lightning:微软开源的框架无关 Agent 训练方案,LangChain/AutoGen 都能用
人工智能·microsoft·langchain·大语言模型·agent·强化学习
萧曵 丶6 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
银烛木6 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076606 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3