Compare commits

..
Author SHA1 Message Date
zlwu a511004812 fix(oa): include discussion comments and attachment links in approval PDFs
Merge discussion comments into the existing printed approval flow on Web and mobile OA, preserving detail timeline order and hiding revoked content. Include mentions and absolute attachment URLs, and add PDF link annotations so rasterized exports retain usable links. Add regression tests for ordering, attachments, mentions and revoked comments. Related to Gitea issue #30.
2026-11-05 10:27:39 +08:00
13 changed files with 368 additions and 46 deletions

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, '【评论】 评论')
})
+46 -8
View File
@@ -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, '【评论】 评论')
})
+50 -8
View File
@@ -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)
+27 -3
View File
@@ -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()
@@ -69,7 +69,8 @@
@selection-change="(rows: any[]) => (selectedRows = rows)"
@row-dblclick="handleEdit"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column prop="projectName" label="所属项目" min-width="160"/><el-table-column prop="teamName" label="项目组" min-width="150"/>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -78,8 +79,6 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="projectName" label="所属项目" min-width="160" />
<el-table-column prop="teamName" label="项目组" min-width="150" />
<el-table-column
prop="orderDate"
label="业务日期"
@@ -132,12 +132,11 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleRowDblClick"
>
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column prop="projectName" label="所属项目" min-width="160"/><el-table-column prop="teamName" label="项目组" min-width="150"/>
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column label="序号" min-width="64" align="center" header-align="center">
<template #default="{ $index }">{{ $index + 1 }}</template>
</el-table-column>
<el-table-column prop="projectName" label="所属项目" min-width="160" />
<el-table-column prop="teamName" label="项目组" min-width="150" />
<el-table-column
prop="collectDate"
label="采集日期"
@@ -144,12 +144,11 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleRowDblClick"
>
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column prop="projectName" label="所属项目" min-width="160"/><el-table-column prop="teamName" label="项目组" min-width="150"/>
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column label="序号" min-width="64" align="center" header-align="center">
<template #default="{ $index }">{{ $index + 1 }}</template>
</el-table-column>
<el-table-column prop="projectName" label="所属项目" min-width="160" />
<el-table-column prop="teamName" label="项目组" min-width="150" />
<el-table-column
prop="collectDate"
label="采集日期"
@@ -105,7 +105,8 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column prop="projectName" label="所属项目" min-width="160"/><el-table-column prop="teamName" label="项目组" min-width="150"/>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -114,8 +115,6 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="projectName" label="所属项目" min-width="160" />
<el-table-column prop="teamName" label="项目组" min-width="150" />
<el-table-column
prop="protocolStatus"
label="状态"
@@ -116,7 +116,9 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column v-if="!isClinical" prop="projectName" label="所属项目" min-width="160" />
<el-table-column v-if="!isClinical" prop="teamName" label="项目组" min-width="150" />
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -125,8 +127,6 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column v-if="!isClinical" prop="projectName" label="所属项目" min-width="160" />
<el-table-column v-if="!isClinical" prop="teamName" label="项目组" min-width="150" />
<el-table-column
prop="applicationNo"
label="申请单号"
@@ -93,7 +93,8 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column prop="projectName" label="所属项目" min-width="160"/><el-table-column prop="teamName" label="项目组" min-width="150"/>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -102,8 +103,6 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="projectName" label="所属项目" min-width="160" />
<el-table-column prop="teamName" label="项目组" min-width="150" />
<el-table-column
prop="donationStatus"
label="状态"