chore: sync local updates
This commit is contained in:
@@ -193,12 +193,13 @@
|
||||
}
|
||||
|
||||
.product-card-description {
|
||||
min-height: 46px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 86px;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-line-clamp: 6;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
@@ -235,7 +236,8 @@
|
||||
}
|
||||
|
||||
.product-card-description {
|
||||
min-height: 38px;
|
||||
min-height: 62px;
|
||||
-webkit-line-clamp: 4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<CheckoutOrderHeader
|
||||
v-if="confirmed || needPayChannel || !(confirming && !needPayChannel)"
|
||||
:order-id="orderResult.orderId"
|
||||
:is-mengya-pending-pay="isMengyaPendingPay"
|
||||
:copy-hint="orderIdCopyHint"
|
||||
@@ -68,6 +69,13 @@
|
||||
:is-manual-delivery="isManualDelivery"
|
||||
/>
|
||||
|
||||
<p
|
||||
v-else-if="confirming && !needPayChannel"
|
||||
class="tag text-[15px] py-8 m-0 text-center"
|
||||
>
|
||||
正在加载购买内容…
|
||||
</p>
|
||||
|
||||
<CheckoutAwaitingFulfillment
|
||||
v-else
|
||||
:order-result="orderResult"
|
||||
@@ -123,6 +131,38 @@ import CheckoutOrderDone from './components/CheckoutOrderDone.vue'
|
||||
/** Shown alone when mengya poll reports cancelled (timeout). Keep in sync with onCancelled below. */
|
||||
const MENGYA_PAYMENT_TIMEOUT_CANCEL_MSG = '订单未在时限内到账,已自动取消'
|
||||
|
||||
/** 统一后端字段(含 snake_case / 大小写差异),并保证数组类型安全 */
|
||||
function normalizeCheckoutResponse(raw) {
|
||||
if (!raw || typeof raw !== 'object') return {}
|
||||
const orderId = raw.orderId ?? raw.order_id
|
||||
const status = raw.status ?? raw.Status ?? ''
|
||||
const rawCodes = raw.deliveredCodes ?? raw.delivered_codes
|
||||
const codes = Array.isArray(rawCodes) ? rawCodes : []
|
||||
const deliveryMode = raw.deliveryMode ?? raw.delivery_mode ?? 'auto'
|
||||
const rawManual = raw.isManual ?? raw.is_manual
|
||||
const isManual = typeof rawManual === 'boolean' ? rawManual : deliveryMode === 'manual'
|
||||
const pay = raw.paymentExpectedTotal ?? raw.payment_expected_total
|
||||
const next = {
|
||||
...raw,
|
||||
orderId,
|
||||
status,
|
||||
deliveredCodes: codes,
|
||||
deliveryMode,
|
||||
isManual
|
||||
}
|
||||
if (typeof pay === 'number') next.paymentExpectedTotal = pay
|
||||
return next
|
||||
}
|
||||
|
||||
/** 结账接口已返回 completed 且含发货结果时,无需再点「确认领取」 */
|
||||
function canRevealCheckoutImmediately(r) {
|
||||
const st = String(r.status || '').toLowerCase()
|
||||
if (!r.orderId || st !== 'completed') return false
|
||||
if ((r.deliveredCodes || []).length > 0) return true
|
||||
if (r.isManual || r.deliveryMode === 'manual') return true
|
||||
return false
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const product = ref(null)
|
||||
@@ -180,10 +220,11 @@ async function finalizeOrderFromServer() {
|
||||
if (!orderResult.value?.orderId) return false
|
||||
try {
|
||||
const result = await confirmOrder(orderResult.value.orderId)
|
||||
deliveredCodes.value = result.deliveredCodes || []
|
||||
isManualDelivery.value = result.isManual || result.deliveryMode === 'manual'
|
||||
const normalized = normalizeCheckoutResponse(result)
|
||||
deliveredCodes.value = normalized.deliveredCodes || []
|
||||
isManualDelivery.value = normalized.isManual || normalized.deliveryMode === 'manual'
|
||||
confirmed.value = true
|
||||
orderResult.value = { ...orderResult.value, ...result }
|
||||
orderResult.value = { ...orderResult.value, ...normalized }
|
||||
stopPollingPayment()
|
||||
stopPaymentCountdown()
|
||||
return true
|
||||
@@ -244,13 +285,42 @@ const submitOrder = async () => {
|
||||
notifyEmail: authState.email || ''
|
||||
}
|
||||
const token = isLoggedIn() ? authState.token : null
|
||||
const response = await createOrder(payload, token)
|
||||
orderResult.value = response
|
||||
const response = normalizeCheckoutResponse(await createOrder(payload, token))
|
||||
if (typeof response.paymentExpectedTotal === 'number') {
|
||||
paidAmountSnapshot.value = response.paymentExpectedTotal
|
||||
} else {
|
||||
paidAmountSnapshot.value = unitPrice.value
|
||||
}
|
||||
|
||||
if (canRevealCheckoutImmediately(response)) {
|
||||
orderResult.value = response
|
||||
deliveredCodes.value = response.deliveredCodes || []
|
||||
isManualDelivery.value = response.isManual || response.deliveryMode === 'manual'
|
||||
confirmed.value = true
|
||||
stopPollingPayment()
|
||||
stopPaymentCountdown()
|
||||
} else if (response.orderId && String(response.status || '').toLowerCase() === 'completed') {
|
||||
orderResult.value = response
|
||||
confirming.value = true
|
||||
confirmError.value = ''
|
||||
try {
|
||||
const result = await confirmOrder(response.orderId)
|
||||
orderResult.value = { ...response, ...normalizeCheckoutResponse(result) }
|
||||
deliveredCodes.value = orderResult.value.deliveredCodes || []
|
||||
isManualDelivery.value =
|
||||
orderResult.value.isManual || orderResult.value.deliveryMode === 'manual'
|
||||
confirmed.value = true
|
||||
stopPollingPayment()
|
||||
stopPaymentCountdown()
|
||||
} catch (err) {
|
||||
confirmError.value = err?.response?.data?.error || '加载购买内容失败,请点击「确认领取」重试'
|
||||
confirmed.value = false
|
||||
} finally {
|
||||
confirming.value = false
|
||||
}
|
||||
} else {
|
||||
orderResult.value = response
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err?.response?.data?.error || '下单失败,请稍候再试'
|
||||
} finally {
|
||||
@@ -264,10 +334,11 @@ const doConfirm = async () => {
|
||||
confirming.value = true
|
||||
try {
|
||||
const result = await confirmOrder(orderResult.value.orderId)
|
||||
deliveredCodes.value = result.deliveredCodes || []
|
||||
isManualDelivery.value = result.isManual || result.deliveryMode === 'manual'
|
||||
const normalized = normalizeCheckoutResponse(result)
|
||||
deliveredCodes.value = normalized.deliveredCodes || []
|
||||
isManualDelivery.value = normalized.isManual || normalized.deliveryMode === 'manual'
|
||||
confirmed.value = true
|
||||
orderResult.value = { ...orderResult.value, ...result }
|
||||
orderResult.value = { ...orderResult.value, ...normalized }
|
||||
stopPollingPayment()
|
||||
stopPaymentCountdown()
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<template>
|
||||
<span class="inline-block px-4 py-1.5 rounded-full bg-gradient-to-br from-accent to-accent2 text-white text-[15px] font-semibold mb-3">订单已完成</span>
|
||||
<div v-if="deliveredCodes.length" class="my-3 flex flex-col items-center gap-2">
|
||||
<textarea
|
||||
class="w-[min(520px,100%)] min-h-[120px] px-4 py-3.5 rounded-lg border border-white/35 bg-white/90 text-apptext text-base leading-[1.7] resize-none"
|
||||
style="font-family: var(--font-serif);"
|
||||
readonly
|
||||
:value="deliveredCodes.join('\n')"
|
||||
></textarea>
|
||||
|
||||
<div v-if="deliveredCodes.length" class="relative w-full max-w-[min(96vw,980px)] mx-auto my-6 sm:my-7">
|
||||
<div ref="deliveredCardRef" class="delivered-shell flex-1 min-w-0 rounded-[var(--shell-radius)] p-[var(--shell-border-width)]">
|
||||
<textarea
|
||||
class="delivered-textarea w-full min-h-[min(54vh,420px)] sm:min-h-[360px] px-5 py-5 sm:px-6 sm:py-6 rounded-[calc(var(--shell-radius)-var(--shell-border-width))] bg-white/92 text-apptext text-[15px] sm:text-base leading-[1.75] resize-y shadow-[inset_0_1px_0_rgba(255,255,255,0.72)]"
|
||||
style="font-family: var(--font-serif);"
|
||||
readonly
|
||||
:value="deliveredCodes.join('\n')"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isManualDelivery" class="flex items-start gap-3 p-4 bg-[rgba(90,180,120,0.08)] border border-[rgba(90,180,120,0.25)] rounded-lg text-apptext max-w-lg mx-auto text-left">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
<div>
|
||||
@@ -19,8 +23,262 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
deliveredCodes: { type: Array, default: () => [] },
|
||||
isManualDelivery: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const deliveredCardRef = ref(null)
|
||||
|
||||
/** 亮暖色粒子,与流光边框统一 */
|
||||
const FIREWORK_COLORS = [
|
||||
'#ff8fa3',
|
||||
'#ffb180',
|
||||
'#ffe680',
|
||||
'#a8f0a5',
|
||||
'#8ee8f5',
|
||||
'#9db7ff',
|
||||
'#d8a7ff'
|
||||
]
|
||||
|
||||
let fireworksCleanup = null
|
||||
|
||||
function prefersReducedMotion() {
|
||||
return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
||||
}
|
||||
|
||||
function startCanvasFireworks() {
|
||||
if (prefersReducedMotion() || typeof document === 'undefined') return
|
||||
|
||||
const target = deliveredCardRef.value
|
||||
if (!target) return
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
canvas.style.position = 'fixed'
|
||||
canvas.style.left = '0'
|
||||
canvas.style.top = '0'
|
||||
canvas.style.width = '100%'
|
||||
canvas.style.height = '100%'
|
||||
canvas.style.zIndex = '9999'
|
||||
canvas.style.pointerEvents = 'none'
|
||||
document.body.appendChild(canvas)
|
||||
|
||||
let width = 0
|
||||
let height = 0
|
||||
let cardRect = null
|
||||
const devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2)
|
||||
const particles = []
|
||||
let isRunning = true
|
||||
let rafId = 0
|
||||
|
||||
function resize() {
|
||||
width = window.innerWidth
|
||||
height = window.innerHeight
|
||||
canvas.width = Math.floor(width * devicePixelRatio)
|
||||
canvas.height = Math.floor(height * devicePixelRatio)
|
||||
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0)
|
||||
cardRect = target.getBoundingClientRect()
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
resize()
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, color) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.color = color
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const force = Math.pow(Math.random(), 2) * 7 + 1.8
|
||||
this.vx = Math.cos(angle) * force
|
||||
this.vy = Math.sin(angle) * force
|
||||
this.radius = Math.random() * 2.4 + 1.1
|
||||
this.alpha = 1
|
||||
this.decay = Math.random() * 0.018 + 0.014
|
||||
this.gravity = 0.045
|
||||
this.friction = 0.95
|
||||
this.history = []
|
||||
for (let i = 0; i < 6; i++) this.history.push({ x, y })
|
||||
}
|
||||
|
||||
update() {
|
||||
this.history.shift()
|
||||
this.history.push({ x: this.x, y: this.y })
|
||||
this.vx *= this.friction
|
||||
this.vy *= this.friction
|
||||
this.vy += this.gravity
|
||||
this.x += this.vx
|
||||
this.y += this.vy
|
||||
this.alpha -= this.decay
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.shadowColor = this.color
|
||||
ctx.shadowBlur = 10
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(this.history[0].x, this.history[0].y)
|
||||
for (let i = 1; i < this.history.length; i++) {
|
||||
ctx.lineTo(this.history[i].x, this.history[i].y)
|
||||
}
|
||||
ctx.strokeStyle = this.color
|
||||
ctx.lineWidth = this.radius
|
||||
ctx.lineCap = 'round'
|
||||
ctx.globalAlpha = this.alpha
|
||||
ctx.stroke()
|
||||
ctx.globalAlpha = 1
|
||||
ctx.shadowBlur = 0
|
||||
}
|
||||
}
|
||||
|
||||
function createFirework(x, y) {
|
||||
if (!isRunning) return
|
||||
const color = FIREWORK_COLORS[Math.floor(Math.random() * FIREWORK_COLORS.length)]
|
||||
for (let i = 0; i < 72; i++) {
|
||||
particles.push(new Particle(x, y, color))
|
||||
}
|
||||
}
|
||||
|
||||
function createCardFirework() {
|
||||
if (!cardRect) return
|
||||
const insetX = Math.min(54, cardRect.width * 0.08)
|
||||
const insetY = Math.min(46, cardRect.height * 0.12)
|
||||
const x = cardRect.left + insetX + Math.random() * Math.max(1, cardRect.width - insetX * 2)
|
||||
const y = cardRect.top + insetY + Math.random() * Math.max(1, cardRect.height - insetY * 2)
|
||||
createFirework(x, y)
|
||||
}
|
||||
|
||||
function loop() {
|
||||
if (isRunning) cardRect = target.getBoundingClientRect()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const p = particles[i]
|
||||
p.update()
|
||||
if (p.alpha <= 0) particles.splice(i, 1)
|
||||
else p.draw()
|
||||
}
|
||||
if (!isRunning && particles.length === 0) {
|
||||
window.clearTimeout(stopSpawningTimer)
|
||||
teardown()
|
||||
fireworksCleanup = null
|
||||
return
|
||||
}
|
||||
rafId = requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
function teardown() {
|
||||
window.removeEventListener('resize', resize)
|
||||
window.removeEventListener('mousedown', onPointerDown)
|
||||
cancelAnimationFrame(rafId)
|
||||
if (canvas.parentNode) canvas.parentNode.removeChild(canvas)
|
||||
}
|
||||
|
||||
const stopSpawningTimer = window.setTimeout(() => {
|
||||
isRunning = false
|
||||
}, 1400)
|
||||
|
||||
function scheduleAuto() {
|
||||
if (!isRunning) return
|
||||
createCardFirework()
|
||||
window.setTimeout(scheduleAuto, Math.random() * 180 + 140)
|
||||
}
|
||||
|
||||
const onPointerDown = (e) => {
|
||||
if (!isRunning || !cardRect) return
|
||||
const isInsideCard =
|
||||
e.clientX >= cardRect.left &&
|
||||
e.clientX <= cardRect.right &&
|
||||
e.clientY >= cardRect.top &&
|
||||
e.clientY <= cardRect.bottom
|
||||
if (isInsideCard) createFirework(e.clientX, e.clientY)
|
||||
}
|
||||
window.addEventListener('mousedown', onPointerDown)
|
||||
|
||||
loop()
|
||||
for (let i = 0; i < 5; i++) createCardFirework()
|
||||
scheduleAuto()
|
||||
|
||||
fireworksCleanup = () => {
|
||||
window.clearTimeout(stopSpawningTimer)
|
||||
isRunning = false
|
||||
teardown()
|
||||
fireworksCleanup = null
|
||||
}
|
||||
}
|
||||
|
||||
function tryStartFireworks() {
|
||||
if (!props.deliveredCodes?.length) return
|
||||
if (fireworksCleanup) fireworksCleanup()
|
||||
nextTick(startCanvasFireworks)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
tryStartFireworks()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.deliveredCodes.length,
|
||||
(n) => {
|
||||
if (n) tryStartFireworks()
|
||||
}
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
fireworksCleanup?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.delivered-shell {
|
||||
--shell-radius: 1.05rem;
|
||||
--shell-border-width: 5px;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 0)) padding-box,
|
||||
conic-gradient(
|
||||
from var(--border-angle),
|
||||
#ff8fa3 0deg,
|
||||
#ffb180 50deg,
|
||||
#ffe680 100deg,
|
||||
#a8f0a5 150deg,
|
||||
#8ee8f5 205deg,
|
||||
#9db7ff 260deg,
|
||||
#d8a7ff 315deg,
|
||||
#ff8fa3 360deg
|
||||
) border-box;
|
||||
border: var(--shell-border-width) solid transparent;
|
||||
padding: 0;
|
||||
animation: warm-border-spin 3.4s linear infinite;
|
||||
box-shadow:
|
||||
0 18px 48px rgba(255, 143, 163, 0.2),
|
||||
0 8px 22px rgba(255, 230, 128, 0.18),
|
||||
0 0 24px rgba(142, 232, 245, 0.16);
|
||||
}
|
||||
|
||||
@property --border-angle {
|
||||
syntax: '<angle>';
|
||||
inherits: false;
|
||||
initial-value: 0deg;
|
||||
}
|
||||
|
||||
@keyframes warm-border-spin {
|
||||
0% {
|
||||
--border-angle: 0deg;
|
||||
}
|
||||
100% {
|
||||
--border-angle: 360deg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.delivered-shell {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user