diff --git a/app/performance-survey/PerformanceSurveyDashboardNavigation.test.tsx b/app/performance-survey/PerformanceSurveyDashboardNavigation.test.tsx index b62fcd7..779eaec 100644 --- a/app/performance-survey/PerformanceSurveyDashboardNavigation.test.tsx +++ b/app/performance-survey/PerformanceSurveyDashboardNavigation.test.tsx @@ -104,6 +104,16 @@ function jsonResponse(data: unknown) { } as unknown as Response } +function jsonErrorResponse(error: string, status = 409) { + return { + ok: false, + status, + headers: { get: () => 'application/json' }, + json: async () => ({ success: false, error }), + text: async () => '', + } as unknown as Response +} + describe('考核调研顶层导航保护', () => { const originalFetch = global.fetch @@ -241,4 +251,67 @@ describe('考核调研顶层导航保护', () => { expect.anything(), )) }) + + test('我发起的草稿发布失败时展示发布进度和后端原因', async () => { + mockSearchParams = new URLSearchParams('tab=personal&workspace=created') + let resolvePublish: (response: Response) => void = () => {} + const publishResponse = new Promise((resolve) => { + resolvePublish = resolve + }) + global.fetch = jest.fn(async (input, init) => { + const url = String(input) + if (url.endsWith('/me')) { + return jsonResponse({ + user: { id: 'user-admin', name: '管理员', email: 'admin@example.com' }, + employee: { id: 'employee-admin', displayName: '管理员' }, + permissions: { + canCreate: true, + isSystemAdmin: true, + isDepartmentAdmin: true, + isLeadership: true, + }, + }) + } + if (url.endsWith('/surveys') && (!init?.method || init.method === 'GET')) { + return jsonResponse({ + summary: { + pendingCount: 0, + submittedCount: 0, + createdCount: 1, + openCreatedCount: 0, + }, + pending: [], + submitted: [], + created: [{ + id: 'survey-draft', + title: '2026年7月 · AI作品评审', + status: 'draft', + departmentName: '综合支持部', + dueAt: null, + counts: { targets: 1, assignments: 3, responses: 0 }, + }], + }) + } + if (url.endsWith('/surveys/survey-draft') && init?.method === 'PATCH') { + return publishResponse + } + return jsonResponse([]) + }) as jest.Mock + + render() + + const publishButtons = await screen.findAllByRole('button', { name: '发布调研:2026年7月 · AI作品评审' }) + fireEvent.click(publishButtons[0]) + + expect(await screen.findAllByText('发布中...')).not.toHaveLength(0) + resolvePublish(jsonErrorResponse('视觉评分作品尚未上传完整:无邪(0/3)')) + + expect(await screen.findAllByRole('alert')).not.toHaveLength(0) + expect(await screen.findAllByText('发布失败:视觉评分作品尚未上传完整:无邪(0/3)')).not.toHaveLength(0) + expect(screen.getAllByRole('link', { name: '继续上传作品' })[0]).toHaveAttribute( + 'href', + '/performance-survey/survey/survey-draft', + ) + expect(screen.getAllByRole('button', { name: '发布调研:2026年7月 · AI作品评审' })[0]).toBeEnabled() + }) }) diff --git a/app/performance-survey/[[...path]]/page.tsx b/app/performance-survey/[[...path]]/page.tsx index e23d249..4d96d12 100644 --- a/app/performance-survey/[[...path]]/page.tsx +++ b/app/performance-survey/[[...path]]/page.tsx @@ -2335,15 +2335,29 @@ function CreatedList({ resultBackHref?: string }) { const [busyId, setBusyId] = useState(null) + const [actionErrors, setActionErrors] = useState>({}) const changeStatus = async (id: string, status: string) => { + const actionLabel = status === 'open' ? '发布' : '关闭' setBusyId(id) + setActionErrors((current) => { + if (!current[id]) return current + const next = { ...current } + delete next[id] + return next + }) try { await apiRequest(`/surveys/${id}`, { method: 'PATCH', body: JSON.stringify({ status }), }) await onRefresh() + } catch (error) { + const message = error instanceof Error ? error.message : `${actionLabel}失败,请稍后重试` + setActionErrors((current) => ({ + ...current, + [id]: `${actionLabel}失败:${message}`, + })) } finally { setBusyId(null) } @@ -2382,6 +2396,8 @@ function CreatedList({ const progressText = assignmentCount > 0 ? `已提交 ${responseCount}/${assignmentCount}` : '尚未生成评价任务' + const actionError = actionErrors[item.id] + const needsWorkItemUpload = actionError?.includes('作品尚未上传完整') return (
- 发布 + {busyId === item.id ? '发布中...' : '发布'} )} {item.status !== 'closed' && item.status !== 'cancelled' && ( @@ -2422,7 +2438,7 @@ function CreatedList({ aria-label={`关闭调研:${item.title}`} className="min-h-11 rounded border border-[#c2c6d8] bg-white px-4 text-sm font-bold text-[#1a1c1c] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0050cb] focus-visible:ring-offset-2 disabled:opacity-50" > - 关闭 + {busyId === item.id ? '处理中...' : '关闭'} )} {item.canCancel && ( @@ -2446,6 +2462,22 @@ function CreatedList({ )} + {actionError && ( +
+ {actionError} + {needsWorkItemUpload && ( + + 继续上传作品 + + )} +
+ )}
) })} @@ -2467,6 +2499,8 @@ function CreatedList({ const targetCount = Number(item.counts?.targets || 0) const assignmentCount = Number(item.counts?.assignments || 0) const responseCount = Number(item.counts?.responses || 0) + const actionError = actionErrors[item.id] + const needsWorkItemUpload = actionError?.includes('作品尚未上传完整') return ( @@ -2492,7 +2526,7 @@ function CreatedList({ aria-label={`发布调研:${item.title}`} className="min-h-9 rounded border border-[#b8d0ff] bg-[#edf4ff] px-3 text-[13px] font-bold text-[#0050cb] hover:bg-[#dae8ff] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0050cb] focus-visible:ring-offset-2 disabled:opacity-50" > - 发布 + {busyId === item.id ? '发布中...' : '发布'} )} {item.status !== 'closed' && item.status !== 'cancelled' && ( @@ -2503,7 +2537,7 @@ function CreatedList({ aria-label={`关闭调研:${item.title}`} className="min-h-9 rounded border border-[#c2c6d8] bg-white px-3 text-[13px] font-bold text-[#1a1c1c] hover:border-[#0050cb] hover:text-[#0050cb] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0050cb] focus-visible:ring-offset-2 disabled:opacity-50" > - 关闭 + {busyId === item.id ? '处理中...' : '关闭'} )} {item.canCancel && ( @@ -2527,6 +2561,22 @@ function CreatedList({ )} + {actionError && ( +
+ {actionError} + {needsWorkItemUpload && ( + + 继续上传作品 + + )} +
+ )} )