This commit is contained in:
Zyronon
2025-11-22 00:43:51 +08:00
parent 20c20ab3e4
commit 1347aa3065
4 changed files with 155 additions and 162 deletions

3
components.d.ts vendored
View File

@@ -43,6 +43,7 @@ declare module 'vue' {
IconFluentArrowBounce20Regular: typeof import('~icons/fluent/arrow-bounce20-regular')['default']
IconFluentArrowCircleRight16Regular: typeof import('~icons/fluent/arrow-circle-right16-regular')['default']
IconFluentArrowClockwise20Regular: typeof import('~icons/fluent/arrow-clockwise20-regular')['default']
IconFluentArrowDownload20Regular: typeof import('~icons/fluent/arrow-download20-regular')['default']
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
IconFluentArrowRepeatAll20Regular: typeof import('~icons/fluent/arrow-repeat-all20-regular')['default']
@@ -64,6 +65,7 @@ declare module 'vue' {
IconFluentChevronLeft20Filled: typeof import('~icons/fluent/chevron-left20-filled')['default']
IconFluentChevronLeft28Filled: typeof import('~icons/fluent/chevron-left28-filled')['default']
IconFluentClock20Regular: typeof import('~icons/fluent/clock20-regular')['default']
IconFluentCopy20Regular: typeof import('~icons/fluent/copy20-regular')['default']
IconFluentCrown20Regular: typeof import('~icons/fluent/crown20-regular')['default']
IconFluentDatabasePerson20Regular: typeof import('~icons/fluent/database-person20-regular')['default']
IconFluentDelete20Regular: typeof import('~icons/fluent/delete20-regular')['default']
@@ -115,6 +117,7 @@ declare module 'vue' {
IconIconParkOutlineAddMusic: typeof import('~icons/icon-park-outline/add-music')['default']
IconIxWechatLogo: typeof import('~icons/ix/wechat-logo')['default']
IconMaterialSymbolsMail: typeof import('~icons/material-symbols/mail')['default']
IconMdiSparkles: typeof import('~icons/mdi/sparkles')['default']
IconPhExportLight: typeof import('~icons/ph/export-light')['default']
IconRiTwitterFill: typeof import('~icons/ri/twitter-fill')['default']
IconSimpleIconsGithub: typeof import('~icons/simple-icons/github')['default']

View File

@@ -45,6 +45,7 @@
"@iconify-json/icon-park-solid": "^1.2.4",
"@iconify-json/ix": "^1.2.10",
"@iconify-json/material-symbols": "^1.2.33",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/oui": "^1.2.6",
"@iconify-json/ph": "^1.2.2",
"@iconify-json/qlementine-icons": "^1.2.11",

10
pnpm-lock.yaml generated
View File

@@ -84,6 +84,9 @@ importers:
'@iconify-json/material-symbols':
specifier: ^1.2.33
version: 1.2.33
'@iconify-json/mdi':
specifier: ^1.2.3
version: 1.2.3
'@iconify-json/oui':
specifier: ^1.2.6
version: 1.2.6
@@ -535,6 +538,9 @@ packages:
'@iconify-json/material-symbols@1.2.33':
resolution: {integrity: sha512-Bs0X1+/vpJydW63olrGh60zkR8/Y70sI14AIWaP7Z6YQXukzWANH4q3I0sIPklbIn1oL6uwLvh0zQyd6Vh79LQ==}
'@iconify-json/mdi@1.2.3':
resolution: {integrity: sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==}
'@iconify-json/oui@1.2.6':
resolution: {integrity: sha512-dBqxbLKztTtb0Cq3kEyLeYAdyJT2un+FzIZB0ei3busps/OwNIHjqowsVqPwRtHXiXTjiwOHUPbxgcVB0SCIsQ==}
@@ -4181,6 +4187,10 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/mdi@1.2.3':
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/oui@1.2.6':
dependencies:
'@iconify/types': 2.0.0

View File

@@ -1,18 +1,20 @@
<script setup lang="ts">
import {APP_NAME, GITHUB} from "@/config/env.ts";
import { APP_NAME, GITHUB, Host, Origin } from "@/config/env.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import {defineAsyncComponent, onMounted, ref, watch} from "vue";
import {usePracticeStore} from "@/stores/practice.ts";
import {useBaseStore} from "@/stores/base.ts";
import {msToHourMinute} from "@/utils";
import { defineAsyncComponent, onMounted, ref, watch } from "vue";
import { usePracticeStore } from "@/stores/practice.ts";
import { useBaseStore } from "@/stores/base.ts";
import { msToHourMinute } from "@/utils";
import dayjs from "dayjs";
import BaseButton from "@/components/BaseButton.vue";
import Toast from "@/components/base/toast/Toast.ts";
import { useUserStore } from "@/stores/user.ts";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
const practiceStore = usePracticeStore()
const baseStore = useBaseStore()
const userStore = useUserStore()
let showWechatDialog = $ref(false)
let showXhsDialog = $ref(false)
@@ -60,8 +62,20 @@ async function generateShareImage() {
// 设置尺寸为1.3倍高度比例 (宽度720高度936)
const width = 720
const height = Math.round(width * 1.3)
canvas.width = width
canvas.height = height
// let canvasRect = canvas.getBoundingClientRect()
// let {width, height} = canvasRect
let dpr = window.devicePixelRatio
if (dpr) {
canvas.style.width = width + "px"
canvas.style.height = height + "px"
canvas.height = height * dpr
canvas.width = width * dpr
ctx.scale(dpr, dpr)
}
// canvas.width = width
// canvas.height = height
if (!ctx) return
@@ -71,111 +85,95 @@ async function generateShareImage() {
gradient.addColorStop(1, '#111827')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, width, height)
// 添加装饰性圆形
ctx.fillStyle = 'rgba(255, 255, 255, 0.05)'
ctx.beginPath()
ctx.arc(width * 0.8, height * 0.2, 60, 0, Math.PI * 2)
ctx.fill()
ctx.beginPath()
ctx.arc(width * 0.2, height * 0.8, 40, 0, Math.PI * 2)
ctx.fill()
// 设置文字样式
ctx.fillStyle = '#ffffff'
ctx.textAlign = 'left'
// 顶部用户信息
const avatarX = width * 0.1
const avatarY = height * 0.08
// 绘制头像背景
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'
ctx.beginPath()
ctx.arc(avatarX + 25, avatarY + 25, 20, 0, Math.PI * 2)
ctx.fill()
ctx.fillStyle = '#ffffff'
ctx.font = 'bold 18px Arial'
ctx.fillText(baseStore.user?.name || '学习者', avatarX + 60, avatarY + 20)
ctx.font = '14px Arial'
ctx.font = '24px Arial'
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
ctx.fillText(dayjs().format('YYYY年MM月DD日'), avatarX + 60, avatarY + 40)
ctx.fillText(dayjs().format('YYYY年MM月DD日'), width * 0.05, height * 0.08)
// 右上角标签
ctx.textAlign = 'right'
ctx.font = '12px Arial'
ctx.font = '24px Arial'
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
ctx.fillText('Type Words | 英语学习', width * 0.9, avatarY + 20)
ctx.fillText('Type Words | 英语学习', width * 0.95, height * 0.08)
// 右上角标签
ctx.textAlign = 'left'
ctx.font = '36px Arial'
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
ctx.fillText(`我在 ${APP_NAME} 学习了${studyStats.time}`, width * 0.05, height * 0.18)
// 统计数据区域 (三个圆角矩形)
const statsY = height * 0.25
const statWidth = width * 0.25
const statHeight = height * 0.12
const statSpacing = width * 0.05
const stats = [
{ label: '总词数', value: studyStats.total, color: '#60a5fa' },
{ label: '学习时长', value: studyStats.time, color: '#34d399' },
{ label: '正确率', value: studyStats.accuracy + '%', color: '#f59e0b' }
{label: '正确率', value: studyStats.accuracy + '%', color: '#f59e0b'},
{label: '新词', value: studyStats.newWords, color: '#60a5fa'},
{label: '复习', value: studyStats.review, color: '#34d399'}
]
stats.forEach((stat, index) => {
const x = width * 0.1 + index * (statWidth + statSpacing)
const y = statsY
// 绘制圆角矩形背景
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'
roundRect(ctx, x, y, statWidth, statHeight, 15)
ctx.fill()
// 数值
ctx.fillStyle = '#ffffff'
ctx.font = 'bold 24px Arial'
ctx.textAlign = 'center'
ctx.fillText(stat.value.toString(), x + statWidth / 2, y + statHeight * 0.4)
// 标签
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
ctx.font = '12px Arial'
ctx.fillText(stat.label, x + statWidth / 2, y + statHeight * 0.7)
})
// stats.forEach((stat, index) => {
// const x = width * 0.1 + index * (statWidth + statSpacing)
// const y = statsY
//
// // 绘制圆角矩形背景
// ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'
// roundRect(ctx, x, y, statWidth, statHeight, 15)
// ctx.fill()
//
// // 数值
// ctx.fillStyle = '#ffffff'
// ctx.font = 'bold 24px Arial'
// ctx.textAlign = 'center'
// ctx.fillText(stat.value.toString(), x + statWidth / 2, y + statHeight * 0.4)
//
// // 标签
// ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
// ctx.font = '12px Arial'
// ctx.fillText(stat.label, x + statWidth / 2, y + statHeight * 0.7)
// })
// 励志语句
ctx.textAlign = 'center'
ctx.fillStyle = '#ffffff'
ctx.font = 'italic 20px Arial'
ctx.fillText('Keep going, never give up!', width / 2, height * 0.45)
ctx.font = '16px Arial'
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
ctx.fillText('坚持就是胜利', width / 2, height * 0.5)
// ctx.textAlign = 'center'
// ctx.fillStyle = '#ffffff'
// ctx.font = 'italic 20px Arial'
// ctx.fillText('Keep going, never give up!', width / 2, height * 0.45)
//
// ctx.font = '16px Arial'
// ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
// ctx.fillText('坚持就是胜利', width / 2, height * 0.5)
// 底部品牌信息
const bottomY = height * 0.65
const brandX = width * 0.1
ctx.textAlign = 'left'
ctx.fillStyle = '#ffffff'
ctx.font = 'bold 20px Arial'
ctx.fillText('Type Words', brandX, bottomY)
ctx.font = '12px Arial'
ctx.font = 'bold 24px Arial'
ctx.fillText(APP_NAME, brandX, bottomY)
ctx.font = '24px Arial'
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
ctx.fillText('词文记 - 高效英语学习', brandX, bottomY + 20)
ctx.fillText(window.location.origin, brandX, bottomY + 35)
ctx.fillText('词文记 - 高效英语学习', brandX, bottomY + 30)
ctx.fillText(Host, brandX, bottomY + 55)
// 二维码区域
const qrX = width * 0.75
const qrY = bottomY - 10
// 二维码背景
ctx.fillStyle = '#ffffff'
roundRect(ctx, qrX - 5, qrY - 5, 50, 50, 5)
ctx.fill()
// 绘制简单二维码
ctx.fillStyle = '#000000'
const moduleSize = 2
@@ -268,10 +266,10 @@ onMounted(generateShareImage)
</BaseIcon>
<a
:href="GITHUB"
target="_blank"
rel="noreferrer"
aria-label="GITHUB 项目地址">
:href="GITHUB"
target="_blank"
rel="noreferrer"
aria-label="GITHUB 项目地址">
<BaseIcon>
<IconSimpleIconsGithub/>
</BaseIcon>
@@ -288,20 +286,20 @@ onMounted(generateShareImage)
</BaseIcon>
<a
href="https://x.com/typewords2"
target="_blank"
rel="noreferrer"
aria-label="关注我的 X 账户 typewords2">
href="https://x.com/typewords2"
target="_blank"
rel="noreferrer"
aria-label="关注我的 X 账户 typewords2">
<BaseIcon>
<IconRiTwitterFill class="color-blue"/>
</BaseIcon>
</a>
<a
href="mailto:zyronon@163.com"
target="_blank"
rel="noreferrer"
aria-label="发送邮件到 zyronon@163.com">
href="mailto:zyronon@163.com"
target="_blank"
rel="noreferrer"
aria-label="发送邮件到 zyronon@163.com">
<BaseIcon>
<IconMaterialSymbolsMail class="color-blue"/>
</BaseIcon>
@@ -310,44 +308,28 @@ onMounted(generateShareImage)
<!-- 学习总结分享图片生成对话框 -->
<Dialog
title="分享"
:close-on-click-bg="true"
@close="generatedImageUrl = null"
custom-class="!max-w-4xl !w-auto">
<div class="flex min-w-160 max-w-200">
title="分享"
:close-on-click-bg="true"
@close="generatedImageUrl = null"
custom-class="!max-w-4xl !w-auto">
<div class="flex min-w-160 max-w-200 p-6 pt-0 gap-space">
<!-- 左侧海报预览区域 -->
<div class="flex-1 p-6 border-r border-gray-200">
<!-- 生成中状态 -->
<div v-if="isGeneratingImage" class="relative">
<div class="w-80 h-104 bg-gradient-to-br from-gray-800 to-gray-900 rounded-xl p-6 text-white relative overflow-hidden">
<!-- 背景装饰 -->
<div class="absolute top-4 right-4 w-16 h-16 bg-white bg-opacity-10 rounded-full"></div>
<div class="absolute bottom-8 left-8 w-12 h-12 bg-white bg-opacity-5 rounded-full"></div>
<!-- 加载状态 -->
<div class="flex items-center justify-center h-full">
<div class="text-center">
<div class="w-12 h-12 border-2 border-white border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<div class="text-white text-sm">正在生成海报...</div>
</div>
</div>
</div>
</div>
<div class="flex-1 border-r border-gray-200">
<!-- 海报预览 -->
<div v-else-if="generatedImageUrl" class="relative">
<img
:src="generatedImageUrl"
alt="学习总结海报"
class="w-80 h-auto rounded-xl shadow-lg">
<div v-if="generatedImageUrl" class="relative">
<img
:src="generatedImageUrl"
alt="学习总结海报"
class="w-full h-auto rounded-xl shadow-lg">
</div>
<!-- 默认预览状态 -->
<div v-else class="w-80 h-104 bg-gradient-to-br from-gray-800 to-gray-900 rounded-xl p-6 text-white relative overflow-hidden">
<div v-else
class="w-80 h-104 bg-gradient-to-br from-gray-800 to-gray-900 rounded-xl p-6 text-white relative overflow-hidden">
<!-- 背景装饰 -->
<div class="absolute top-4 right-4 w-16 h-16 bg-white bg-opacity-10 rounded-full"></div>
<div class="absolute bottom-8 left-8 w-12 h-12 bg-white bg-opacity-5 rounded-full"></div>
<!-- 顶部用户信息 -->
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-gray-600 rounded-full mr-3 flex items-center justify-center">
@@ -361,7 +343,7 @@ onMounted(generateShareImage)
Type Words | 英语学习
</div>
</div>
<!-- 统计数据 -->
<div class="grid grid-cols-3 gap-4 mb-8">
<div class="text-center">
@@ -377,13 +359,13 @@ onMounted(generateShareImage)
<div class="text-gray-300 text-xs">正确率</div>
</div>
</div>
<!-- 励志语句 -->
<div class="text-center mb-8">
<div class="text-lg italic mb-2">Keep going, never give up!</div>
<div class="text-sm text-gray-300">坚持就是胜利</div>
</div>
<!-- 底部品牌信息 -->
<div class="absolute bottom-6 left-6 right-6">
<div class="flex justify-between items-end">
@@ -401,58 +383,55 @@ onMounted(generateShareImage)
</div>
</div>
</div>
<!-- 右侧分享引导区域 -->
<div class="flex-1 p-8 pt-0 space-y-6">
<div class="mb-8">
<h2 class="text-2xl font-bold text-gray-800 mb-4 flex items-center">
<div class="flex-1 pt-0 space-y-6">
<div class="">
<div class="text-2xl font-bold text-gray-800 mb-4 flex items-center">
<span class="mr-2">🎯</span>
分享你的进步
</h2>
<div class="">
<div class="flex items-start">
<span class="mr-2">🚀</span>
{{ APP_NAME }}学习英语也能成为超酷的事情
</div>
<div class="flex items-start">
<span class="mr-2">📸</span>
快来分享你的学习图片让你的进步刷屏朋友圈成为最受瞩目的英语学霸😎
</div>
<div class="flex items-start">
<span class="mr-2">💪</span>
这不只是简单的打卡更是你秀出英语实力的舞台
</div>
<div class="flex items-start">
<span class="mr-2">🔥</span>
分享你的战绩收获朋友们的点赞和认可让你的朋友圈也掀起一股英语学习的热潮
</div>
</div>
<div class="flex items-start">
<span class="mr-2">🚀</span>
{{ APP_NAME }}学习英语也能成为超酷的事情
</div>
<div class="flex items-start">
<span class="mr-2">📸</span>
快来分享你的学习图片让你的进步刷屏朋友圈成为最受瞩目的英语学霸😎
</div>
<div class="flex items-start">
<span class="mr-2">💪</span>
这不只是简单的打卡更是你秀出英语实力的舞台
</div>
<div class="flex items-start">
<span class="mr-2">🔥</span>
分享你的战绩收获朋友们的点赞和认可让你的朋友圈也掀起一股英语学习的热潮
</div>
</div>
<!-- 个性化装扮 -->
<div
class="flex items-center justify-between px-6 py-3 bg-gray-200 rounded-lg cp hover:bg-gray-100 transition-all duration-200">
class="flex items-center justify-between px-6 py-3 bg-gray-200 rounded-lg cp hover:bg-gray-100 transition-all duration-200">
<div
@click="changeBackground"
class="flex items-center gap-2">
@click="changeBackground"
class="flex items-center gap-2">
<IconMdiSparkles class="w-4 h-4 text-yellow-500"/>
换个背景
</div>
<span class="text-sm text-gray-500 bg-gray-100 px-2 py-1 rounded-full">随心装扮</span>
</div>
<!-- 分享战绩 -->
<div
@click="copyImageToClipboard"
class="flex items-center justify-start gap-space px-6 py-3 bg-gradient-to-r from-green-500 to-green-600 text-white cp rounded-lg hover:from-green-600 hover:to-green-700 transition-all duration-200">
@click="copyImageToClipboard"
class="flex items-center justify-start gap-space px-6 py-3 bg-gradient-to-r from-green-500 to-green-600 text-white cp rounded-lg hover:from-green-600 hover:to-green-700 transition-all duration-200">
<IconFluentCopy20Regular class="w-5 h-5"/>
<span class="font-medium">复制到剪贴板</span>
</div>
<div
@click="downloadImage"
class="flex items-center justify-start gap-space px-6 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white cp rounded-lg hover:from-purple-600 hover:to-purple-700 transition-all duration-200">
@click="downloadImage"
class="flex items-center justify-start gap-space px-6 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white cp rounded-lg hover:from-purple-600 hover:to-purple-700 transition-all duration-200">
<IconFluentArrowDownload20Regular class="w-5 h-5"/>
<span class="font-medium">保存高清海报</span>
</div>