forked from natuka/web.puabadge.com
新增:抖音支持一个链接下多单;图片按裁剪3:4比例;根据返回的数据显示门店名称和门店logo
This commit is contained in:
1 parent
6bf542bf1d
commit
8e8cde9ef5
19 files changed
+2158
-404
No files matched your search
+185
-70
@@ -18,8 +18,10 @@
|
||||
<div class="stage-box" :style="stageStyle">
|
||||
<div class="stage-layer">
|
||||
<ImagePointCloud
|
||||
:key="`${stageWidth}x${stageHeight}`"
|
||||
point-shape="circle"
|
||||
:side="stageSide"
|
||||
:width="stageWidth"
|
||||
:height="stageHeight"
|
||||
:fading="cloudFading"
|
||||
:image-url="sourceUrl"
|
||||
:mask-url="maskUrl"
|
||||
@@ -77,42 +79,49 @@
|
||||
</div>
|
||||
|
||||
<p class="save-hint">效果已保存到生图记录 · 可随时回来查看</p>
|
||||
<OrderSizeQtyPanel ref="orderPanelRef" @change="onOrderChange" />
|
||||
<div v-if="errorTip" class="error-tip light">{{ errorTip }}</div>
|
||||
</div>
|
||||
|
||||
<div class="result-footer">
|
||||
<button class="btn-remake" type="button" @click="goHome">重新制作</button>
|
||||
<button
|
||||
class="btn-submit"
|
||||
type="button"
|
||||
:disabled="ordering || !taskId"
|
||||
@click="openConfirmModal"
|
||||
>
|
||||
满意,提交制作
|
||||
</button>
|
||||
</div>
|
||||
<!-- Teleport 到 body:相对视口固定,避开 app-shell transform -->
|
||||
<Teleport to="body">
|
||||
<div class="dy-result-footer">
|
||||
<button class="dy-result-footer__remake" type="button" @click="goHome">重新制作</button>
|
||||
<button
|
||||
class="dy-result-footer__submit"
|
||||
type="button"
|
||||
:disabled="ordering || !taskId"
|
||||
@click="openConfirmModal"
|
||||
>
|
||||
满意,提交制作
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 确认弹窗 -->
|
||||
<div v-if="showConfirmModal" class="confirm-mask" @click.self="showConfirmModal = false">
|
||||
<div class="confirm-sheet">
|
||||
<div class="confirm-title">确认效果满意</div>
|
||||
<div class="confirm-body">
|
||||
<p>请仔细确认效果是否满意后再提交</p>
|
||||
<p class="confirm-sub">提交制作后将进入制作流程,请确认效果满意后再继续</p>
|
||||
</div>
|
||||
<div class="confirm-actions">
|
||||
<button class="btn-cancel" type="button" @click="showConfirmModal = false">返回查看</button>
|
||||
<button
|
||||
class="btn-ok"
|
||||
type="button"
|
||||
:disabled="ordering"
|
||||
@click="confirmSubmit"
|
||||
>
|
||||
提交制作
|
||||
</button>
|
||||
<div v-if="showConfirmModal" class="dy-result-confirm-mask" @click.self="showConfirmModal = false">
|
||||
<div class="dy-result-confirm-sheet">
|
||||
<div class="dy-result-confirm-title">确认效果满意</div>
|
||||
<div class="dy-result-confirm-body">
|
||||
<p>请仔细确认效果是否满意后再提交</p>
|
||||
<p class="dy-result-confirm-sub">
|
||||
将提交 {{ orderQty }} 件
|
||||
<template v-if="orderSize">(尺寸 {{ orderSize }})</template>
|
||||
,进入制作流程
|
||||
</p>
|
||||
</div>
|
||||
<div class="dy-result-confirm-actions">
|
||||
<button class="dy-result-confirm-cancel" type="button" @click="showConfirmModal = false">返回查看</button>
|
||||
<button
|
||||
class="dy-result-confirm-ok"
|
||||
type="button"
|
||||
:disabled="ordering"
|
||||
@click="confirmSubmit"
|
||||
>
|
||||
提交制作
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -125,6 +134,7 @@ import * as douyinApi from '@/api/douyin'
|
||||
import { douyinConfig as config } from '@/config/douyin'
|
||||
import ImagePointCloud from './components/ImagePointCloud.vue'
|
||||
import DyNavBar from './components/DyNavBar.vue'
|
||||
import OrderSizeQtyPanel from './components/OrderSizeQtyPanel.vue'
|
||||
import { takePendingUpload } from './pendingUpload'
|
||||
|
||||
function isSubmittedBizError(msg: string) {
|
||||
@@ -148,24 +158,53 @@ const cloudFading = ref(false)
|
||||
const ordering = ref(false)
|
||||
const errorTip = ref('')
|
||||
const showConfirmModal = ref(false)
|
||||
const orderPanelRef = ref<InstanceType<typeof OrderSizeQtyPanel> | null>(null)
|
||||
const orderQty = ref(1)
|
||||
const orderSize = ref('')
|
||||
|
||||
/** 红框正方形边长:按 app-shell 实际宽度算,避免桌面端用整窗宽导致偏大/错位 */
|
||||
function calcStageSide() {
|
||||
if (typeof window === 'undefined') return 280
|
||||
const shell = document.querySelector('.app-shell') as HTMLElement | null
|
||||
const w = shell?.clientWidth || Math.min(window.innerWidth || 375, 540)
|
||||
const h = window.innerHeight || 667
|
||||
// 约容器宽 72%、屏高 40%,上限 320,贴近预览卡片区域
|
||||
return Math.max(180, Math.round(Math.min(w * 0.72, h * 0.4, 320)))
|
||||
function onOrderChange(payload: {
|
||||
quantity: number
|
||||
remainTotal: number
|
||||
selected: douyinApi.OrderRemainItem | null
|
||||
}) {
|
||||
orderQty.value = Math.max(1, Number(payload.quantity) || 1)
|
||||
orderSize.value = String(payload.selected?.size || '')
|
||||
}
|
||||
|
||||
const stageSide = ref(calcStageSide())
|
||||
/** 上传图宽高比(裁剪后默认 3:4),舞台高度按此比例,不再强制正方形 */
|
||||
const imageAspect = ref(3 / 4)
|
||||
|
||||
function calcStageSize() {
|
||||
if (typeof window === 'undefined') {
|
||||
return { width: 320, height: Math.round(320 * 4 / 3) }
|
||||
}
|
||||
const shell = document.querySelector('.app-shell') as HTMLElement | null
|
||||
const shellW = shell?.clientWidth || Math.min(window.innerWidth || 375, 540)
|
||||
const shellH = window.innerHeight || 667
|
||||
const maxW = Math.max(200, shellW * 0.9)
|
||||
const maxH = Math.max(220, shellH * 0.58)
|
||||
const aspect = Math.max(0.35, Math.min(2.5, imageAspect.value || 3 / 4))
|
||||
let width = maxW
|
||||
let height = width / aspect
|
||||
if (height > maxH) {
|
||||
height = maxH
|
||||
width = height * aspect
|
||||
}
|
||||
return {
|
||||
width: Math.round(width),
|
||||
height: Math.round(height),
|
||||
}
|
||||
}
|
||||
|
||||
const stageSize = ref(calcStageSize())
|
||||
const stageWidth = computed(() => stageSize.value.width)
|
||||
const stageHeight = computed(() => stageSize.value.height)
|
||||
const stageStyle = computed(() => ({
|
||||
width: `${stageSide.value}px`,
|
||||
height: `${stageSide.value}px`,
|
||||
width: `${stageWidth.value}px`,
|
||||
height: `${stageHeight.value}px`,
|
||||
}))
|
||||
const shellStyle = computed(() => ({
|
||||
width: showResult.value ? `${stageSide.value + 32}px` : `${stageSide.value}px`,
|
||||
width: showResult.value ? `${stageWidth.value + 32}px` : `${stageWidth.value}px`,
|
||||
}))
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
@@ -175,8 +214,34 @@ let maskAborted = false
|
||||
let uploading = false
|
||||
let fadeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function refreshStageSize() {
|
||||
stageSize.value = calcStageSize()
|
||||
}
|
||||
|
||||
function syncImageAspect(url: string) {
|
||||
if (!url) {
|
||||
imageAspect.value = 3 / 4
|
||||
refreshStageSize()
|
||||
return
|
||||
}
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const w = img.naturalWidth || 0
|
||||
const h = img.naturalHeight || 0
|
||||
if (w > 0 && h > 0) {
|
||||
imageAspect.value = w / h
|
||||
refreshStageSize()
|
||||
}
|
||||
}
|
||||
img.onerror = () => {
|
||||
imageAspect.value = 3 / 4
|
||||
refreshStageSize()
|
||||
}
|
||||
img.src = url
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
stageSide.value = calcStageSide()
|
||||
refreshStageSize()
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
@@ -355,7 +420,10 @@ async function beginPendingUpload() {
|
||||
return
|
||||
}
|
||||
uploading = true
|
||||
if (preview) sourceUrl.value = preview
|
||||
if (preview) {
|
||||
sourceUrl.value = preview
|
||||
syncImageAspect(preview)
|
||||
}
|
||||
tip.value = '上传中'
|
||||
subTip.value = '请稍候'
|
||||
progress.value = Math.max(progress.value, 8)
|
||||
@@ -410,34 +478,73 @@ function openZoom() {
|
||||
|
||||
function openConfirmModal() {
|
||||
if (ordering.value || !taskId.value) return
|
||||
const payload = orderPanelRef.value?.getOrderPayload?.()
|
||||
if (!payload?.canOrder) {
|
||||
showToast(config.messages.noOrderRemain)
|
||||
return
|
||||
}
|
||||
if (payload.quantity > payload.remainTotal) {
|
||||
showToast(config.records.messages.exceedOrderRemain)
|
||||
return
|
||||
}
|
||||
showConfirmModal.value = true
|
||||
}
|
||||
|
||||
async function confirmSubmit() {
|
||||
if (ordering.value || !taskId.value) return
|
||||
const payload = orderPanelRef.value?.getOrderPayload?.()
|
||||
if (!payload?.canOrder) {
|
||||
showToast(config.messages.noOrderRemain)
|
||||
return
|
||||
}
|
||||
const qty = Math.max(1, Number(payload.quantity) || 1)
|
||||
if (qty > payload.remainTotal) {
|
||||
showToast(config.records.messages.exceedOrderRemain)
|
||||
return
|
||||
}
|
||||
|
||||
ordering.value = true
|
||||
errorTip.value = ''
|
||||
try {
|
||||
const res = await douyinApi.submitProduction({
|
||||
task_id: taskId.value,
|
||||
image_id: imageId.value,
|
||||
count: qty,
|
||||
})
|
||||
if (res.imageId) {
|
||||
imageId.value = res.imageId
|
||||
sessionStorage.setItem('douyin_image_id', res.imageId)
|
||||
}
|
||||
if (resultUrl.value) {
|
||||
sessionStorage.setItem('douyin_result_url', resultUrl.value)
|
||||
}
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
'douyin_success_results',
|
||||
JSON.stringify([{
|
||||
url: resultUrl.value || '',
|
||||
count: qty,
|
||||
label: '',
|
||||
taskId: taskId.value,
|
||||
imageId: imageId.value,
|
||||
}].filter((item) => item.url)),
|
||||
)
|
||||
} catch (_) {
|
||||
sessionStorage.removeItem('douyin_success_results')
|
||||
}
|
||||
showConfirmModal.value = false
|
||||
router.replace(config.routes.success)
|
||||
} catch (err: any) {
|
||||
errorTip.value = err?.message || '提交失败,请重试'
|
||||
showConfirmModal.value = false
|
||||
orderPanelRef.value?.refresh?.()
|
||||
} finally {
|
||||
ordering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
stageSide.value = calcStageSide()
|
||||
syncImageAspect(sourceUrl.value)
|
||||
window.addEventListener('resize', onResize)
|
||||
startSoftProgress()
|
||||
|
||||
@@ -452,7 +559,10 @@ onMounted(() => {
|
||||
return
|
||||
}
|
||||
const fileUrl = sessionStorage.getItem('douyin_file_url')
|
||||
if (fileUrl) sourceUrl.value = fileUrl
|
||||
if (fileUrl) {
|
||||
sourceUrl.value = fileUrl
|
||||
syncImageAspect(fileUrl)
|
||||
}
|
||||
startMaskFetch()
|
||||
pollStatus()
|
||||
pollTimer = setInterval(pollStatus, config.poll.interval)
|
||||
@@ -483,7 +593,7 @@ onUnmounted(() => {
|
||||
.gen-page.is-result {
|
||||
background: #ffffff;
|
||||
color: #1a1a1a;
|
||||
padding-bottom: calc(88px + env(safe-area-inset-bottom));
|
||||
padding-bottom: calc(108px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.stage-area {
|
||||
@@ -503,7 +613,7 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
max-width: calc(100% - 32px);
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.stage-shell {
|
||||
@@ -710,20 +820,26 @@ onUnmounted(() => {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.result-footer {
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* Teleport 到 body:相对视口固定在全屏底部(与 preview 共用类名) */
|
||||
.dy-result-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #ffffff 28%);
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #ffffff 28%);
|
||||
}
|
||||
|
||||
.btn-remake {
|
||||
.dy-result-footer__remake {
|
||||
flex: 0 0 108px;
|
||||
height: 48px;
|
||||
border: none;
|
||||
@@ -734,7 +850,7 @@ onUnmounted(() => {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
.dy-result-footer__submit {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border: none;
|
||||
@@ -746,16 +862,15 @@ onUnmounted(() => {
|
||||
box-shadow: 0 6px 16px rgba(95, 191, 69, 0.35);
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
.dy-result-footer__submit:disabled {
|
||||
opacity: 0.55;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 确认弹窗 */
|
||||
.confirm-mask {
|
||||
.dy-result-confirm-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
z-index: 1100;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -764,7 +879,7 @@ onUnmounted(() => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.confirm-sheet {
|
||||
.dy-result-confirm-sheet {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
border-radius: 18px;
|
||||
@@ -774,7 +889,7 @@ onUnmounted(() => {
|
||||
border: 2px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.confirm-title {
|
||||
.dy-result-confirm-title {
|
||||
background: #5fbf45;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
@@ -783,7 +898,7 @@ onUnmounted(() => {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.confirm-body {
|
||||
.dy-result-confirm-body {
|
||||
padding: 20px 18px 8px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
@@ -791,20 +906,20 @@ onUnmounted(() => {
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.confirm-sub {
|
||||
.dy-result-confirm-sub {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.confirm-actions {
|
||||
.dy-result-confirm-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 12px 16px 18px;
|
||||
}
|
||||
|
||||
.btn-cancel,
|
||||
.btn-ok {
|
||||
.dy-result-confirm-cancel,
|
||||
.dy-result-confirm-ok {
|
||||
flex: 1;
|
||||
height: 42px;
|
||||
border-radius: 21px;
|
||||
@@ -812,19 +927,19 @@ onUnmounted(() => {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
.dy-result-confirm-cancel {
|
||||
border: 1.5px solid #8fd96f;
|
||||
background: #f3faf0;
|
||||
color: #5fbf45;
|
||||
}
|
||||
|
||||
.btn-ok {
|
||||
.dy-result-confirm-ok {
|
||||
border: none;
|
||||
background: #5fbf45;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-ok:disabled {
|
||||
.dy-result-confirm-ok:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user