feat: add work review revision submission flow
This commit is contained in:
parent
f5ea657784
commit
50ea94ce3c
@ -241,23 +241,29 @@ export default function WorkReviewBoard() {
|
||||
const requestedProject = expandedProjectId
|
||||
? data?.items.find((item) => item.id === expandedProjectId)
|
||||
: null
|
||||
const reviewerCandidate = requestedProject
|
||||
const focusedCandidate = requestedProject
|
||||
const reviewerEmployeeId = me?.employee?.id
|
||||
const reviewerFocusProject = reviewerCandidate && (
|
||||
reviewerCandidate.permissions.pendingReviewRoles.length > 0
|
||||
const focusedProject = focusedCandidate && (
|
||||
focusedCandidate.permissions.canResubmit
|
||||
|| focusedCandidate.permissions.pendingReviewRoles.length > 0
|
||||
|| Boolean(reviewerEmployeeId && (
|
||||
reviewerCandidate.manager.id === reviewerEmployeeId
|
||||
|| reviewerCandidate.finalReviewer.id === reviewerEmployeeId
|
||||
focusedCandidate.manager.id === reviewerEmployeeId
|
||||
|| focusedCandidate.finalReviewer.id === reviewerEmployeeId
|
||||
))
|
||||
) ? reviewerCandidate : null
|
||||
) ? focusedCandidate : null
|
||||
|
||||
if (reviewerFocusProject) {
|
||||
if (focusedProject) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f6f8fb] text-[#101828]">
|
||||
<WorkReviewHeader
|
||||
name={me?.user.name || session.user?.name || '当前用户'}
|
||||
avatar={me?.user.avatar || (session.user as any)?.avatar || null}
|
||||
wide
|
||||
secondaryAction={focusedProject.permissions.canResubmit ? {
|
||||
label: '提交修改',
|
||||
icon: 'add',
|
||||
onClick: () => setSubmissionDialog({ mode: 'resubmit', project: focusedProject }),
|
||||
} : undefined}
|
||||
primaryAction={{
|
||||
label: '返回看板',
|
||||
icon: 'back',
|
||||
@ -275,13 +281,14 @@ export default function WorkReviewBoard() {
|
||||
/>
|
||||
<main className="mx-auto w-full max-w-[1600px] px-3 py-3 sm:px-5 sm:py-4">
|
||||
<WorkReviewReviewerWorkspace
|
||||
project={reviewerFocusProject}
|
||||
project={focusedProject}
|
||||
onSubmitReview={(version, input) => submitReview(version.id, input)}
|
||||
/>
|
||||
</main>
|
||||
|
||||
{submissionDialog && (
|
||||
<WorkReviewSubmissionDialog
|
||||
key={`${submissionDialog.mode}-${submissionDialog.project?.id || 'new'}-${submissionDialog.project?.latestVersion || 0}`}
|
||||
mode={submissionDialog.mode}
|
||||
project={submissionDialog.project}
|
||||
onClose={() => setSubmissionDialog(null)}
|
||||
@ -500,6 +507,7 @@ export default function WorkReviewBoard() {
|
||||
|
||||
{submissionDialog && (
|
||||
<WorkReviewSubmissionDialog
|
||||
key={`${submissionDialog.mode}-${submissionDialog.project?.id || 'new'}-${submissionDialog.project?.latestVersion || 0}`}
|
||||
mode={submissionDialog.mode}
|
||||
project={submissionDialog.project}
|
||||
onClose={() => setSubmissionDialog(null)}
|
||||
|
||||
@ -62,6 +62,7 @@ function WorkReviewDialogShell({
|
||||
}) {
|
||||
const [ready, setReady] = useState(false)
|
||||
const titleId = useId()
|
||||
const subtitleId = useId()
|
||||
const dialogRef = useRef<HTMLDivElement | null>(null)
|
||||
const closeRef = useRef<HTMLButtonElement | null>(null)
|
||||
|
||||
@ -117,6 +118,7 @@ function WorkReviewDialogShell({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
aria-describedby={subtitle ? subtitleId : undefined}
|
||||
aria-busy={busy}
|
||||
tabIndex={-1}
|
||||
className={`flex max-h-[94dvh] w-full flex-col overflow-hidden rounded-t-2xl bg-white text-[#101828] shadow-2xl sm:max-h-[calc(100dvh-40px)] sm:rounded-xl ${widthClass}`}
|
||||
@ -124,7 +126,7 @@ function WorkReviewDialogShell({
|
||||
<div className="flex shrink-0 items-start justify-between gap-4 border-b border-[#e4e7ec] px-4 py-4 sm:px-5">
|
||||
<div className="min-w-0">
|
||||
<h2 id={titleId} className="text-[18px] font-bold">{title}</h2>
|
||||
{subtitle && <p className="mt-1 text-[12px] font-medium leading-5 text-[#667085]">{subtitle}</p>}
|
||||
{subtitle && <p id={subtitleId} className="mt-1 text-[12px] font-medium leading-5 text-[#667085]">{subtitle}</p>}
|
||||
</div>
|
||||
<button
|
||||
ref={closeRef}
|
||||
@ -138,7 +140,7 @@ function WorkReviewDialogShell({
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-5 sm:px-5">{children}</div>
|
||||
<div className="shrink-0 border-t border-[#e4e7ec] bg-white px-4 py-4 sm:px-5">{footer}</div>
|
||||
<div className="shrink-0 border-t border-[#e4e7ec] bg-white px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] sm:px-5">{footer}</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
@ -170,7 +172,9 @@ export function WorkReviewSubmissionDialog({
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [progress, setProgress] = useState<{ message: string; percent?: number } | null>(null)
|
||||
const submitLockRef = useRef(false)
|
||||
const isCreate = mode === 'create'
|
||||
const nextVersion = Number(project?.latestVersion || 0) + 1
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCreate) return
|
||||
@ -195,6 +199,7 @@ export function WorkReviewSubmissionDialog({
|
||||
}, [isCreate, reviewerLoadKey])
|
||||
|
||||
const submit = async () => {
|
||||
if (submitLockRef.current) return
|
||||
if (isCreate && !title.trim()) {
|
||||
setError('请填写作品名称')
|
||||
return
|
||||
@ -216,6 +221,7 @@ export function WorkReviewSubmissionDialog({
|
||||
return
|
||||
}
|
||||
|
||||
submitLockRef.current = true
|
||||
setBusy(true)
|
||||
setError(null)
|
||||
setProgress({ message: '正在准备图片' })
|
||||
@ -245,24 +251,28 @@ export function WorkReviewSubmissionDialog({
|
||||
workDate,
|
||||
reviewerEmployeeId,
|
||||
}
|
||||
: {}),
|
||||
: {
|
||||
expectedLatestVersion: project?.latestVersion,
|
||||
}),
|
||||
submissionNote: submissionNote.trim(),
|
||||
processedImages,
|
||||
processedReferenceImages,
|
||||
})
|
||||
} catch (submitError) {
|
||||
setProgress(null)
|
||||
setError(submitError instanceof Error ? submitError.message : '提交失败,请重试')
|
||||
} finally {
|
||||
submitLockRef.current = false
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkReviewDialogShell
|
||||
title={isCreate ? '提交新作品' : `提交 ${project?.title || '作品'} 的新版本`}
|
||||
title={isCreate ? '提交新作品' : `提交作品修改 V${nextVersion}`}
|
||||
subtitle={isCreate
|
||||
? '手动选择一名评审人,并由元泽共同进行逐图评审'
|
||||
: `将创建 V${Number(project?.latestVersion || 0) + 1},历史图片和评审意见会完整保留`}
|
||||
: `沿用首次提交的作品名称和评审人;V${nextVersion} 提交后,历史图片与评审意见会完整保留`}
|
||||
busy={busy}
|
||||
onClose={onClose}
|
||||
widthClass="max-w-4xl"
|
||||
@ -274,7 +284,14 @@ export function WorkReviewSubmissionDialog({
|
||||
<span>{progress.message}</span>
|
||||
{typeof progress.percent === 'number' && <span>{progress.percent}%</span>}
|
||||
</div>
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-[#e8eef6]">
|
||||
<div
|
||||
className="h-1.5 overflow-hidden rounded-full bg-[#e8eef6]"
|
||||
role="progressbar"
|
||||
aria-label={progress.message}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={typeof progress.percent === 'number' ? progress.percent : undefined}
|
||||
>
|
||||
<div
|
||||
className={`h-full rounded-full bg-[#0050cb] transition-[width] ${typeof progress.percent !== 'number' ? 'w-2/3 animate-pulse' : ''}`}
|
||||
style={typeof progress.percent === 'number' ? { width: `${progress.percent}%` } : undefined}
|
||||
@ -297,7 +314,7 @@ export function WorkReviewSubmissionDialog({
|
||||
onClick={() => void submit()}
|
||||
className="min-h-11 rounded bg-[#0050cb] px-5 text-[13px] font-bold text-white hover:bg-[#003fa3] disabled:cursor-not-allowed disabled:bg-[#9bbcec]"
|
||||
>
|
||||
{busy ? '正在提交...' : isCreate ? '提交并通知评审人' : '提交新版本'}
|
||||
{busy ? '正在提交...' : isCreate ? '提交并通知评审人' : `提交修改 V${nextVersion}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -369,9 +386,25 @@ export function WorkReviewSubmissionDialog({
|
||||
</label>
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-lg border border-[#f3d29b] bg-[#fffaf0] px-4 py-3 text-[12px] font-medium leading-5 text-[#8a5200]">
|
||||
请确认已处理当前版本中的指定评审人和元泽评审建议。提交后,两位评审人会收到新一轮通知。
|
||||
</div>
|
||||
<>
|
||||
<dl className="grid gap-x-5 gap-y-4 rounded-lg border border-[#dfe6f0] bg-[#f8fafc] px-4 py-4 sm:grid-cols-3" aria-label={`V${nextVersion} 提交信息`}>
|
||||
<div className="min-w-0">
|
||||
<dt className="text-[11px] font-bold text-[#667085]">作品名称</dt>
|
||||
<dd className="mt-1 break-words text-[13px] font-bold text-[#101828]">{project?.title || '-'}</dd>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<dt className="text-[11px] font-bold text-[#667085]">评审人</dt>
|
||||
<dd className="mt-1 break-words text-[13px] font-bold text-[#101828]">{project?.manager.name || '-'}</dd>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<dt className="text-[11px] font-bold text-[#667085]">修改提交日期</dt>
|
||||
<dd className="mt-1 text-[13px] font-bold tabular-nums text-[#101828]">{workDate}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="rounded-lg border border-[#f3d29b] bg-[#fffaf0] px-4 py-3 text-[12px] font-medium leading-5 text-[#8a5200]">
|
||||
请确认已处理 V{project?.latestVersion || 1} 中的评审建议。提交后,原评审人与元泽会收到 V{nextVersion} 的新一轮评审通知。
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<label className="grid gap-2">
|
||||
|
||||
@ -6,11 +6,17 @@ import Link from 'next/link'
|
||||
export function WorkReviewHeader({
|
||||
name,
|
||||
avatar,
|
||||
secondaryAction,
|
||||
primaryAction,
|
||||
wide = false,
|
||||
}: {
|
||||
name: string
|
||||
avatar?: string | null
|
||||
secondaryAction?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
icon: 'add' | 'back'
|
||||
}
|
||||
primaryAction?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
@ -22,7 +28,7 @@ export function WorkReviewHeader({
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 bg-[#1f2937] text-white shadow-sm">
|
||||
<div className={`mx-auto flex min-h-[64px] w-full items-center justify-between gap-4 px-4 py-3 sm:px-6 ${wide ? 'max-w-[1600px]' : 'max-w-[1280px]'}`}>
|
||||
<div className={`mx-auto flex min-h-[64px] w-full flex-wrap items-center justify-between gap-x-4 gap-y-2 px-4 py-3 sm:flex-nowrap sm:px-6 ${wide ? 'max-w-[1600px]' : 'max-w-[1280px]'}`}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full border border-white/15 bg-white/15 text-[12px] font-bold">
|
||||
{avatar ? (
|
||||
@ -42,12 +48,28 @@ export function WorkReviewHeader({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<div className={`flex shrink-0 items-center gap-2 ${secondaryAction ? 'w-full sm:w-auto' : ''}`}>
|
||||
{secondaryAction && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={secondaryAction.onClick}
|
||||
className="inline-flex min-h-10 flex-1 items-center justify-center rounded bg-[#0050cb] px-3 text-[12px] font-bold text-white transition-colors hover:bg-[#003fa3] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#1f2937] sm:min-h-9 sm:flex-none sm:px-4 sm:text-[13px]"
|
||||
>
|
||||
<span className="mr-1 text-[17px] font-light leading-none" aria-hidden="true">
|
||||
{secondaryAction.icon === 'add' ? '+' : '←'}
|
||||
</span>
|
||||
{secondaryAction.label}
|
||||
</button>
|
||||
)}
|
||||
{primaryAction && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={primaryAction.onClick}
|
||||
className="inline-flex min-h-9 items-center justify-center rounded bg-[#0050cb] px-3 text-[12px] font-bold text-white transition-colors hover:bg-[#003fa3] sm:px-4 sm:text-[13px]"
|
||||
className={`inline-flex items-center justify-center rounded px-3 text-[12px] font-bold text-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#1f2937] sm:min-h-9 sm:px-4 sm:text-[13px] ${
|
||||
secondaryAction
|
||||
? 'min-h-10 flex-1 border border-white/25 bg-white/10 hover:bg-white/15 sm:flex-none'
|
||||
: 'min-h-9 bg-[#0050cb] hover:bg-[#003fa3]'
|
||||
}`}
|
||||
>
|
||||
<span className="mr-1 text-[17px] font-light leading-none" aria-hidden="true">
|
||||
{primaryAction.icon === 'add' ? '+' : '←'}
|
||||
@ -57,7 +79,7 @@ export function WorkReviewHeader({
|
||||
)}
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex min-h-9 items-center justify-center rounded px-2 text-[12px] font-semibold text-white/80 transition-colors hover:bg-white/10 hover:text-white sm:px-3 sm:text-[13px]"
|
||||
className="inline-flex min-h-10 items-center justify-center rounded px-2 text-[12px] font-semibold text-white/80 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white sm:min-h-9 sm:px-3 sm:text-[13px]"
|
||||
>
|
||||
返回素材库
|
||||
</Link>
|
||||
|
||||
@ -935,7 +935,13 @@ export function WorkReviewReviewerWorkspace({
|
||||
</>
|
||||
) : (
|
||||
<div className="flex min-h-11 items-center justify-center text-center text-[12px] font-semibold text-[#667085]">
|
||||
{isLatestSelected ? '你的本轮评审已提交;两位评审完成后系统会通知发起人' : '历史版本仅供查看'}
|
||||
{!isLatestSelected
|
||||
? '历史版本仅供查看'
|
||||
: project.permissions.canResubmit
|
||||
? '当前版本需要修改,请通过顶部“提交修改”上传新版本'
|
||||
: project.status === 'approved'
|
||||
? '作品评审已通过,本作品已完结'
|
||||
: '你的本轮评审已提交;两位评审完成后系统会通知发起人'}
|
||||
</div>
|
||||
)}
|
||||
<div className="sr-only" aria-live="polite">{liveMessage}</div>
|
||||
|
||||
111
lib/__tests__/work-review-resubmission-ui.test.tsx
Normal file
111
lib/__tests__/work-review-resubmission-ui.test.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
/** @jest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { WorkReviewSubmissionDialog } from '@/components/work-review/WorkReviewDialogs'
|
||||
import { WorkReviewHeader } from '@/components/work-review/WorkReviewHeader'
|
||||
import { uploadWorkReviewImages } from '@/lib/work-review-client'
|
||||
import type { WorkReviewProject } from '@/types/work-review'
|
||||
|
||||
jest.mock('@/lib/work-review-client', () => ({
|
||||
getWorkReviewReviewerOptions: jest.fn(),
|
||||
uploadWorkReviewImages: jest.fn(),
|
||||
}))
|
||||
|
||||
const project: WorkReviewProject = {
|
||||
id: 'project-1',
|
||||
title: '夏季新品主视觉',
|
||||
description: '第一版说明',
|
||||
workDate: '2026-07-30',
|
||||
status: 'revision_required',
|
||||
latestVersion: 1,
|
||||
submitter: { id: 'submitter-1', name: '提交人' },
|
||||
manager: { id: 'manager-1', name: '清云' },
|
||||
finalReviewer: { id: 'final-1', name: '元泽' },
|
||||
createdAt: '2026-07-30T00:00:00.000Z',
|
||||
updatedAt: '2026-08-01T00:00:00.000Z',
|
||||
versions: [],
|
||||
permissions: { canResubmit: true, pendingReviewRoles: [] },
|
||||
}
|
||||
|
||||
describe('作品修改版本入口', () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: jest.fn(() => 'blob:preview') })
|
||||
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: jest.fn() })
|
||||
})
|
||||
|
||||
test('顶部按提交修改、返回看板的顺序展示操作', () => {
|
||||
render(
|
||||
<WorkReviewHeader
|
||||
name="提交人"
|
||||
secondaryAction={{ label: '提交修改', icon: 'add', onClick: jest.fn() }}
|
||||
primaryAction={{ label: '返回看板', icon: 'back', onClick: jest.fn() }}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getAllByRole('button').map((button) => button.textContent?.trim())).toEqual([
|
||||
'+提交修改',
|
||||
'←返回看板',
|
||||
])
|
||||
})
|
||||
|
||||
test('V2 弹窗显示首版名称、原评审人与当天修改日期,并携带基准版本提交', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSubmit = jest.fn().mockResolvedValue(undefined)
|
||||
jest.mocked(uploadWorkReviewImages).mockResolvedValue([{
|
||||
uploadId: 'upload-1',
|
||||
mimeType: 'image/webp',
|
||||
width: 100,
|
||||
height: 100,
|
||||
kind: 'slice',
|
||||
sourceName: '修改稿.png',
|
||||
sourceIndex: 0,
|
||||
sliceIndex: 0,
|
||||
sliceCount: 1,
|
||||
}])
|
||||
|
||||
render(
|
||||
<WorkReviewSubmissionDialog
|
||||
mode="resubmit"
|
||||
project={project}
|
||||
onClose={jest.fn()}
|
||||
onSubmit={onSubmit}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(await screen.findByRole('heading', { name: '提交作品修改 V2' })).toBeInTheDocument()
|
||||
const summary = screen.getByLabelText('V2 提交信息')
|
||||
expect(within(summary).getByText('夏季新品主视觉')).toBeInTheDocument()
|
||||
expect(within(summary).getByText('清云')).toBeInTheDocument()
|
||||
const now = new Date()
|
||||
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60_000)
|
||||
expect(within(summary).getByText(local.toISOString().slice(0, 10))).toBeInTheDocument()
|
||||
expect(screen.queryByRole('combobox')).not.toBeInTheDocument()
|
||||
|
||||
const file = new File(['image'], '修改稿.png', { type: 'image/png' })
|
||||
fireEvent.change(screen.getByLabelText('选择作品图片'), { target: { files: [file] } })
|
||||
await user.click(screen.getByRole('button', { name: '提交修改 V2' }))
|
||||
|
||||
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1))
|
||||
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expectedLatestVersion: 1,
|
||||
submissionNote: '',
|
||||
processedImages: [expect.objectContaining({ uploadId: 'upload-1' })],
|
||||
processedReferenceImages: [],
|
||||
}))
|
||||
})
|
||||
|
||||
test('版本号随最新版本递增', async () => {
|
||||
render(
|
||||
<WorkReviewSubmissionDialog
|
||||
mode="resubmit"
|
||||
project={{ ...project, latestVersion: 3 }}
|
||||
onClose={jest.fn()}
|
||||
onSubmit={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(await screen.findByRole('heading', { name: '提交作品修改 V4' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '提交修改 V4' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -199,4 +199,39 @@ describe('WorkReviewReviewerWorkspace', () => {
|
||||
expect(screen.queryByRole('button', { name: '作品图' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '参考图' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('guides the submitter to upload a revision only when changes are required', () => {
|
||||
render(
|
||||
<WorkReviewReviewerWorkspace
|
||||
project={{
|
||||
...project,
|
||||
status: 'revision_required',
|
||||
versions: [{ ...project.versions[0], status: 'revision_required' }],
|
||||
permissions: { canResubmit: true, pendingReviewRoles: [] },
|
||||
}}
|
||||
onSubmitReview={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('当前版本需要修改,请通过顶部“提交修改”上传新版本')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '通过' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '提交给修改人' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('treats an approved result as the completed end state', () => {
|
||||
render(
|
||||
<WorkReviewReviewerWorkspace
|
||||
project={{
|
||||
...project,
|
||||
status: 'approved',
|
||||
versions: [{ ...project.versions[0], status: 'approved', completedAt: '2026-08-01T08:00:00.000Z' }],
|
||||
permissions: { canResubmit: false, pendingReviewRoles: [] },
|
||||
}}
|
||||
onSubmitReview={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('作品评审已通过,本作品已完结')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '提交给修改人' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -738,13 +738,22 @@ async function withOptionalStoredImages<T>(input: {
|
||||
|
||||
async function queueSubmittedNotifications(project: any, version: any) {
|
||||
const actionUrl = getWorkReviewActionUrl(project.id)
|
||||
const isInitialVersion = Number(version.version) === 1
|
||||
const submittedDate = isInitialVersion
|
||||
? new Date(project.workDate).toISOString().slice(0, 10)
|
||||
: new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).format(new Date(version.submittedAt || Date.now()))
|
||||
const content = [
|
||||
'### ◆ 作品待评审',
|
||||
'',
|
||||
`**${escapeDingtalkMarkdown(project.title)}**`,
|
||||
'',
|
||||
`**提交人:** ${escapeDingtalkMarkdown(project.submitterName)} `,
|
||||
`**作品日期:** ${new Date(project.workDate).toISOString().slice(0, 10)} `,
|
||||
`**${isInitialVersion ? '作品日期' : '修改提交日期'}:** ${submittedDate} `,
|
||||
`**版本:** V${version.version} `,
|
||||
'',
|
||||
'> 请逐张查看作品图片并填写评审修改建议。',
|
||||
@ -891,12 +900,19 @@ export async function createWorkReviewProject(session: Session | null, body: any
|
||||
export async function createWorkReviewVersion(session: Session | null, projectId: string, body: any) {
|
||||
const ctx = await getContext(session)
|
||||
ensureCanSubmit(ctx)
|
||||
const expectedLatestVersion = Number(body?.expectedLatestVersion)
|
||||
if (!Number.isInteger(expectedLatestVersion) || expectedLatestVersion < 1) {
|
||||
throw new WorkReviewApiError('修改版本信息无效,请刷新后重试')
|
||||
}
|
||||
const existing = await prisma.workReviewProject.findUnique({
|
||||
where: { id: projectId },
|
||||
include: { versions: { orderBy: { version: 'desc' }, take: 1 } },
|
||||
})
|
||||
if (!existing) throw new WorkReviewApiError('作品不存在', 404)
|
||||
if (existing.submitterUserId !== ctx.user.id) throw new WorkReviewApiError('只能重新提交自己的作品', 403)
|
||||
if (existing.latestVersion !== expectedLatestVersion) {
|
||||
throw new WorkReviewApiError('作品版本已更新,请刷新后重新提交', 409)
|
||||
}
|
||||
if (existing.status !== VERSION_STATUS.REVISION_REQUIRED) {
|
||||
throw new WorkReviewApiError('当前版本尚未完成评审,不能再次提交', 409)
|
||||
}
|
||||
@ -926,6 +942,9 @@ export async function createWorkReviewVersion(session: Session | null, projectId
|
||||
if (locked[0].status !== VERSION_STATUS.REVISION_REQUIRED) {
|
||||
throw new WorkReviewApiError('当前版本尚未完成评审,不能再次提交', 409)
|
||||
}
|
||||
if (locked[0].latest_version !== expectedLatestVersion) {
|
||||
throw new WorkReviewApiError('作品版本已更新,请刷新后重新提交', 409)
|
||||
}
|
||||
const version = locked[0].latest_version + 1
|
||||
await tx.workReviewVersion.create({
|
||||
data: {
|
||||
|
||||
@ -149,6 +149,7 @@ export type WorkReviewSubmissionInput = {
|
||||
description?: string
|
||||
workDate?: string
|
||||
reviewerEmployeeId?: string
|
||||
expectedLatestVersion?: number
|
||||
submissionNote?: string
|
||||
processedImages: WorkReviewProcessedImage[]
|
||||
processedReferenceImages: WorkReviewProcessedImage[]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user