forked from natuka/web.puabadge.com
修改:京东下单页面样式与引导图、编辑购买交互优化
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
789d4f106c
commit
2f9d443a6a
6 files changed
+371
-256
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
+12
-5
@@ -1,6 +1,7 @@
|
||||
import logoAvatar from '@/assets/jingdong/logo-mark.png'
|
||||
import logoIcon from '@/assets/jingdong/logo_1.png'
|
||||
import userAvatar from '@/assets/jingdong/jingdong.png'
|
||||
import guideImage from '@/assets/jingdong/yindao.png'
|
||||
import caseOriginal from '@/assets/jingdong/原图.png'
|
||||
import caseEffect from '@/assets/jingdong/效果图.png'
|
||||
import caseScene from '@/assets/jingdong/场景图.png'
|
||||
@@ -17,7 +18,7 @@ export const jingdongConfig = {
|
||||
brandName: '速哇手办制作Agent',
|
||||
primaryColor: '#3A8E4D',
|
||||
primarySoft: '#E8F5E9',
|
||||
background: '#F5F5F5',
|
||||
background: '#FFFFFF',
|
||||
|
||||
images: {
|
||||
/** 顶栏 / Agent 对话头像 */
|
||||
@@ -27,6 +28,8 @@ export const jingdongConfig = {
|
||||
userAvatar,
|
||||
/** 浏览器标签 favicon */
|
||||
logoIcon,
|
||||
/** 对话引导图(照片要求) */
|
||||
guide: guideImage,
|
||||
caseOriginal,
|
||||
caseEffect,
|
||||
caseScene,
|
||||
@@ -43,18 +46,22 @@ export const jingdongConfig = {
|
||||
pollTimeoutMs: 5 * 60 * 1000,
|
||||
|
||||
messages: {
|
||||
welcome: '你好~先上传一张照片,我来帮你做成专属手办效果图。',
|
||||
caseIntro: '先看看效果案例:原图 → 效果图 → 场景应用',
|
||||
welcome: '你好,上传一张照片,或直接说说想做成什么样的手办。',
|
||||
caseIntro: '上传照片前,先看下照片要求',
|
||||
caseLabels: ['原图', '效果图', '场景应用'] as const,
|
||||
placeholder: '描述你想要的手办,例如:橘猫,戴黄色围巾...',
|
||||
placeholderNeedPhoto: '请先点击左侧按钮上传照片',
|
||||
placeholderNeedPhoto: '描述你想要的手办,例如:橘猫,戴黄色围巾...',
|
||||
placeholderEditing: '说说想怎么改,例如:换成蓝色围巾、表情更开心…',
|
||||
customTextLabel: '自定义文字',
|
||||
customTextPlaceholder: '输入自定义文字...',
|
||||
customTextMax: 18,
|
||||
defaultPrompt: '按所选产品、风格生成手办效果图~',
|
||||
editingHint: '正在编辑效果图 · 描述想调整的地方后发送',
|
||||
editingHint: '正在编辑效果图 · 输入文字描述想调整的地方',
|
||||
exitEdit: '退出编辑',
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
buyNow: '立即购买',
|
||||
buyDisabledTip: '编辑调整中,暂不可购买',
|
||||
buyToast: '购买功能即将开放',
|
||||
noRemain: '生图次数已用完,暂时无法继续生成',
|
||||
remainTip: (n: number) => (n > 0 ? `还可以生成 ${n} 次效果图` : '生图次数已用完'),
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="composer">
|
||||
<div v-if="editing" class="composer__edit-bar">
|
||||
<span class="composer__edit-left">
|
||||
<el-icon :size="14"><EditPen /></el-icon>
|
||||
<span>{{ editingHint }}</span>
|
||||
<el-icon class="composer__edit-icon" :size="14"><EditPen /></el-icon>
|
||||
<span class="composer__edit-text">{{ editingHint }}</span>
|
||||
</span>
|
||||
<button class="composer__exit" type="button" @click="emit('exit-edit')">
|
||||
<el-icon :size="12"><Close /></el-icon>
|
||||
@@ -13,7 +13,36 @@
|
||||
|
||||
<p v-if="statusTip" class="composer__tip" :class="{ 'is-warn': tipWarn }">{{ statusTip }}</p>
|
||||
|
||||
<div ref="boxRef" class="composer__box" :class="{ 'is-editing': editing, 'is-disabled': disabled }">
|
||||
<div
|
||||
ref="boxRef"
|
||||
class="composer__box"
|
||||
:class="{ 'is-editing': editing, 'is-disabled': disabled, 'has-custom': !!titleSpec }"
|
||||
>
|
||||
<template v-if="titleSpec">
|
||||
<div class="composer__custom" :class="{ 'is-invalid': titleInvalid }">
|
||||
<label class="composer__custom-label" :for="titleInputId">
|
||||
<el-icon class="composer__custom-icon" :size="14"><EditPen /></el-icon>
|
||||
<span>{{ customLabel }}</span>
|
||||
</label>
|
||||
<input
|
||||
:id="titleInputId"
|
||||
class="composer__custom-input"
|
||||
type="text"
|
||||
:value="productTitle"
|
||||
:maxlength="titleInputMaxlength"
|
||||
:placeholder="customPlaceholder"
|
||||
:disabled="disabled"
|
||||
:aria-invalid="titleInvalid"
|
||||
@input="onTitleInput"
|
||||
/>
|
||||
<span class="composer__custom-count">{{ titleCount }}/{{ customMax }}</span>
|
||||
</div>
|
||||
<p v-if="titleSpec.hint" class="composer__custom-hint">{{ titleSpec.hint }}</p>
|
||||
<p v-if="titleInvalid" class="composer__custom-error" role="alert">
|
||||
请填写{{ customLabel }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div v-if="previewUrl" ref="previewRef" class="composer__preview">
|
||||
<img class="composer__preview-img" :src="previewUrl" alt="已选图片" />
|
||||
<span v-if="inspecting" class="composer__preview-mask">识别中</span>
|
||||
@@ -28,28 +57,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="titleSpec" class="composer__title" :class="{ 'is-invalid': titleInvalid }">
|
||||
<label class="composer__title-label" :for="titleInputId">{{ titleSpec.label }}</label>
|
||||
<div class="composer__title-field">
|
||||
<input
|
||||
:id="titleInputId"
|
||||
class="composer__title-input"
|
||||
type="text"
|
||||
:value="productTitle"
|
||||
:maxlength="titleInputMaxlength"
|
||||
:placeholder="titleSpec.placeholder"
|
||||
:disabled="disabled"
|
||||
:aria-invalid="titleInvalid"
|
||||
@input="onTitleInput"
|
||||
/>
|
||||
<span class="composer__title-count">{{ titleCount }} / {{ titleSpec.max }}</span>
|
||||
</div>
|
||||
<p v-if="titleSpec.hint" class="composer__title-hint">{{ titleSpec.hint }}</p>
|
||||
<p v-if="titleInvalid" class="composer__title-error" role="alert">
|
||||
请填写{{ titleSpec.label }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
class="composer__input"
|
||||
@@ -69,7 +76,7 @@
|
||||
:disabled="disabled || editing || inspecting"
|
||||
@click="emit('upload')"
|
||||
>
|
||||
<el-icon v-if="inspecting" class="is-loading" :size="26"><Loading /></el-icon>
|
||||
<el-icon v-if="inspecting" class="is-loading" :size="28"><Loading /></el-icon>
|
||||
<el-icon v-else :size="28"><Picture /></el-icon>
|
||||
</button>
|
||||
|
||||
@@ -115,6 +122,9 @@ const props = withDefaults(
|
||||
placeholder?: string
|
||||
placeholderNeedPhoto?: string
|
||||
placeholderEditing?: string
|
||||
customTextLabel?: string
|
||||
customTextPlaceholder?: string
|
||||
customTextMax?: number
|
||||
previewUrl?: string
|
||||
tags?: ComposerTag[]
|
||||
editing?: boolean
|
||||
@@ -127,7 +137,7 @@ const props = withDefaults(
|
||||
/** 底部状态提示 */
|
||||
statusTip?: string
|
||||
tipWarn?: boolean
|
||||
/** 产品标题规则(有值时展示标题输入) */
|
||||
/** 产品标题规则(有值时参与校验 / 场景道具模式) */
|
||||
titleSpec?: ProductTitleSpec | null
|
||||
productTitle?: string
|
||||
/** 标题未填时高亮错误 */
|
||||
@@ -136,8 +146,11 @@ const props = withDefaults(
|
||||
{
|
||||
modelValue: '',
|
||||
placeholder: '',
|
||||
placeholderNeedPhoto: '请先点击左侧按钮上传照片',
|
||||
placeholderNeedPhoto: '描述你想要的手办,例如:橘猫,戴黄色围巾...',
|
||||
placeholderEditing: '说说想怎么改…',
|
||||
customTextLabel: '自定义文字',
|
||||
customTextPlaceholder: '输入自定义文字...',
|
||||
customTextMax: 18,
|
||||
previewUrl: '',
|
||||
tags: () => [],
|
||||
editing: false,
|
||||
@@ -166,11 +179,23 @@ const emit = defineEmits<{
|
||||
const titleInputId = 'jd-composer-product-title'
|
||||
const isPropsTitle = computed(() => props.titleSpec?.mode === 'props')
|
||||
|
||||
/** 按产品显示:场景道具 / 飘带标题 / 相框标题;无规则时回退自定义文字 */
|
||||
const customLabel = computed(
|
||||
() => props.titleSpec?.label || props.customTextLabel || '自定义文字',
|
||||
)
|
||||
const customPlaceholder = computed(() => {
|
||||
if (props.titleSpec?.placeholder) return props.titleSpec.placeholder
|
||||
return props.customTextPlaceholder || '输入自定义文字...'
|
||||
})
|
||||
const customMax = computed(() => {
|
||||
if (props.titleSpec) return props.titleSpec.max
|
||||
return props.customTextMax || 18
|
||||
})
|
||||
|
||||
/** 道具模式不按字符截断;字符模式仍用 max */
|
||||
const titleInputMaxlength = computed(() => {
|
||||
if (!props.titleSpec) return undefined
|
||||
if (props.titleSpec.mode === 'props') return undefined
|
||||
return props.titleSpec.max
|
||||
if (props.titleSpec?.mode === 'props') return undefined
|
||||
return customMax.value
|
||||
})
|
||||
|
||||
const titleCount = computed(() => {
|
||||
@@ -273,33 +298,57 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl })
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
background: #eaf6ec;
|
||||
color: #3a8e4d;
|
||||
font-size: 12px;
|
||||
padding: 8px 8px 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: #f2f2f2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.composer__edit-left {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.composer__edit-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
color: #3a8e4d;
|
||||
}
|
||||
|
||||
.composer__edit-text {
|
||||
min-width: 0;
|
||||
color: #3a8e4d;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.composer__exit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #3a8e4d;
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: #1a1a1a;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
|
||||
&:active {
|
||||
opacity: 0.88;
|
||||
}
|
||||
}
|
||||
|
||||
.composer__box {
|
||||
@@ -308,6 +357,7 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl })
|
||||
background: #ffffff;
|
||||
border: 1px solid #ebebeb;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&.is-editing {
|
||||
@@ -320,6 +370,90 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl })
|
||||
}
|
||||
}
|
||||
|
||||
.composer__custom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
margin: 0 0 10px;
|
||||
padding: 0 0 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&.is-invalid .composer__custom-input {
|
||||
border-color: rgba(196, 92, 38, 0.55);
|
||||
background: #fff8f4;
|
||||
}
|
||||
}
|
||||
|
||||
.composer__custom-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
color: #3a8e4d;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.composer__custom-icon {
|
||||
color: #3a8e4d;
|
||||
}
|
||||
|
||||
.composer__custom-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
background: #fafafa;
|
||||
font-size: 13px;
|
||||
color: #1a1a1a;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: rgba(58, 142, 77, 0.55);
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.composer__custom-count {
|
||||
flex-shrink: 0;
|
||||
min-width: 32px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.composer__custom-hint {
|
||||
margin: -4px 0 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.composer__custom-error {
|
||||
margin: -4px 0 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
color: #c45c26;
|
||||
}
|
||||
|
||||
.composer__preview {
|
||||
position: relative;
|
||||
width: 56px;
|
||||
@@ -365,80 +499,6 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl })
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.composer__title {
|
||||
margin: 0 0 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 12px;
|
||||
background: #f7f8f7;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.is-invalid {
|
||||
border-color: rgba(196, 92, 38, 0.45);
|
||||
background: #fff8f4;
|
||||
}
|
||||
}
|
||||
|
||||
.composer__title-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #3a8e4d;
|
||||
}
|
||||
|
||||
.composer__title-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.composer__title-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
color: #1a1a1a;
|
||||
font-family: inherit;
|
||||
|
||||
&::placeholder {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: rgba(58, 142, 77, 0.55);
|
||||
}
|
||||
}
|
||||
|
||||
.composer__title-count {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.composer__title-hint {
|
||||
margin: 6px 0 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.composer__title-error {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
color: #c45c26;
|
||||
}
|
||||
|
||||
.composer__input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -474,9 +534,9 @@ defineExpose({ focus, getPreviewEl, getFlyTargetEl })
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: -6px 0 -6px -6px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: -2px 0 -2px -4px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
|
||||
@@ -234,7 +234,7 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.jd-stage {
|
||||
width: min(280px, 72vw);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -242,10 +242,9 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
max-width: 360px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background: #f0f4f9;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.jd-stage__gif,
|
||||
@@ -254,8 +253,8 @@ onUnmounted(() => {
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
object-fit: contain;
|
||||
object-position: center top;
|
||||
pointer-events: none;
|
||||
will-change: opacity, transform;
|
||||
backface-visibility: hidden;
|
||||
@@ -263,13 +262,14 @@ onUnmounted(() => {
|
||||
|
||||
.jd-stage__gif {
|
||||
z-index: 1;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.jd-stage__result {
|
||||
z-index: 3;
|
||||
/* 与结果卡片一致,避免切换时构图跳变 */
|
||||
object-position: center top;
|
||||
background: #f0f4f9;
|
||||
/* 与结果卡片一致:完整显示效果图 */
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.jd-stage__status {
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<template>
|
||||
<div class="result-card" :class="{ 'is-editing': editing }">
|
||||
<div class="result-card__media">
|
||||
<img ref="imgRef" class="result-card__img" :src="image" alt="效果图" />
|
||||
<button class="result-card__view" type="button" @click="emit('view')">
|
||||
<span>{{ viewLabel }}</span>
|
||||
<el-icon :size="12"><FullScreen /></el-icon>
|
||||
</button>
|
||||
<img
|
||||
ref="imgRef"
|
||||
class="result-card__img"
|
||||
:src="image"
|
||||
alt="效果图"
|
||||
draggable="false"
|
||||
@click="onImageClick"
|
||||
@pointerdown="onPointerDown"
|
||||
@pointerup="onPointerUp"
|
||||
@pointerleave="onPointerUp"
|
||||
@pointercancel="onPointerUp"
|
||||
@contextmenu.prevent
|
||||
/>
|
||||
<button
|
||||
class="result-card__edit"
|
||||
:class="{ 'is-exit': editing }"
|
||||
type="button"
|
||||
@click="onEditClick"
|
||||
>
|
||||
@@ -20,21 +29,25 @@
|
||||
<span>{{ editLabel }}</span>
|
||||
</template>
|
||||
</button>
|
||||
<button class="result-card__download" type="button" aria-label="下载" @click="emit('download')">
|
||||
<el-icon :size="16"><Download /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="result-card__meta">{{ meta }}</div>
|
||||
<button v-if="showBuy" class="result-card__buy" type="button" @click="emit('buy')">
|
||||
<button
|
||||
class="result-card__buy"
|
||||
type="button"
|
||||
:disabled="editing"
|
||||
:aria-disabled="editing"
|
||||
@click="onBuyClick"
|
||||
>
|
||||
<el-icon :size="16"><ShoppingCart /></el-icon>
|
||||
<span>{{ buyLabel }}</span>
|
||||
</button>
|
||||
<p v-if="editing" class="result-card__buy-tip">{{ buyDisabledTip }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Close, Download, EditPen, FullScreen, ShoppingCart } from '@element-plus/icons-vue'
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
import { Close, EditPen, ShoppingCart } from '@element-plus/icons-vue'
|
||||
import { rectFromElement, type FlyRect } from '@/utils/fly-image'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -42,19 +55,17 @@ const props = withDefaults(
|
||||
image: string
|
||||
meta: string
|
||||
editing?: boolean
|
||||
showBuy?: boolean
|
||||
viewLabel?: string
|
||||
editLabel?: string
|
||||
exitEditLabel?: string
|
||||
buyLabel?: string
|
||||
buyDisabledTip?: string
|
||||
}>(),
|
||||
{
|
||||
editing: false,
|
||||
showBuy: true,
|
||||
viewLabel: '查看',
|
||||
editLabel: '编辑',
|
||||
exitEditLabel: '退出编辑',
|
||||
buyLabel: '立即购买',
|
||||
buyDisabledTip: '编辑调整中,暂不可购买',
|
||||
},
|
||||
)
|
||||
|
||||
@@ -67,6 +78,39 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const imgRef = ref<HTMLImageElement | null>(null)
|
||||
const LONG_PRESS_MS = 520
|
||||
let pressTimer: number | null = null
|
||||
let longPressed = false
|
||||
|
||||
function clearPressTimer() {
|
||||
if (pressTimer != null) {
|
||||
window.clearTimeout(pressTimer)
|
||||
pressTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerDown() {
|
||||
longPressed = false
|
||||
clearPressTimer()
|
||||
pressTimer = window.setTimeout(() => {
|
||||
longPressed = true
|
||||
pressTimer = null
|
||||
emit('download')
|
||||
}, LONG_PRESS_MS)
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
clearPressTimer()
|
||||
}
|
||||
|
||||
function onImageClick() {
|
||||
// 长按保存后不再触发查看大图
|
||||
if (longPressed) {
|
||||
longPressed = false
|
||||
return
|
||||
}
|
||||
emit('view')
|
||||
}
|
||||
|
||||
function onEditClick() {
|
||||
if (props.editing) {
|
||||
@@ -75,6 +119,15 @@ function onEditClick() {
|
||||
}
|
||||
emit('edit', { sourceRect: rectFromElement(imgRef.value) })
|
||||
}
|
||||
|
||||
function onBuyClick() {
|
||||
if (props.editing) return
|
||||
emit('buy')
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearPressTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -86,7 +139,7 @@ function onEditClick() {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
background: #f0f0f0;
|
||||
background: #f7f7f7;
|
||||
border: 1.5px solid transparent;
|
||||
}
|
||||
|
||||
@@ -109,51 +162,39 @@ function onEditClick() {
|
||||
.result-card__img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
object-position: center top;
|
||||
cursor: pointer;
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.result-card__view,
|
||||
.result-card__edit,
|
||||
.result-card__download {
|
||||
.result-card__edit {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(4px);
|
||||
font-size: 12px;
|
||||
animation: jd-result-chrome-in 0.55s ease both;
|
||||
}
|
||||
|
||||
.result-card__view {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result-card__edit {
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result-card__download {
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
&.is-exit {
|
||||
color: #1a1a1a;
|
||||
background: #ffffff;
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.result-card__meta {
|
||||
@@ -179,12 +220,25 @@ function onEditClick() {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
||||
animation: jd-result-chrome-in 0.55s ease 0.12s both;
|
||||
|
||||
&:active {
|
||||
&:active:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #ececec;
|
||||
color: #b0b0b0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.result-card__buy-tip {
|
||||
margin: 8px 0 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #b0b0b0;
|
||||
}
|
||||
</style>
|
||||
@@ -27,19 +27,14 @@
|
||||
</div>
|
||||
|
||||
<div v-else-if="msg.type === 'case'" class="msg__bubble msg__bubble--case">
|
||||
<p class="msg__case-intro">{{ config.messages.caseIntro }}</p>
|
||||
<div class="msg__case-grid">
|
||||
<figure
|
||||
v-for="(item, idx) in caseItems"
|
||||
:key="item.label"
|
||||
class="msg__case-item"
|
||||
>
|
||||
<div class="msg__case-thumb">
|
||||
<img :src="item.src" :alt="item.label" @click="previewImages(caseItems.map((c) => c.src), idx)" />
|
||||
</div>
|
||||
<figcaption>{{ item.label }}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<button
|
||||
class="msg__guide"
|
||||
type="button"
|
||||
:aria-label="config.messages.caseIntro"
|
||||
@click="previewImages([config.images.guide], 0)"
|
||||
>
|
||||
<img class="msg__guide-img" :src="config.images.guide" :alt="config.messages.caseIntro" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -65,11 +60,10 @@
|
||||
:image="msg.image"
|
||||
:meta="msg.meta"
|
||||
:editing="editingResultId === msg.id"
|
||||
:show-buy="editingResultId !== msg.id"
|
||||
:view-label="config.messages.view"
|
||||
:edit-label="config.messages.edit"
|
||||
:exit-edit-label="config.messages.exitEdit"
|
||||
:buy-label="config.messages.buyNow"
|
||||
:buy-disabled-tip="config.messages.buyDisabledTip"
|
||||
@view="previewImages([msg.image], 0)"
|
||||
@edit="(payload) => enterEdit(msg.id, payload?.sourceRect)"
|
||||
@exit-edit="exitEdit"
|
||||
@@ -91,6 +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"
|
||||
:preview-url="pendingPreview"
|
||||
:tags="composerTags"
|
||||
:editing="isEditing"
|
||||
@@ -189,12 +186,6 @@ function formatTime(date = new Date()) {
|
||||
return `${h}:${m}`
|
||||
}
|
||||
|
||||
const caseItems = [
|
||||
{ label: config.messages.caseLabels[0], src: config.images.caseOriginal },
|
||||
{ label: config.messages.caseLabels[1], src: config.images.caseEffect },
|
||||
{ label: config.messages.caseLabels[2], src: config.images.caseScene },
|
||||
]
|
||||
|
||||
/**
|
||||
* 剩余次数:
|
||||
* - 首屏用接口 remain_count 打底
|
||||
@@ -814,6 +805,7 @@ async function startGenerate(opts: {
|
||||
/** 编辑效果图:直接 submit,传 image_id,不走 url/confirm */
|
||||
imageId?: string
|
||||
sceneProp?: string
|
||||
customText?: string
|
||||
}) {
|
||||
if (remainCount.value <= 0) {
|
||||
showToast(config.messages.noRemain)
|
||||
@@ -877,13 +869,14 @@ async function startGenerate(opts: {
|
||||
}
|
||||
|
||||
const sceneProp = String(opts.sceneProp || '').trim()
|
||||
const customText = String(opts.customText || '').trim()
|
||||
const task = await jingdongApi.submitImageGen({
|
||||
session_id: sid,
|
||||
...(isEdit ? { image_id: editImageId } : { upload_id: uploadId }),
|
||||
prompt: opts.userText,
|
||||
mode: opts.mode,
|
||||
scene_prop: sceneProp,
|
||||
custom_text: '',
|
||||
custom_text: customText,
|
||||
})
|
||||
let taskId = task.task_id
|
||||
if (!taskId) throw new Error('提交生成失败')
|
||||
@@ -940,13 +933,23 @@ function onSend() {
|
||||
|
||||
if (titleSpec.value && !productTitle.value.trim()) {
|
||||
titleInvalid.value = true
|
||||
showToast(config.messages.needProductTitle(titleSpec.value.label))
|
||||
showToast(
|
||||
config.messages.needProductTitle(
|
||||
titleSpec.value.label || config.messages.customTextLabel,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
titleInvalid.value = false
|
||||
|
||||
const text = draft.value.trim()
|
||||
const sceneProp = resolveScenePropFromOrder(orderStat.value, productTitle.value)
|
||||
// 必须在 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) {
|
||||
@@ -976,6 +979,7 @@ function onSend() {
|
||||
mode: 'edit',
|
||||
imageId,
|
||||
sceneProp,
|
||||
customText,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1018,6 +1022,7 @@ function onSend() {
|
||||
mode: 'generate',
|
||||
uploadId,
|
||||
sceneProp,
|
||||
customText,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1158,7 +1163,10 @@ onBeforeUnmount(() => {
|
||||
<style scoped lang="scss">
|
||||
.jd-page {
|
||||
--jd-primary: #3a8e4d;
|
||||
--jd-bg: #f5f5f5;
|
||||
--jd-bg: #ffffff;
|
||||
--jd-avatar: 36px;
|
||||
--jd-avatar-gap: 10px;
|
||||
--jd-avatar-slot: calc(var(--jd-avatar) + var(--jd-avatar-gap));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
@@ -1198,8 +1206,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.msg__avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: var(--jd-avatar);
|
||||
height: var(--jd-avatar);
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
@@ -1227,7 +1235,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.msg__body {
|
||||
max-width: calc(100% - 52px);
|
||||
/* 左侧头像占位后,右侧再留一个头像宽度,与用户侧对称 */
|
||||
max-width: calc(100% - var(--jd-avatar-slot) - var(--jd-avatar));
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -1235,11 +1244,11 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
max-width: min(268px, calc(100% - 52px));
|
||||
max-width: min(268px, calc(100% - var(--jd-avatar-slot)));
|
||||
}
|
||||
|
||||
.msg.is-case .msg__body {
|
||||
max-width: calc(100% - 46px);
|
||||
max-width: calc(100% - var(--jd-avatar-slot) - var(--jd-avatar));
|
||||
}
|
||||
|
||||
.msg__bubble {
|
||||
@@ -1259,6 +1268,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
.msg:not(.is-user) .msg__bubble {
|
||||
border-radius: 4px 16px 16px 16px;
|
||||
border: 1px solid #e6e6e6;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.msg__bubble--user {
|
||||
@@ -1303,62 +1314,45 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.msg__bubble--case {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.msg__case-intro {
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.msg__case-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.msg__case-item {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
|
||||
figcaption {
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.msg__case-thumb {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: 10px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
background: #ffffff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 200%;
|
||||
object-fit: contain;
|
||||
object-position: center top;
|
||||
cursor: pointer;
|
||||
}
|
||||
.msg__guide {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.msg__guide-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.msg__bubble--generating {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
background: #ffffff;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
width: 60vw;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.msg__bubble--result {
|
||||
padding: 10px;
|
||||
width: min(280px, 72vw);
|
||||
/* 效果图最大占整屏宽度 60% */
|
||||
width: 60vw;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user