feat: PDF 转换前端(epub 上传/进度轮询/下载/用户软删,复用 zTools2 API)

便携式 Vue3 前端:上传 epub 转 PDF,显示上传与转换进度,完成后下载;
用户可软删任务(不再对自己展示,管理员仍可见)。凭 cookie 标识用户。
复用 zTools2 后端 /api/pdf/*(同源),独立 i18n(中英懒加载),
可独立运行或经 mainPage iframe 同源集成。含 vitest 单测 20 项。
This commit is contained in:
2026-07-27 11:33:26 +08:00
commit 30f5cee4a1
19 changed files with 3918 additions and 0 deletions

46
src/i18n/index.js Normal file
View File

@@ -0,0 +1,46 @@
import { createI18n } from 'vue-i18n'
import zhCN from './locales/zh-CN.js'
// ★ zPDF_package 独立 i18n 实例。中文同步注入(默认语言);英文动态 import() 懒加载。
// 嵌入 mainPage 时,父项目把当前语言经 :locale prop 传入 App.vuewatch 同步到本实例,
// 从而跟随父项目语言开关切换(不共享父的 messages仅同步 locale 值,文案来自本子项目自有 locale
// 注:组件内不用 useI18n() 注入(会解析到宿主实例),改用 ./composables/useLocale.js 直接读本实例。
const i18n = createI18n({
legacy: false,
locale: 'zh-CN',
fallbackLocale: 'zh-CN',
messages: {
'zh-CN': zhCN
}
})
const loadedLocales = new Set(['zh-CN'])
/**
* 懒加载并切换到目标语言。首次切换某语言时动态 import 其语言包并 mergeLocaleMessage。
* @param {string} locale
*/
export async function loadLocaleAsync(locale) {
if (i18n.global.locale.value === locale) return
if (loadedLocales.has(locale)) {
i18n.global.locale.value = locale
document.documentElement.setAttribute('lang', locale)
return
}
let messages
if (locale === 'en') {
messages = (await import('./locales/en.js')).default
} else {
i18n.global.locale.value = 'zh-CN'
document.documentElement.setAttribute('lang', 'zh-CN')
return
}
i18n.global.mergeLocaleMessage(locale, messages)
loadedLocales.add(locale)
i18n.global.locale.value = locale
document.documentElement.setAttribute('lang', locale)
}
export default i18n

41
src/i18n/locales/en.js Normal file
View File

@@ -0,0 +1,41 @@
// 英文语言包 -- 通过动态 import() 加载,被打成独立 chunk
// 只看中文的访客不会下载此文件。
export default {
app: {
title: 'PDF Converter'
},
upload: {
hint: 'Drop an .epub file, or',
pick: 'click to choose',
limit: 'Only epub, up to 250MB',
requireEpub: 'Only epub files are supported',
oversize: 'File exceeds the 250MB limit',
uploading: 'Uploading…',
submitted: 'Submitted, converting…',
failed: 'Upload failed',
networkError: 'Network error'
},
list: {
count: '{total} task(s)',
empty: 'No tasks yet. Upload an epub to start.',
loading: 'Loading…',
loadFailed: 'Load failed: {msg}'
},
job: {
name: 'Filename',
size: 'Size',
status: 'Status',
created: 'Created',
actions: 'Actions',
statusDone: 'Done',
statusFailed: 'Failed',
statusConverting: 'Converting',
statusPending: 'Queued',
download: 'Download',
delete: 'Delete',
deleteConfirm: 'Delete "{name}"?\nIt will no longer be shown to you (admin can still see it; only admin hard-delete truly removes it).',
deleted: 'Deleted',
deleteFailed: 'Delete failed: {msg}'
},
refresh: 'Refresh'
}

40
src/i18n/locales/zh-CN.js Normal file
View File

@@ -0,0 +1,40 @@
// 中文语言包(默认语言,同步注入主 chunk
export default {
app: {
title: 'PDF 转换'
},
upload: {
hint: '拖入 .epub 文件,或',
pick: '点击选择文件',
limit: '仅支持 epub最大 250MB',
requireEpub: '仅支持 epub 文件',
oversize: '文件超过 250MB 上限',
uploading: '上传中…',
submitted: '已提交,转换中…',
failed: '上传失败',
networkError: '网络错误'
},
list: {
count: '共 {total} 个任务',
empty: '还没有任务。上传一个 epub 试试吧。',
loading: '加载中…',
loadFailed: '加载失败:{msg}'
},
job: {
name: '文件名',
size: '大小',
status: '状态',
created: '创建时间',
actions: '操作',
statusDone: '完成',
statusFailed: '失败',
statusConverting: '转换中',
statusPending: '排队中',
download: '下载',
delete: '删除',
deleteConfirm: '确定删除「{name}」?\n删除后将不再显示管理员仍可见需管理员彻底删除才会清除。',
deleted: '已删除',
deleteFailed: '删除失败:{msg}'
},
refresh: '刷新'
}