diff --git a/src/services/jingdong/image-inspect.ts b/src/services/jingdong/image-inspect.ts index 56fd9f6..339890b 100644 --- a/src/services/jingdong/image-inspect.ts +++ b/src/services/jingdong/image-inspect.ts @@ -9,6 +9,8 @@ export interface ImageInspect { isCartoon: boolean personCount: number petCount: number + /** 道具数量(若接口有返回) */ + propCount: number reviewStatus: string conclusion: string /** 主体识别 check_id,后续下单可用 */ @@ -40,12 +42,70 @@ export function mapInspectImageType(raw: unknown): ImageKind { function countField(data: Record, keys: string[]) { for (const key of keys) { + if (!(key in data)) continue const value = Number(data[key]) if (Number.isFinite(value)) return Math.max(0, Math.trunc(value)) } return 0 } +function hasOwnCount(data: Record, keys: string[]) { + return keys.some((key) => key in data && Number.isFinite(Number(data[key]))) +} + +type KeywordBucket = 'person' | 'pet' | 'prop' | 'other' + +/** 按 category / 中英文关键词区分人物、宠物、道具 */ +function classifyKeywordBucket(item: Record): KeywordBucket { + const category = Number(item.category ?? item.Category ?? 0) + if (category === 1) return 'person' + if (category === 2) return 'pet' + if (category === 3) return 'prop' + + const text = `${item.cn || ''} ${item.en || ''} ${item.name || ''} ${item.label || ''}`.toLowerCase() + if (/人|人物|人脸|儿童|成人|person|people|human|face|child|man|woman|boy|girl/.test(text)) { + return 'person' + } + if ( + /宠|宠物|猫|狗|兔|鸟|鼠|仓鼠|龙猫|鹦鹉|pet|cat|dog|rabbit|bird|hamster|animal/.test( + text, + ) + ) { + return 'pet' + } + if (/道|具|prop|道具|物件|object/.test(text)) return 'prop' + return 'other' +} + +function countFromKeywords(raw: unknown): { + personCount: number + petCount: number + propCount: number + hasKeywords: boolean +} { + if (!Array.isArray(raw) || raw.length === 0) { + return { personCount: 0, petCount: 0, propCount: 0, hasKeywords: false } + } + let personCount = 0 + let petCount = 0 + let propCount = 0 + let useful = false + raw.forEach((entry) => { + const item = asRecord(entry) + if (!item) return + const n = Number(item.count ?? item.num ?? 1) + const count = Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 1 + if (count <= 0) return + const bucket = classifyKeywordBucket(item) + if (bucket === 'other') return + useful = true + if (bucket === 'person') personCount += count + else if (bucket === 'pet') petCount += count + else if (bucket === 'prop') propCount += count + }) + return { personCount, petCount, propCount, hasKeywords: useful } +} + /** 规范化 classify 接口 data(拦截器已解包 code=1000 的 data) */ export function normalizeInspect(body: unknown): ImageInspect { const root = asRecord(body) || {} @@ -56,23 +116,58 @@ export function normalizeInspect(body: unknown): ImageInspect { (nested.image_type != null || nested.person_count != null || nested.is_cartoon != null || - nested.check_id != null) + nested.check_id != null || + nested.keywords != null) ? nested : root const imageType = mapInspectImageType(data.image_type ?? data.imageType) const isCartoonRaw = data.is_cartoon ?? data.isCartoon const isCartoon = typeof isCartoonRaw === 'boolean' ? isCartoonRaw : imageType === 'cartoon' + + const fieldPerson = countField(data, [ + 'person_count', + 'personCount', + 'headcount', + 'head_count', + ]) + const fieldPet = countField(data, ['pet_count', 'petCount']) + const fieldProp = countField(data, ['prop_count', 'propCount']) + const hasFieldPerson = hasOwnCount(data, [ + 'person_count', + 'personCount', + 'headcount', + 'head_count', + ]) + const hasFieldPet = hasOwnCount(data, ['pet_count', 'petCount']) + const hasFieldProp = hasOwnCount(data, ['prop_count', 'propCount']) + + const fromKeywords = countFromKeywords(data.keywords) + + /** + * 严格按类别取数: + * - keywords 能分出人物/宠物时优先用(避免把宠物误记成人物) + * - 否则回退 person_count / pet_count + */ + let personCount = fieldPerson + let petCount = fieldPet + let propCount = fieldProp + if (fromKeywords.hasKeywords) { + personCount = fromKeywords.personCount + petCount = fromKeywords.petCount + propCount = fromKeywords.propCount + } else { + if (!hasFieldPerson) personCount = 0 + if (!hasFieldPet) petCount = 0 + if (!hasFieldProp) propCount = 0 + } + return { imageType, isCartoon, - personCount: countField(data, [ - 'person_count', - 'personCount', - 'headcount', - 'head_count', - ]), - petCount: countField(data, ['pet_count', 'petCount']), + personCount, + petCount, + propCount, reviewStatus: String(data.review_status || ''), conclusion: String(data.conclusion || ''), checkId: countField(data, ['check_id', 'checkId']), @@ -91,5 +186,6 @@ export function formatSubjectFromInspect(result: ImageInspect) { const parts: string[] = [] if (result.personCount > 0) parts.push(`${result.personCount}人`) if (result.petCount > 0) parts.push(`${result.petCount}宠物`) + if (result.propCount > 0) parts.push(`${result.propCount}道具`) return parts.join('+') } diff --git a/src/views/jingdong/components/ChatComposer.vue b/src/views/jingdong/components/ChatComposer.vue index 5e46b02..9ee8c96 100644 --- a/src/views/jingdong/components/ChatComposer.vue +++ b/src/views/jingdong/components/ChatComposer.vue @@ -18,8 +18,8 @@ class="composer__box" :class="{ 'is-editing': editing, 'is-disabled': disabled, 'has-custom': !!titleSpec }" > - +
已选图片 @@ -370,11 +370,7 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl }) } } -.composer__custom { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; +.composer__custom-block { margin: 0 0 10px; padding: 0 0 10px; border-bottom: 1px solid #f0f0f0; @@ -385,6 +381,13 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl }) } } +.composer__custom { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + .composer__custom-label { display: inline-flex; align-items: center; @@ -441,14 +444,14 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl }) } .composer__custom-hint { - margin: -4px 0 10px; + margin: 6px 0 0; font-size: 11px; line-height: 1.4; color: #888; } .composer__custom-error { - margin: -4px 0 10px; + margin: 6px 0 0; font-size: 12px; line-height: 1.3; color: #c45c26; diff --git a/src/views/jingdong/index.vue b/src/views/jingdong/index.vue index abac591..864bedd 100644 --- a/src/views/jingdong/index.vue +++ b/src/views/jingdong/index.vue @@ -85,9 +85,9 @@ :placeholder="config.messages.placeholder" :placeholder-need-photo="config.messages.placeholderNeedPhoto" :placeholder-editing="config.messages.placeholderEditing" - :custom-text-label="titleSpec?.label || config.messages.customTextLabel" - :custom-text-placeholder="titleSpec?.placeholder || config.messages.customTextPlaceholder" - :custom-text-max="titleSpec?.max || config.messages.customTextMax" + :custom-text-label="composerTitleSpec?.label || config.messages.customTextLabel" + :custom-text-placeholder="composerTitleSpec?.placeholder || config.messages.customTextPlaceholder" + :custom-text-max="composerTitleSpec?.max || config.messages.customTextMax" :preview-url="pendingPreview" :tags="composerTags" :editing="isEditing" @@ -98,7 +98,7 @@ :can-send="canSend" :status-tip="composerStatusTip" :tip-warn="composerTipWarn" - :title-spec="titleSpec" + :title-spec="composerTitleSpec" :title-invalid="titleInvalid" @upload="triggerUpload" @clear-preview="onClearPreview" @@ -331,7 +331,11 @@ function resetRemainQuota() { remainBaselineUsed.value = 0 } const titleSpec = computed(() => productTitleSpecFromOrder(orderStat.value)) +/** 编辑效果图不展示子标题输入;导入生图仍按产品规则展示 */ +const composerTitleSpec = computed(() => (isEditing.value ? null : titleSpec.value)) const hasRequiredTitle = computed(() => { + // 编辑态不要求填写场景道具 / 飘带标题 / 相框标题 + if (isEditing.value) return true if (!titleSpec.value) return true return productTitle.value.trim().length > 0 }) @@ -417,11 +421,21 @@ function checkSubjectMatch(inspectResult: ImageInspect) { const detected = { headcount: Number(inspectResult.personCount) || 0, petCount: Number(inspectResult.petCount) || 0, + propCount: Number(inspectResult.propCount) || 0, } - // 严格比对:订单「1人」不允许识别出「1人+1宠物」 + /** + * 严格按类别比对: + * - 人物数量必须相等(订单「1人」不能用纯宠物图冒充) + * - 宠物数量必须相等(订单「1人」不允许多出宠物;订单「1宠物」也不允许用人图) + * - 订单写了道具时,道具数量也要一致 + */ const peopleOk = detected.headcount === orderSubject.headcount const petOk = detected.petCount === orderSubject.petCount - const matched = peopleOk && petOk + const propOk = + orderSubject.propCount > 0 + ? detected.propCount === orderSubject.propCount + : true + const matched = peopleOk && petOk && propOk subjectMatched.value = matched if (!matched) { const need = subjectNameFromStat.value || formatSubjectCountsLabel(orderSubject) @@ -685,8 +699,9 @@ async function enterEdit(resultId: string, sourceRect?: FlyRect | null) { } editingResultId.value = resultId - // 保留用户已输入的提示词,不清空 draft + // 保留用户已输入的提示词,不清空 draft;编辑态不展示/不校验子标题 clearPendingPreview() + titleInvalid.value = false pendingPreview.value = msg.image subjectMatched.value = true @@ -931,7 +946,8 @@ function onSend() { return } - if (titleSpec.value && !productTitle.value.trim()) { + // 仅导入生图校验子标题必填;编辑效果图不展示、不校验 + if (!isEditing.value && titleSpec.value && !productTitle.value.trim()) { titleInvalid.value = true showToast( config.messages.needProductTitle( @@ -943,13 +959,6 @@ function onSend() { titleInvalid.value = false const text = draft.value.trim() - // 必须在 reset 前取出:场景道具走 scene_prop,其余走 custom_text - const titleValue = productTitle.value.trim() - const isPropsTitle = titleSpec.value?.mode === 'props' - const sceneProp = isPropsTitle - ? resolveScenePropFromOrder(orderStat.value, titleValue) - : '' - const customText = isPropsTitle ? '' : titleValue if (isEditing.value) { if (!text) { @@ -973,17 +982,25 @@ function onSend() { return } resetComposerAfterSubmit() - // 编辑:不走 url/confirm,submit 只传 image_id + // 编辑:不传子标题相关字段 void startGenerate({ userText: text, mode: 'edit', imageId, - sceneProp, - customText, + sceneProp: '', + customText: '', }) return } + // 必须在 reset 前取出:场景道具走 scene_prop,其余走 custom_text + const titleValue = productTitle.value.trim() + const isPropsTitle = titleSpec.value?.mode === 'props' + const sceneProp = isPropsTitle + ? resolveScenePropFromOrder(orderStat.value, titleValue) + : '' + const customText = isPropsTitle ? '' : titleValue + if (uploading.value) { showToast(config.messages.uploadingTip) return