forked from natuka/web.puabadge.com
修改:京东下单页面优化
This commit is contained in:
1 parent
007c675603
commit
9124c07ad2
7 files changed
+600
-39
No files matched your search
+160
-28
@@ -4,6 +4,7 @@
|
||||
:title="config.brandName"
|
||||
:remain="remainCount"
|
||||
:logo="config.images.logo"
|
||||
@remain="onRemainClick"
|
||||
/>
|
||||
|
||||
<div ref="listRef" class="jd-page__list">
|
||||
@@ -39,7 +40,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="msg.type === 'user'" class="msg__bubble msg__bubble--user">
|
||||
<div
|
||||
v-else-if="msg.type === 'user'"
|
||||
class="msg__bubble msg__bubble--user"
|
||||
:class="{ 'is-text-only': !msg.image }"
|
||||
>
|
||||
<img v-if="msg.image" class="msg__user-thumb" :src="msg.image" alt="" />
|
||||
<p class="msg__user-text">{{ msg.text }}</p>
|
||||
</div>
|
||||
@@ -80,15 +85,24 @@
|
||||
<ChatComposer
|
||||
ref="composerRef"
|
||||
v-model="draft"
|
||||
v-model:product-title="productTitle"
|
||||
:placeholder="config.messages.placeholder"
|
||||
:placeholder-need-photo="config.messages.placeholderNeedPhoto"
|
||||
:placeholder-editing="config.messages.placeholderEditing"
|
||||
:placeholder-cartoon="config.messages.placeholderCartoon"
|
||||
:preview-url="pendingPreview"
|
||||
:tags="composerTags"
|
||||
:editing="isEditing"
|
||||
:editing-hint="config.messages.editingHint"
|
||||
:exit-edit-label="config.messages.exitEdit"
|
||||
:disabled="isBusy || inspecting"
|
||||
:disabled="isBusy"
|
||||
:inspecting="inspecting"
|
||||
:cartoon-effect="isPendingCartoon"
|
||||
:can-send="canSend"
|
||||
:status-tip="composerStatusTip"
|
||||
:tip-warn="composerTipWarn"
|
||||
:title-spec="titleSpec"
|
||||
:title-invalid="titleInvalid"
|
||||
@upload="triggerUpload"
|
||||
@clear-preview="onClearPreview"
|
||||
@send="onSend"
|
||||
@@ -106,7 +120,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { showImagePreview, showToast } from 'vant'
|
||||
import * as badgeApi from '@/api/badge'
|
||||
import * as jingdongApi from '@/api/jingdong'
|
||||
@@ -118,6 +132,11 @@ import {
|
||||
type ImageInspect,
|
||||
} from '@/services/jingdong/image-inspect'
|
||||
import { flyImageToRect, rectFromElement, type FlyRect } from '@/utils/fly-image'
|
||||
import { friendlyError } from '@/utils/jingdong-error'
|
||||
import {
|
||||
productTitleSpecFromOrder,
|
||||
resolveScenePropFromOrder,
|
||||
} from '@/utils/jingdong-product-title'
|
||||
import JdNavBar from './components/JdNavBar.vue'
|
||||
import ChatComposer, { type ComposerTag } from './components/ChatComposer.vue'
|
||||
import GenerationStage from './components/GenerationStage.vue'
|
||||
@@ -189,6 +208,8 @@ const orderStat = ref<Record<string, any> | null>(null)
|
||||
const subjectMatched = ref(true)
|
||||
const sessionDetail = ref<ReturnType<typeof jingdongApi.parseSessionDetail> | null>(null)
|
||||
const draft = ref('')
|
||||
const productTitle = ref('')
|
||||
const titleInvalid = ref(false)
|
||||
const pendingPreview = ref('')
|
||||
const pendingObjectUrl = ref('')
|
||||
const pendingFile = ref<File | null>(null)
|
||||
@@ -245,8 +266,30 @@ const composerRef = ref<{
|
||||
} | null>(null)
|
||||
|
||||
const isEditing = computed(() => !!editingResultId.value)
|
||||
const isPendingCartoon = computed(
|
||||
() => Boolean(pendingPreview.value && photoInspect.value && isCartoonImage(photoInspect.value)),
|
||||
)
|
||||
const titleSpec = computed(() => productTitleSpecFromOrder(orderStat.value))
|
||||
const hasRequiredTitle = computed(() => {
|
||||
if (!titleSpec.value) return true
|
||||
return productTitle.value.trim().length > 0
|
||||
})
|
||||
|
||||
watch(productTitle, (value) => {
|
||||
if (String(value || '').trim()) titleInvalid.value = false
|
||||
})
|
||||
|
||||
watch(
|
||||
() => titleSpec.value?.label || '',
|
||||
() => {
|
||||
productTitle.value = ''
|
||||
titleInvalid.value = false
|
||||
},
|
||||
)
|
||||
const canSend = computed(() => {
|
||||
if (isBusy.value || inspecting.value) return false
|
||||
if (remainCount.value <= 0) return false
|
||||
if (!hasRequiredTitle.value) return false
|
||||
if (isEditing.value) {
|
||||
// 编辑:需有飞入的效果图 + 调整文案
|
||||
return !!pendingPreview.value && draft.value.trim().length > 0
|
||||
@@ -256,6 +299,20 @@ const canSend = computed(() => {
|
||||
return !!pendingPreview.value
|
||||
})
|
||||
|
||||
const composerStatusTip = computed(() => {
|
||||
if (isBusy.value) return config.messages.generatingTip
|
||||
if (inspecting.value) return config.messages.inspectingTip
|
||||
if (remainCount.value <= 0) return config.messages.noRemain
|
||||
if (pendingPreview.value && !subjectMatched.value) return config.messages.subjectMismatchTip
|
||||
return ''
|
||||
})
|
||||
|
||||
const composerTipWarn = computed(
|
||||
() =>
|
||||
remainCount.value <= 0 ||
|
||||
(Boolean(pendingPreview.value) && !subjectMatched.value),
|
||||
)
|
||||
|
||||
/** 解析「1人」「2人+1宠物」等主体文案 */
|
||||
function parseSubjectCounts(text: string) {
|
||||
const raw = String(text || '').trim()
|
||||
@@ -321,7 +378,18 @@ function scrollToBottom() {
|
||||
}
|
||||
|
||||
function triggerUpload() {
|
||||
if (isBusy.value || isEditing.value || inspecting.value) return
|
||||
if (isBusy.value) {
|
||||
showToast(config.messages.generatingTip)
|
||||
return
|
||||
}
|
||||
if (isEditing.value) {
|
||||
showToast('编辑效果图时请直接描述想调整的内容')
|
||||
return
|
||||
}
|
||||
if (inspecting.value) {
|
||||
showToast(config.messages.inspectingTip)
|
||||
return
|
||||
}
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
@@ -337,7 +405,11 @@ function revokePendingObjectUrl() {
|
||||
}
|
||||
}
|
||||
|
||||
function clearPendingPreview() {
|
||||
/**
|
||||
* 清空输入区预览。
|
||||
* keepObjectUrl=true:气泡仍要用 blob 预览,只断开 pending 引用,不 revoke。
|
||||
*/
|
||||
function clearPendingPreview(options?: { keepObjectUrl?: boolean }) {
|
||||
inspectAbort?.abort()
|
||||
inspectAbort = null
|
||||
inspecting.value = false
|
||||
@@ -345,7 +417,12 @@ function clearPendingPreview() {
|
||||
photoInspect.value = null
|
||||
subjectMatched.value = true
|
||||
if (subjectNameFromStat.value) subjectName.value = subjectNameFromStat.value
|
||||
revokePendingObjectUrl()
|
||||
if (options?.keepObjectUrl) {
|
||||
// 交给 blobUrls 在页面卸载时统一释放
|
||||
pendingObjectUrl.value = ''
|
||||
} else {
|
||||
revokePendingObjectUrl()
|
||||
}
|
||||
pendingPreview.value = ''
|
||||
}
|
||||
|
||||
@@ -387,13 +464,11 @@ async function runPhotoRecognize(file: File) {
|
||||
if (!matched) return
|
||||
|
||||
if (isCartoonImage(inspectResult)) {
|
||||
showToast('识别为卡通效果图,可直接描述后生成/下单')
|
||||
showToast('已识别为效果图,可直接发送;也可补充想调整的地方')
|
||||
}
|
||||
} catch (error) {
|
||||
if (serial !== inspectSerial || error instanceof InspectCancelled) return
|
||||
const message =
|
||||
error instanceof Error && error.message ? error.message : '图片识别失败,请稍后重试'
|
||||
showToast(message)
|
||||
showToast(friendlyError(error, 'inspect'))
|
||||
} finally {
|
||||
if (serial === inspectSerial) inspecting.value = false
|
||||
}
|
||||
@@ -433,6 +508,7 @@ async function downloadImage(url: string) {
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
showToast(config.messages.downloadOk)
|
||||
} catch {
|
||||
// 跨域或本地资源失败时直接打开新窗口兜底
|
||||
try {
|
||||
@@ -444,8 +520,9 @@ async function downloadImage(url: string) {
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
} catch {
|
||||
showToast(config.messages.downloadFail)
|
||||
showToast(config.messages.downloadOk)
|
||||
} catch (error) {
|
||||
showToast(friendlyError(error, 'download', config.messages.downloadFail))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,6 +531,10 @@ function onBuy() {
|
||||
showToast(config.messages.buyToast)
|
||||
}
|
||||
|
||||
function onRemainClick() {
|
||||
showToast(config.messages.remainTip(remainCount.value))
|
||||
}
|
||||
|
||||
async function enterEdit(resultId: string, sourceRect?: FlyRect | null) {
|
||||
if (isBusy.value) return
|
||||
const msg = messages.value.find((m) => m.id === resultId && m.type === 'result')
|
||||
@@ -471,9 +552,9 @@ async function enterEdit(resultId: string, sourceRect?: FlyRect | null) {
|
||||
if (editingResultId.value !== resultId) return
|
||||
pendingFile.value = file
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((error) => {
|
||||
if (editingResultId.value === resultId) {
|
||||
showToast('效果图加载失败,请重试')
|
||||
showToast(friendlyError(error, 'common', '效果图加载失败,请重试'))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -499,10 +580,12 @@ function exitEdit() {
|
||||
}
|
||||
|
||||
/** 提交后恢复编辑态与输入区文案 */
|
||||
function resetComposerAfterSubmit() {
|
||||
function resetComposerAfterSubmit(options?: { keepObjectUrl?: boolean }) {
|
||||
editingResultId.value = null
|
||||
draft.value = ''
|
||||
clearPendingPreview()
|
||||
productTitle.value = ''
|
||||
titleInvalid.value = false
|
||||
clearPendingPreview(options)
|
||||
}
|
||||
|
||||
function resolveProductId() {
|
||||
@@ -595,6 +678,7 @@ async function startGenerate(opts: {
|
||||
mode: 'generate' | 'edit'
|
||||
file?: File | null
|
||||
useCartoonEffect?: boolean
|
||||
sceneProp?: string
|
||||
}) {
|
||||
if (remainCount.value <= 0) {
|
||||
showToast(config.messages.noRemain)
|
||||
@@ -603,13 +687,13 @@ async function startGenerate(opts: {
|
||||
|
||||
const sid = String(sessionId.value || '').trim()
|
||||
if (!sid) {
|
||||
showToast('缺少会话信息,请刷新后重试')
|
||||
showToast(friendlyError('缺少会话信息,请刷新后重试', 'common'))
|
||||
return
|
||||
}
|
||||
|
||||
const productId = resolveProductId()
|
||||
if (!productId) {
|
||||
showToast('缺少产品信息,请刷新后重试')
|
||||
showToast(friendlyError('缺少产品信息,请刷新后重试', 'common'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -659,6 +743,8 @@ async function startGenerate(opts: {
|
||||
let imageId = ''
|
||||
let resultUrl = ''
|
||||
|
||||
const sceneProp = String(opts.sceneProp || '').trim()
|
||||
|
||||
if (opts.useCartoonEffect && opts.mode === 'generate') {
|
||||
const effect = await jingdongApi.submitExternalEffect({
|
||||
session_id: sid,
|
||||
@@ -668,6 +754,7 @@ async function startGenerate(opts: {
|
||||
user_message: opts.userText,
|
||||
style_id: styleId,
|
||||
prompt: opts.userText,
|
||||
scene_prop: sceneProp,
|
||||
})
|
||||
taskId = effect.task_id
|
||||
resultUrl = effect.url || effect.images[0]?.url || ''
|
||||
@@ -691,6 +778,7 @@ async function startGenerate(opts: {
|
||||
prompt: opts.userText,
|
||||
n: 1,
|
||||
mode: opts.mode,
|
||||
scene_prop: sceneProp,
|
||||
})
|
||||
taskId = task.task_id
|
||||
if (!taskId) throw new Error('提交生成失败')
|
||||
@@ -716,14 +804,32 @@ async function startGenerate(opts: {
|
||||
removeGenerating(generatingId)
|
||||
pendingFinish.value = null
|
||||
isBusy.value = false
|
||||
const message =
|
||||
error instanceof Error && error.message ? error.message : '生成失败,请稍后重试'
|
||||
showToast(message)
|
||||
const tip = friendlyError(
|
||||
error,
|
||||
opts.mode === 'edit' ? 'edit' : 'generate',
|
||||
opts.mode === 'edit' ? config.messages.editFail : config.messages.generateFail,
|
||||
)
|
||||
showToast({ message: tip, duration: 2800 })
|
||||
// 对话里留下可读失败提示,避免用户错过 Toast
|
||||
messages.value.push({
|
||||
id: uid('fail'),
|
||||
role: 'agent',
|
||||
type: 'text',
|
||||
text: tip,
|
||||
})
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
function onSend() {
|
||||
if (isBusy.value || inspecting.value) return
|
||||
if (isBusy.value) {
|
||||
showToast(config.messages.generatingTip)
|
||||
return
|
||||
}
|
||||
if (inspecting.value) {
|
||||
showToast(config.messages.inspectingTip)
|
||||
return
|
||||
}
|
||||
|
||||
if (remainCount.value <= 0) {
|
||||
showToast(config.messages.noRemain)
|
||||
@@ -736,17 +842,25 @@ function onSend() {
|
||||
return
|
||||
}
|
||||
|
||||
if (titleSpec.value && !productTitle.value.trim()) {
|
||||
titleInvalid.value = true
|
||||
showToast(config.messages.needProductTitle(titleSpec.value.label))
|
||||
return
|
||||
}
|
||||
titleInvalid.value = false
|
||||
|
||||
const text = draft.value.trim()
|
||||
const sceneProp = resolveScenePropFromOrder(orderStat.value, productTitle.value)
|
||||
|
||||
if (isEditing.value) {
|
||||
if (!text) {
|
||||
showToast(config.messages.needPhotoOrText)
|
||||
showToast(config.messages.needEditText)
|
||||
return
|
||||
}
|
||||
const file = pendingFile.value
|
||||
const image = pendingPreview.value
|
||||
if (!file || !image) {
|
||||
showToast('效果图准备中,请稍后再试')
|
||||
showToast(config.messages.effectPreparing)
|
||||
return
|
||||
}
|
||||
resetComposerAfterSubmit()
|
||||
@@ -755,6 +869,7 @@ function onSend() {
|
||||
userText: text,
|
||||
mode: 'edit',
|
||||
file,
|
||||
sceneProp,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -762,7 +877,7 @@ function onSend() {
|
||||
const image = pendingPreview.value
|
||||
const file = pendingFile.value
|
||||
if (!file || !image) {
|
||||
showToast('请先上传照片')
|
||||
showToast(config.messages.needPhoto)
|
||||
return
|
||||
}
|
||||
if (!text && !image) {
|
||||
@@ -770,19 +885,30 @@ function onSend() {
|
||||
return
|
||||
}
|
||||
|
||||
const sendText = text || config.messages.defaultPrompt
|
||||
const useCartoonEffect = Boolean(photoInspect.value && isCartoonImage(photoInspect.value))
|
||||
// 卡通效果图:无文案 → 直接提交;有文案 → 按效果图编辑(传 mode=edit)
|
||||
const hasUserPrompt = text.length > 0
|
||||
const mode: 'generate' | 'edit' =
|
||||
useCartoonEffect && hasUserPrompt ? 'edit' : 'generate'
|
||||
const sendText = hasUserPrompt
|
||||
? text
|
||||
: useCartoonEffect
|
||||
? config.messages.cartoonDirectPrompt
|
||||
: config.messages.defaultPrompt
|
||||
// 与 detail 一致:origin 右侧显示图;external_effect 只显示提示词
|
||||
const sendImage = useCartoonEffect ? undefined : image || undefined
|
||||
// 本地 blob 预览会进气泡,提交后不能 revoke,否则裂图
|
||||
const keepObjectUrl = Boolean(sendImage && String(sendImage).startsWith('blob:'))
|
||||
|
||||
resetComposerAfterSubmit()
|
||||
resetComposerAfterSubmit({ keepObjectUrl })
|
||||
|
||||
void startGenerate({
|
||||
userText: sendText,
|
||||
userImage: sendImage,
|
||||
mode: 'generate',
|
||||
mode,
|
||||
file,
|
||||
useCartoonEffect,
|
||||
sceneProp,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1007,6 +1133,12 @@ onBeforeUnmount(() => {
|
||||
background: #f0f1f3;
|
||||
box-shadow: none;
|
||||
color: #222;
|
||||
|
||||
&.is-text-only {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.msg__user-thumb {
|
||||
|
||||
Reference in new issue
Block a user