修改:京东demo页面编辑效果图流程调整
continuous-integration/drone/push Build was killed
continuous-integration/drone/promote/prod Build is passing

This commit is contained in:
Linzm committed 2026-11-09 15:53:14 +08:00
1 parent e1da41c2f8
commit 0199b924ac
3 files changed
+95 -135

No files matched your search

+17 -34
View File
@@ -98,25 +98,15 @@ export interface UploadTicket {
export interface ImageGenSubmitInput { export interface ImageGenSubmitInput {
session_id: string session_id: string
upload_id: string /** 导入生图传 upload_id;编辑效果图不传 */
product_id: number upload_id?: string
product_name?: string /** 编辑效果图传 image_id;导入生图不传 */
style_id?: number image_id?: string
prompt?: string prompt?: string
n?: number
mounts_id?: number
scene_prop?: string scene_prop?: string
custom_text?: string custom_text?: string
base_type?: number
frame_type?: number
mount_id?: number
pid?: number
proportion_id?: number
pose_id?: number
/** 效果图编辑 edit;原图编辑 / 普通立即生成 generate */ /** 效果图编辑 edit;原图编辑 / 普通立即生成 generate */
mode?: 'generate' | 'edit' mode?: 'generate' | 'edit'
/** 主体数量文案,来自 orders/stat.subject_name,如「1人」「2人+1宠物」 */
subject_name?: string
} }
export interface ImageGenImage { export interface ImageGenImage {
@@ -773,28 +763,21 @@ export const classifySubject = (input: {
/** POST /jd/imagegen/submit */ /** POST /jd/imagegen/submit */
export const submitImageGen = (input: ImageGenSubmitInput) => { export const submitImageGen = (input: ImageGenSubmitInput) => {
const data: Record<string, string> = {
session_id: input.session_id,
prompt: input.prompt || '',
scene_prop: input.scene_prop || '',
custom_text: input.custom_text || '',
mode: input.mode || 'generate',
}
const uploadId = String(input.upload_id || '').trim()
const imageId = String(input.image_id || '').trim()
// 导入生图:upload_id;编辑效果图:image_id(互斥)
if (uploadId) data.upload_id = uploadId
if (imageId) data.image_id = imageId
return request(`${PREFIX}/imagegen/submit`, { return request(`${PREFIX}/imagegen/submit`, {
method: 'POST', method: 'POST',
data: { data,
session_id: input.session_id,
upload_id: input.upload_id,
product_id: input.product_id,
product_name: input.product_name || '',
style_id: input.style_id ?? 0,
prompt: input.prompt || '',
n: input.n ?? 1,
mounts_id: input.mounts_id ?? 0,
scene_prop: input.scene_prop || '',
custom_text: input.custom_text || '',
base_type: input.base_type ?? 0,
frame_type: input.frame_type ?? 0,
mount_id: input.mount_id ?? 0,
pid: input.pid ?? 0,
proportion_id: input.proportion_id ?? 0,
pose_id: input.pose_id ?? 0,
mode: input.mode || 'generate',
subject_name: input.subject_name || '',
},
}) as Promise<ImageGenTask> }) as Promise<ImageGenTask>
} }
@@ -95,13 +95,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Close, EditPen, Loading, Picture, Present, Star, Top, User } from '@element-plus/icons-vue' import { Close, EditPen, Loading, Lock, Picture, Top } from '@element-plus/icons-vue'
import type { ProductTitleSpec } from '@/utils/jingdong-product-title' import type { ProductTitleSpec } from '@/utils/jingdong-product-title'
export type ComposerTag = { export type ComposerTag = {
key: string key: string
label: string label: string
icon?: 'user' | 'star' | 'present' icon?: 'lock'
} }
const props = withDefaults( const props = withDefaults(
@@ -162,9 +162,7 @@ const titleInputId = 'jd-composer-product-title'
const productTitleLength = computed(() => String(props.productTitle || '').length) const productTitleLength = computed(() => String(props.productTitle || '').length)
const iconMap = { const iconMap = {
user: User, lock: Lock,
star: Star,
present: Present,
} }
const tagItems = computed(() => const tagItems = computed(() =>
@@ -173,7 +171,7 @@ const tagItems = computed(() =>
.map((tag) => ({ .map((tag) => ({
key: tag.key || tag.label, key: tag.key || tag.label,
label: String(tag.label).trim(), label: String(tag.label).trim(),
icon: iconMap[tag.icon || 'present'] || Present, icon: iconMap[tag.icon || 'lock'] || Lock,
})), })),
) )
@@ -504,7 +502,7 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl })
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: clip;
} }
.composer__send { .composer__send {
+73 -94
View File
@@ -252,14 +252,15 @@ const messages = ref<ChatMessage[]>(createBaseMessages())
const composerTags = computed<ComposerTag[]>(() => { const composerTags = computed<ComposerTag[]>(() => {
const tags: ComposerTag[] = [] const tags: ComposerTag[] = []
// 顺序:主体 → 产品 → 风格;图标统一锁
if (subjectName.value) { if (subjectName.value) {
tags.push({ key: 'subject', label: subjectName.value, icon: 'user' }) tags.push({ key: 'subject', label: subjectName.value, icon: 'lock' })
}
if (styleName.value) {
tags.push({ key: 'style', label: styleName.value, icon: 'star' })
} }
if (typeName.value) { if (typeName.value) {
tags.push({ key: 'type', label: typeName.value, icon: 'present' }) tags.push({ key: 'type', label: typeName.value, icon: 'lock' })
}
if (styleName.value) {
tags.push({ key: 'style', label: styleName.value, icon: 'lock' })
} }
return tags return tags
}) })
@@ -353,14 +354,13 @@ const canSend = computed(() => {
if (isBusy.value || uploading.value || inspecting.value) return false if (isBusy.value || uploading.value || inspecting.value) return false
if (remainCount.value <= 0) return false if (remainCount.value <= 0) return false
if (!hasRequiredTitle.value) return false if (!hasRequiredTitle.value) return false
// 上传未完成时不可发(避免缺少 upload_id)
if (pendingPreview.value && !pendingUploadId.value) return false
if (isEditing.value) { if (isEditing.value) {
// 编辑:需有飞入的效果图 + 调整文案 // 编辑:效果图预览 + 调整文案(不走 upload,用结果卡 image_id)
return !!pendingPreview.value && draft.value.trim().length > 0 return !!pendingPreview.value && draft.value.trim().length > 0
} }
// 导入生图:上传未完成时不可发(避免缺少 upload_id)
if (pendingPreview.value && !pendingUploadId.value) return false
// 主体不一致只提示,不拦截发送 // 主体不一致只提示,不拦截发送
// 首轮生图需上传照片(对齐 agent:upload → submit → poll)
return !!pendingPreview.value return !!pendingPreview.value
}) })
@@ -569,15 +569,6 @@ function onClearPreview() {
} }
} }
async function fileFromImageUrl(url: string, filename = 'effect.jpg') {
const res = await fetch(url)
if (!res.ok) throw new Error('效果图加载失败')
const blob = await res.blob()
const type = blob.type || 'image/jpeg'
const name = filename || (type.includes('png') ? 'effect.png' : 'effect.jpg')
return new File([blob], name, { type })
}
function onFileChange(e: Event) { function onFileChange(e: Event) {
const input = e.target as HTMLInputElement const input = e.target as HTMLInputElement
const file = input.files?.[0] const file = input.files?.[0]
@@ -648,25 +639,21 @@ async function enterEdit(resultId: string, sourceRect?: FlyRect | null) {
const msg = messages.value.find((m) => m.id === resultId && m.type === 'result') const msg = messages.value.find((m) => m.id === resultId && m.type === 'result')
if (!msg || msg.type !== 'result' || !msg.image) return if (!msg || msg.type !== 'result' || !msg.image) return
// 编辑直接用效果图 image_id 调 submit,不再走 upload/url + confirm
const imageId = String(msg.imageId || '').trim()
if (!imageId) {
showToast(config.messages.effectPreparing)
return
}
editingResultId.value = resultId editingResultId.value = resultId
// 保留用户已输入的提示词,不清空 draft // 保留用户已输入的提示词,不清空 draft
clearPendingPreview() clearPendingPreview()
// 先放入预览(飞入过程中隐藏),并准备重新上传用的文件
pendingPreview.value = msg.image pendingPreview.value = msg.image
subjectMatched.value = true subjectMatched.value = true
pendingUploadId.value = '' pendingUploadId.value = ''
void fileFromImageUrl(msg.image) pendingFile.value = null
.then((file) => {
if (editingResultId.value !== resultId) return
pendingFile.value = file
void startPendingUploadAndClassify(file)
})
.catch((error) => {
if (editingResultId.value === resultId) {
showToast(friendlyError(error, 'common', '效果图加载失败,请重试'))
}
})
await nextTick() await nextTick()
const thumb = composerRef.value?.getPreviewEl() const thumb = composerRef.value?.getPreviewEl()
@@ -698,17 +685,6 @@ function resetComposerAfterSubmit(options?: { keepObjectUrl?: boolean }) {
clearPendingPreview(options) clearPendingPreview(options)
} }
function resolveProductId() {
// submit 的 product_id 取自 orders/stat 的 type_id
const id = Number(orderStat.value?.type_id || 0)
return Number.isFinite(id) && id > 0 ? id : 0
}
function resolveStyleId() {
const id = Number(orderStat.value?.style_id || 0)
return Number.isFinite(id) && id > 0 ? id : 0
}
function patchGeneratingResult(generatingId: string, resultUrl: string) { function patchGeneratingResult(generatingId: string, resultUrl: string) {
const idx = messages.value.findIndex((m) => m.id === generatingId && m.type === 'generating') const idx = messages.value.findIndex((m) => m.id === generatingId && m.type === 'generating')
if (idx < 0) return if (idx < 0) return
@@ -786,8 +762,10 @@ async function startGenerate(opts: {
userText: string userText: string
userImage?: string userImage?: string
mode: 'generate' | 'edit' mode: 'generate' | 'edit'
/** 导入阶段已拿到的 upload_id;提交只 confirm,不再调 upload/url */ /** 导入生图:confirm + submit 用 upload_id */
uploadId: string uploadId?: string
/** 编辑效果图:直接 submit,传 image_id,不走 url/confirm */
imageId?: string
sceneProp?: string sceneProp?: string
}) { }) {
if (remainCount.value <= 0) { if (remainCount.value <= 0) {
@@ -801,17 +779,16 @@ async function startGenerate(opts: {
return return
} }
const productId = resolveProductId() const isEdit = opts.mode === 'edit'
if (!productId) {
showToast(friendlyError('缺少产品信息,请刷新后重试', 'common'))
return
}
const uploadId = String(opts.uploadId || '').trim() const uploadId = String(opts.uploadId || '').trim()
if (!uploadId) { const editImageId = String(opts.imageId || '').trim()
showToast( if (isEdit) {
opts.mode === 'edit' ? config.messages.effectPreparing : config.messages.needPhoto, if (!editImageId) {
) showToast(config.messages.effectPreparing)
return
}
} else if (!uploadId) {
showToast(config.messages.needPhoto)
return return
} }
@@ -841,31 +818,25 @@ async function startGenerate(opts: {
isBusy.value = true isBusy.value = true
pendingFinish.value = null pendingFinish.value = null
lastUploadId.value = uploadId if (uploadId) lastUploadId.value = uploadId
try { try {
// 提交阶段:仅 confirm → imagegen/submit(upload/url 已在导入时完成) // 导入生图:confirm → submit(upload_id);编辑:直接 submit(image_id)
await jingdongApi.confirmUpload({ if (!isEdit) {
session_id: sid, await jingdongApi.confirmUpload({
upload_id: uploadId, session_id: sid,
}) upload_id: uploadId,
})
}
const styleId = resolveStyleId()
const sceneProp = String(opts.sceneProp || '').trim() const sceneProp = String(opts.sceneProp || '').trim()
const task = await jingdongApi.submitImageGen({ const task = await jingdongApi.submitImageGen({
session_id: sid, session_id: sid,
upload_id: uploadId, ...(isEdit ? { image_id: editImageId } : { upload_id: uploadId }),
product_id: productId,
product_name: String(orderStat.value?.prod_name || orderStat.value?.product_name || ''),
style_id: styleId,
prompt: opts.userText, prompt: opts.userText,
n: 1,
mode: opts.mode, mode: opts.mode,
scene_prop: sceneProp, scene_prop: sceneProp,
// 主体数量:沿用 orders/stat.subject_name custom_text: '',
subject_name:
subjectNameFromStat.value ||
String(orderStat.value?.subject_name || '').trim(),
}) })
let taskId = task.task_id let taskId = task.task_id
if (!taskId) throw new Error('提交生成失败') if (!taskId) throw new Error('提交生成失败')
@@ -881,7 +852,7 @@ async function startGenerate(opts: {
resultUrl, resultUrl,
taskId, taskId,
imageId, imageId,
uploadId, uploadId: uploadId || '',
startedAt, startedAt,
} }
patchGeneratingResult(generatingId, resultUrl) patchGeneratingResult(generatingId, resultUrl)
@@ -894,8 +865,8 @@ async function startGenerate(opts: {
isBusy.value = false isBusy.value = false
const tip = friendlyError( const tip = friendlyError(
error, error,
opts.mode === 'edit' ? 'edit' : 'generate', isEdit ? 'edit' : 'generate',
opts.mode === 'edit' ? config.messages.editFail : config.messages.generateFail, isEdit ? config.messages.editFail : config.messages.generateFail,
) )
showToast({ message: tip, duration: 2800 }) showToast({ message: tip, duration: 2800 })
// 对话里留下可读失败提示,避免用户错过 Toast // 对话里留下可读失败提示,避免用户错过 Toast
@@ -914,18 +885,6 @@ function onSend() {
showToast(config.messages.generatingTip) showToast(config.messages.generatingTip)
return return
} }
if (uploading.value) {
showToast(config.messages.uploadingTip)
return
}
if (inspecting.value) {
showToast(config.messages.inspectingTip)
return
}
if (pendingPreview.value && !pendingUploadId.value) {
showToast(config.messages.uploadingTip)
return
}
if (remainCount.value <= 0) { if (remainCount.value <= 0) {
showToast(config.messages.noRemain) showToast(config.messages.noRemain)
@@ -942,13 +901,6 @@ function onSend() {
const text = draft.value.trim() const text = draft.value.trim()
const sceneProp = resolveScenePropFromOrder(orderStat.value, productTitle.value) const sceneProp = resolveScenePropFromOrder(orderStat.value, productTitle.value)
// 必须在 resetComposer 之前取出:reset 会清空 pendingUploadId
const uploadId = String(pendingUploadId.value || '').trim()
if (!uploadId) {
showToast(config.messages.uploadingTip)
return
}
if (isEditing.value) { if (isEditing.value) {
if (!text) { if (!text) {
showToast(config.messages.needEditText) showToast(config.messages.needEditText)
@@ -959,17 +911,43 @@ function onSend() {
showToast(config.messages.effectPreparing) showToast(config.messages.effectPreparing)
return return
} }
const editingMsg = messages.value.find(
(m) => m.id === editingResultId.value && m.type === 'result',
)
const imageId =
editingMsg && editingMsg.type === 'result'
? String(editingMsg.imageId || '').trim()
: ''
if (!imageId) {
showToast(config.messages.effectPreparing)
return
}
resetComposerAfterSubmit() resetComposerAfterSubmit()
// 编辑提交:右侧气泡只显示提示词(与 session/detail 一致) // 编辑:不走 url/confirm,submit 只传 image_id
void startGenerate({ void startGenerate({
userText: text, userText: text,
mode: 'edit', mode: 'edit',
uploadId, imageId,
sceneProp, sceneProp,
}) })
return return
} }
if (uploading.value) {
showToast(config.messages.uploadingTip)
return
}
if (inspecting.value) {
showToast(config.messages.inspectingTip)
return
}
// 必须在 resetComposer 之前取出:reset 会清空 pendingUploadId
const uploadId = String(pendingUploadId.value || '').trim()
if (!uploadId) {
showToast(config.messages.uploadingTip)
return
}
const image = pendingPreview.value const image = pendingPreview.value
if (!image) { if (!image) {
showToast(config.messages.needPhoto) showToast(config.messages.needPhoto)
@@ -1067,7 +1045,8 @@ function fetchOrderStat() {
subjectNameFromStat.value = String(res?.subject_name || '').trim() subjectNameFromStat.value = String(res?.subject_name || '').trim()
subjectName.value = subjectNameFromStat.value subjectName.value = subjectNameFromStat.value
styleName.value = String(res?.style_name || '').trim() styleName.value = String(res?.style_name || '').trim()
typeName.value = String(res?.type_name || '').trim() // 产品名称展示取 cate_name
typeName.value = String(res?.cate_name || '').trim()
sessionId.value = String(res?.session_id || '').trim() sessionId.value = String(res?.session_id || '').trim()
subjectMatched.value = true subjectMatched.value = true
// 先记下接口值;等会话历史回来后再按已生成次数扣除 // 先记下接口值;等会话历史回来后再按已生成次数扣除