Compare commits

..
Author SHA1 Message Date
zlwu f1b4a5ce26 fix(web-admin): place project columns after index in lists
Move 所属项目/项目组 after selection and serial-number columns so multi-select and index stay leftmost across affected business lists.
2026-11-05 10:26:59 +08:00
13 changed files with 46 additions and 368 deletions

No files matched your search

@@ -1,4 +1,3 @@
import { attachmentFullUrl } from '../../utils/attachment.js'
/**
* OA 审批单移动端打印(S6)
* 对齐 web-admin `oa-approval-print.ts` 字段/流程结构;支持 html2canvas + jsPDF 导出。
@@ -67,6 +66,7 @@ function escapeHtml(s) {
.replace(/"/g, '"')
}
function formatDateOnly(createTime) {
const raw = String(createTime || '').trim()
if (!raw) return '-'
@@ -99,12 +99,10 @@ 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)
@@ -399,13 +397,7 @@ export function buildOaPrintModel(detail, opts = {}) {
stampClass,
stampText,
rows: [...headerRows, ...buildFormRows(detail)],
flowItems: buildFlowItems(detail, (url) => {
try {
return new URL(attachmentFullUrl(url), window.location.origin).href
} catch {
return ''
}
}),
flowItems: buildFlowItems(detail),
printTime,
printerName,
watermarkName: printerName === '-' ? '审批' : printerName
@@ -443,9 +435,6 @@ 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
}
@@ -483,10 +472,9 @@ 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 { white-space: pre-wrap; overflow-wrap: anywhere; flex: 1; min-width: 0; }
.oa-print-flow__left { 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; }
@@ -614,7 +602,7 @@ function renderPageLabelCanvas(text) {
return c
}
async function canvasToPdfBlob(canvas, sheet) {
async function canvasToPdfBlob(canvas) {
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' })
const pageWidth = pdf.internal.pageSize.getWidth()
const pageHeight = pdf.internal.pageSize.getHeight()
@@ -632,23 +620,6 @@ async function canvasToPdfBlob(canvas, sheet) {
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) {
@@ -693,7 +664,7 @@ export async function generateOaApprovalPdfBlob(detail, opts = {}) {
windowWidth: el.scrollWidth,
windowHeight: el.scrollHeight
})
const blob = await canvasToPdfBlob(canvas, el)
const blob = await canvasToPdfBlob(canvas)
return { blob, filename }
} finally {
host.remove()
@@ -1,95 +0,0 @@
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, '【评论】 评论')
})
+8 -46
View File
@@ -1,4 +1,3 @@
import { mergeProgressWithComments } from '../oa-progress-merge-comments.js'
/**
* 移动端打印「审批流程」行组装(无 html2canvas/API 副作用,便于单测)
*/
@@ -38,22 +37,16 @@ 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, 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
export function buildFlowItems(detail) {
const list = Array.isArray(detail?.progress) ? detail.progress : []
const items = []
let ccBuf = null
const flushCc = () => {
@@ -77,37 +70,6 @@ export function buildFlowItems(detail, resolveUrl = (url) => url) {
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,9 +86,6 @@
<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>
@@ -224,10 +221,6 @@
background: #fff;
}
.oa-print-url a {
color: inherit;
}
.oa-print-url {
color: #3370ff;
word-break: break-all;
@@ -255,8 +248,6 @@
}
.oa-print-flow__left {
white-space: pre-wrap;
overflow-wrap: anywhere;
flex: 1 1 auto;
min-width: 0;
}
@@ -1,90 +0,0 @@
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, '【评论】 评论')
})
+8 -50
View File
@@ -1,4 +1,3 @@
import { mergeProgressWithComments } from './oa-progress-merge-comments'
/**
* OA 打印「审批流程」行组装(无 Vue/canvas 副作用,便于单测)
*/
@@ -12,7 +11,6 @@ export type OaPrintPersonLine = {
export type OaPrintFlowItem = {
left: string
right: string
attachmentLinks?: string[]
/** @deprecated 多人节点改用 personLines;单人/发起人可保留 */
comment?: string
/** 或签/会签按人行:左侧文案 + 右侧时间(右对齐) */
@@ -68,29 +66,20 @@ 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,
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
export function buildFlowItems(detail: any): OaPrintFlowItem[] {
const list = Array.isArray(detail?.progress) ? detail.progress : []
const items: OaPrintFlowItem[] = []
let ccBuf: { names: string[]; ts: string } | null = null
@@ -115,37 +104,6 @@ export function buildFlowItems(
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)
+3 -27
View File
@@ -1,4 +1,3 @@
import { attachmentFullUrl } from '@/composables/useAttachmentActions'
/**
* OA 审批单钉钉风格 PDF 导出
*/
@@ -399,13 +398,7 @@ export async function buildOaPrintModel(
stampClass,
stampText,
rows: [...headerRows, ...formRows],
flowItems: buildFlowItems(detail, (url) => {
try {
return new URL(attachmentFullUrl(url), window.location.origin).href
} catch {
return ''
}
}),
flowItems: buildFlowItems(detail),
printTime,
printerName,
watermarkName: printerName === '-' ? '审批' : printerName
@@ -454,7 +447,7 @@ function renderPageLabelCanvas(text: string): HTMLCanvasElement {
return c
}
async function canvasToPdf(canvas: HTMLCanvasElement, filename: string, sheet: HTMLElement) {
async function canvasToPdf(canvas: HTMLCanvasElement, filename: string) {
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' })
const pageWidth = pdf.internal.pageSize.getWidth()
const pageHeight = pdf.internal.pageSize.getHeight()
@@ -473,23 +466,6 @@ async function canvasToPdf(canvas: HTMLCanvasElement, filename: string, sheet: H
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) {
@@ -550,7 +526,7 @@ export async function exportOaApprovalPdf(
windowWidth: el.scrollWidth,
windowHeight: el.scrollHeight
})
await canvasToPdf(canvas, filename, el)
await canvasToPdf(canvas, filename)
} finally {
app.unmount()
host.remove()
@@ -69,8 +69,7 @@
@selection-change="(rows: any[]) => (selectedRows = rows)"
@row-dblclick="handleEdit"
>
<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="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -79,6 +78,8 @@
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,11 +132,12 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleRowDblClick"
>
<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 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,11 +144,12 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleRowDblClick"
>
<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 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,8 +105,7 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<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="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -115,6 +114,8 @@
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,9 +116,7 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<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="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -127,6 +125,8 @@
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,8 +93,7 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<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="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -103,6 +102,8 @@
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="状态"