Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline

This commit is contained in:
WuYongJie 2026-05-21 18:54:51 +08:00
commit aac475f982
4 changed files with 130 additions and 91 deletions

View File

@ -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

View File

@ -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',

View File

@ -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')
})
})

View File

@ -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)