docs: add mobile OA print-to-PDF download implementation plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
433eb5d113
commit
5897b35b4e
1 file changed
+611
@@ -0,0 +1,611 @@
|
||||
# Mobile OA Print → PDF Download Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make mobile OA「打印」generate a web-aligned PDF and open WeChat system document preview (download + `openDocument`), instead of `window.print()`.
|
||||
|
||||
**Architecture:** `mobile-oa-h5` renders the existing print HTML off-screen, captures with `html2canvas`, builds PDF via `jspdf`, uploads via existing attachment API, then `wx.miniProgram.navigateTo` a new `mobile-h5` preview page that `downloadFile` + `openDocument({ showMenu: true })`. Outside WeChat, fall back to blob download via `<a download>`.
|
||||
|
||||
**Tech Stack:** Vue 3, Vite (`mobile-oa-h5`), uni-app (`mobile-h5`), `html2canvas@^1.4.1`, `jspdf@^4.2.1`, Node assert tests
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-13-mobile-oa-print-pdf-download-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| File | Responsibility |
|
||||
|------|----------------|
|
||||
| `mobile-oa-h5/src/oa/print/oa-print-filename.js` | Pure: build PDF filename from detail |
|
||||
| `mobile-oa-h5/src/oa/print/oa-print-filename.test.js` | Unit tests for filename |
|
||||
| `mobile-oa-h5/src/oa/print/exportOaApprovalPdf.js` | Model + HTML + canvas→PDF + upload/navigate/save |
|
||||
| `mobile-oa-h5/package.json` | Add `html2canvas`, `jspdf` |
|
||||
| `mobile-oa-h5/src/views/OaDetail.vue` | Toast copy for export failures |
|
||||
| `mobile-h5/pages/oa/pdf-preview/index.vue` | Host: download + openDocument + navigateBack |
|
||||
| `mobile-h5/pages.json` | Register pdf-preview route |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: PDF filename helper (TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `mobile-oa-h5/src/oa/print/oa-print-filename.js`
|
||||
- Create: `mobile-oa-h5/src/oa/print/oa-print-filename.test.js`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```js
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildOaPrintFilename } from './oa-print-filename.js'
|
||||
|
||||
assert.equal(
|
||||
buildOaPrintFilename({
|
||||
initiatorUserName: '张三',
|
||||
appName: '请假申请'
|
||||
}),
|
||||
'张三提交的请假申请.pdf'
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
buildOaPrintFilename({
|
||||
initiatorUserId: 'u1',
|
||||
title: '报销'
|
||||
}),
|
||||
'u1提交的报销.pdf'
|
||||
)
|
||||
|
||||
assert.equal(buildOaPrintFilename({}), '用户提交的审批单.pdf')
|
||||
|
||||
assert.equal(
|
||||
buildOaPrintFilename({
|
||||
initiatorUserName: 'a/b:c*',
|
||||
appName: 'x?y'
|
||||
}),
|
||||
'a_b_c_提交的x_y.pdf'
|
||||
)
|
||||
|
||||
console.log('oa-print-filename: ok')
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd mobile-oa-h5 && node src/oa/print/oa-print-filename.test.js`
|
||||
|
||||
Expected: FAIL — module not found
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
```js
|
||||
/** Align with web-admin `buildOaPrintFilename` in oa-approval-print.ts */
|
||||
export function buildOaPrintFilename(detail) {
|
||||
const initiator =
|
||||
String(detail?.initiatorUserName || '').trim() ||
|
||||
String(detail?.initiatorUserId || '').trim() ||
|
||||
'用户'
|
||||
const appName =
|
||||
String(detail?.appName || '').trim() ||
|
||||
String(detail?.title || '').trim() ||
|
||||
'审批单'
|
||||
const safe = (s) => String(s).replace(/[\\/:*?"<>|]/g, '_').slice(0, 80)
|
||||
return `${safe(initiator)}提交的${safe(appName)}.pdf`
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd mobile-oa-h5 && node src/oa/print/oa-print-filename.test.js`
|
||||
|
||||
Expected: `oa-print-filename: ok`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-oa-h5/src/oa/print/oa-print-filename.js \
|
||||
mobile-oa-h5/src/oa/print/oa-print-filename.test.js
|
||||
git commit -m "feat(oa): add mobile print PDF filename helper"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Align print HTML stamp + CSS with web v4
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-oa-h5/src/oa/print/exportOaApprovalPdf.js` (`buildPrintHtml` styles + stamp DOM)
|
||||
|
||||
Goal: PDF visual matches web `OaApprovalPrintSheet` (dingding stamp DOM, page width 794px, min-height ~1123px, table/meta/footer metrics). Keep field assembly as-is (related still 「已关联 N 条」).
|
||||
|
||||
- [ ] **Step 1: Replace simplified `.oa-stamp` CSS and DOM in `buildPrintHtml`**
|
||||
|
||||
Key CSS tokens (mirror web sheet):
|
||||
|
||||
```css
|
||||
.oa-print-page {
|
||||
position: relative;
|
||||
width: 794px;
|
||||
max-width: 794px;
|
||||
margin: 0 auto;
|
||||
min-height: 1123px;
|
||||
padding: 48px 56px 56px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
color: #1f2329;
|
||||
font-family: "PingFang SC","Microsoft YaHei","Helvetica Neue",Arial,sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.oa-print-title { margin: 0 0 14px; text-align: center; font-size: 28px;
|
||||
font-weight: 700; letter-spacing: 2px; line-height: 1.3; }
|
||||
.oa-print-meta { display: flex; flex-wrap: wrap; gap: 16px 48px; margin-bottom: 12px;
|
||||
font-size: 12px; line-height: 1.5; }
|
||||
.oa-print-table { width: 100%; border-collapse: separate; border-spacing: 0;
|
||||
table-layout: fixed; font-size: 13px; border-top: 1px solid #1f2329; border-left: 1px solid #1f2329; }
|
||||
.oa-print-table th, .oa-print-table td { border: none; border-right: 1px solid #1f2329;
|
||||
border-bottom: 1px solid #1f2329; padding: 9px 12px; vertical-align: middle;
|
||||
line-height: 1.5; word-break: break-all; }
|
||||
.oa-print-table th { width: 118px; font-weight: 400; text-align: left; background: #fff; }
|
||||
.oa-print-url { color: #3370ff; word-break: break-all; }
|
||||
.oa-stamp { --stamp: #3ec9a8; position: absolute; top: -10px; right: 18px;
|
||||
width: 118px; height: 118px; z-index: 5; pointer-events: none;
|
||||
transform: rotate(-18deg); opacity: 0.88; color: var(--stamp); }
|
||||
.oa-stamp.ok { --stamp: #3ec9a8; }
|
||||
.oa-stamp.run { --stamp: #f0a020; }
|
||||
.oa-stamp.rej { --stamp: #e85d5d; }
|
||||
.oa-stamp.ret { --stamp: #e07070; }
|
||||
.oa-stamp.can { --stamp: #9aa0a6; }
|
||||
.oa-stamp .outer { position: absolute; inset: 0; border-radius: 50%;
|
||||
border: 5px solid currentColor; box-sizing: border-box; }
|
||||
.oa-stamp .arc-top, .oa-stamp .arc-bottom { position: absolute; left: 14px; right: 14px;
|
||||
border: 2px solid currentColor; opacity: 0.95; pointer-events: none; }
|
||||
.oa-stamp .arc-top { top: 11px; height: 36px; border-bottom: none;
|
||||
border-left-color: transparent; border-right-color: transparent;
|
||||
border-radius: 50% 50% 0 0 / 100% 100% 0 0; }
|
||||
.oa-stamp .arc-bottom { bottom: 11px; height: 36px; border-top: none;
|
||||
border-left-color: transparent; border-right-color: transparent;
|
||||
border-radius: 0 0 50% 50% / 0 0 100% 100%; }
|
||||
.oa-stamp .label-top { position: absolute; top: 22px; left: 0; right: 0;
|
||||
text-align: center; font-size: 10px; font-weight: 500; letter-spacing: 2px;
|
||||
line-height: 1; color: currentColor; opacity: 0.95; }
|
||||
.oa-stamp .label-bottom { position: absolute; bottom: 20px; left: 0; right: 0;
|
||||
text-align: center; font-size: 9px; font-weight: 500; letter-spacing: 0.5px;
|
||||
line-height: 1; color: currentColor; opacity: 0.9;
|
||||
font-family: Arial, "Helvetica Neue", sans-serif; }
|
||||
.oa-stamp .main { position: absolute; inset: 0; display: flex; align-items: center;
|
||||
justify-content: center; font-size: 32px; font-weight: 700; letter-spacing: 2px;
|
||||
line-height: 1; color: currentColor; }
|
||||
.oa-stamp .main.is-chars3 { font-size: 26px; letter-spacing: 1px; }
|
||||
```
|
||||
|
||||
Keep flow/footer/wm rules from the current file. Drop `@media print` after Task 3 if unused.
|
||||
|
||||
Replace stamp DOM:
|
||||
|
||||
```html
|
||||
<div class="oa-stamp ${escapeHtml(model.stampClass)}" aria-label="${escapeHtml(model.stampText)}">
|
||||
<div class="outer"></div>
|
||||
<div class="arc-top"></div>
|
||||
<div class="arc-bottom"></div>
|
||||
<div class="label-top">OA审批</div>
|
||||
<div class="main${stampChars3}">${escapeHtml(model.stampText)}</div>
|
||||
<div class="label-bottom">approval</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Where `stampChars3 = model.stampText.length >= 3 ? ' is-chars3' : ''` (class on `.main`, not on root).
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-oa-h5/src/oa/print/exportOaApprovalPdf.js
|
||||
git commit -m "fix(oa): align mobile print HTML stamp and layout with web PDF"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Dependencies + canvas→PDF generation
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-oa-h5/package.json`
|
||||
- Modify: `mobile-oa-h5/src/oa/print/exportOaApprovalPdf.js`
|
||||
|
||||
- [ ] **Step 1: Install deps (same major as web-admin)**
|
||||
|
||||
```bash
|
||||
cd mobile-oa-h5 && pnpm add html2canvas@^1.4.1 jspdf@^4.2.1
|
||||
```
|
||||
|
||||
If the package uses npm instead, run `npm install html2canvas@^1.4.1 jspdf@^4.2.1`.
|
||||
|
||||
- [ ] **Step 2: Refactor HTML builder for off-screen mount**
|
||||
|
||||
Add `buildPrintBodyHtml(model)` that returns `<style>...</style>` + `.oa-print-page` DOM (no full document). Have `buildPrintHtml` wrap it in `<!DOCTYPE html>...` only if still needed for tests; otherwise keep one function used by generation.
|
||||
|
||||
- [ ] **Step 3: Add canvas helpers + `generateOaApprovalPdfBlob`**
|
||||
|
||||
Imports:
|
||||
|
||||
```js
|
||||
import html2canvas from 'html2canvas'
|
||||
import { jsPDF } from 'jspdf'
|
||||
import { buildOaPrintFilename } from './oa-print-filename.js'
|
||||
```
|
||||
|
||||
Port `renderPageLabelCanvas` and `canvasToPdfBlob` from web-admin `oa-approval-print.ts` (`EPS_MM = 1.5`, JPEG 0.95, Chinese page labels via canvas).
|
||||
|
||||
```js
|
||||
export async function generateOaApprovalPdfBlob(detail, opts = {}) {
|
||||
if (!detail) throw new Error('暂无审批详情,无法导出')
|
||||
const model = buildOaPrintModel(detail, {
|
||||
printerName: opts.printerName || ''
|
||||
})
|
||||
const filename = buildOaPrintFilename(detail)
|
||||
const host = document.createElement('div')
|
||||
host.setAttribute('data-oa-print-host', '1')
|
||||
host.style.cssText =
|
||||
'position:fixed;left:-100000px;top:0;width:794px;z-index:-1;opacity:1;pointer-events:none;'
|
||||
document.body.appendChild(host)
|
||||
host.innerHTML = buildPrintBodyHtml(model)
|
||||
try {
|
||||
await new Promise((r) => requestAnimationFrame(() => r()))
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
const el = host.querySelector('.oa-print-page')
|
||||
if (!el) throw new Error('print sheet missing')
|
||||
const canvas = await html2canvas(el, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
backgroundColor: '#ffffff',
|
||||
logging: false,
|
||||
windowWidth: el.scrollWidth,
|
||||
windowHeight: el.scrollHeight
|
||||
})
|
||||
const blob = await canvasToPdfBlob(canvas)
|
||||
return { blob, filename }
|
||||
} finally {
|
||||
host.remove()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-oa-h5/package.json mobile-oa-h5/src/oa/print/exportOaApprovalPdf.js
|
||||
# also add lockfile if changed (pnpm-lock.yaml or package-lock.json)
|
||||
git commit -m "feat(oa): generate mobile OA approval PDF via html2canvas and jspdf"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Export orchestration — WeChat upload + navigate / browser save
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-oa-h5/src/oa/print/exportOaApprovalPdf.js`
|
||||
|
||||
- [ ] **Step 1: Replace `exportOaApprovalPdf` (remove `window.print` path)**
|
||||
|
||||
Additional imports:
|
||||
|
||||
```js
|
||||
import { uploadAttachment, fetchAttachmentDownloadUrl } from '../../api/oa.js'
|
||||
import { attachmentFullUrl } from '../../utils/attachment.js'
|
||||
```
|
||||
|
||||
```js
|
||||
function navigateToHostPdfPreview({ url, fileName }) {
|
||||
const q =
|
||||
'url=' +
|
||||
encodeURIComponent(url) +
|
||||
'&name=' +
|
||||
encodeURIComponent(fileName || 'approval.pdf')
|
||||
const path = '/pages/oa/pdf-preview/index?' + q
|
||||
try {
|
||||
if (typeof wx !== 'undefined' && wx.miniProgram?.navigateTo) {
|
||||
wx.miniProgram.navigateTo({ url: path })
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
if (typeof uni !== 'undefined' && uni.webView?.navigateTo) {
|
||||
uni.webView.navigateTo({ url: path })
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<{ ok: boolean, message?: string, mode?: 'preview' | 'download' }>}
|
||||
*/
|
||||
export async function exportOaApprovalPdf(detail, opts = {}) {
|
||||
if (!detail) {
|
||||
return { ok: false, message: '暂无审批详情,无法导出 PDF' }
|
||||
}
|
||||
|
||||
let blob
|
||||
let filename
|
||||
try {
|
||||
;({ blob, filename } = await generateOaApprovalPdfBlob(detail, opts))
|
||||
} catch (e) {
|
||||
return {
|
||||
ok: false,
|
||||
message: (e && e.message) || '生成 PDF 失败'
|
||||
}
|
||||
}
|
||||
|
||||
if (isWeChatMiniWebView()) {
|
||||
try {
|
||||
const file = new File([blob], filename, { type: 'application/pdf' })
|
||||
const res = await uploadAttachment(file)
|
||||
const fileUrl = String(res?.fileUrl || res?.data?.fileUrl || '').trim()
|
||||
if (!fileUrl) {
|
||||
return { ok: false, message: '上传 PDF 失败:无文件地址' }
|
||||
}
|
||||
|
||||
let openUrl = ''
|
||||
try {
|
||||
const dl = await fetchAttachmentDownloadUrl({
|
||||
fileUrl,
|
||||
fileName: filename
|
||||
})
|
||||
openUrl = String(dl?.downloadUrl || dl?.data?.downloadUrl || '').trim()
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
if (!openUrl) {
|
||||
openUrl = attachmentFullUrl(fileUrl) || fileUrl
|
||||
}
|
||||
if (!/^https?:\/\//i.test(openUrl)) {
|
||||
return { ok: false, message: '无法打开 PDF:下载地址无效' }
|
||||
}
|
||||
|
||||
const navigated = navigateToHostPdfPreview({
|
||||
url: openUrl,
|
||||
fileName: filename
|
||||
})
|
||||
if (!navigated) {
|
||||
return {
|
||||
ok: false,
|
||||
message: '无法跳转预览页,请升级微信后重试'
|
||||
}
|
||||
}
|
||||
return { ok: true, mode: 'preview' }
|
||||
} catch (e) {
|
||||
return {
|
||||
ok: false,
|
||||
message: (e && e.message) || '上传或打开 PDF 失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const href = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = href
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
setTimeout(() => URL.revokeObjectURL(href), 2000)
|
||||
return { ok: true, mode: 'download' }
|
||||
} catch (e) {
|
||||
return {
|
||||
ok: false,
|
||||
message: (e && e.message) || '下载 PDF 失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep exporting `buildOaPrintModel`, `OA_PRINT_COMPANY_NAME`, and `resolveStamp` re-export for existing imports.
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-oa-h5/src/oa/print/exportOaApprovalPdf.js
|
||||
git commit -m "feat(oa): export mobile PDF via upload and WeChat preview navigate"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: `mobile-h5` PDF preview page
|
||||
|
||||
**Files:**
|
||||
- Create: `mobile-h5/pages/oa/pdf-preview/index.vue`
|
||||
- Modify: `mobile-h5/pages.json` (insert after `pages/oa/webview/index`)
|
||||
|
||||
- [ ] **Step 1: Register route in `pages.json`**
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "pages/oa/pdf-preview/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "文档预览",
|
||||
"navigationBarBackgroundColor": "#ecfdf5",
|
||||
"navigationBarTextStyle": "black"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create preview page**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="pdf-preview">
|
||||
<view class="pdf-preview__tip">{{ tip }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tip: '正在打开文档…',
|
||||
_opened: false
|
||||
}
|
||||
},
|
||||
onLoad(query) {
|
||||
const url = decodeURIComponent(String((query && query.url) || '').trim())
|
||||
const name = decodeURIComponent(
|
||||
String((query && query.name) || 'approval.pdf').trim()
|
||||
)
|
||||
if (!url || !/^https?:\/\//i.test(url)) {
|
||||
this.tip = '文件地址无效'
|
||||
uni.showToast({ title: '文件地址无效', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.openPdf(url, name)
|
||||
},
|
||||
methods: {
|
||||
openPdf(url, name) {
|
||||
if (this._opened) return
|
||||
this._opened = true
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success: (res) => {
|
||||
if (!res || res.statusCode !== 200 || !res.tempFilePath) {
|
||||
this.tip = '下载失败'
|
||||
uni.showToast({ title: '下载 PDF 失败', icon: 'none' })
|
||||
this._opened = false
|
||||
return
|
||||
}
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fileType: 'pdf',
|
||||
showMenu: true,
|
||||
success: () => {
|
||||
this.tip = '已打开预览'
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({ fail: () => {} })
|
||||
}, 300)
|
||||
},
|
||||
fail: () => {
|
||||
this.tip = '无法打开文档'
|
||||
uni.showToast({ title: '无法打开 PDF', icon: 'none' })
|
||||
this._opened = false
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
this.tip = '下载失败'
|
||||
uni.showToast({ title: '下载 PDF 失败', icon: 'none' })
|
||||
this._opened = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pdf-preview {
|
||||
min-height: 100vh;
|
||||
padding: 48rpx 32rpx;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.pdf-preview__tip {
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
Ops note: WeChat download合法域名 must include MinIO / download host.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-h5/pages/oa/pdf-preview/index.vue mobile-h5/pages.json
|
||||
git commit -m "feat(oa): add WeChat host page to preview exported OA PDF"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Wire `OaDetail` toast copy
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-oa-h5/src/views/OaDetail.vue` (`onPrint`)
|
||||
|
||||
- [ ] **Step 1: Update failure toasts**
|
||||
|
||||
```js
|
||||
async function onPrint() {
|
||||
if (!detail.value || printing.value) return
|
||||
printing.value = true
|
||||
try {
|
||||
const printerName =
|
||||
currentUserName.value ||
|
||||
String(detail.value?.initiatorUserName || '').trim() ||
|
||||
'-'
|
||||
const result = await exportOaApprovalPdf(detail.value, { printerName })
|
||||
if (!result?.ok) {
|
||||
showToast(result?.message || '导出 PDF 失败')
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e?.message || '导出 PDF 失败')
|
||||
} finally {
|
||||
printing.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Do not toast on success when `mode === 'preview'`.
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-oa-h5/src/views/OaDetail.vue
|
||||
git commit -m "fix(oa): update mobile print button toasts for PDF export"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Verification
|
||||
|
||||
- [ ] **Step 1: Unit tests**
|
||||
|
||||
```bash
|
||||
cd mobile-oa-h5 && node src/oa/print/oa-print-filename.test.js
|
||||
cd mobile-oa-h5 && node src/oa/oa-status-stamp.test.js
|
||||
```
|
||||
|
||||
Expected: both print `ok`
|
||||
|
||||
- [ ] **Step 2: Manual (WeChat)**
|
||||
|
||||
1. Deploy `mobile-oa-h5` + `mobile-h5` used by the mini program web-view.
|
||||
2. 详情点「打印」→ 系统 PDF 预览,可转发 / 用其他应用打开。
|
||||
3. 列表长按「打印」→ 详情自动导出并预览。
|
||||
4. 对比 web PDF:标题、公司名、印章、水印、页脚、页码大体一致。
|
||||
5. 失败场景:详情 toast,不关闭。
|
||||
|
||||
- [ ] **Step 3: Manual (browser)**
|
||||
|
||||
Open `mobile-oa-h5` outside WeChat →「打印」直接下载 `.pdf`。
|
||||
|
||||
---
|
||||
|
||||
## Spec coverage checklist
|
||||
|
||||
| Spec item | Task |
|
||||
|-----------|------|
|
||||
| html2canvas + jspdf generate PDF | Task 3 |
|
||||
| Upload via existing attachment API | Task 4 |
|
||||
| WeChat navigateTo + openDocument preview | Task 4–5 |
|
||||
| Browser download fallback | Task 4 |
|
||||
| Filename `{发起人}提交的{流程}.pdf` | Task 1 |
|
||||
| Layout/stamp align web | Task 2 |
|
||||
| autoPrint same path | Task 4 |
|
||||
| Preview then navigateBack | Task 5 |
|
||||
| Error toasts, stay on detail | Task 4 + 6 |
|
||||
| Related nested out of scope | unchanged |
|
||||
|
||||
## Consistency notes
|
||||
|
||||
- Functions: `buildOaPrintFilename`, `generateOaApprovalPdfBlob`, `exportOaApprovalPdf`, `navigateToHostPdfPreview`
|
||||
- Host path: `/pages/oa/pdf-preview/index`
|
||||
- Upload fields: `fileUrl` (+ `data?.fileUrl` fallback like `OaUploadField`)
|
||||
- Download fields: `downloadUrl` via `fetchAttachmentDownloadUrl`
|
||||
Reference in new issue
Block a user