Claude Code 基于 VUE + KonvaJS 实现海报生成器(附源码)

接上文,光说不练假把式,直接动手,让 Claude Code 根据我提供的海报图片,基于 VUE + KonvaJS,实现海报生成器!


  • 先把机会交给Trae(免费版,不中用,要排队!!),果断弃用!
  • 再用➡️ anyrouter 试试,最近发现又能用了!坚持签到,每天送 $25~ 真香!

可能是需求过于简单,很快就完工了,先看效果!

  1. 卖家秀:

  1. 买家秀:

  1. 海报生成器界面(花费$5):

大模型是真爱用紫色做背景。。。


  1. 代码分享
javascript 复制代码
<script>
import QRCode from 'qrcode'
import Konva from 'konva'

export default {
  name: 'PosterGenerator',
  data() {
    return {
      posterData: {
        username: '不老刘',
        continueDays: '1',
        studyHours: '5h',
        todayStudy: '14m',
        motivationEn: 'Pains make stronger,\ntears make braver,and\nheartbreaks make wiser.',
        motivationCn: '伤痛铸坚强,眼泪淬勇敢,心碎炼智慧。',
        qrUrl: 'https://julebu.co'
      },
      canDownload: false,
      displaySize: { width: 375, height: 667 },
      exportSize: { width: 1600, height: 2600 },
      backgroundImage: null,
      qrCodeImage: null,
      avatarImage: null,
      avatarPreviewUrl: null,
      currentGradient: null,
      Konva
    }
  },
  methods: {
    scale(value) {
      return value * (this.displaySize.width / 375)
    },
    
    scaleForExport(value) {
      return value * (this.exportSize.width / 375)
    },
    
    getCurrentDate() {
      const now = new Date()
      const months = [
        'January', 'February', 'March', 'April', 'May', 'June',
        'July', 'August', 'September', 'October', 'November', 'December'
      ]
      return `${months[now.getMonth()]} ${now.getDate()}, ${now.getFullYear()}`
    },
    
    async handleImageUpload(event) {
      const file = event.target.files[0]
      if (file) {
        const img = new Image()
        img.onload = () => {
          this.backgroundImage = img
          this.currentGradient = null
          this.generatePoster()
        }
        img.src = URL.createObjectURL(file)
      }
    },
    
    async handleAvatarUpload(event) {
      const file = event.target.files[0]
      if (file) {
        const img = new Image()
        img.onload = () => {
          this.avatarImage = img
          this.generatePoster()
        }
        const url = URL.createObjectURL(file)
        this.avatarPreviewUrl = url
        img.src = url
      }
    },
    
    async selectPresetImage(imageName) {
      const img = new Image()
      img.onload = () => {
        this.backgroundImage = img
        this.generatePoster()
      }
      img.src = `/${imageName}`
    },
    
    setGradientBackground(type) {
      this.currentGradient = type
      this.backgroundImage = null
      this.generatePoster()
    },
    
    getGradientConfig() {
      const gradients = {
        sunset: [0, '#ff9a9e', 0.5, '#fecfef', 1, '#fecfef'],
        ocean: [0, '#667eea', 0.5, '#764ba2', 1, '#667eea'],
        forest: [0, '#11998e', 0.5, '#38ef7d', 1, '#11998e'],
        purple: [0, '#a8edea', 0.5, '#fed6e3', 1, '#a8edea'],
        warm: [0, '#ffecd2', 0.5, '#fcb69f', 1, '#ffecd2']
      }
      
      return {
        x: 0,
        y: 0,
        width: this.displaySize.width,
        height: this.displaySize.height,
        fillLinearGradientStartPoint: { x: 0, y: 0 },
        fillLinearGradientEndPoint: { x: this.displaySize.width, y: this.displaySize.height },
        fillLinearGradientColorStops: gradients[this.currentGradient] || gradients.sunset
      }
    },
    
    async generateQRCode() {
      try {
        const canvas = document.createElement('canvas')
        await QRCode.toCanvas(canvas, this.posterData.qrUrl, {
          width: 200,
          margin: 2,
          color: {
            dark: '#000000',
            light: '#FFFFFF'
          }
        })
        
        const img = new Image()
        img.onload = () => {
          this.qrCodeImage = img
          this.$nextTick(() => {
            this.$refs.layer.getNode().batchDraw()
          })
        }
        img.src = canvas.toDataURL()
      } catch (error) {
        console.error('生成二维码失败:', error)
      }
    },
    
    async generatePoster() {
      await this.generateQRCode()
      this.canDownload = true
      this.$nextTick(() => {
        this.$refs.layer.getNode().batchDraw()
      })
    },
    
    async downloadPoster() {
      if (!this.canDownload) return
      
      // 创建高分辨率的舞台用于导出
      const exportStage = new Konva.Stage({
        container: document.createElement('div'),
        width: this.exportSize.width,
        height: this.exportSize.height
      })
      
      const exportLayer = new Konva.Layer()
      exportStage.add(exportLayer)
      
      // 重新创建所有元素但使用导出尺寸
      const scaleRatio = this.exportSize.width / 375
      
      // 背景图片或渐变
      if (this.backgroundImage) {
        exportLayer.add(new Konva.Image({
          x: 0,
          y: 0,
          width: this.exportSize.width,
          height: this.exportSize.height,
          image: this.backgroundImage,
          filters: [Konva.Filters.Blur],
          blurRadius: 30
        }))
      } else if (this.currentGradient) {
        const gradients = {
          sunset: [0, '#ff9a9e', 0.5, '#fecfef', 1, '#fecfef'],
          ocean: [0, '#667eea', 0.5, '#764ba2', 1, '#667eea'],
          forest: [0, '#11998e', 0.5, '#38ef7d', 1, '#11998e'],
          purple: [0, '#a8edea', 0.5, '#fed6e3', 1, '#a8edea'],
          warm: [0, '#ffecd2', 0.5, '#fcb69f', 1, '#ffecd2']
        }
        
        exportLayer.add(new Konva.Rect({
          x: 0,
          y: 0,
          width: this.exportSize.width,
          height: this.exportSize.height,
          fillLinearGradientStartPoint: { x: 0, y: 0 },
          fillLinearGradientEndPoint: { x: this.exportSize.width, y: this.exportSize.height },
          fillLinearGradientColorStops: gradients[this.currentGradient] || gradients.sunset
        }))
      } else {
        exportLayer.add(new Konva.Rect({
          x: 0,
          y: 0,
          width: this.exportSize.width,
          height: this.exportSize.height,
          fillLinearGradientStartPoint: { x: 0, y: 0 },
          fillLinearGradientEndPoint: { x: this.exportSize.width, y: this.exportSize.height },
          fillLinearGradientColorStops: [0, '#667eea', 1, '#764ba2']
        }))
      }
      
      // 背景覆盖层
      exportLayer.add(new Konva.Rect({
        x: 0,
        y: 0,
        width: this.exportSize.width,
        height: this.exportSize.height,
        fillLinearGradientStartPoint: { x: 0, y: 0 },
        fillLinearGradientEndPoint: { x: 0, y: this.exportSize.height },
        fillLinearGradientColorStops: [0, 'rgba(0,0,0,0.3)', 1, 'rgba(0,0,0,0.6)']
      }))
      
      // 用户头像
      if (this.avatarImage) {
        // 头像图片(裁剪为圆形)
        exportLayer.add(new Konva.Image({
          x: 35 * scaleRatio,
          y: 55 * scaleRatio,
          width: 50 * scaleRatio,
          height: 50 * scaleRatio,
          image: this.avatarImage,
          clipFunc: (ctx) => {
            ctx.arc(60 * scaleRatio, 80 * scaleRatio, 25 * scaleRatio, 0, Math.PI * 2, false)
          }
        }))
      } else {
        // 默认头像(风景渐变圆形)
        exportLayer.add(new Konva.Circle({
          x: 60 * scaleRatio,
          y: 80 * scaleRatio,
          radius: 25 * scaleRatio,
          fillLinearGradientStartPoint: { x: 35 * scaleRatio, y: 55 * scaleRatio },
          fillLinearGradientEndPoint: { x: 85 * scaleRatio, y: 105 * scaleRatio },
          fillLinearGradientColorStops: [0, '#87CEEB', 0.3, '#98FB98', 0.6, '#F0E68C', 1, '#DDA0DD']
        }))
      }
      
      // 头像边框
      exportLayer.add(new Konva.Circle({
        x: 60 * scaleRatio,
        y: 80 * scaleRatio,
        radius: 25 * scaleRatio,
        fill: 'transparent',
        stroke: '#fff',
        strokeWidth: 2 * scaleRatio
      }))
      
      // 用户名
      exportLayer.add(new Konva.Text({
        x: 100 * scaleRatio,
        y: 60 * scaleRatio,
        text: this.posterData.username,
        fontSize: 24 * scaleRatio,
        fontFamily: 'Arial',
        fill: '#fff',
        fontStyle: 'bold'
      }))
      
      // 日期
      exportLayer.add(new Konva.Text({
        x: 100 * scaleRatio,
        y: 85 * scaleRatio,
        text: this.getCurrentDate(),
        fontSize: 16 * scaleRatio,
        fontFamily: 'Arial',
        fill: '#fff',
        opacity: 0.8
      }))
      
      // 右上角标签
      exportLayer.add(new Konva.Text({
        x: 250 * scaleRatio,
        y: 60 * scaleRatio,
        text: '句乐部 | 英语学习',
        fontSize: 14 * scaleRatio,
        fontFamily: 'Arial',
        fill: '#fff',
        opacity: 0.9
      }))
      
      // 数据卡片
      const cardPositions = [{x: 30, y: 140}, {x: 142, y: 140}, {x: 254, y: 140}]
      const cardData = [
        {value: this.posterData.continueDays, label: '连续打卡天数'},
        {value: this.posterData.studyHours, label: '总学习时长'},
        {value: this.posterData.todayStudy, label: '今日学习'}
      ]
      
      cardPositions.forEach((pos, index) => {
        // 卡片背景
        exportLayer.add(new Konva.Rect({
          x: pos.x * scaleRatio,
          y: pos.y * scaleRatio,
          width: 90 * scaleRatio,
          height: 80 * scaleRatio,
          fill: 'rgba(255,255,255,0.25)',
          cornerRadius: 15 * scaleRatio
        }))
        
        // 数值
        exportLayer.add(new Konva.Text({
          x: (pos.x + 45) * scaleRatio,
          y: (pos.y + 15) * scaleRatio,
          text: cardData[index].value,
          fontSize: 32 * scaleRatio,
          fontFamily: 'Arial',
          fill: '#fff',
          fontStyle: 'bold',
          align: 'center',
          width: 90 * scaleRatio,
          offsetX: 45 * scaleRatio
        }))
        
        // 标签
        exportLayer.add(new Konva.Text({
          x: (pos.x + 45) * scaleRatio,
          y: (pos.y + 50) * scaleRatio,
          text: cardData[index].label,
          fontSize: 11 * scaleRatio,
          fontFamily: 'Arial',
          fill: '#fff',
          align: 'center',
          width: 90 * scaleRatio,
          offsetX: 45 * scaleRatio,
          opacity: 0.9
        }))
      })
      
      // 励志语句背景
      exportLayer.add(new Konva.Rect({
        x: 30 * scaleRatio,
        y: 250 * scaleRatio,
        width: 315 * scaleRatio,
        height: 160 * scaleRatio,
        fill: 'rgba(255,255,255,0.2)',
        cornerRadius: 20 * scaleRatio
      }))
      
      // 英文励志语句
      exportLayer.add(new Konva.Text({
        x: 50 * scaleRatio,
        y: 270 * scaleRatio,
        text: this.posterData.motivationEn,
        fontSize: 18 * scaleRatio,
        fontFamily: 'Georgia, serif',
        fill: '#fff',
        width: 275 * scaleRatio,
        lineHeight: 1.5,
        fontStyle: 'italic'
      }))
      
      // 中文励志语句
      exportLayer.add(new Konva.Text({
        x: 50 * scaleRatio,
        y: 350 * scaleRatio,
        text: this.posterData.motivationCn,
        fontSize: 16 * scaleRatio,
        fontFamily: 'Microsoft YaHei, Arial',
        fill: '#fff',
        width: 275 * scaleRatio,
        lineHeight: 1.6,
        opacity: 0.95
      }))
      
      // 底部品牌区域背景
      exportLayer.add(new Konva.Rect({
        x: 30 * scaleRatio,
        y: 450 * scaleRatio,
        width: 315 * scaleRatio,
        height: 120 * scaleRatio,
        fill: 'rgba(0,0,0,0.5)',
        cornerRadius: 15 * scaleRatio
      }))
      
      // 品牌名称
      exportLayer.add(new Konva.Text({
        x: 50 * scaleRatio,
        y: 480 * scaleRatio,
        text: '句乐部',
        fontSize: 28 * scaleRatio,
        fontFamily: 'Microsoft YaHei, Arial',
        fill: '#fff',
        fontStyle: 'bold'
      }))
      
      // 网站地址
      exportLayer.add(new Konva.Text({
        x: 50 * scaleRatio,
        y: 515 * scaleRatio,
        text: 'julebu.co',
        fontSize: 14 * scaleRatio,
        fontFamily: 'Arial',
        fill: '#fff',
        opacity: 0.9
      }))
      
      // 描述文字
      exportLayer.add(new Konva.Text({
        x: 50 * scaleRatio,
        y: 535 * scaleRatio,
        text: '用游戏化的形式通过句子学英语',
        fontSize: 12 * scaleRatio,
        fontFamily: 'Microsoft YaHei, Arial',
        fill: '#fff',
        opacity: 0.8
      }))
      
      // 二维码
      if (this.qrCodeImage) {
        exportLayer.add(new Konva.Image({
          x: 280 * scaleRatio,
          y: 475 * scaleRatio,
          width: 55 * scaleRatio,
          height: 55 * scaleRatio,
          image: this.qrCodeImage
        }))
      }
      
      const dataURL = exportStage.toDataURL({
        mimeType: 'image/png',
        quality: 1.0,
        pixelRatio: 1
      })
      
      const link = document.createElement('a')
      link.download = `英语学习海报_${this.posterData.username}_${new Date().getTime()}.png`
      link.href = dataURL
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
      
      exportStage.destroy()
    },
    
    handleStageMouseDown() {
      // 处理舞台点击事件
    }
  },
  
  mounted() {
    // 默认使用暖色渐变
    this.setGradientBackground('warm')
  }
}
</script>

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

This is a Vue 3 + KonvaJS poster generator application that creates English learning progress posters. The app allows users to generate personalized study posters with customizable backgrounds, avatars, study statistics, and motivational quotes, then export them as high-resolution images.

Development Commands

bash 复制代码
# Install dependencies
npm install

# Start development server (runs on port 3000)
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview

Architecture

Core Technology Stack

  • Vue 3: Main framework with Composition API
  • KonvaJS + vue-konva: Canvas-based graphics rendering for poster generation
  • Vite: Build tool and dev server
  • QRCode library: QR code generation

Application Structure

The app is a single-page application with one main component:

  • src/App.vue: Root component with global styles and layout
  • src/components/PosterGenerator.vue: Main poster generation component
  • src/main.js: App entry point with VueKonva plugin registration

Key Architecture Patterns

Dual Canvas System

The app uses a dual-canvas approach for optimal performance:

  • Display Canvas: 375x667 resolution for real-time preview
  • Export Canvas: 1600x2600 resolution for high-quality downloads
  • Scaling system converts coordinates between display and export resolutions
Poster Generation Pipeline
  1. Data Collection: Form inputs for user data, background selection, avatar upload
  2. Canvas Rendering: KonvaJS renders all elements (background, avatar, text, QR code)
  3. Export Process: Creates separate high-resolution canvas for download
Background System
  • Gradient Backgrounds: 5 preset gradients (sunset, ocean, forest, purple, warm)
  • Image Backgrounds: User-uploaded images with automatic blur filter
  • Background Priority: Uploaded images override gradients
Avatar Handling
  • Default Avatar: Circular gradient when no avatar uploaded
  • Uploaded Avatar: Circular clipping mask applied to user images
  • Consistent Styling: White border and circular shape for all avatars

Component Data Structure

javascript 复制代码
posterData: {
  username: String,
  continueDays: String,
  studyHours: String, 
  todayStudy: String,
  motivationEn: String,  // English quote
  motivationCn: String,  // Chinese quote
  qrUrl: String
}

Text Positioning System

All text elements use a scaling system based on 375px base width:

  • scale(value): Converts base coordinates to display size
  • scaleForExport(value): Converts base coordinates to export size
  • Text centering uses width + offsetX properties for proper alignment

Critical Implementation Details

QR Code Integration
  • QR codes are generated as canvas elements using the qrcode library
  • Converted to Image objects for KonvaJS rendering
  • Positioned in bottom-right of poster with consistent sizing
Export Functionality
  • Creates new KonvaJS stage at export resolution
  • Recreates all visual elements with scaled coordinates
  • Exports as PNG with maximum quality settings
  • Automatically downloads with timestamped filename
Image Processing
  • Background images receive blur filter (blurRadius: 8 for display, 30 for export)
  • Avatar images use circular clipping functions
  • All images loaded asynchronously with proper error handling

File Organization

Sample poster images (1.png - 5.png) serve as design references and demonstrate the target visual style. These should not be used as backgrounds but can guide styling decisions.

相关推荐
云安全助手8 小时前
2026年企业级Claude中转服务深度评测:安全、稳定与速度的终极答案
人工智能·安全·claude·ai大模型
yaocheng的ai分身9 小时前
【转载】Scaling Managed Agents:将“大脑”和“手”解耦
claude
xcLeigh11 小时前
聚合AI工具KULAAI:GPT、Claude、Gemini、DeepSeek热门模型一键使用
人工智能·gpt·claude·gemini·deepseek·聚合ai·kulaai
DO_Community13 小时前
为AI编程降本!OpenCode 原生支持 DigitalOcean 推理路由器
智能路由器·ai编程·claude
Bigger13 小时前
mini-cc 的 MCP 协议:给 AI 装个 USB-C 接口
人工智能·ai编程·claude
ZzT13 小时前
Claude Code 把编排写进代码:Dynamic Workflows 详解
claude
创世宇图14 小时前
Claude Opus 4.8 深度实测:动态多 Agent 协同、Effort Control 与幻觉抑制的工程化解析
ai·llm·agent·claude·ai工程化
Curvatureflight14 小时前
前端国际化 i18n 落地实践:语言包、动态文案和格式化问题怎么处理?
前端·c++·vue
m0_5358175516 小时前
macOS上Claude Code安装配置保姆级教程:国内直连API,从0到1跑通(附避坑指南)
gpt·macos·ai·node.js·claude·claudecode·88api
沉默王二18 小时前
每月13亿免费Token,14家AI大厂的API任你用,包括Gemini
github·claude·gemini