forked from natuka/web.puabadge.com
修改:下单页面优化
This commit is contained in:
1 parent
add2409f1f
commit
6dd4b4a724
7 files changed
+325
-49
No files matched your search
@@ -6,20 +6,20 @@
|
||||
:src="loadingGif"
|
||||
alt=""
|
||||
draggable="false"
|
||||
:style="{ opacity: gifOpacity }"
|
||||
:style="gifStyle"
|
||||
/>
|
||||
<img
|
||||
v-if="resultUrl"
|
||||
class="jd-stage__result"
|
||||
:src="resultUrl"
|
||||
alt=""
|
||||
:style="{ opacity: reveal }"
|
||||
:style="resultStyle"
|
||||
@load="onResultLoad"
|
||||
/>
|
||||
<p class="jd-stage__status" :style="{ opacity: Math.max(0, 1 - reveal * 1.4) }">
|
||||
<p class="jd-stage__status" :style="{ opacity: overlayOpacity }">
|
||||
{{ statusLabel }}
|
||||
</p>
|
||||
<span class="jd-stage__percent" aria-label="生成进度" :style="{ opacity: Math.max(0, 1 - reveal * 1.2) }">
|
||||
<span class="jd-stage__percent" aria-label="生成进度" :style="{ opacity: overlayOpacity }">
|
||||
{{ progressLabel }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import loadingGif from '@/assets/jingdong/picture_cloud.gif'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -41,9 +41,14 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const WAIT_STATUS = ['正在创建图像', '正在构思构图', '正在丰富细节', '正在打磨光影'] as const
|
||||
const REVEAL_MS = 1600
|
||||
/** 交叉淡入总时长:偏慢,避免突兀闪切 */
|
||||
const REVEAL_MS = 2800
|
||||
/** 效果图已完全显现后再停留片刻,再交给结果卡片,减少组件切换闪烁 */
|
||||
const HOLD_MS = 480
|
||||
/** gif 从何时开始明显淡出(0~1) */
|
||||
const GIF_FADE_START = 0.28
|
||||
|
||||
const phase = ref<'waiting' | 'reveal'>('waiting')
|
||||
const phase = ref<'waiting' | 'reveal' | 'hold'>('waiting')
|
||||
const progress = ref(0)
|
||||
const displayProgress = ref(0)
|
||||
const reveal = ref(0)
|
||||
@@ -55,6 +60,8 @@ let raf = 0
|
||||
let finished = false
|
||||
let startedAt = performance.now()
|
||||
let revealStarted = 0
|
||||
let holdStarted = 0
|
||||
let readyToken = 0
|
||||
|
||||
const progressLabel = computed(() => {
|
||||
const value = Number.isFinite(displayProgress.value) ? displayProgress.value : 0
|
||||
@@ -62,12 +69,27 @@ const progressLabel = computed(() => {
|
||||
})
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (phase.value === 'reveal') return '即将完成'
|
||||
if (phase.value === 'reveal' || phase.value === 'hold') return '即将完成'
|
||||
const elapsed = Math.max(0, clock.value - startedAt)
|
||||
const index = Math.floor(elapsed / 3200) % WAIT_STATUS.length
|
||||
return WAIT_STATUS[index]
|
||||
})
|
||||
|
||||
const overlayOpacity = computed(() => {
|
||||
if (phase.value === 'hold') return 0
|
||||
return Math.max(0, 1 - reveal.value * 1.15)
|
||||
})
|
||||
|
||||
const gifStyle = computed(() => ({
|
||||
opacity: gifOpacity.value,
|
||||
transform: `scale(${1 + reveal.value * 0.02})`,
|
||||
}))
|
||||
|
||||
const resultStyle = computed(() => ({
|
||||
opacity: reveal.value,
|
||||
transform: `scale(${1.03 - reveal.value * 0.03})`,
|
||||
}))
|
||||
|
||||
function safeProgress(value: number) {
|
||||
if (!Number.isFinite(value)) return 0
|
||||
return Math.max(0, Math.min(100, value))
|
||||
@@ -83,6 +105,19 @@ function smoother(value: number) {
|
||||
return amount * amount * amount * (amount * (amount * 6 - 15) + 10)
|
||||
}
|
||||
|
||||
/** 结果先淡入,gif 稍后错开淡出,中间始终有一层可见,避免空闪 */
|
||||
function applyRevealAmount(raw: number) {
|
||||
const amount = smoother(raw)
|
||||
reveal.value = amount
|
||||
if (raw <= GIF_FADE_START) {
|
||||
gifOpacity.value = 1
|
||||
} else {
|
||||
const gifRaw = (raw - GIF_FADE_START) / (1 - GIF_FADE_START)
|
||||
gifOpacity.value = Math.max(0, 1 - smoother(gifRaw))
|
||||
}
|
||||
progress.value = safeProgress(88 + amount * 12)
|
||||
}
|
||||
|
||||
function finish() {
|
||||
if (finished) return
|
||||
finished = true
|
||||
@@ -105,40 +140,73 @@ function tick(now: number) {
|
||||
revealStarted = now
|
||||
}
|
||||
} else if (phase.value === 'reveal') {
|
||||
const amount = smoother((now - revealStarted) / REVEAL_MS)
|
||||
reveal.value = amount
|
||||
gifOpacity.value = Math.max(0, 1 - amount)
|
||||
progress.value = safeProgress(88 + amount * 12)
|
||||
if (amount >= 1) {
|
||||
const raw = Math.min(1, (now - revealStarted) / REVEAL_MS)
|
||||
applyRevealAmount(raw)
|
||||
if (raw >= 1) {
|
||||
phase.value = 'hold'
|
||||
holdStarted = now
|
||||
gifOpacity.value = 0
|
||||
reveal.value = 1
|
||||
progress.value = 100
|
||||
}
|
||||
} else if (phase.value === 'hold') {
|
||||
gifOpacity.value = 0
|
||||
reveal.value = 1
|
||||
progress.value = 100
|
||||
if (now - holdStarted >= HOLD_MS) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const gap = progress.value - displayProgress.value
|
||||
displayProgress.value = safeProgress(displayProgress.value + gap * 0.12)
|
||||
displayProgress.value = safeProgress(displayProgress.value + gap * 0.08)
|
||||
if (Math.abs(gap) < 0.05) displayProgress.value = progress.value
|
||||
|
||||
if (!finished) raf = window.requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
function onResultLoad() {
|
||||
async function prepareResult(url: string) {
|
||||
const token = ++readyToken
|
||||
resultReady.value = false
|
||||
if (!url) return
|
||||
try {
|
||||
const img = new Image()
|
||||
img.src = url
|
||||
if (typeof img.decode === 'function') {
|
||||
await img.decode()
|
||||
} else {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve()
|
||||
img.onerror = () => reject(new Error('load failed'))
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// 解码失败仍继续,避免卡住
|
||||
}
|
||||
if (token !== readyToken) return
|
||||
await nextTick()
|
||||
// 再等一帧,确保 DOM 已挂上结果层再开淡入
|
||||
await new Promise<void>((resolve) => {
|
||||
window.requestAnimationFrame(() => resolve())
|
||||
})
|
||||
if (token !== readyToken) return
|
||||
resultReady.value = true
|
||||
}
|
||||
|
||||
function onResultLoad() {
|
||||
if (props.resultUrl) void prepareResult(props.resultUrl)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.resultUrl,
|
||||
(url) => {
|
||||
if (!url) {
|
||||
readyToken += 1
|
||||
resultReady.value = false
|
||||
return
|
||||
}
|
||||
// 可能已缓存,直接进入 reveal
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
resultReady.value = true
|
||||
}
|
||||
img.src = url
|
||||
void prepareResult(url)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
@@ -159,6 +227,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
readyToken += 1
|
||||
window.cancelAnimationFrame(raf)
|
||||
})
|
||||
</script>
|
||||
@@ -188,6 +257,8 @@ onUnmounted(() => {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
pointer-events: none;
|
||||
will-change: opacity, transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.jd-stage__gif {
|
||||
@@ -196,7 +267,8 @@ onUnmounted(() => {
|
||||
|
||||
.jd-stage__result {
|
||||
z-index: 3;
|
||||
object-fit: contain;
|
||||
/* 与结果卡片一致,避免切换时构图跳变 */
|
||||
object-position: center top;
|
||||
background: #f0f4f9;
|
||||
}
|
||||
|
||||
@@ -212,6 +284,7 @@ onUnmounted(() => {
|
||||
line-height: 1.3;
|
||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.9);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.jd-stage__percent {
|
||||
@@ -233,5 +306,6 @@ onUnmounted(() => {
|
||||
line-height: 1;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.12);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -90,6 +90,18 @@ function onEditClick() {
|
||||
border: 1.5px solid transparent;
|
||||
}
|
||||
|
||||
/* 从生成态切过来时,操作层缓入,避免整卡突兀弹出 */
|
||||
@keyframes jd-result-chrome-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.result-card.is-editing .result-card__media {
|
||||
border-color: #3a8e4d;
|
||||
}
|
||||
@@ -114,6 +126,7 @@ function onEditClick() {
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(4px);
|
||||
animation: jd-result-chrome-in 0.55s ease both;
|
||||
}
|
||||
|
||||
.result-card__view {
|
||||
@@ -148,6 +161,7 @@ function onEditClick() {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
animation: jd-result-chrome-in 0.55s ease 0.06s both;
|
||||
}
|
||||
|
||||
.result-card__buy {
|
||||
@@ -166,6 +180,7 @@ function onEditClick() {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
animation: jd-result-chrome-in 0.55s ease 0.12s both;
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
|
||||
@@ -132,6 +132,7 @@ import {
|
||||
type ImageInspect,
|
||||
} from '@/services/jingdong/image-inspect'
|
||||
import { flyImageToRect, rectFromElement, type FlyRect } from '@/utils/fly-image'
|
||||
import { downloadFallbackTip, downloadImageCompat } from '@/utils/jingdong-download'
|
||||
import { friendlyError } from '@/utils/jingdong-error'
|
||||
import {
|
||||
productTitleSpecFromOrder,
|
||||
@@ -497,33 +498,30 @@ function previewImages(images: string[], startPosition = 0) {
|
||||
}
|
||||
|
||||
async function downloadImage(url: string) {
|
||||
const href = String(url || '').trim()
|
||||
if (!href) {
|
||||
showToast(config.messages.downloadFail)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url)
|
||||
const blob = await res.blob()
|
||||
const objectUrl = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = objectUrl
|
||||
a.download = `suwa-figure-${Date.now()}.png`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
showToast(config.messages.downloadOk)
|
||||
} catch {
|
||||
// 跨域或本地资源失败时直接打开新窗口兜底
|
||||
try {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.download = `suwa-figure-${Date.now()}.png`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
const outcome = await downloadImageCompat(href, `suwa-figure-${Date.now()}.png`)
|
||||
if (outcome === 'saved') {
|
||||
showToast(config.messages.downloadOk)
|
||||
} catch (error) {
|
||||
showToast(friendlyError(error, 'download', config.messages.downloadFail))
|
||||
return
|
||||
}
|
||||
if (outcome === 'opened') {
|
||||
showToast({
|
||||
message: downloadFallbackTip() || config.messages.downloadOpened,
|
||||
duration: 3600,
|
||||
})
|
||||
return
|
||||
}
|
||||
showToast({ message: config.messages.downloadFail, duration: 3200 })
|
||||
} catch (error) {
|
||||
showToast({
|
||||
message: friendlyError(error, 'download', config.messages.downloadFail),
|
||||
duration: 3200,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user