Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline
This commit is contained in:
commit
aac475f982
@ -761,19 +761,6 @@ function buildVariantIdentityPrefix(familyCode: string): string {
|
||||
return normalizedCode.endsWith('-') ? normalizedCode : `${normalizedCode}-`
|
||||
}
|
||||
|
||||
function shouldReplaceIdentityPrefill(value: string, identityPrefix: string): boolean {
|
||||
const normalizedValue = value.trim()
|
||||
return normalizedValue.length === 0 || (identityPrefix.length > 0 && normalizedValue === identityPrefix)
|
||||
}
|
||||
|
||||
function mergeIdentityPrefix(identityPrefix: string, value: string): string {
|
||||
const normalizedValue = value.trim()
|
||||
if (!identityPrefix) return normalizedValue
|
||||
if (!normalizedValue) return identityPrefix
|
||||
if (normalizedValue.startsWith(identityPrefix)) return normalizedValue
|
||||
return `${identityPrefix}${normalizedValue}`
|
||||
}
|
||||
|
||||
function emptyVariantForm(assetFamilyId: string, identityPrefix = '') {
|
||||
return {
|
||||
assetFamilyId,
|
||||
@ -834,7 +821,6 @@ type CadImportFieldKey =
|
||||
| 'familyCode'
|
||||
| 'familyName'
|
||||
| 'connectionRoles'
|
||||
| 'variantCode'
|
||||
| 'variantName'
|
||||
| 'substrateId'
|
||||
| 'file'
|
||||
@ -850,8 +836,9 @@ function splitConnectionRoles(value: string): string[] {
|
||||
|
||||
function buildCadImportFieldErrors(params: {
|
||||
form: CadImportForm
|
||||
assetSeriesId?: string | null
|
||||
}): CadImportFieldErrors {
|
||||
const { form } = params
|
||||
const { form, assetSeriesId } = params
|
||||
const errors: CadImportFieldErrors = {}
|
||||
|
||||
if (form.familyMode === 'existing') {
|
||||
@ -859,7 +846,7 @@ function buildCadImportFieldErrors(params: {
|
||||
errors.familyId = '请选择目标资产族'
|
||||
}
|
||||
} else {
|
||||
if (!form.familyCode.trim()) {
|
||||
if (!assetSeriesId && !form.familyCode.trim()) {
|
||||
errors.familyCode = '请填写资产族编码'
|
||||
}
|
||||
if (!form.familyName.trim()) {
|
||||
@ -867,9 +854,6 @@ function buildCadImportFieldErrors(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if (!form.variantCode.trim()) {
|
||||
errors.variantCode = '请填写规格款编码'
|
||||
}
|
||||
if (!form.variantName.trim()) {
|
||||
errors.variantName = '请填写规格款名称'
|
||||
}
|
||||
@ -1509,8 +1493,9 @@ export default function AssetCenterClient({
|
||||
() =>
|
||||
buildCadImportFieldErrors({
|
||||
form: cadImportForm,
|
||||
assetSeriesId: selectedSeriesId,
|
||||
}),
|
||||
[cadImportForm],
|
||||
[cadImportForm, selectedSeriesId],
|
||||
)
|
||||
|
||||
const cadImportChecklist = useMemo(
|
||||
@ -1552,13 +1537,10 @@ export default function AssetCenterClient({
|
||||
}
|
||||
|
||||
const nextForm = emptyCadImportForm(family.type, true)
|
||||
const identityPrefix = buildVariantIdentityPrefix(family.code)
|
||||
setCadImportForm({
|
||||
...nextForm,
|
||||
familyMode: 'existing',
|
||||
familyId: family.id,
|
||||
variantCode: identityPrefix,
|
||||
variantName: identityPrefix,
|
||||
})
|
||||
setCadImportSubmitAttempted(false)
|
||||
setCadImportResult(null)
|
||||
@ -1575,14 +1557,10 @@ export default function AssetCenterClient({
|
||||
}
|
||||
|
||||
function prepareNextCadImport() {
|
||||
const currentFamily = families.find((family) => family.id === cadImportForm.familyId) ?? null
|
||||
const identityPrefix = currentFamily ? buildVariantIdentityPrefix(currentFamily.code) : ''
|
||||
setCadImportForm((current) => ({
|
||||
...emptyCadImportForm(current.assetType, true),
|
||||
familyMode: 'existing',
|
||||
familyId: current.familyId,
|
||||
variantCode: identityPrefix,
|
||||
variantName: identityPrefix,
|
||||
}))
|
||||
setCadImportSubmitAttempted(false)
|
||||
setCadImportResult(null)
|
||||
@ -1635,31 +1613,18 @@ export default function AssetCenterClient({
|
||||
zipFileName: file.name,
|
||||
cadExportData,
|
||||
})
|
||||
const currentFamily =
|
||||
current.familyMode === 'existing'
|
||||
? families.find((family) => family.id === current.familyId) ?? null
|
||||
: null
|
||||
const nextFamilyCode =
|
||||
current.familyMode === 'create' && !current.familyCode.trim()
|
||||
? defaults.familyCode
|
||||
: current.familyCode
|
||||
const identityPrefix =
|
||||
current.familyMode === 'existing'
|
||||
? buildVariantIdentityPrefix(currentFamily?.code ?? '')
|
||||
: buildVariantIdentityPrefix(nextFamilyCode)
|
||||
const shouldInferVariantCode = shouldReplaceIdentityPrefill(
|
||||
current.variantCode,
|
||||
identityPrefix,
|
||||
)
|
||||
const shouldInferVariantName = shouldReplaceIdentityPrefill(
|
||||
current.variantName,
|
||||
identityPrefix,
|
||||
)
|
||||
const shouldUseGeneratedFamilyCode = Boolean(selectedSeriesId)
|
||||
const shouldInferVariantName = current.variantName.trim().length === 0
|
||||
|
||||
const inferred = new Set<string>()
|
||||
if (current.familyMode === 'create' && !current.familyCode.trim()) inferred.add('familyCode')
|
||||
if (
|
||||
current.familyMode === 'create' &&
|
||||
!current.familyCode.trim() &&
|
||||
!shouldUseGeneratedFamilyCode
|
||||
) {
|
||||
inferred.add('familyCode')
|
||||
}
|
||||
if (current.familyMode === 'create' && !current.familyName.trim()) inferred.add('familyName')
|
||||
if (shouldInferVariantCode) inferred.add('variantCode')
|
||||
if (shouldInferVariantName) inferred.add('variantName')
|
||||
setAutoInferredFields(inferred)
|
||||
|
||||
@ -1667,18 +1632,17 @@ export default function AssetCenterClient({
|
||||
...current,
|
||||
file,
|
||||
familyCode:
|
||||
current.familyMode === 'create' && !current.familyCode.trim()
|
||||
current.familyMode === 'create' &&
|
||||
!current.familyCode.trim() &&
|
||||
!shouldUseGeneratedFamilyCode
|
||||
? defaults.familyCode
|
||||
: current.familyCode,
|
||||
familyName:
|
||||
current.familyMode === 'create' && !current.familyName.trim()
|
||||
? defaults.familyName
|
||||
: current.familyName,
|
||||
variantCode: shouldInferVariantCode
|
||||
? mergeIdentityPrefix(identityPrefix, defaults.variantCode)
|
||||
: current.variantCode,
|
||||
variantName: shouldInferVariantName
|
||||
? mergeIdentityPrefix(identityPrefix, defaults.variantName)
|
||||
? defaults.variantName
|
||||
: current.variantName,
|
||||
}
|
||||
})
|
||||
@ -2852,7 +2816,10 @@ export default function AssetCenterClient({
|
||||
assetType: cadImportForm.assetType,
|
||||
familyMode: cadImportForm.familyMode,
|
||||
familyId: cadImportForm.familyMode === 'existing' ? cadImportForm.familyId || null : null,
|
||||
familyCode: cadImportForm.familyMode === 'create' ? cadImportForm.familyCode : null,
|
||||
familyCode:
|
||||
cadImportForm.familyMode === 'create' && !selectedSeriesId
|
||||
? cadImportForm.familyCode
|
||||
: null,
|
||||
familyName: cadImportForm.familyMode === 'create' ? cadImportForm.familyName : null,
|
||||
familyDescription:
|
||||
cadImportForm.familyMode === 'create' ? cadImportForm.familyDescription : null,
|
||||
@ -2861,7 +2828,7 @@ export default function AssetCenterClient({
|
||||
cadImportForm.familyMode === 'create'
|
||||
? cadImportConnectionRoles
|
||||
: undefined,
|
||||
variantCode: cadImportForm.variantCode,
|
||||
variantCode: null,
|
||||
variantName: cadImportForm.variantName,
|
||||
sizeLabel: cadImportForm.sizeLabel || null,
|
||||
substrateId:
|
||||
@ -4187,14 +4154,20 @@ export default function AssetCenterClient({
|
||||
{!cadImportResult && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-3 grid-cols-2">
|
||||
<Input
|
||||
label="编码"
|
||||
required
|
||||
value={cadImportForm.variantCode}
|
||||
onChange={(e) => setCadImportForm((current) => ({ ...current, variantCode: e.target.value }))}
|
||||
placeholder={`${cadImportForm.assetType}-VARIANT-001`}
|
||||
errorText={cadImportSubmitAttempted ? cadImportFieldErrors.variantCode : undefined}
|
||||
/>
|
||||
<div
|
||||
className="rounded-md border px-3 py-2"
|
||||
style={{
|
||||
backgroundColor: 'var(--obs-shell-surface-bg)',
|
||||
borderColor: 'var(--obs-shell-border)',
|
||||
}}
|
||||
>
|
||||
<p className="mb-1 text-[11px] font-semibold text-[var(--obs-shell-muted)]">
|
||||
规格款编码
|
||||
</p>
|
||||
<p className="text-sm font-medium text-[var(--obs-shell-title)]">
|
||||
系统自动生成
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
label="名称"
|
||||
required
|
||||
|
||||
@ -555,12 +555,15 @@ describe('AssetCenterClient workflow', () => {
|
||||
family: {
|
||||
id: 'family-1',
|
||||
type: 'BOARD',
|
||||
code: 'BOARD-140',
|
||||
code: 'DBZ-SPJK001',
|
||||
name: '140台面',
|
||||
description: null,
|
||||
productCategoryId: null,
|
||||
productCategoryName: null,
|
||||
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
|
||||
assetSeriesId: 'template-1',
|
||||
assetSeriesCode: 'DBZ',
|
||||
assetSeriesName: '单板桌',
|
||||
compatibleSeries: [{ id: 'template-1', code: 'DBZ', name: '单板桌' }],
|
||||
connectionRoles: [],
|
||||
metadata: null,
|
||||
status: 'DRAFT',
|
||||
@ -573,9 +576,9 @@ describe('AssetCenterClient workflow', () => {
|
||||
variant: {
|
||||
id: 'variant-1',
|
||||
assetFamilyId: 'family-1',
|
||||
assetFamilyCode: 'BOARD-140',
|
||||
assetFamilyCode: 'DBZ-SPJK001',
|
||||
assetFamilyName: '140台面',
|
||||
code: 'TM-001',
|
||||
code: 'DBZ-SPJ00001',
|
||||
name: '140台面-A',
|
||||
assetType: 'BOARD',
|
||||
sizeLabel: '1400x600x25',
|
||||
@ -621,12 +624,15 @@ describe('AssetCenterClient workflow', () => {
|
||||
{
|
||||
id: 'family-1',
|
||||
type: 'BOARD',
|
||||
code: 'BOARD-140',
|
||||
code: 'DBZ-SPJK001',
|
||||
name: '140台面资产族',
|
||||
description: null,
|
||||
productCategoryId: null,
|
||||
productCategoryName: null,
|
||||
compatibleSeries: [{ id: 'template-1', code: 'single-board-desk', name: '单板桌' }],
|
||||
assetSeriesId: 'template-1',
|
||||
assetSeriesCode: 'DBZ',
|
||||
assetSeriesName: '单板桌',
|
||||
compatibleSeries: [{ id: 'template-1', code: 'DBZ', name: '单板桌' }],
|
||||
connectionRoles: ['desktop_surface'],
|
||||
metadata: {
|
||||
connectionRoleDefinitions: [
|
||||
@ -634,7 +640,7 @@ describe('AssetCenterClient workflow', () => {
|
||||
value: 'desktop_surface',
|
||||
label: '桌面板',
|
||||
assetType: 'BOARD',
|
||||
seriesCode: 'single-board-desk',
|
||||
seriesCode: 'DBZ',
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -658,13 +664,13 @@ describe('AssetCenterClient workflow', () => {
|
||||
initialAssetSeries={[
|
||||
{
|
||||
id: 'template-1',
|
||||
code: 'single-board-desk',
|
||||
code: 'DBZ',
|
||||
name: '单板桌',
|
||||
description: null,
|
||||
sortOrder: 0,
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-04-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-04-01T00:00:00.000Z'),
|
||||
sortOrder: 0,
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-04-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-04-01T00:00:00.000Z'),
|
||||
},
|
||||
] as AssetSeriesDTO[]}
|
||||
finishes={[
|
||||
@ -696,7 +702,7 @@ describe('AssetCenterClient workflow', () => {
|
||||
|
||||
await actor.click(screen.getByText('140台面资产族'))
|
||||
await actor.click(screen.getByRole('button', { name: 'CAD 导入到该资产族' }))
|
||||
await actor.type(screen.getByPlaceholderText('BOARD-VARIANT-001'), 'TM-001')
|
||||
expect(screen.getByText('系统自动生成')).toBeInTheDocument()
|
||||
await actor.type(screen.getByPlaceholderText('例如 桌面板 1200 / 方管桌腿 / 十字连接件'), '140台面-A')
|
||||
|
||||
await actor.click(screen.getByRole('combobox', { name: '基材' }))
|
||||
@ -716,8 +722,9 @@ describe('AssetCenterClient workflow', () => {
|
||||
await waitFor(() => {
|
||||
expect(importAssetCad).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variantCode: 'BOARD-140-TM-001',
|
||||
variantName: 'BOARD-140-140台面-A',
|
||||
assetSeriesId: 'template-1',
|
||||
variantCode: null,
|
||||
variantName: '140台面-A',
|
||||
lossFactor: 1.2,
|
||||
substrateId: 'substrate-1',
|
||||
finishId: 'finish-1',
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
import { importAssetCad } from './platform-api'
|
||||
|
||||
describe('platform-api importAssetCad', () => {
|
||||
const originalFetch = global.fetch
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch
|
||||
})
|
||||
|
||||
it('does not submit blank CAD import codes so backend can generate series codes', async () => {
|
||||
let submittedFormData: FormData | null = null
|
||||
global.fetch = jest.fn(async (_input, init) => {
|
||||
submittedFormData = init?.body as FormData
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
data: {
|
||||
result: {
|
||||
family: { id: 'family-1' },
|
||||
variant: { id: 'variant-1' },
|
||||
summary: {},
|
||||
warnings: [],
|
||||
similarCandidates: [],
|
||||
similarityNotice: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
} as Response
|
||||
})
|
||||
|
||||
await importAssetCad({
|
||||
assetType: 'BOARD',
|
||||
familyMode: 'existing',
|
||||
familyId: 'family-1',
|
||||
assetSeriesId: 'series-dbz',
|
||||
familyCode: '',
|
||||
variantCode: '',
|
||||
variantName: '桌面板 1200',
|
||||
substrateId: 'substrate-1',
|
||||
finishId: '',
|
||||
file: new File(['zip-content'], 'asset.zip', { type: 'application/zip' }),
|
||||
})
|
||||
|
||||
expect(submittedFormData).not.toBeNull()
|
||||
expect(submittedFormData?.has('familyCode')).toBe(false)
|
||||
expect(submittedFormData?.has('variantCode')).toBe(false)
|
||||
expect(submittedFormData?.has('finishId')).toBe(false)
|
||||
expect(submittedFormData?.get('variantName')).toBe('桌面板 1200')
|
||||
expect(submittedFormData?.get('assetSeriesId')).toBe('series-dbz')
|
||||
})
|
||||
})
|
||||
@ -311,8 +311,8 @@ export interface ImportAssetCadPayload {
|
||||
familyDescription?: string | null
|
||||
assetSeriesId?: string | null
|
||||
connectionRoles?: string[]
|
||||
variantCode: string
|
||||
variantName: string
|
||||
variantCode?: string | null
|
||||
variantName?: string | null
|
||||
sizeLabel?: string | null
|
||||
substrateId?: string | null
|
||||
finishId?: string | null
|
||||
@ -320,23 +320,30 @@ export interface ImportAssetCadPayload {
|
||||
file: File
|
||||
}
|
||||
|
||||
function appendNonBlankFormValue(formData: FormData, key: string, value?: string | null) {
|
||||
const normalized = value?.trim()
|
||||
if (normalized) {
|
||||
formData.append(key, normalized)
|
||||
}
|
||||
}
|
||||
|
||||
export async function importAssetCad(input: ImportAssetCadPayload): Promise<AssetCadImportResult> {
|
||||
const formData = new FormData()
|
||||
formData.append('assetType', input.assetType)
|
||||
formData.append('familyMode', input.familyMode)
|
||||
if (input.familyId) formData.append('familyId', input.familyId)
|
||||
if (input.familyCode) formData.append('familyCode', input.familyCode)
|
||||
if (input.familyName) formData.append('familyName', input.familyName)
|
||||
if (input.familyDescription) formData.append('familyDescription', input.familyDescription)
|
||||
if (input.assetSeriesId) formData.append('assetSeriesId', input.assetSeriesId)
|
||||
appendNonBlankFormValue(formData, 'familyId', input.familyId)
|
||||
appendNonBlankFormValue(formData, 'familyCode', input.familyCode)
|
||||
appendNonBlankFormValue(formData, 'familyName', input.familyName)
|
||||
appendNonBlankFormValue(formData, 'familyDescription', input.familyDescription)
|
||||
appendNonBlankFormValue(formData, 'assetSeriesId', input.assetSeriesId)
|
||||
if (input.connectionRoles && input.connectionRoles.length > 0) {
|
||||
formData.append('connectionRoles', JSON.stringify(input.connectionRoles))
|
||||
}
|
||||
formData.append('variantCode', input.variantCode)
|
||||
formData.append('variantName', input.variantName)
|
||||
if (input.sizeLabel) formData.append('sizeLabel', input.sizeLabel)
|
||||
if (input.substrateId) formData.append('substrateId', input.substrateId)
|
||||
if (input.finishId) formData.append('finishId', input.finishId)
|
||||
appendNonBlankFormValue(formData, 'variantCode', input.variantCode)
|
||||
appendNonBlankFormValue(formData, 'variantName', input.variantName)
|
||||
appendNonBlankFormValue(formData, 'sizeLabel', input.sizeLabel)
|
||||
appendNonBlankFormValue(formData, 'substrateId', input.substrateId)
|
||||
appendNonBlankFormValue(formData, 'finishId', input.finishId)
|
||||
if (input.lossFactor != null) formData.append('lossFactor', String(input.lossFactor))
|
||||
formData.append('file', input.file)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user