按文件名排序视觉评分切片
This commit is contained in:
parent
365e79f3a0
commit
aadea673d6
@ -7,6 +7,7 @@ import { useParams, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { pinyin } from 'pinyin-pro'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { sortPerformanceSurveyImagesByName } from '@/lib/performance-survey-image-order'
|
||||
|
||||
type ApiState<T> = {
|
||||
loading: boolean
|
||||
@ -183,7 +184,7 @@ function keepFocusInsideModal(event: KeyboardEvent, container: HTMLElement | nul
|
||||
function validatePerformanceSurveyImageFiles(files: File[]) {
|
||||
if (files.length === 0) return '请至少选择 1 张图片'
|
||||
if (files.length > PERFORMANCE_SURVEY_MAX_IMAGE_COUNT) {
|
||||
return `单个案例最多上传 ${PERFORMANCE_SURVEY_MAX_IMAGE_COUNT} 张图片`
|
||||
return `单个案例最多上传 ${PERFORMANCE_SURVEY_MAX_IMAGE_COUNT} 张切片`
|
||||
}
|
||||
|
||||
const invalidType = files.find((file) => !file.type.startsWith('image/'))
|
||||
@ -199,6 +200,7 @@ async function uploadPerformanceSurveyImageFiles(
|
||||
) {
|
||||
const { chunkUpload } = await import('@/lib/chunk-upload')
|
||||
const uploaded: Array<{ uploadId: string; mimeType: string }> = []
|
||||
const orderedFiles = sortPerformanceSurveyImagesByName(files, (file) => file.name)
|
||||
const dingtalkToken = getStoredDingtalkPerformanceToken()
|
||||
const requestHeaders = dingtalkToken
|
||||
? {
|
||||
@ -207,9 +209,9 @@ async function uploadPerformanceSurveyImageFiles(
|
||||
}
|
||||
: undefined
|
||||
|
||||
for (let index = 0; index < files.length; index += 1) {
|
||||
const file = files[index]
|
||||
onProgress?.(`正在上传图片 ${index + 1}/${files.length}`)
|
||||
for (let index = 0; index < orderedFiles.length; index += 1) {
|
||||
const file = orderedFiles[index]
|
||||
onProgress?.(`正在上传图片 ${index + 1}/${orderedFiles.length}`)
|
||||
const result = await chunkUpload(
|
||||
file,
|
||||
{ materialType: '考核调研-视觉评分', fileName: file.name },
|
||||
@ -271,7 +273,7 @@ function normalizePerformanceSurveyImageGroups(value: unknown): PerformanceSurve
|
||||
}] : []
|
||||
}
|
||||
|
||||
return value.flatMap((item, index): PerformanceSurveyImageGroupData[] => {
|
||||
const groups = value.flatMap((item, index): PerformanceSurveyImageGroupData[] => {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) return []
|
||||
const record = item as Record<string, unknown>
|
||||
const width = Math.max(1, Number(record.width) || 1200)
|
||||
@ -307,6 +309,22 @@ function normalizePerformanceSurveyImageGroups(value: unknown): PerformanceSurve
|
||||
slices,
|
||||
}]
|
||||
})
|
||||
|
||||
if (groups.length <= 1) return groups
|
||||
|
||||
const orderedGroups = sortPerformanceSurveyImagesByName(groups, (group) => group.sourceName)
|
||||
const slices = orderedGroups.flatMap((group) => group.slices)
|
||||
const firstGroup = orderedGroups[0]
|
||||
return [{
|
||||
key: `ordered-slices-${orderedGroups.map((group) => group.key).join('-')}`,
|
||||
sourceName: `详情切片(${orderedGroups.length} 张)`,
|
||||
previewUrl: firstGroup.previewUrl,
|
||||
previewWidth: firstGroup.previewWidth,
|
||||
previewHeight: firstGroup.previewHeight,
|
||||
width: Math.max(...orderedGroups.map((group) => group.width)),
|
||||
height: slices.reduce((sum, slice) => sum + slice.height, 0),
|
||||
slices,
|
||||
}]
|
||||
}
|
||||
|
||||
function getStoredDingtalkPerformanceToken() {
|
||||
@ -3054,7 +3072,7 @@ function PerformanceSurveyImageDropzone({
|
||||
>
|
||||
<span className={`flex h-8 w-8 items-center justify-center rounded-full text-[20px] font-light ${dragging ? 'bg-[#0050cb] text-white' : 'bg-[#eef5ff] text-[#0050cb]'}`}>↑</span>
|
||||
<span className="mt-2 text-[13px] font-bold text-[#0050cb]">{dragging ? '松开即可添加' : '拖拽图片到这里'}</span>
|
||||
<span className="mt-0.5 text-[11px] text-[#98a2b3]">或点击选择 · 最多 60 张,单张 200MB</span>
|
||||
<span className="mt-0.5 text-[11px] text-[#98a2b3]">或点击选择 · 单张长图,或最多 60 张有序切片;单张 200MB</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@ -3168,13 +3186,14 @@ function VisualCaseDraftEditor({
|
||||
const key = `${file.name}-${file.size}-${file.lastModified}`
|
||||
return items.findIndex((candidate) => `${candidate.name}-${candidate.size}-${candidate.lastModified}` === key) === fileIndex
|
||||
})
|
||||
const validationError = validatePerformanceSurveyImageFiles(deduplicated)
|
||||
const orderedFiles = sortPerformanceSurveyImagesByName(deduplicated, (file) => file.name)
|
||||
const validationError = validatePerformanceSurveyImageFiles(orderedFiles)
|
||||
if (validationError) {
|
||||
setMessage(validationError)
|
||||
return
|
||||
}
|
||||
setMessage(null)
|
||||
onChange({ ...draft, files: deduplicated })
|
||||
onChange({ ...draft, files: orderedFiles })
|
||||
}
|
||||
|
||||
return (
|
||||
@ -3182,7 +3201,7 @@ function VisualCaseDraftEditor({
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-[13px] font-bold text-[#111827]">案例 {index + 1}</span>
|
||||
<span className={`text-[11px] font-semibold ${draft.files.length > 0 ? 'text-[#0050cb]' : 'text-[#98a2b3]'}`}>
|
||||
{draft.files.length > 0 ? `${draft.files.length} 张` : '待上传'}
|
||||
{draft.files.length > 1 ? `${draft.files.length} 张切片` : draft.files.length === 1 ? '1 张图片' : '待上传'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -4349,12 +4368,13 @@ function WorkItemUploadForm({
|
||||
const key = `${file.name}-${file.size}-${file.lastModified}`
|
||||
return items.findIndex((candidate) => `${candidate.name}-${candidate.size}-${candidate.lastModified}` === key) === index
|
||||
})
|
||||
const validationError = validatePerformanceSurveyImageFiles(merged)
|
||||
const orderedFiles = sortPerformanceSurveyImagesByName(merged, (file) => file.name)
|
||||
const validationError = validatePerformanceSurveyImageFiles(orderedFiles)
|
||||
if (validationError) {
|
||||
setMessage(validationError)
|
||||
return
|
||||
}
|
||||
setFiles(merged)
|
||||
setFiles(orderedFiles)
|
||||
setMessage(null)
|
||||
}
|
||||
|
||||
|
||||
34
lib/__tests__/performance-survey-image-order.test.ts
Normal file
34
lib/__tests__/performance-survey-image-order.test.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { sortPerformanceSurveyImagesByName } from '@/lib/performance-survey-image-order'
|
||||
|
||||
describe('performance survey image slice order', () => {
|
||||
it('按文件名中的数字自然排序', () => {
|
||||
const files = ['10.jpg', '2.jpg', '1.jpg', '11.jpg', '3.jpg']
|
||||
expect(sortPerformanceSurveyImagesByName(files, (name) => name)).toEqual([
|
||||
'1.jpg',
|
||||
'2.jpg',
|
||||
'3.jpg',
|
||||
'10.jpg',
|
||||
'11.jpg',
|
||||
])
|
||||
})
|
||||
|
||||
it('支持带前缀和补零的切片名称', () => {
|
||||
const files = ['详情_10.png', '详情_02.png', '详情_1.png']
|
||||
expect(sortPerformanceSurveyImagesByName(files, (name) => name)).toEqual([
|
||||
'详情_1.png',
|
||||
'详情_02.png',
|
||||
'详情_10.png',
|
||||
])
|
||||
})
|
||||
|
||||
it('名称相同时保留用户选择顺序', () => {
|
||||
const files = [
|
||||
{ name: '1.jpg', id: 'first' },
|
||||
{ name: '1.jpg', id: 'second' },
|
||||
]
|
||||
expect(sortPerformanceSurveyImagesByName(files, (file) => file.name).map((file) => file.id)).toEqual([
|
||||
'first',
|
||||
'second',
|
||||
])
|
||||
})
|
||||
})
|
||||
@ -49,7 +49,7 @@ describe('performance survey image processing', () => {
|
||||
sourceName: `${index}.png`,
|
||||
}))
|
||||
|
||||
await expect(processPerformanceSurveyImages(inputs)).rejects.toThrow('最多上传 60 张图片')
|
||||
await expect(processPerformanceSurveyImages(inputs)).rejects.toThrow('最多上传 60 张切片')
|
||||
})
|
||||
|
||||
test('downscales wide sources before slicing and creates a lightweight top preview', async () => {
|
||||
|
||||
24
lib/performance-survey-image-order.ts
Normal file
24
lib/performance-survey-image-order.ts
Normal file
@ -0,0 +1,24 @@
|
||||
const performanceSurveyImageNameCollator = new Intl.Collator('zh-CN', {
|
||||
numeric: true,
|
||||
sensitivity: 'base',
|
||||
})
|
||||
|
||||
/**
|
||||
* 视觉评分的多图表示同一张详情长图的连续切片。
|
||||
* 按文件名做自然数字排序,并在名称相同时保留原始顺序。
|
||||
*/
|
||||
export function sortPerformanceSurveyImagesByName<T>(
|
||||
items: T[],
|
||||
getName: (item: T) => string,
|
||||
): T[] {
|
||||
return items
|
||||
.map((item, originalIndex) => ({ item, originalIndex }))
|
||||
.sort((left, right) => {
|
||||
const byName = performanceSurveyImageNameCollator.compare(
|
||||
String(getName(left.item) || '').trim(),
|
||||
String(getName(right.item) || '').trim(),
|
||||
)
|
||||
return byName || left.originalIndex - right.originalIndex
|
||||
})
|
||||
.map(({ item }) => item)
|
||||
}
|
||||
@ -314,7 +314,7 @@ export async function processPerformanceSurveyImages(
|
||||
}
|
||||
if (inputs.length > PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount) {
|
||||
throw new PerformanceSurveyImageProcessingError(
|
||||
`单个案例最多上传 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount} 张图片`,
|
||||
`单个案例最多上传 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount} 张切片`,
|
||||
)
|
||||
}
|
||||
const totalBytes = inputs.reduce((sum, input) => sum + input.buffer.length, 0)
|
||||
|
||||
@ -43,6 +43,7 @@ import {
|
||||
type PerformanceSurveyStoredImageGroup,
|
||||
} from '@/lib/performance-survey-image-processing'
|
||||
import { getPerformanceSurveyTargetEmployeeIds } from '@/lib/performance-survey-target-policy'
|
||||
import { sortPerformanceSurveyImagesByName } from '@/lib/performance-survey-image-order'
|
||||
import { deleteFromS3, uploadToS3 } from '@/lib/s3'
|
||||
import {
|
||||
deleteOwnedUploadSession,
|
||||
@ -2776,7 +2777,7 @@ async function createPerformanceSurveyWorkItemFromChunkedUploads(
|
||||
const declaredImages = Array.isArray(body?.images) ? body.images as PerformanceSurveyChunkedImage[] : []
|
||||
if (declaredImages.length === 0) throw new PerformanceSurveyApiError('每个案例至少上传 1 张图片')
|
||||
if (declaredImages.length > PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount) {
|
||||
throw new PerformanceSurveyApiError(`单个案例最多上传 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount} 张图片`)
|
||||
throw new PerformanceSurveyApiError(`单个案例最多上传 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount} 张切片`)
|
||||
}
|
||||
|
||||
const uploads: Array<{ uploadId: string; filePath: string }> = []
|
||||
@ -2812,7 +2813,7 @@ async function createPerformanceSurveyWorkItemFromChunkedUploads(
|
||||
targetId: clean(body?.targetId),
|
||||
title: clean(body?.title),
|
||||
description: clean(body?.description),
|
||||
sources,
|
||||
sources: sortPerformanceSurveyImagesByName(sources, (source) => source.name),
|
||||
})
|
||||
} finally {
|
||||
await cleanupPerformanceSurveyChunkedUploads(userId, uploads)
|
||||
@ -2830,7 +2831,7 @@ export async function createPerformanceSurveyWorkItem(session: Session | null, s
|
||||
targetId: readFormText(formData, 'targetId'),
|
||||
title: readFormText(formData, 'title'),
|
||||
description: readFormText(formData, 'description'),
|
||||
sources: files.map((file) => ({
|
||||
sources: sortPerformanceSurveyImagesByName(files, (file) => file.name).map((file) => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
mimeType: file.type,
|
||||
@ -2878,7 +2879,7 @@ async function createPerformanceSurveyWorkItemFromSources(
|
||||
const sources = input.sources
|
||||
if (sources.length === 0) throw new PerformanceSurveyApiError('每个案例至少上传 1 张图片')
|
||||
if (sources.length > PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount) {
|
||||
throw new PerformanceSurveyApiError(`单个案例最多上传 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount} 张图片`)
|
||||
throw new PerformanceSurveyApiError(`单个案例最多上传 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount} 张切片`)
|
||||
}
|
||||
|
||||
const title = input.title || `案例 ${survey.workItems.length + 1}`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user