save
8
components.d.ts
vendored
@@ -153,11 +153,3 @@ declare module 'vue' {
|
||||
WordList: typeof import('./src/components/list/WordList.vue')['default']
|
||||
}
|
||||
}
|
||||
e')['default']
|
||||
Toast: typeof import('./src/components/base/toast/Toast.vue')['default']
|
||||
Tooltip: typeof import('./src/components/base/Tooltip.vue')['default']
|
||||
VolumeIcon: typeof import('./src/components/icon/VolumeIcon.vue')['default']
|
||||
WordItem: typeof import('./src/components/WordItem.vue')['default']
|
||||
WordList: typeof import('./src/components/list/WordList.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"@imengyu/vue3-context-menu": "^1.5.1",
|
||||
"@vueuse/core": "14.0.0-alpha.0",
|
||||
"@zumer/snapdom": "^2.0.0",
|
||||
"axios": "^1.10.0",
|
||||
"compromise": "^14.14.4",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
|
||||
29
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@vueuse/core':
|
||||
specifier: 14.0.0-alpha.0
|
||||
version: 14.0.0-alpha.0(vue@3.5.18(typescript@5.9.2))
|
||||
'@zumer/snapdom':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
axios:
|
||||
specifier: ^1.10.0
|
||||
version: 1.11.0
|
||||
@@ -623,25 +626,21 @@ packages:
|
||||
resolution: {integrity: sha512-mMB1AvqzTH25rbUo1eRfvFzNqBopX6aRlDmO1fIVVzIWi6YJNKckxbkGaatez4hH/n86IR6aEdZFM3qBUjn3Tg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@4.2.0':
|
||||
resolution: {integrity: sha512-9oPBU8Yb35z15/14LzALn/8rRwwrtfe19l25N1MRZVSONGiOwfzWNqDNjWiDdyW+EUt/hlylmFOItZmreL6iIw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@4.2.0':
|
||||
resolution: {integrity: sha512-8wU4fwHb0b45i0qMBJ24UYBEtaLyvYWUOqVVCn0SpQZ1mhWWC8dvD6+zIVAKRVex/cKdgzi3imXoKGIDqVEu9w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@4.2.0':
|
||||
resolution: {integrity: sha512-5CS2wlGxzESPJCj4NlNGr73QCku75VpGtkwNp8qJF4hLELKAzkoqIB0eBbcvNPg8m2rB7YeXb1u+puGUKXDhNQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-wasm32-wasi@4.2.0':
|
||||
resolution: {integrity: sha512-VOLpvmVAQZjvj/7Et/gYzW6yBqL9VKjLWOGaFiQ7cvTpY9R9d/1mrNKEuP3beDHF2si2fM5f2pl9bL+N4tvwiA==}
|
||||
@@ -687,42 +686,36 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
@@ -792,67 +785,56 @@ packages:
|
||||
resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.46.2':
|
||||
resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.46.2':
|
||||
resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.46.2':
|
||||
resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.46.2':
|
||||
resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.46.2':
|
||||
resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.46.2':
|
||||
resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==}
|
||||
@@ -1272,6 +1254,9 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@zumer/snapdom@2.0.0':
|
||||
resolution: {integrity: sha512-e/fkm5wCUd+9CssUIyH09xTeR4DvRTmZLGVOlnXLhr4HeI7sdc6ed8cLPiZKFtiQDRiwD3EKx4RIUrpQOJQY7A==}
|
||||
|
||||
acorn@8.15.0:
|
||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -5071,6 +5056,8 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.18(typescript@5.9.2)
|
||||
|
||||
'@zumer/snapdom@2.0.0': {}
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
address@1.2.2: {}
|
||||
|
||||
BIN
public/imgs/0.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/imgs/1.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/imgs/2.jpg
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
public/imgs/3.jpg
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
public/imgs/4.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/imgs/5.jpg
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
public/imgs/6.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/imgs/7.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/imgs/8.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/imgs/9.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/imgs/qr.png
Normal file
|
After Width: | Height: | Size: 547 B |
@@ -1,14 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { APP_NAME, GITHUB, Host, Origin } from "@/config/env.ts";
|
||||
import { APP_NAME, GITHUB, Origin } from "@/config/env.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import { defineAsyncComponent, onMounted, ref, watch } from "vue";
|
||||
import { defineAsyncComponent, 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";
|
||||
import Progress from "@/components/base/Progress.vue";
|
||||
import { snapdom } from "@zumer/snapdom";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
@@ -19,14 +20,12 @@ const userStore = useUserStore()
|
||||
let showWechatDialog = $ref(false)
|
||||
let showXhsDialog = $ref(false)
|
||||
let showQQDialog = $ref(false)
|
||||
let showShareImageDialog = $ref(false)
|
||||
let isGeneratingImage = $ref(false)
|
||||
let generatedImageUrl = $ref<string | null>(null)
|
||||
let showShareDialog = $ref(false)
|
||||
let posterEl = $ref<HTMLDivElement | null>(null)
|
||||
|
||||
// 计算学习统计数据
|
||||
const studyStats = $computed(() => {
|
||||
const accuracyRate = practiceStore.total === 0 ? 100 : Math.round(((practiceStore.total - practiceStore.wrong) / practiceStore.total) * 100)
|
||||
const studyTime = msToHourMinute(practiceStore.spend).replace('小时', 'h ').replace('分钟', 'm')
|
||||
|
||||
return {
|
||||
total: practiceStore.total,
|
||||
@@ -35,241 +34,102 @@ const studyStats = $computed(() => {
|
||||
wrong: practiceStore.wrong,
|
||||
correct: practiceStore.total - practiceStore.wrong,
|
||||
accuracy: accuracyRate,
|
||||
time: studyTime,
|
||||
time: msToHourMinute(practiceStore.spend),
|
||||
date: dayjs().format('MM月DD日'),
|
||||
dictionary: baseStore.sdict.name || '未知词书'
|
||||
}
|
||||
})
|
||||
|
||||
// 监听对话框打开事件,自动生成图片
|
||||
watch(() => showShareImageDialog, (newVal) => {
|
||||
if (newVal && !generatedImageUrl) {
|
||||
generateShareImage()
|
||||
watch(() => showShareDialog, (newVal) => {
|
||||
if (newVal) {
|
||||
}
|
||||
})
|
||||
|
||||
// 生成分享图片
|
||||
async function generateShareImage() {
|
||||
if (isGeneratingImage || generatedImageUrl) return
|
||||
|
||||
isGeneratingImage = true
|
||||
|
||||
try {
|
||||
// 创建canvas元素
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// 设置尺寸为1.3倍高度比例 (宽度720,高度936)
|
||||
const width = 720
|
||||
const height = Math.round(width * 1.3)
|
||||
|
||||
// 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
|
||||
|
||||
// 创建灰黑色渐变背景
|
||||
const gradient = ctx.createLinearGradient(0, 0, width, height)
|
||||
gradient.addColorStop(0, '#1f2937')
|
||||
gradient.addColorStop(1, '#111827')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
|
||||
// 设置文字样式
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
ctx.font = '24px Arial'
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
|
||||
ctx.fillText(dayjs().format('YYYY年MM月DD日'), width * 0.05, height * 0.08)
|
||||
|
||||
// 右上角标签
|
||||
ctx.textAlign = 'right'
|
||||
ctx.font = '24px Arial'
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
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.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)
|
||||
// })
|
||||
|
||||
// 励志语句
|
||||
// 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 24px Arial'
|
||||
ctx.fillText(APP_NAME, brandX, bottomY)
|
||||
|
||||
ctx.font = '24px Arial'
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
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
|
||||
for (let row = 0; row < 20; row++) {
|
||||
for (let col = 0; col < 20; col++) {
|
||||
if (Math.random() > 0.5) {
|
||||
ctx.fillRect(qrX + col * moduleSize, qrY + row * moduleSize, moduleSize, moduleSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将canvas转换为图片
|
||||
const imageUrl = canvas.toDataURL('image/png', 1.0)
|
||||
generatedImageUrl = imageUrl
|
||||
|
||||
} catch (error) {
|
||||
console.error('生成图片失败:', error)
|
||||
alert('生成图片失败,请重试')
|
||||
} finally {
|
||||
isGeneratingImage = false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制图片到剪贴板
|
||||
async function copyImageToClipboard() {
|
||||
if (!generatedImageUrl) return
|
||||
|
||||
try {
|
||||
const response = await fetch(generatedImageUrl)
|
||||
const blob = await response.blob()
|
||||
const blob = await snapdom.toBlob(posterEl, {scale: 2, type: 'png'})
|
||||
if (!blob) throw new Error('capture failed')
|
||||
|
||||
if (navigator.clipboard && window.ClipboardItem) {
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
'image/png': blob
|
||||
})
|
||||
])
|
||||
if (navigator.clipboard && (window as any).ClipboardItem) {
|
||||
await navigator.clipboard.write([new (window as any).ClipboardItem({[blob.type || 'image/png']: blob})])
|
||||
Toast.success('图片已复制到剪贴板!')
|
||||
} else {
|
||||
// 降级方案:下载图片
|
||||
downloadImage()
|
||||
await downloadImage()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
// 降级方案:下载图片
|
||||
downloadImage()
|
||||
Toast.error('复制失败!')
|
||||
await downloadImage()
|
||||
}
|
||||
}
|
||||
|
||||
// 下载图片
|
||||
function downloadImage() {
|
||||
if (!generatedImageUrl) return
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.download = `${APP_NAME} 分享_${studyStats.date}_${studyStats.dictionary}.png`
|
||||
link.href = generatedImageUrl
|
||||
link.click()
|
||||
async function downloadImage() {
|
||||
snapdom.download(posterEl, {scale: 2})
|
||||
}
|
||||
|
||||
let imgIndex = $ref(Math.floor(Math.random() * 10))
|
||||
|
||||
// 切换背景
|
||||
function changeBackground() {
|
||||
// 这里可以实现背景切换逻辑
|
||||
console.log('切换背景')
|
||||
const newIndex = Math.floor(Math.random() * 9) // 0-8
|
||||
imgIndex = newIndex >= imgIndex ? newIndex + 1 : newIndex
|
||||
}
|
||||
|
||||
// 绘制圆角矩形辅助函数
|
||||
function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + radius, y)
|
||||
ctx.lineTo(x + width - radius, y)
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + radius)
|
||||
ctx.lineTo(x + width, y + height - radius)
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height)
|
||||
ctx.lineTo(x + radius, y + height)
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - radius)
|
||||
ctx.lineTo(x, y + radius)
|
||||
ctx.quadraticCurveTo(x, y, x + radius, y)
|
||||
ctx.closePath()
|
||||
}
|
||||
// 计算学习进度百分比
|
||||
const studyProgress = $computed(() => {
|
||||
if (!baseStore.sdict.length) return 0
|
||||
return Math.round((baseStore.sdict.lastLearnIndex / baseStore.sdict.length) * 100)
|
||||
})
|
||||
|
||||
const sentence = $computed(() => {
|
||||
let list = [
|
||||
{en: 'Actions speak louder than words.', cn: '行动胜于言语'},
|
||||
{en: 'Keep going, never give up!', cn: '坚持就是胜利'},
|
||||
{en: 'Where there\'s a will, there\'s a way.', cn: '有志者事竟成'},
|
||||
{en: 'Every cloud has a silver lining.', cn: '黑暗中总有一线光明'},
|
||||
{en: 'Time heals all wounds.', cn: '时间能治愈一切创伤'},
|
||||
{en: 'Never say die.', cn: '永不言败'},
|
||||
{en: 'The best is yet to come.', cn: '最好的尚未到来'},
|
||||
{en: 'Believe you can and you\'re halfway there.', cn: '相信你自己,你已经成功了一半'},
|
||||
{en: 'No pain, no gain.', cn: '没有付出就没有收获'},
|
||||
{en: 'Dream big and dare to fail.', cn: '大胆梦想,勇于失败'},
|
||||
{en: 'Home is where the heart is.', cn: '心在哪里,家就在哪里'},
|
||||
{en: 'Knowledge is power.', cn: '知识就是力量'},
|
||||
{en: 'Practice makes perfect.', cn: '熟能生巧'},
|
||||
{en: 'When in Rome, do as the Romans do.', cn: '入乡随俗'},
|
||||
{en: 'Just do it.', cn: '只管去做'},
|
||||
{en: 'So far, so good.', cn: '到目前为止,一切还好'},
|
||||
{en: 'The early bird catches the worm.', cn: '早起的鸟儿有虫吃'},
|
||||
{en: 'Every day is a new beginning.', cn: '每一天都是新的开始'},
|
||||
{en: 'Success is a journey, not a destination.', cn: '成功是旅程,不是终点'},
|
||||
{en: 'Your only limit is your mind.', cn: '你唯一的限制是你的思维'},
|
||||
{en: 'A friend in need is a friend indeed.', cn: '患难见真情'},
|
||||
{en: 'Silence is golden.', cn: '沉默是金'},
|
||||
{en: 'Let bygones be bygones.', cn: '让过去的成为过去'},
|
||||
{en: 'Keep calm and carry on.', cn: '保持冷静,继续前进'},
|
||||
{en: 'Live and learn.', cn: '活到老,学到老'},
|
||||
{en: 'Mistakes are proof that you are trying.', cn: '错误证明你在努力尝试'},
|
||||
{en: 'Better late than never.', cn: '迟做总比不做好'},
|
||||
{en: 'Be the change you wish to see in the world.', cn: '成为你希望在世界上看到的改变'},
|
||||
{en: 'The journey of a thousand miles begins with a single step.', cn: '千里之行,始于足下'},
|
||||
{en: 'When one door closes, another opens.', cn: '当一扇门关闭时,另一扇会打开'},
|
||||
]
|
||||
return list[Math.floor(Math.random() * list.length)]
|
||||
})
|
||||
|
||||
onMounted(generateShareImage)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col center gap-1">
|
||||
<!-- 分享学习总结按钮 -->
|
||||
<BaseIcon @click="showShareImageDialog = true"
|
||||
<BaseIcon @click="showShareDialog = true"
|
||||
class="cursor-pointer hover:scale-110 transition-transform duration-200">
|
||||
<IconFluentShare20Regular class="text-blue-500 hover:text-blue-600"/>
|
||||
</BaseIcon>
|
||||
|
||||
<a
|
||||
:href="GITHUB"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="GITHUB 项目地址">
|
||||
<a :href="GITHUB" target="_blank" rel="noreferrer" aria-label="GITHUB 项目地址">
|
||||
<BaseIcon>
|
||||
<IconSimpleIconsGithub/>
|
||||
</BaseIcon>
|
||||
@@ -285,21 +145,13 @@ onMounted(generateShareImage)
|
||||
<IconSimpleIconsXiaohongshu class="color-red-500"/>
|
||||
</BaseIcon>
|
||||
|
||||
<a
|
||||
href="https://x.com/typewords2"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="关注我的 X 账户 typewords2">
|
||||
<a 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">
|
||||
<a href="mailto:zyronon@163.com" target="_blank" rel="noreferrer" aria-label="发送邮件到 zyronon@163.com">
|
||||
<BaseIcon>
|
||||
<IconMaterialSymbolsMail class="color-blue"/>
|
||||
</BaseIcon>
|
||||
@@ -308,84 +160,80 @@ onMounted(generateShareImage)
|
||||
|
||||
<!-- 学习总结分享图片生成对话框 -->
|
||||
<Dialog
|
||||
title="分享"
|
||||
:close-on-click-bg="true"
|
||||
@close="generatedImageUrl = null"
|
||||
custom-class="!max-w-4xl !w-auto">
|
||||
v-model="showShareDialog"
|
||||
title="分享" :close-on-click-bg="true" 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 border-r border-gray-200">
|
||||
<!-- 海报预览 -->
|
||||
<div v-if="generatedImageUrl" class="relative">
|
||||
<img
|
||||
:src="generatedImageUrl"
|
||||
alt="学习总结海报"
|
||||
class="w-full h-auto rounded-xl shadow-lg">
|
||||
</div>
|
||||
<div ref="posterEl" class="flex-1 border-r border-gray-200 bg-gray-100 rounded-xl overflow-hidden relative">
|
||||
<div class="flex p-5 gap-space flex-col justify-between relative z-2 color-white h-full box-border">
|
||||
<div class="flex flex-col flex-1 space-y-3">
|
||||
<!-- 顶部用户信息 -->
|
||||
<div class="flex items-center">
|
||||
<div v-if="userStore.user?.username"
|
||||
class="w-12 h-12 bg-gray-600 rounded-full mr-3 flex items-center justify-center">
|
||||
<IconSimpleIconsGithub class="w-6 h-6 text-white"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-lg">{{ userStore.user?.username }}</div>
|
||||
<div class="">{{ dayjs().format('YYYY年MM月DD日') }}</div>
|
||||
</div>
|
||||
<div class="ml-auto text-xs">
|
||||
Type Words | 英语学习
|
||||
</div>
|
||||
</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 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="bg-gray-900/30 py-4 center flex-col rounded-2xl">
|
||||
<div class="text-center mb-2 text-xl">
|
||||
我在 {{ APP_NAME }} 学习了 {{ studyStats.time }}
|
||||
</div>
|
||||
<!-- Progress Overview -->
|
||||
<div class="w-90/100 flex items-center gap-space">
|
||||
<div class="shrink-0">进度</div>
|
||||
<Progress :percentage="studyProgress" size="normal"/>
|
||||
</div>
|
||||
</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">
|
||||
<IconSimpleIconsGithub class="w-6 h-6 text-white"/>
|
||||
<!-- 统计数据 -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="stat-card">
|
||||
<div class="text-2xl font-bold">{{ studyStats.accuracy }}%</div>
|
||||
<div class="text-base">正确率</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="text-2xl font-bold">{{ studyStats.newWords }}</div>
|
||||
<div class="text-base">新词</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="text-2xl font-bold">{{ studyStats.review }}</div>
|
||||
<div class="text-base">复习</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-lg">{{ baseStore.user?.name || '学习者' }}</div>
|
||||
<div class="text-gray-300 text-sm">{{ dayjs().format('YYYY年MM月DD日') }}</div>
|
||||
</div>
|
||||
<div class="ml-auto text-xs text-gray-300">
|
||||
Type Words | 英语学习
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<div class="grid grid-cols-3 gap-4 mb-8">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold">{{ studyStats.total }}</div>
|
||||
<div class="text-gray-300 text-xs">总词数</div>
|
||||
<!-- 励志语句 -->
|
||||
<div class="bg-gray-900/30 py-4 rounded-2xl center flex-col flex-1 p-4">
|
||||
<div class="text-3xl text-center italic mb-2 en-article-family">{{ sentence.en }}</div>
|
||||
<div class="text-base italic">{{ sentence.cn }}</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold">{{ studyStats.time }}</div>
|
||||
<div class="text-gray-300 text-xs">学习时长</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold">{{ studyStats.accuracy }}%</div>
|
||||
<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="bg-gray-900/30 py-4 rounded-2xl p-4">
|
||||
<div class="flex justify-between items-end">
|
||||
<div>
|
||||
<div class="font-bold text-lg">Type Words</div>
|
||||
<div class="text-xs text-gray-300">词文记 - 高效英语学习</div>
|
||||
<div class="text-xs text-gray-400">{{ window.location.origin }}</div>
|
||||
</div>
|
||||
<div class="w-16 h-16 bg-white rounded p-2">
|
||||
<div class="w-full h-full bg-black grid grid-cols-8 gap-0.5">
|
||||
<div v-for="i in 64" :key="i" :class="Math.random() > 0.5 ? 'bg-black' : 'bg-white'"></div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="font-bold text-2xl">Type Words</div>
|
||||
<div class="text-base ">{{ Origin }}</div>
|
||||
<div class="text-xs ">一次敲击,一点进步,开源单词学习工具</div>
|
||||
</div>
|
||||
<img src="/imgs/qr.png" class="w-20 w-20 rounded-md overflow-hidden" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img :src="`/imgs/${imgIndex}.jpg`" class="w-full object-cover object-center absolute top-0 " alt="">
|
||||
</div>
|
||||
|
||||
<!-- 右侧:分享引导区域 -->
|
||||
<div class="flex-1 pt-0 space-y-6">
|
||||
<div class="flex-1 pt-0 ">
|
||||
<div class="">
|
||||
<div class="text-2xl font-bold text-gray-800 mb-4 flex items-center">
|
||||
<span class="mr-2">🎯</span>
|
||||
@@ -405,35 +253,30 @@ onMounted(generateShareImage)
|
||||
</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">
|
||||
<div
|
||||
@click="changeBackground"
|
||||
class="flex items-center gap-2">
|
||||
<div class="space-y-4 mt-30">
|
||||
<!-- 个性化装扮 -->
|
||||
<div @click="changeBackground"
|
||||
class="flex items-center justify-start gap-space px-6 py-3 bg-gray-200 rounded-lg cp hover:bg-gray-300 transition-all duration-200">
|
||||
<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">
|
||||
<IconFluentCopy20Regular class="w-5 h-5"/>
|
||||
<span class="font-medium">复制到剪贴板</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">
|
||||
<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">
|
||||
<IconFluentArrowDownload20Regular class="w-5 h-5"/>
|
||||
<span class="font-medium">保存高清海报</span>
|
||||
<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">
|
||||
<IconFluentArrowDownload20Regular class="w-5 h-5"/>
|
||||
<span class="font-medium">保存高清海报</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -485,4 +328,8 @@ a {
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
.stat-card {
|
||||
@apply text-center bg-gray-900/30 py-4 rounded-2xl;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface IProps {
|
||||
percentage: number;
|
||||
@@ -34,10 +34,7 @@ const trackStyle = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const progressTextSize = computed(() => {
|
||||
const baseSize = props.strokeWidth * 0.83 + 6;
|
||||
return props.size === 'large' ? baseSize * 1.2 : baseSize;
|
||||
});
|
||||
const progressTextSize = 18
|
||||
|
||||
const content = computed(() => {
|
||||
if (typeof props.format === 'function') {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio} from "@/hooks/sound.ts";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import {_dateFormat, _nextTick, msToHourMinute, total} from "@/utils";
|
||||
import { _dateFormat, _nextTick, isMobile, msToHourMinute, total } from "@/utils";
|
||||
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
|
||||
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
@@ -19,7 +19,6 @@ import nlp from "compromise/three";
|
||||
import {nanoid} from "nanoid";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {PracticeSaveArticleKey} from "@/config/env.ts";
|
||||
import useMobile from "@/hooks/useMobile.ts";
|
||||
|
||||
interface IProps {
|
||||
article: Article,
|
||||
@@ -86,7 +85,7 @@ const {
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
const statStore = usePracticeStore()
|
||||
const isMobile = useMobile()
|
||||
const isMob = isMobile()
|
||||
|
||||
watch([() => sectionIndex, () => sentenceIndex, () => wordIndex, () => stringIndex], ([a, b, c,]) => {
|
||||
localStorage.setItem(PracticeSaveArticleKey.key, JSON.stringify({
|
||||
@@ -184,7 +183,7 @@ function checkCursorPosition(a = sectionIndex, b = sentenceIndex, c = wordIndex)
|
||||
function checkTranslateLocation() {
|
||||
// console.log('checkTranslateLocation')
|
||||
return new Promise<void>(resolve => {
|
||||
if (isMobile) {
|
||||
if (isMob) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
@@ -213,7 +212,7 @@ function checkTranslateLocation() {
|
||||
}
|
||||
|
||||
function focusMobileInput() {
|
||||
if (!isMobile) return
|
||||
if (!isMob) return
|
||||
mobileInputRef?.focus()
|
||||
}
|
||||
|
||||
@@ -230,7 +229,7 @@ function processMobileCharacter(char: string) {
|
||||
}
|
||||
|
||||
function handleMobileInput(event: Event) {
|
||||
if (!isMobile) return
|
||||
if (!isMob) return
|
||||
const target = event.target as HTMLInputElement
|
||||
const value = target?.value ?? ''
|
||||
if (!value) return
|
||||
@@ -241,7 +240,7 @@ function handleMobileInput(event: Event) {
|
||||
}
|
||||
|
||||
function handleMobileBeforeInput(event: InputEvent) {
|
||||
if (!isMobile) return
|
||||
if (!isMob) return
|
||||
if (event.inputType === 'deleteContentBackward') {
|
||||
event.preventDefault()
|
||||
del()
|
||||
@@ -570,7 +569,7 @@ onMounted(() => {
|
||||
wrong = input = ''
|
||||
})
|
||||
emitter.on(EventKey.onTyping, onTyping)
|
||||
if (isMobile) {
|
||||
if (isMob) {
|
||||
focusMobileInput()
|
||||
}
|
||||
})
|
||||
@@ -600,7 +599,7 @@ const currentPractice = inject('currentPractice', [])
|
||||
<template>
|
||||
<div class="typing-article" ref="typeArticleRef" @click="focusMobileInput">
|
||||
<input
|
||||
v-if="isMobile"
|
||||
v-if="isMob"
|
||||
ref="mobileInputRef"
|
||||
class="mobile-input"
|
||||
type="text"
|
||||
@@ -664,7 +663,7 @@ const currentPractice = inject('currentPractice', [])
|
||||
</span>
|
||||
<span
|
||||
class="sentence-translate-mobile"
|
||||
v-if="isMobile && settingStore.translate && sentence.translate">
|
||||
v-if="isMob && settingStore.translate && sentence.translate">
|
||||
{{ sentence.translate }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DictId } from "@/types/types.ts";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { computed, onMounted, reactive, ref, shallowReactive } from "vue";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { _getDictDataByUrl, _nextTick, convertToWord, loadJsLib, useNav } from "@/utils";
|
||||
import { _getDictDataByUrl, _nextTick, convertToWord, isMobile, loadJsLib, useNav } from "@/utils";
|
||||
import { nanoid } from "nanoid";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import BaseTable from "@/components/BaseTable.vue";
|
||||
@@ -28,13 +28,12 @@ import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { MessageBox } from "@/utils/MessageBox.tsx";
|
||||
import { AppEnv, Origin, PracticeSaveWordKey } from "@/config/env.ts";
|
||||
import { detail } from "@/apis";
|
||||
import useMobile from "@/hooks/useMobile.ts";
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const base = useBaseStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const isMobile = useMobile()
|
||||
const isMob = isMobile()
|
||||
|
||||
let loading = $ref(false)
|
||||
|
||||
@@ -160,20 +159,20 @@ function word2Str(word) {
|
||||
function editWord(word) {
|
||||
isOperate = true
|
||||
wordForm = word2Str(word)
|
||||
if (isMobile) activeTab = 'edit'
|
||||
if (isMob) activeTab = 'edit'
|
||||
}
|
||||
|
||||
function addWord() {
|
||||
// setTimeout(wordListRef?.scrollToBottom, 100)
|
||||
isOperate = true
|
||||
wordForm = getDefaultFormWord()
|
||||
if (isMobile) activeTab = 'edit'
|
||||
if (isMob) activeTab = 'edit'
|
||||
}
|
||||
|
||||
function closeWordForm() {
|
||||
isOperate = false
|
||||
wordForm = getDefaultFormWord()
|
||||
if (isMobile) activeTab = 'list'
|
||||
if (isMob) activeTab = 'list'
|
||||
}
|
||||
|
||||
let isEdit = $ref(false)
|
||||
@@ -395,7 +394,7 @@ defineRender(() => {
|
||||
<div class="line my-3"></div>
|
||||
|
||||
{/* 移动端标签页导航 */}
|
||||
{isMobile && isOperate && (
|
||||
{isMob && isOperate && (
|
||||
<div class="tab-navigation mb-3">
|
||||
<div
|
||||
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
|
||||
@@ -413,7 +412,7 @@ defineRender(() => {
|
||||
)}
|
||||
|
||||
<div class="flex flex-1 overflow-hidden content-area">
|
||||
<div class={`word-list-section ${isMobile && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
|
||||
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
class="h-full"
|
||||
@@ -462,7 +461,7 @@ defineRender(() => {
|
||||
</div>
|
||||
{
|
||||
isOperate ? (
|
||||
<div class={`edit-section flex-1 flex flex-col ${isMobile && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
|
||||
<div class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
|
||||
<div class="common-title">
|
||||
{wordForm.id ? '修改' : '添加'}单词
|
||||
</div>
|
||||
|
||||
@@ -134,6 +134,7 @@ calcWeekList(); // 新增:计算本周学习记录
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="model"
|
||||
:close-on-click-bg="false"
|
||||
:header="false"
|
||||
:keyboard="false"
|
||||
|
||||