Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a511004812 | ||
|
|
7ac2ff282c |
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { attachmentFullUrl } from '../../utils/attachment.js'
|
||||
/**
|
||||
* OA 审批单移动端打印(S6)
|
||||
* 对齐 web-admin `oa-approval-print.ts` 字段/流程结构;支持 html2canvas + jsPDF 导出。
|
||||
@@ -66,7 +67,6 @@ function escapeHtml(s) {
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
|
||||
function formatDateOnly(createTime) {
|
||||
const raw = String(createTime || '').trim()
|
||||
if (!raw) return '-'
|
||||
@@ -99,10 +99,12 @@ function formatScalar(value, sep = '、') {
|
||||
return String(value)
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((x) => formatScalar(x, sep))
|
||||
.filter((x) => x && x !== '-')
|
||||
.join(sep) || '-'
|
||||
return (
|
||||
value
|
||||
.map((x) => formatScalar(x, sep))
|
||||
.filter((x) => x && x !== '-')
|
||||
.join(sep) || '-'
|
||||
)
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
if (value.name != null) return String(value.name)
|
||||
@@ -397,7 +399,13 @@ export function buildOaPrintModel(detail, opts = {}) {
|
||||
stampClass,
|
||||
stampText,
|
||||
rows: [...headerRows, ...buildFormRows(detail)],
|
||||
flowItems: buildFlowItems(detail),
|
||||
flowItems: buildFlowItems(detail, (url) => {
|
||||
try {
|
||||
return new URL(attachmentFullUrl(url), window.location.origin).href
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}),
|
||||
printTime,
|
||||
printerName,
|
||||
watermarkName: printerName === '-' ? '审批' : printerName
|
||||
@@ -435,6 +443,9 @@ function flowCellHtml(f) {
|
||||
} else if (f.comment) {
|
||||
html += `<div class="oa-print-flow__comment">意见:${escapeHtml(f.comment)}</div>`
|
||||
}
|
||||
for (const url of f.attachmentLinks || []) {
|
||||
html += `<div class="oa-print-url"><a data-oa-print-link href="${escapeHtml(url)}">${escapeHtml(url)}</a></div>`
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
@@ -472,9 +483,10 @@ function buildPrintStylesContent() {
|
||||
line-height: 1.5; word-break: break-all; }
|
||||
.oa-print-table th { width: 118px; font-weight: 400; text-align: left; background: #fff; }
|
||||
.oa-print-text { white-space: pre-wrap; }
|
||||
.oa-print-url a { color: inherit; }
|
||||
.oa-print-url { color: #3370ff; word-break: break-all; }
|
||||
.oa-print-flow__row { display: flex; justify-content: space-between; gap: 12px; }
|
||||
.oa-print-flow__left { flex: 1; min-width: 0; }
|
||||
.oa-print-flow__left { white-space: pre-wrap; overflow-wrap: anywhere; flex: 1; min-width: 0; }
|
||||
.oa-print-flow__right { flex: 0 0 auto; color: #646464; white-space: nowrap; }
|
||||
.oa-print-flow__person-lines { margin-top: 4px; padding-left: 8px; }
|
||||
.oa-print-flow__person-line { margin-top: 2px; font-size: 12px; }
|
||||
@@ -602,7 +614,7 @@ function renderPageLabelCanvas(text) {
|
||||
return c
|
||||
}
|
||||
|
||||
async function canvasToPdfBlob(canvas) {
|
||||
async function canvasToPdfBlob(canvas, sheet) {
|
||||
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' })
|
||||
const pageWidth = pdf.internal.pageSize.getWidth()
|
||||
const pageHeight = pdf.internal.pageSize.getHeight()
|
||||
@@ -620,6 +632,23 @@ async function canvasToPdfBlob(canvas) {
|
||||
const position = -pageIndex * pageHeight
|
||||
pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
|
||||
|
||||
// html2canvas 只保留像素,单独把附件链接写为 PDF 注解;跨页部分按页裁切。
|
||||
const sheetRect = sheet.getBoundingClientRect()
|
||||
const ratio = pageWidth / sheetRect.width
|
||||
for (const link of sheet.querySelectorAll('a[data-oa-print-link]')) {
|
||||
if (!/^https?:\/\//i.test(link.href)) continue
|
||||
for (const rect of link.getClientRects()) {
|
||||
const top = (rect.top - sheetRect.top) * ratio - pageIndex * pageHeight
|
||||
const bottom = Math.min(pageHeight, top + rect.height * ratio)
|
||||
const y = Math.max(0, top)
|
||||
if (bottom > y) {
|
||||
pdf.link((rect.left - sheetRect.left) * ratio, y, rect.width * ratio, bottom - y, {
|
||||
url: link.href
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const label = `第${pageIndex + 1}页`
|
||||
const labelCanvas = renderPageLabelCanvas(label)
|
||||
if (labelCanvas.width > 1) {
|
||||
@@ -664,7 +693,7 @@ export async function generateOaApprovalPdfBlob(detail, opts = {}) {
|
||||
windowWidth: el.scrollWidth,
|
||||
windowHeight: el.scrollHeight
|
||||
})
|
||||
const blob = await canvasToPdfBlob(canvas)
|
||||
const blob = await canvasToPdfBlob(canvas, el)
|
||||
return { blob, filename }
|
||||
} finally {
|
||||
host.remove()
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildFlowItems } from './oa-print-flow.js'
|
||||
|
||||
describe('打印讨论评论', () => {
|
||||
it('按时间展示正文、提及和附件,并排在待审节点前', () => {
|
||||
const items = buildFlowItems({
|
||||
progress: [
|
||||
{
|
||||
kind: 'submit',
|
||||
title: '发起人',
|
||||
status: 'submitted',
|
||||
timestamp: '2026-11-01 10:00:00'
|
||||
},
|
||||
{
|
||||
kind: 'pending_node',
|
||||
title: '审批人',
|
||||
status: 'pending',
|
||||
timestamp: '2026-11-01 10:00:00'
|
||||
}
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: '2',
|
||||
createByName: '李四',
|
||||
createTime: '2026-11-01 12:00:00',
|
||||
content: '',
|
||||
attachmentRefs: [{ url: 'https://example.com/file.pdf', fileName: '附件.pdf' }]
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
createByName: '张三',
|
||||
createTime: '2026-11-01 11:00:00',
|
||||
content: '@李四 请处理\n第二行',
|
||||
mentions: [{ userId: 'u2', userName: '李四' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
assert.equal(items.length, 4)
|
||||
assert.equal(items[1].left, '【评论】 张三')
|
||||
assert.equal(items[1].right, '2026-11-01 11:00:00')
|
||||
assert.deepEqual(items[1].personLines, [
|
||||
{ left: '@李四 请处理\n第二行', right: '' },
|
||||
{ left: '提到:@李四', right: '' }
|
||||
])
|
||||
assert.deepEqual(items[2].attachmentLinks, ['https://example.com/file.pdf'])
|
||||
assert.match(items[3].left, /待处理/)
|
||||
})
|
||||
it('撤回评论不泄露正文、提及和附件', () => {
|
||||
const [item] = buildFlowItems({
|
||||
comments: [
|
||||
{
|
||||
id: '1',
|
||||
createBy: 'u1',
|
||||
status: 'revoked',
|
||||
content: 'secret',
|
||||
mentions: [{ userName: 'secret' }],
|
||||
attachmentRefs: [{ url: 'https://example.com/secret' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
assert.equal(item.left, '【评论】 u1 已撤回')
|
||||
assert.equal(JSON.stringify(item).includes('secret'), false)
|
||||
})
|
||||
it('安全过滤附件协议,保留纯附件与纯提及评论', () => {
|
||||
const items = buildFlowItems({
|
||||
comments: [
|
||||
{
|
||||
id: '1',
|
||||
mentions: [{ userId: 'u1' }],
|
||||
attachmentRefs: [{ url: 'javascript:alert(1)' }, { url: 'https://example.com/a?x=1&y=2' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
assert.deepEqual(items[0].attachmentLinks, ['https://example.com/a?x=1&y=2'])
|
||||
assert.deepEqual(items[0].personLines, [{ left: '提到:@u1', right: '' }])
|
||||
})
|
||||
})
|
||||
|
||||
it('同秒节点使用详情页的稳定排序键', () => {
|
||||
const progress = Array.from({ length: 12 }, (_, index) => ({
|
||||
kind: 'task',
|
||||
nodeKey: `node-${String(11 - index).padStart(2, '0')}`,
|
||||
title: `节点${11 - index}`,
|
||||
status: 'approved',
|
||||
timestamp: '2026-11-01 10:00:00'
|
||||
}))
|
||||
const items = buildFlowItems({
|
||||
progress,
|
||||
comments: [{ id: 'comment', content: '讨论', createTime: '2026-11-01 10:00:00' }]
|
||||
})
|
||||
assert.equal(items[0].left, '【节点0】 已同意')
|
||||
assert.equal(items[11].left, '【节点11】 已同意')
|
||||
assert.equal(items[12].left, '【评论】 评论')
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mergeProgressWithComments } from '../oa-progress-merge-comments.js'
|
||||
/**
|
||||
* 移动端打印「审批流程」行组装(无 html2canvas/API 副作用,便于单测)
|
||||
*/
|
||||
@@ -37,16 +38,22 @@ function isCcProgressItem(p) {
|
||||
const taskType = String(p?.taskType || p?.task_type || '').trim()
|
||||
const signMode = String(p?.signMode || p?.sign_mode || '').trim()
|
||||
const title = String(p?.title || p?.nodeName || '').trim()
|
||||
return (
|
||||
taskType === 'cc' ||
|
||||
signMode === 'cc' ||
|
||||
title === '抄送人' ||
|
||||
/^抄送/.test(title)
|
||||
)
|
||||
return taskType === 'cc' || signMode === 'cc' || title === '抄送人' || /^抄送/.test(title)
|
||||
}
|
||||
|
||||
export function buildFlowItems(detail) {
|
||||
const list = Array.isArray(detail?.progress) ? detail.progress : []
|
||||
export function buildFlowItems(detail, resolveUrl = (url) => url) {
|
||||
const progress = Array.isArray(detail?.progress) ? detail.progress : []
|
||||
// 沿用详情页的时间顺序;无讨论时保持原有审批节点顺序。
|
||||
const list =
|
||||
Array.isArray(detail?.comments) && detail.comments.length
|
||||
? mergeProgressWithComments(
|
||||
progress.map((p, index) => ({
|
||||
...p,
|
||||
key: `${p.kind}-${p.nodeKey || p.taskId || index}-${p.roundNo || 1}`
|
||||
})),
|
||||
detail.comments
|
||||
)
|
||||
: progress
|
||||
const items = []
|
||||
let ccBuf = null
|
||||
const flushCc = () => {
|
||||
@@ -70,6 +77,37 @@ export function buildFlowItems(detail) {
|
||||
const signMode = String(p.signMode || p.sign_mode || '')
|
||||
const signLabel = signMode === 'and' ? '会签' : signMode === 'or' ? '或签' : ''
|
||||
|
||||
if (kind === 'comment') {
|
||||
flushCc()
|
||||
const revoked = p.status === 'revoked'
|
||||
const author = String(p.assignee || p.title || '评论').trim()
|
||||
const item = {
|
||||
left: `【评论】 ${author}${revoked ? ' 已撤回' : ''}`,
|
||||
right: ts
|
||||
}
|
||||
if (!revoked) {
|
||||
const lines = []
|
||||
if (comment) lines.push({ left: comment, right: '' })
|
||||
const mentions = (p.mentions || [])
|
||||
.map((m) => String(m.userName || m.userId || '').trim())
|
||||
.filter(Boolean)
|
||||
if (mentions.length)
|
||||
lines.push({
|
||||
left: `提到:${mentions.map((name) => `@${name}`).join('、')}`,
|
||||
right: ''
|
||||
})
|
||||
if (lines.length) item.personLines = lines
|
||||
const links = (p.attachmentRefs || [])
|
||||
.map((file) => String(file.url || '').trim())
|
||||
.filter((url) => url && !/^(?!https?:)[a-z][a-z0-9+.-]*:/i.test(url))
|
||||
.map(resolveUrl)
|
||||
.filter((url) => /^https?:\/\//i.test(url))
|
||||
if (links.length) item.attachmentLinks = links
|
||||
}
|
||||
items.push(item)
|
||||
continue
|
||||
}
|
||||
|
||||
if (isCcProgressItem(p)) {
|
||||
if (!ccBuf) ccBuf = { names: [], ts }
|
||||
if (names.length) ccBuf.names.push(...names)
|
||||
|
||||
@@ -86,6 +86,9 @@
|
||||
<div v-else-if="f.comment" class="oa-print-flow__comment">
|
||||
意见:{{ f.comment }}
|
||||
</div>
|
||||
<div v-for="url in f.attachmentLinks" :key="url" class="oa-print-url">
|
||||
<a :href="url" data-oa-print-link>{{ url }}</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -221,6 +224,10 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.oa-print-url a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.oa-print-url {
|
||||
color: #3370ff;
|
||||
word-break: break-all;
|
||||
@@ -248,6 +255,8 @@
|
||||
}
|
||||
|
||||
.oa-print-flow__left {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildFlowItems } from './oa-approval-print-flow'
|
||||
|
||||
describe('打印讨论评论', () => {
|
||||
it('按时间展示正文、提及和附件,并排在待审节点前', () => {
|
||||
const items = buildFlowItems({
|
||||
progress: [
|
||||
{ kind: 'submit', title: '发起人', status: 'submitted', timestamp: '2026-11-01 10:00:00' },
|
||||
{
|
||||
kind: 'pending_node',
|
||||
title: '审批人',
|
||||
status: 'pending',
|
||||
timestamp: '2026-11-01 10:00:00'
|
||||
}
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: '2',
|
||||
createByName: '李四',
|
||||
createTime: '2026-11-01 12:00:00',
|
||||
content: '',
|
||||
attachmentRefs: [{ url: 'https://example.com/file.pdf', fileName: '附件.pdf' }]
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
createByName: '张三',
|
||||
createTime: '2026-11-01 11:00:00',
|
||||
content: '@李四 请处理\n第二行',
|
||||
mentions: [{ userId: 'u2', userName: '李四' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
assert.equal(items.length, 4)
|
||||
assert.equal(items[1].left, '【评论】 张三')
|
||||
assert.equal(items[1].right, '2026-11-01 11:00:00')
|
||||
assert.deepEqual(items[1].personLines, [
|
||||
{ left: '@李四 请处理\n第二行', right: '' },
|
||||
{ left: '提到:@李四', right: '' }
|
||||
])
|
||||
assert.deepEqual(items[2].attachmentLinks, ['https://example.com/file.pdf'])
|
||||
assert.match(items[3].left, /待处理/)
|
||||
})
|
||||
it('撤回评论不泄露正文、提及和附件', () => {
|
||||
const [item] = buildFlowItems({
|
||||
comments: [
|
||||
{
|
||||
id: '1',
|
||||
createBy: 'u1',
|
||||
status: 'revoked',
|
||||
content: 'secret',
|
||||
mentions: [{ userName: 'secret' }],
|
||||
attachmentRefs: [{ url: 'https://example.com/secret' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
assert.equal(item.left, '【评论】 u1 已撤回')
|
||||
assert.equal(JSON.stringify(item).includes('secret'), false)
|
||||
})
|
||||
it('安全过滤附件协议,保留纯附件与纯提及评论', () => {
|
||||
const items = buildFlowItems({
|
||||
comments: [
|
||||
{
|
||||
id: '1',
|
||||
mentions: [{ userId: 'u1' }],
|
||||
attachmentRefs: [{ url: 'javascript:alert(1)' }, { url: 'https://example.com/a?x=1&y=2' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
assert.deepEqual(items[0].attachmentLinks, ['https://example.com/a?x=1&y=2'])
|
||||
assert.deepEqual(items[0].personLines, [{ left: '提到:@u1', right: '' }])
|
||||
})
|
||||
})
|
||||
|
||||
it('同秒节点使用详情页的稳定排序键', () => {
|
||||
const progress = Array.from({ length: 12 }, (_, index) => ({
|
||||
kind: 'task',
|
||||
nodeKey: `node-${String(11 - index).padStart(2, '0')}`,
|
||||
title: `节点${11 - index}`,
|
||||
status: 'approved',
|
||||
timestamp: '2026-11-01 10:00:00'
|
||||
}))
|
||||
const items = buildFlowItems({
|
||||
progress,
|
||||
comments: [{ id: 'comment', content: '讨论', createTime: '2026-11-01 10:00:00' }]
|
||||
})
|
||||
assert.equal(items[0].left, '【节点0】 已同意')
|
||||
assert.equal(items[11].left, '【节点11】 已同意')
|
||||
assert.equal(items[12].left, '【评论】 评论')
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mergeProgressWithComments } from './oa-progress-merge-comments'
|
||||
/**
|
||||
* OA 打印「审批流程」行组装(无 Vue/canvas 副作用,便于单测)
|
||||
*/
|
||||
@@ -11,6 +12,7 @@ export type OaPrintPersonLine = {
|
||||
export type OaPrintFlowItem = {
|
||||
left: string
|
||||
right: string
|
||||
attachmentLinks?: string[]
|
||||
/** @deprecated 多人节点改用 personLines;单人/发起人可保留 */
|
||||
comment?: string
|
||||
/** 或签/会签按人行:左侧文案 + 右侧时间(右对齐) */
|
||||
@@ -66,20 +68,29 @@ function isCcProgressItem(p: any): boolean {
|
||||
const taskType = String(p?.taskType || p?.task_type || '').trim()
|
||||
const signMode = String(p?.signMode || p?.sign_mode || '').trim()
|
||||
const title = String(p?.title || p?.nodeName || '').trim()
|
||||
return (
|
||||
taskType === 'cc' ||
|
||||
signMode === 'cc' ||
|
||||
title === '抄送人' ||
|
||||
/^抄送/.test(title)
|
||||
)
|
||||
return taskType === 'cc' || signMode === 'cc' || title === '抄送人' || /^抄送/.test(title)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 progress 组装审批流程行;抄送节点聚合为「抄送A, B, C」对齐钉钉导出。
|
||||
* 若 progress 漏抄送,则用 tasks 兜底补一行。
|
||||
*/
|
||||
export function buildFlowItems(detail: any): OaPrintFlowItem[] {
|
||||
const list = Array.isArray(detail?.progress) ? detail.progress : []
|
||||
export function buildFlowItems(
|
||||
detail: any,
|
||||
resolveUrl: (url: string) => string = (url) => url
|
||||
): OaPrintFlowItem[] {
|
||||
const progress = Array.isArray(detail?.progress) ? detail.progress : []
|
||||
// 沿用详情页的时间顺序;无讨论时保持原有审批节点顺序。
|
||||
const list =
|
||||
Array.isArray(detail?.comments) && detail.comments.length
|
||||
? mergeProgressWithComments(
|
||||
progress.map((p: any, index: number) => ({
|
||||
...p,
|
||||
key: `${p.kind}-${p.nodeKey || p.taskId || index}-${p.roundNo || 1}`
|
||||
})),
|
||||
detail.comments
|
||||
)
|
||||
: progress
|
||||
const items: OaPrintFlowItem[] = []
|
||||
|
||||
let ccBuf: { names: string[]; ts: string } | null = null
|
||||
@@ -104,6 +115,37 @@ export function buildFlowItems(detail: any): OaPrintFlowItem[] {
|
||||
const signMode = String(p.signMode || p.sign_mode || '')
|
||||
const signLabel = signMode === 'and' ? '会签' : signMode === 'or' ? '或签' : ''
|
||||
|
||||
if (kind === 'comment') {
|
||||
flushCc()
|
||||
const revoked = p.status === 'revoked'
|
||||
const author = String(p.assignee || p.title || '评论').trim()
|
||||
const item: OaPrintFlowItem = {
|
||||
left: `【评论】 ${author}${revoked ? ' 已撤回' : ''}`,
|
||||
right: ts
|
||||
}
|
||||
if (!revoked) {
|
||||
const lines = []
|
||||
if (comment) lines.push({ left: comment, right: '' })
|
||||
const mentions = (p.mentions || [])
|
||||
.map((m: any) => String(m.userName || m.userId || '').trim())
|
||||
.filter(Boolean)
|
||||
if (mentions.length)
|
||||
lines.push({
|
||||
left: `提到:${mentions.map((name: string) => `@${name}`).join('、')}`,
|
||||
right: ''
|
||||
})
|
||||
if (lines.length) item.personLines = lines
|
||||
const links = (p.attachmentRefs || [])
|
||||
.map((file: any) => String(file.url || '').trim())
|
||||
.filter((url: string) => url && !/^(?!https?:)[a-z][a-z0-9+.-]*:/i.test(url))
|
||||
.map(resolveUrl)
|
||||
.filter((url: string) => /^https?:\/\//i.test(url))
|
||||
if (links.length) item.attachmentLinks = links
|
||||
}
|
||||
items.push(item)
|
||||
continue
|
||||
}
|
||||
|
||||
if (isCcProgressItem(p)) {
|
||||
if (!ccBuf) ccBuf = { names: [], ts }
|
||||
if (names.length) ccBuf.names.push(...names)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { attachmentFullUrl } from '@/composables/useAttachmentActions'
|
||||
/**
|
||||
* OA 审批单钉钉风格 PDF 导出
|
||||
*/
|
||||
@@ -398,7 +399,13 @@ export async function buildOaPrintModel(
|
||||
stampClass,
|
||||
stampText,
|
||||
rows: [...headerRows, ...formRows],
|
||||
flowItems: buildFlowItems(detail),
|
||||
flowItems: buildFlowItems(detail, (url) => {
|
||||
try {
|
||||
return new URL(attachmentFullUrl(url), window.location.origin).href
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}),
|
||||
printTime,
|
||||
printerName,
|
||||
watermarkName: printerName === '-' ? '审批' : printerName
|
||||
@@ -447,7 +454,7 @@ function renderPageLabelCanvas(text: string): HTMLCanvasElement {
|
||||
return c
|
||||
}
|
||||
|
||||
async function canvasToPdf(canvas: HTMLCanvasElement, filename: string) {
|
||||
async function canvasToPdf(canvas: HTMLCanvasElement, filename: string, sheet: HTMLElement) {
|
||||
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' })
|
||||
const pageWidth = pdf.internal.pageSize.getWidth()
|
||||
const pageHeight = pdf.internal.pageSize.getHeight()
|
||||
@@ -466,6 +473,23 @@ async function canvasToPdf(canvas: HTMLCanvasElement, filename: string) {
|
||||
const position = -pageIndex * pageHeight
|
||||
pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
|
||||
|
||||
// html2canvas 只保留像素,单独把附件链接写为 PDF 注解;跨页部分按页裁切。
|
||||
const sheetRect = sheet.getBoundingClientRect()
|
||||
const ratio = pageWidth / sheetRect.width
|
||||
for (const link of sheet.querySelectorAll<HTMLAnchorElement>('a[data-oa-print-link]')) {
|
||||
if (!/^https?:\/\//i.test(link.href)) continue
|
||||
for (const rect of link.getClientRects()) {
|
||||
const top = (rect.top - sheetRect.top) * ratio - pageIndex * pageHeight
|
||||
const bottom = Math.min(pageHeight, top + rect.height * ratio)
|
||||
const y = Math.max(0, top)
|
||||
if (bottom > y) {
|
||||
pdf.link((rect.left - sheetRect.left) * ratio, y, rect.width * ratio, bottom - y, {
|
||||
url: link.href
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const label = `第${pageIndex + 1}页`
|
||||
const labelCanvas = renderPageLabelCanvas(label)
|
||||
if (labelCanvas.width > 1) {
|
||||
@@ -526,7 +550,7 @@ export async function exportOaApprovalPdf(
|
||||
windowWidth: el.scrollWidth,
|
||||
windowHeight: el.scrollHeight
|
||||
})
|
||||
await canvasToPdf(canvas, filename)
|
||||
await canvasToPdf(canvas, filename, el)
|
||||
} finally {
|
||||
app.unmount()
|
||||
host.remove()
|
||||
|
||||
Reference in new issue
Block a user