feat: PDF 页签从 iframe 改为构建期组件挂载,清理 iframe 配置与文档

前后端分离:UI/交互在 zPDF_package,能力由 zTools2 的 /api/pdf/* REST API 提供。
组件经同源 fetch 调用,httpOnly cookie(zk_pdf)经反代第一方自动携带,无需 CORS。
与 timeTableFix 集成范式完全一致。

- src/modules/pdf/Pdf.vue:iframe 包装器重写为组件挂载包装器,套 PageShell +
  透传 :locale;删除 pageSrc/getLiveness/scheduleFailCheck/openExternal 等 iframe 逻辑
- src/modules/pdf/index.js:注释从 iframe 改为构建期组件 import(requiresAlive 探针保留)
- src/config/app.config.js:移除 pdf.pagePath(iframe src),保留探针配置
- src/i18n/{zh-CN,en}.js:移除 pdf.openExternal(iframe 降级按钮文案,已无用)
- docs/submodule-zPDF_package.md:集成方式改为构建期组件 import,补 cookie 认证说明
- docs/deployment.md:拓扑去 /api/pdf 页面行,措辞改为组件 fetch
- third_party/zPDF_package:子模块指针升级到 4d12b61(去 iframe 表述)
This commit is contained in:
2026-07-27 17:14:07 +08:00
parent 304ebc9448
commit ae540a1a70
8 changed files with 50 additions and 142 deletions

View File

@@ -2,8 +2,8 @@
// 模块按需 import { appConfig } 使用,改 URL 只改这里即可(需重新 build
// 每个服务项都带默认值,避免缺失字段导致渲染崩溃。
//
// 路由约定zTools2 所有入口(页面/静态/探针/WS/API)统一挂在 /api/ 下,
// iframe 与 fetch 一律用**同源相对路径**/api/pdf、/api/health 等)。
// 路由约定zTools2 所有入口(API/静态/探针/WS统一挂在 /api/ 下,
// fetch 与探针一律用**同源相对路径**/api/pdf/jobs、/api/health 等)。
// 无论是 vite devserver.proxy 转发 /api 到本地 zTools2、还是任意部署环境
// Apache/Nginx 反代 /api 到当前环境的 zTools2同源相对路径都自动指向
// **当前环境**的服务,无需区分 dev/prod也不会误打到其它环境域名。
@@ -26,14 +26,12 @@ export const appConfig = {
// 探测超时(毫秒)
healthTimeoutMs: 4000
},
// PDF 转换服务zTools2 托管的 /api/pdf 页面iframe 同源嵌入
// PDF 转换服务zTools2 提供 /api/pdf/* REST APIzPDF_package 组件同源 fetch 调用
pdf: {
// 服务根地址(含协议与域名);默认空串 = 同源相对路径
baseUrl: BASE_URL,
// 存活探针路径;周期探测,挂掉则隐藏页签并卸载组件
healthPath: '/api/health',
// PDF 转换页面路径
pagePath: '/api/pdf',
// 探测超时(毫秒)
healthTimeoutMs: 4000
}

View File

@@ -20,10 +20,8 @@ export default {
openExternal: 'Open in new tab',
boardId: 'Board ID',
switch: 'Switch'
},
pdf: {
openExternal: 'Open in new tab'
}
// Mobile-game copy is provided by the z449 submodule's own i18n;
// mainPage keeps only tabs.mobileGame. Locale syncs to z449 via :locale prop.
// PDF tab copy is provided by the zPDF_package submodule's own i18n; mainPage keeps only tabs.pdf.
}

View File

@@ -19,10 +19,8 @@ export default {
openExternal: '在新标签页打开',
boardId: '白板 ID',
switch: '切换'
},
pdf: {
openExternal: '在新标签页打开'
}
// 注移动游戏项目mobile-game的文案由子模块 z449 自有 i18n 提供,
// mainPage 仅保留 tabs.mobileGame 页签标题。切换语言时经 :locale prop 同步给 z449。
// PDF 转换页签的文案由子模块 zPDF_package 自有 i18n 提供mainPage 仅保留 tabs.pdf。
}

View File

@@ -1,138 +1,47 @@
<script setup>
import { ref, computed, watch } from 'vue'
// 组件名与模块 id 一致,供 <KeepAlive :include> 按 id 精确匹配;
// PDF 服务后端失活时把它从 include 移除即可卸载组件
// ★ pdf 页签的 mainPage 侧包装器。
// 子模块 zPDF_package 的 App.vue 作为便携式组件被 import本包装器负责
// 1. 套 PageShell 统一容器宽度与标题(与其他页签对齐)
// 2. 持有 KeepAlive 匹配用的组件名(与模块 id 一致),子模块自身不持 name
// 3. 把 mainPage 当前语言经 :locale prop 透传给子模块的独立 i18n 实例,
// 使其跟随父项目语言开关响应式切换(不共享 messages仅同步 locale 值)
// 前后端分离UI/交互在 zPDF_package能力由 zTools2 的 /api/pdf/* REST API 提供,
// 同源 fetch 自动携带 httpOnly cookiezk_pdf无需 CORS。与 timeTableFix 集成范式一致。
// 子模块独立运行时不经过本包装器,保持纯净。
defineOptions({ name: 'pdf' })
import { appConfig, healthUrl } from '../../config/app.config.js'
import { useLocale } from '../../composables/useLocale.js'
import { getLiveness } from '../../composables/useLiveness.js'
import { computed } from 'vue'
import PageShell from '../../components/ui/PageShell.vue'
import i18n from '../../i18n/index.js'
import ZPdfApp from '../../../third_party/zPDF_package/src/App.vue'
const { t } = useLocale()
const pdfConfig = appConfig.pdf
// 完整 PDF 转换页 URLzTools2 同源托管,可被 iframe 嵌入)
const pageSrc = computed(() => `${pdfConfig.baseUrl}${pdfConfig.pagePath}`)
// 存活态:服务挂掉时显示降级提示(页签本身也已隐藏,但用户若直链进入仍可见)
const aliveState = getLiveness(healthUrl('pdf'))
const isDown = computed(() => aliveState.value === 'down')
const loaded = ref(false)
const failed = ref(false)
function onLoad() {
loaded.value = true
}
function scheduleFailCheck() {
setTimeout(() => {
if (!loaded.value) failed.value = true
}, 6000)
}
scheduleFailCheck()
function openExternal() {
window.open(pageSrc.value, '_blank', 'noopener')
}
// 透传父项目当前语言给子应用(响应式:切语言时子应用内容跟随)
const locale = computed(() => i18n.global.locale.value)
</script>
<template>
<PageShell title-key="tabs.pdf">
<!-- 服务不可达 / iframe 加载超时降级 -->
<div v-if="isDown || failed" class="pdf-fallback">
<p class="pdf-fallback__text">{{ t('common.serviceUnavailable') }}</p>
<button type="button" class="pdf-fallback__open" @click="openExternal">
{{ t('pdf.openExternal') }}
</button>
</div>
<div v-else class="pdf-frame">
<!-- 加载骨架iframe 加载完成后淡出 -->
<Transition name="fade">
<div v-if="!loaded" class="pdf-frame__skeleton">
<div class="pdf-frame__skeleton-text">{{ t('common.loading') }}</div>
</div>
</Transition>
<iframe
:src="pageSrc"
class="pdf-frame__iframe"
frameborder="0"
:title="t('tabs.pdf')"
@load="onLoad"
/>
<div class="pdf__frame">
<ZPdfApp :locale="locale" />
</div>
</PageShell>
</template>
<style scoped>
.pdf-frame {
/* 让子应用填满外壳容器,保留其内部布局自洽;转换页以上传与列表为主,给足高度 */
.pdf__frame {
position: relative;
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow);
border: 1px solid var(--border);
width: 100%;
background: var(--surface);
/* 转换页以上传与列表为主,给足高度 */
height: 78vh;
min-height: 520px;
background: var(--surface);
}
.pdf-frame__iframe {
width: 100%;
height: 100%;
display: block;
border: 0;
}
.pdf-frame__skeleton {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
background: var(--surface);
}
.pdf-frame__skeleton-text {
color: var(--text-tertiary);
font-size: 0.95rem;
}
.pdf-fallback {
text-align: center;
padding: var(--space-2xl) var(--space-lg);
border: 1px dashed var(--border-strong);
border-radius: var(--radius);
background: var(--surface);
}
.pdf-fallback__text {
color: var(--text-secondary);
margin-bottom: var(--space-md);
}
.pdf-fallback__open {
display: inline-flex;
align-items: center;
padding: 8px 18px;
font-size: 0.9rem;
font-weight: 500;
color: #fff;
background: var(--accent);
border-radius: var(--radius-sm);
transition: background var(--transition);
}
.pdf-fallback__open:hover {
background: var(--accent-hover);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
@media (max-width: 768px) {
.pdf-frame {
.pdf__frame {
height: 72vh;
min-height: 400px;
}

View File

@@ -1,7 +1,9 @@
// 模块元数据 -- PDF 转换页签
// 构建期组件集成mainPage 侧包装器 Pdf.vue import 子模块 zPDF_package 的 App.vue
// 套 PageShell 统一容器与标题。zPDF_package 作为 git submodule 独立维护,
// 其 UI/交互经同源 fetch 调用 zTools2 的 /api/pdf/* REST API前后端分离
// requiresAlive 指向存活探针 URL启动时探测zTools2 不可达则隐藏该页签。
// 页面用 iframe 嵌入 zTools2 托管的 /pdf同源cookie 第一方生效,无需 CORS
// 删除本目录即可移除此页签,其余模块不受影响。
// 升级cd third_party/zPDF_package && git pull回 mainPage 根 git add 该子模块并重新 build
import { healthUrl } from '../../config/app.config.js'
export default {