ece.suwa3d.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1554 lines
48 KiB

<template>
<div class="preview-container">
<view class="backstyle_1"></view>
<view class="backstyle_2"></view>
<div class="header" @click="goBack">
<span class="back-icon"><van-icon name="arrow-left" size="16px" /> {{ toValueWithout("效果预览") }}</span>
</div>
<div class="step-container">
<div class="step-item">
<div class="step-content">
<div class="step-title">{{ toValueWithout("正面照片") }}</div>
<div class="step-desc">{{ toValueWithout("清晰的正面照片") }}</div>
</div>
</div>
<div class="step-item active">
<div class="step-num">2</div>
<div class="step-content">
<div class="step-title">{{ toValueWithout("确认下单") }}</div>
<div class="step-desc">{{ toValueWithout("选择一个你喜欢的效果图下单") }}</div>
</div>
</div>
</div>
<div v-if="!imageUrl" class="progress-section">
<div class="progress-bar-bg">
<div class="progress-bar-fg" :style="{ width: progress + '%' }"></div>
</div>
<div class="progress-text">{{progressText}} {{progress}}%</div>
<div class="progress-desc">{{ toValueWithout(`总计大约需要${config.preview.progress.totalTime}秒,请耐心等待...`) }}</div>
</div>
<div v-else class="progress-section-picture" :style="`width: ${config.preview.imageSize.previewWidth}px;height: ${config.preview.imageSize.previewHeight}px;`">
<div class="progress-section-img" :style="`width: ${config.preview.imageSize.previewWidth}px;height: ${config.preview.imageSize.previewHeight}px;`">
<img v-if="shapeImage" class="box1-back-image" :src="getImageUrl(shapeImage)" :style="`width: ${config.preview.imageSize.previewWidth}px;height: ${config.preview.imageSize.previewHeight}px;`" alt="">
<div class="box1-front-box" :style="getStyle">
<img class="box1-front-image" :src="imageUrl" alt="" :style="`max-height: ${config.preview.imageSize.previewHeight}px;`">
</div>
<div class="shape-text" :style="shapeTextStyle" v-if="shapeText">{{shapeText}}</div>
<div class="shape-text" :style="topTextStyle" v-if="topText">{{topText}}</div>
<div class="shape-text" :style="bottomTextStyle" v-if="bottomText">{{bottomText}}</div>
<div v-if="badgeTypeId == 6 && axisz == 1 && roundTextConfigFront.width && roundTextConfigFront.charList" class="roundText font-chinese" :style="`left: ${roundTextConfigFront.x}px;top: ${roundTextConfigFront.y}px;width: ${roundTextConfigFront.width}px;height: ${roundTextConfigFront.height}px;position: absolute;pointer-events: none;z-index: 10;`">
<div v-for="item in roundTextConfigFront.charList" :key="item.char" class="round-text-char" :style="`position: absolute;left: ${item.x}px;top: ${item.y}px;font-size: ${item.fontSize}px;font-weight: ${item.fontWeight};color: ${item.color};transform: translate(-50%, -50%) rotate(${item.rotateDeg}deg);transform-origin: center;white-space: nowrap;`">
<span>{{item.char}}</span>
</div>
</div>
<div v-if="badgeTypeId == 6 && axisz == -1 && roundTextConfigBack.width && roundTextConfigBack.charList" class="roundText font-chinese" :style="`left: ${roundTextConfigBack.x}px;top: ${roundTextConfigBack.y}px;width: ${roundTextConfigBack.width}px;height: ${roundTextConfigBack.height}px;position: absolute;pointer-events: none;z-index: 10;`">
<div v-for="(item, index) in roundTextConfigBack.charList" :key="index" class="round-text-char" :style="`position: absolute;left: ${item.x}px;top: ${item.y}px;font-size: ${item.fontSize}px;font-weight: ${item.fontWeight};color: ${item.color};transform: translate(-50%, -50%) rotate(${item.rotateDeg}deg);transform-origin: center;white-space: nowrap;`">
<span>{{item.char}}</span>
</div>
</div>
</div>
</div>
<div class="image-list-box" v-if="imageList.length > 1">
<div class="image-list-item" v-for="item in imageList" :key="item.key">
<img v-if="item.status == 1" class="image-list-item-img" :class="{ imgActive: item.key == imgKey }" :src="item.origin_url" alt="" @click="changeImage(item)">
<div v-else-if="item.status == 0" class="image-list-item-loading">
{{ toValueWithout(config.preview.messages.designing) }}
</div>
<div v-else-if="item.status == 2" class="image-list-item-loading">
{{ toValueWithout(config.preview.messages.designFailed) }}
</div>
</div>
</div>
<div class="info-section">
<div class="info-item">
<div class="info-title" v-if="productName">{{ toValueWithout(productName) }}</div>
<div class="info-content">{{ toValueWithout(config.preview.messages.productType) }}</div>
</div>
<div class="info-item">
<div class="info-title">{{ toValueWithout("3D全彩打印") }}</div>
<div class="info-content">{{ toValueWithout(config.preview.messages.process) }}</div>
</div>
<div class="info-item">
<div class="info-title">ID</div>
<div class="info-content">{{ pid }}</div>
</div>
</div>
<div class="shape-body">
<div class="shape-type" v-if="imageUrl && typeId == 3">
<div class="shape-type-item" :class="{ 'shape-active': item.id == shapeId }" v-for="item in shapeList" :key="item.id" @click="shapeChange(item)">{{item.name}}</div>
</div>
<div class="shape-box" v-if="imageUrl && typeId != 3 && typeId != 4">
<block v-for="item in shapeList" :key="item.id">
<div class="shape-item" @click="shapeChange(item)">
<div class="shape-item-list">
<img class="shape-item-image" :class="{ 'shape-item-image-round': config.preview.shapeIds.round.includes(item.id) }" :src="getImageUrl(item.cover_path) || imageUrl"/>
<van-icon v-if="item.id == shapeId" class="shape-icon" :color="config.styles.colors.success" name="checked" size="18px" />
</div>
<div class="shape-item-text">{{ toValueWithout(item.name) }}</div>
</div>
</block>
</div>
<div class="shape-box-input" v-if="imageUrl && custom_switch == 1 && typeId == 2">
<div v-if="config.preview.shapeIds.topBottomText.includes(shapeId)">
<input class="shape-box-input-text" type="text" :placeholder="toValueWithout('请输入顶部文字')" v-model="topText" @change="changeTopText" @input="changeTopText" @onBlur="changeTopText" />
<input class="shape-box-input-text" type="text" :placeholder="toValueWithout('请输入底部文字')" style="margin-top: 10px;" v-model="bottomText" @change="changeBottomText" @input="changeBottomText" @onBlur="changeBottomText" />
</div>
<div v-else style="width: 100%;">
<input class="shape-box-input-text" type="text" :placeholder="toValueWithout('请输入文字')" v-model="shapeText" @change="changeShapeText" @input="changeShapeText" @onBlur="changeShapeText" />
</div>
</div>
<div class="shape-box-input-box" v-if="imageUrl && custom_switch == 1 && typeId == 6">
<input class="shape-box-input-text" v-if="axisz == 1" type="text" :placeholder="toValueWithout('请输入正面文字')" v-model="frontText" @change="changeFrontText" @input="changeFrontText" @onBlur="changeFrontText" />
<input class="shape-box-input-text" v-if="axisz == -1" type="text" :placeholder="toValueWithout('请输入背面文字')" v-model="backText" @change="changeBackText" @input="changeBackText" @onBlur="changeBackText" />
</div>
</div>
<div style="padding: 0 16px;">
<van-divider />
</div>
<div class="order-section" v-if="imageUrl">
<div class="order-title">{{ toValueWithout(config.preview.messages.orderQuantity) }}</div>
<block v-for="item in sizeList" :key="item.id">
<block>
<div class="order-item">
<span class="order-size">{{ item.size }}</span>
<span class="order-free">({{ toValueWithout(config.preview.messages.remainingExchange) }}:{{ item.remaining }})</span>
<div class="order-ctrl">
<van-stepper v-model="item.count" :min="0" :max="item.remaining" @change="changeValue" />
</div>
</div>
</block>
</block>
</div>
<div class="address-box" v-if="orderStat.use_type == 2 && imageUrl">
<div class="order-title" style="padding: 0 16px;">{{ toValueWithout(config.preview.messages.shippingAddress) }}</div>
<div class="address-item">
<van-address-edit
:area-list="areaList"
:tel-validator="true"
tel-maxlength="11"
show-delete
show-search-result
:search-result="searchResult"
:area-columns-placeholder="[toValueWithout('请选择'), toValueWithout('请选择'), toValueWithout('请选择')]"
@change="nameTelChange"
@change-area="changeArea"
@change-detail="onChangeDetail"
/>
</div>
</div>
<div style="height: 130px;"></div>
<div class="confirm-box">
<div class="action-section">
<!-- <button @click="sureReload" :disabled="flag < 1" class="action-btn"><img class="action-img" :src="reloadImage" alt=""> {{ toValueWithout(config.preview.messages.regenerate) }}</button> -->
<button @click="compare" :disabled="flag < 1" class="action-btn"><img class="action-img" :src="compareImage" alt=""> {{ toValueWithout(config.preview.messages.compare) }}</button>
<button @click="save" :disabled="flag < 1" class="action-btn"><img class="action-img" :src="downloadImage" alt=""> {{ toValueWithout(config.preview.messages.saveImage) }}</button>
</div>
<div class="btn-box">
<button @click="confirm" :disabled="flag < 1" class="confirm-btn">{{ toValueWithout(config.preview.messages.confirmSelect) }}</button>
</div>
</div>
</div>
<van-action-sheet v-model:show="showCompare" :title="toValueWithout(config.preview.messages.compare)" :close-on-click-overlay="true" closeable>
<div style="padding: 16px;">
<div style="display: flex; align-items: center; justify-content: center; gap: 20px;">
<div style="text-align: center;">
<img :src="compareList.origin_path" :alt="toValueWithout(config.preview.messages.originalImage)" :style="`max-width: ${config.preview.imageSize.compareImageSize}px; max-height: ${config.preview.imageSize.compareImageSize}px; border-radius: 8px;`">
</div>
<div style="font-size: 24px; color: #333;">
<van-icon name="arrow" size="20px" />
</div>
<div style="text-align: center;">
<img :src="imageUrl" :alt="toValueWithout(config.preview.messages.previewImage)" :style="`width: ${config.preview.imageSize.compareImageSize}px; height: ${config.preview.imageSize.compareImageSize}px; border-radius: 8px;`">
</div>
</div>
<div style="text-align: center; margin-top: 16px; color: #999; font-size: 12px;">
{{ toValueWithout(config.preview.messages.compareTip) }}
</div>
</div>
</van-action-sheet>
<div v-if="showPreview">
<div class="preview-mask" @click="showPreview = false">
<img :src="imageWater" :alt="toValueWithout(config.preview.messages.badgePreview)" />
<p>{{ toValueWithout(config.preview.messages.saveImageTip) }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, onUnmounted, nextTick, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { showSuccessToast, showToast, showConfirmDialog } from 'vant';
import { localStorage } from '@/utils/local-storage'
import * as badgeApi from '@/api/badge'
import { areaList } from '@/utils/area'
import { toValueWithout } from '@/lang/utils'
import { createTextPosition } from '@/utils/textHelper'
import { cartoonConfig as config } from '@/config/cartoon'
// 导入图片资源
import reloadImageSrc from '@/assets/badge/reload.png'
import compareImageSrc from '@/assets/badge/duibi.png'
import downloadImageSrc from '@/assets/badge/down.png'
const reloadImage = reloadImageSrc
const compareImage = compareImageSrc
const downloadImage = downloadImageSrc
const router = useRouter();
const showCompare = ref(false)
const imageList = ref([])
const imageWater = ref('')
const frontText = ref('')
const backText = ref('')
const frontKey = ref(1)
// 圆形文字配置
const roundTextConfigFront = ref({})
const roundTextConfigBack = ref({})
const cachedImageInfo = ref(null)
const imageInfo = ref(null)
const badgeTypeId = ref(0)
const axisz = ref(1)
// 创建适配 Vue textHelper 实例
const textHelper = createTextPosition({
setData: (data: any) => {
Object.keys(data).forEach(key => {
if (key === 'roundTextConfigFront') {
roundTextConfigFront.value = data[key]
} else if (key === 'roundTextConfigBack') {
roundTextConfigBack.value = data[key]
} else if (key === 'cachedImageInfo') {
cachedImageInfo.value = data[key]
} else if (key === 'imageInfo') {
imageInfo.value = data[key]
}
})
}
})
// 计算产品名称
const productName = computed(() => {
if (!typeId.value || !prodId.value) return ''
const typeConfig = config.preview.productNames[typeId.value]
if (typeConfig && typeConfig[prodId.value]) {
return typeConfig[prodId.value]
}
return ''
})
function compare() {
showCompare.value = true
}
function sureReload() {
console.log('orderStat', orderStat.value)
if (orderStat.value.remain_count <= 0) {
showToast(toValueWithout(config.preview.messages.noRemainingCount))
return
}
isPreview.value = false
showConfirmDialog({
title: toValueWithout(config.preview.messages.regenerate),
message: toValueWithout(config.preview.messages.confirmRegenerate),
})
.then(() => {
group.value = newGroup.value
const params = {
pid: pid.value,
group: config.preview.defaults.group,
prod_id: prodId.value,
type_id: typeId.value,
kind_id: kindId.value
}
badgeApi.putModeling(params).then((res: any) => {
console.log('putModeling', res)
imageUrl.value = '';
imageList.value = []
imgKey.value = '';
createLog()
}).catch((err) => {
console.log('putModeling', err)
showToast({
message: err.message,
duration: 2000,
})
})
})
.catch(() => {
// on cancel
});
}
const imgKey = ref(config.preview.defaults.imgKey)
const frameUrl = ref('')
const changeImage = (item: any) => {
console.log('changeImage', item)
imgKey.value = item.key
imageUrl.value = config.preview.shapeIds.specialFrame.includes(shapeId.value) ? item.frame_url : item.origin_url
frameUrl.value = item.frame_url
imageWater.value = item.image_url
}
const createLog = () => {
badgeApi.createLog({
pid: pid.value,
group: config.preview.defaults.group,
prod_id: prodId.value,
type_id: typeId.value,
kind_id: kindId.value
}).then((res: any) => {
console.log('createLog', res)
progressTimer.value = null
getImageList()
progressList()
getCompareImage()
timer.value = setInterval(() => {
getImageList()
}, config.preview.polling.interval)
}).catch((err) => {
console.log('createLog', err)
showToast({
message: err.message,
duration: 2000,
})
})
}
const showPreview = ref(false)
function save() {
showPreview.value = true;
}
const compareList = ref({})
const style_name = ref('')
function getCompareImage() {
badgeApi.getCompareImage({
pid: pid.value,
group: config.preview.defaults.group,
prod_id: prodId.value,
type_id: typeId.value
}).then((res: any) => {
console.log('getCompareImage', res)
compareList.value = res
style_name.value = res.list[0].kind_name || res.kind_name
}).catch((err) => {
console.log('getCompareImage', err)
showToast({
message: err.message,
duration: 2000,
})
})
}
function goBack() {
router.back()
}
const sizeList = ref([])
const getSizeList = () => {
badgeApi.getOrderPrice({}).then((res: any) => {
console.log('getSizeList', res);
sizeList.value = res
if (typeId.value != 4) {
getShapeList()
}
})
}
const contact_name = ref('')
const contact_mobile = ref('')
const province_id = ref(0)
const city_id = ref(0)
const county_id = ref(0)
const province_name = ref('')
const city_name = ref('')
const county_name = ref('')
const address = ref('')
const nameTelChange = (res: any) => {
console.log('nameTelChange', res)
if (res.key == 'name') {
contact_name.value = res.value
} else {
contact_mobile.value = res.value
}
}
const changeArea = (value: any) => {
console.log('changeArea', value)
province_id.value = value[0].value
city_id.value = value[1].value
county_id.value = value[2].value
province_name.value = value[0].text
city_name.value = value[1].text
county_name.value = value[2].text
}
const onChangeDetail = (value: any) => {
console.log('onChangeDetail', value)
address.value = value
}
const payAmount = ref(0)
const changeValue = (value: number) => {
console.log(value)
payAmount.value = value
}
const loading = ref(false)
const confirm = () => {
if (loading.value) return
console.log('confirm')
if (payAmount.value <= 0) {
showToast(toValueWithout(config.preview.messages.selectQuantity))
return
}
if (orderStat.value.use_type == 2 && !contact_name.value) {
showToast(toValueWithout(config.preview.messages.fillContactName))
return
}
if (orderStat.value.use_type == 2 && !contact_mobile.value) {
showToast(toValueWithout(config.preview.messages.fillContactMobile))
return
}
if (orderStat.value.use_type == 2 && !province_id.value) {
showToast(toValueWithout(config.preview.messages.selectArea))
return
}
if (orderStat.value.use_type == 2 && !address.value) {
showToast(toValueWithout(config.preview.messages.fillAddress))
return
}
getPosition()
loading.value = true
showConfirmDialog({
title: toValueWithout(config.preview.messages.confirmSelect),
message: toValueWithout(config.preview.messages.confirmOrder),
})
.then(() => {
const parms = {
pid: pid.value,
key: imgKey.value,
pay_amount: payAmount.value,
products: sizeList.value,
prod_id: prodId.value,
shape_id: shapeId.value,
custom_text: shapeText.value ? shapeText.value : frontText.value,
back_text: backText.value ? backText.value : '',
shape_ids: shapeIds.value,
type_id: typeId.value,
up_text: topText.value ? topText.value : '',
down_text: bottomText.value ? bottomText.value : '',
subject_id: subjectId.value,
}
if (orderStat.value.use_type == 2) {
parms.province_id = province_id.value
parms.city_id = city_id.value
parms.county_id = county_id.value
parms.province_name = province_name.value
parms.city_name = city_name.value
parms.county_name = county_name.value
parms.address = address.value
parms.contact_name = contact_name.value
parms.contact_mobile = contact_mobile.value
parms.addr_id = config.preview.defaults.addrId
parms.country_id = config.preview.defaults.countryId
}
console.log('parms', parms)
badgeApi.creatOrder(parms).then((res: any) => {
console.log('creatOrder', res)
showSuccessToast({
message: toValueWithout(config.preview.messages.orderSuccess),
duration: 2000,
})
router.push({
path: config.routes.myOrder
})
loading.value = false
}).catch((err) => {
console.log('creatOrder', err)
loading.value = false
showToast({
message: err.message,
duration: 2000,
})
}).finally(() => {
loading.value = false
})
})
.catch(() => {
loading.value = false
});
}
const getPosition = () => {
badgeApi.composite({
pid: pid.value,
num: imgKey.value,
custom_text: shapeText.value ? shapeText.value : frontSaveText.value,
back_text: backSaveText.value ? backSaveText.value : '',
shape_id: shapeId.value,
type_id: typeId.value,
shape_ids: shapeIds.value,
up_text: topText.value ? topText.value : '',
down_text: bottomText.value ? bottomText.value : '',
}).then((res: any) => {
console.log('getPosition', res)
}).catch((err) => {
console.log('getPosition', err)
})
}
const imageUrl = ref('')
const originUrl = ref('')
const group = ref(config.preview.defaults.group);
// 轮询获取图片
const flag = ref(1)
const timer = ref()
const progress = ref(0)
const progressTimer = ref()
const progressText = ref('')
const progressList = () => {
progress.value = 0
if (!progressTimer.value) {
progressTimer.value = setInterval(() => {
if (progress.value < config.preview.progress.maxProgress) {
progress.value += 1
const thresholds = config.preview.progress.thresholds
if (progress.value < thresholds.stage1) {
progressText.value = toValueWithout(config.preview.progress.stages.stage1)
} else if (progress.value < thresholds.stage2) {
progressText.value = toValueWithout(config.preview.progress.stages.stage2)
} else if (progress.value < thresholds.stage3) {
progressText.value = toValueWithout(config.preview.progress.stages.stage3)
} else {
progressText.value = toValueWithout(config.preview.progress.stages.stage4)
}
}
}, config.preview.progress.interval)
}
}
const newGroup = ref(0)
const isPreview = ref(false)
const getImageList = () => {
badgeApi.getImageList({
pid: pid.value,
group: config.preview.defaults.group,
prod_id: prodId.value
}).then((res: any) => {
console.log('getImageList', res)
const data = res || []
flag.value = data.flag
if (data.flag === 1) {
newGroup.value = data.next_group
nextTick(() => {
const newList = data.list.map(item => ({
...item,
isNew: true
}))
const firstGeneratedImage = newList.find(item => item.status === 1)
if (firstGeneratedImage && !isPreview.value) {
originUrl.value = firstGeneratedImage.origin_url
imageUrl.value = firstGeneratedImage.origin_url
frameUrl.value = firstGeneratedImage.frame_url
imageWater.value = firstGeneratedImage.image_url
imgKey.value = firstGeneratedImage.key
isPreview.value = true
}
const mergedList = imageList.value.map(item => {
const newItem = newList.find(n => n.key === item.key)
return newItem || item
})
newList.forEach(newItem => {
if (!mergedList.find(item => item.key === newItem.key)) {
mergedList.push(newItem)
}
})
imageList.value = mergedList
console.log('imgKey.value', imgKey.value)
if (imgKey.value) {
const currentImage = mergedList.filter((item: any) => item.key === imgKey.value)[0]
console.log('currentImage', currentImage)
if (currentImage && currentImage.status === 1) {
originUrl.value = currentImage.origin_url
imageUrl.value = currentImage.origin_url
frameUrl.value = currentImage.frame_url
imageWater.value = currentImage.image_url
isPreview.value = true
} else {
originUrl.value = currentImage.origin_url
imageUrl.value = currentImage.origin_url
frameUrl.value = currentImage.frame_url
imageWater.value = currentImage.image_url
isPreview.value = true
}
}
})
}
if (data.flag === 2) {
newGroup.value = data.next_group
clearInterval(timer.value)
clearInterval(progressTimer.value)
progress.value = 100
imageList.value = data.list
if (imgKey.value) {
originUrl.value = data.list.filter((item: any) => item.key == imgKey.value)[0].origin_url
imageUrl.value = data.list.filter((item: any) => item.key == imgKey.value)[0].origin_url
frameUrl.value = data.list.filter((item: any) => item.key == imgKey.value)[0].frame_url
imageWater.value = data.list.filter((item: any) => item.key == imgKey.value)[0].image_url
} else {
originUrl.value = data.list[0].origin_url
imageUrl.value = data.list[0].origin_url
frameUrl.value = data.list[0].frame_url
imageWater.value = data.list[0].image_url
imgKey.value = data.list[0].key
}
localStorage.remove('userId')
}
}).catch((err) => {
showToast({
message: err.message,
duration: 2000,
})
})
}
const shapeId = ref(0)
const shapeIds = ref([])
const shapeImage = ref('')
const shapeList = ref([])
const custom_switch = ref(0)
const limitCount = ref(0)
const getShapeList = () => {
badgeApi.getShapeList({
prod_id: prodId.value,
type_id: typeId.value
}).then((res: any) => {
console.log('getShapeList', res)
shapeList.value = res.list
shapeId.value = res.list[0].id
shapeImage.value = res.list[0]?.frame_path
custom_switch.value = res.list[0]?.custom_switch
limitCount.value = res.list[0]?.text_limit_max
ImageShow(res.list[0])
if (custom_switch.value == 1 && typeId.value == 2) {
shapeText.value = ''
topText.value = ''
bottomText.value = ''
topTextStyle.value = ''
bottomTextStyle.value = ''
shapeTextStyle.value = ''
textShow(res.list[0])
}
if (custom_switch.value == 1 && typeId.value == 6) {
shapeIds.value = res.list.map((item: any) => item.id)
if (res.list[0].axisz == 1) {
axisz.value = 1
frontKey.value = 1
frontTextShow(res.list[0], true)
} else if (res.list[0].axisz == -1) {
axisz.value = -1
frontKey.value = 2
frontTextShow(res.list[0], false)
} else {
axisz.value = 1
frontKey.value = 1
frontTextShow(res.list[0], true)
}
}
}).catch((err) => {
console.log('getShapeList', err)
})
}
const getImageUrl = (path: string) => {
return 'https://suwa3d-3dview.oss-cn-shanghai.aliyuncs.com/' + path
}
const getStyle = ref('')
const ImageShow = (item: any) => {
const scale = config.preview.imageScale;
const img = new Image()
img.src = getImageUrl(item.frame_path)
img.onload = () => {
console.log('img', img)
const ratioWidth = config.preview.imageSize.previewWidth / img.width;
const ratioHeight = config.preview.imageSize.previewHeight / img.height;
const x = item.axisx * ratioWidth * scale;
const y = item.axisy * ratioHeight * scale;
const path_width = item.width * ratioWidth * scale;
const path_height = item.height * ratioHeight * scale;
getStyle.value = `left: ${x}px;top: ${y}px;width: ${path_width}px;height: ${path_height}px;`
}
}
const shapeTextStyle = ref('')
const topTextStyle = ref('')
const bottomTextStyle = ref('')
const textShow = (item: any, type: string = 'shape') => {
const scale = config.preview.imageScale;
const img = new Image()
img.src = getImageUrl(item.frame_path)
img.onload = () => {
console.log('img', img)
const ratioWidth = config.preview.imageSize.previewWidth / img.width;
const ratioHeight = config.preview.imageSize.previewHeight / img.height;
const x = item.text_axisx * ratioWidth * scale;
const y = item.text_axisy * ratioHeight * scale;
const path_width = item.text_width * ratioWidth * scale;
const path_height = item.text_height * ratioHeight * scale;
const text_size = item.font_size * Math.min(ratioWidth * scale, ratioHeight * scale);
if (type == 'shape') {
shapeTextStyle.value = `left: ${x}px;top: ${y}px;width: ${path_width}px;height: ${path_height}px;text-align: center;line-height: ${path_height}px;font-size: ${text_size}px;color: ${item.font_color};font-weight: ${item.font_weight};`
} else if (type == 'top') {
topTextStyle.value = `left: ${x}px;top: ${y}px;width: ${path_width}px;height: ${path_height}px;text-align: center;line-height: ${path_height}px;font-size: ${text_size}px;color: ${item.font_color};font-weight: ${item.font_weight};`
} else if (type == 'bottom') {
bottomTextStyle.value = `left: ${x}px;top: ${y}px;width: ${path_width}px;height: ${path_height}px;text-align: center;line-height: ${path_height}px;font-size: ${text_size}px;color: ${item.font_color};font-weight: ${item.font_weight};`
}
}
}
const frontTextShow = (event: any, isFront: boolean | null = null) => {
if (isFront === null) {
isFront = frontKey.value == 1;
}
isFront = Boolean(isFront);
let config_text: any = {};
if (isFront) {
config_text = {
x: event.front_text_axisx || config.preview.textConfig.front.x,
y: event.front_text_axisy || config.preview.textConfig.front.y,
width: event.front_text_width || config.preview.textConfig.front.width,
height: event.front_text_height || config.preview.textConfig.front.height,
radius: event.front_text_radius || config.preview.textConfig.front.radius,
fontSize: event.front_font_size || config.preview.textConfig.front.fontSize,
maxLength: config.preview.textConfig.front.maxLength
};
} else {
config_text = {
x: event.back_text_axisx !== undefined ? event.back_text_axisx : config.preview.textConfig.back.x,
y: event.back_text_axisy !== undefined ? event.back_text_axisy : config.preview.textConfig.back.y,
width: event.back_text_width !== undefined ? event.back_text_width : config.preview.textConfig.back.width,
height: event.back_text_height !== undefined ? event.back_text_height : config.preview.textConfig.back.height,
radius: event.back_text_radius !== undefined ? event.back_text_radius : config.preview.textConfig.back.radius,
fontSize: event.back_font_size !== undefined ? event.back_font_size : config.preview.textConfig.back.fontSize,
maxLength: config.preview.textConfig.back.maxLength
};
}
const text = isFront ? frontText.value : backText.value;
const bindKey = isFront ? 'roundTextConfigFront' : 'roundTextConfigBack';
const shapeImagePath = event.frame_path || shapeImage.value;
const cachedInfo = cachedImageInfo.value;
const needFetchImage = !cachedInfo || cachedInfo.src !== shapeImagePath;
if (!needFetchImage && cachedInfo) {
textHelper.roundText({
shapeImage: shapeImagePath,
x: config_text.x,
y: config_text.y,
width: config_text.width,
height: config_text.height,
radius: config_text.radius,
text: text,
isFront: isFront,
style: {
fontSize: config_text.fontSize,
color: event.font_color || config.preview.textConfig.defaultColor,
fontWeight: event.font_weight || config.preview.textConfig.defaultWeight,
textAlign: 'center',
},
bindKey: bindKey,
cachedImageInfo: cachedInfo
});
nextTick(() => {
const currentConfig = isFront ? roundTextConfigFront.value : roundTextConfigBack.value;
if (frontKey.value == (isFront ? 1 : 2) && currentConfig && currentConfig.charList) {
// Vue 会自动响应式更新
}
});
} else {
textHelper.roundText({
shapeImage: shapeImagePath,
x: config_text.x,
y: config_text.y,
width: config_text.width,
height: config_text.height,
radius: config_text.radius,
text: text,
isFront: isFront,
style: {
fontSize: config_text.fontSize,
color: event.font_color || config.preview.textConfig.defaultColor,
fontWeight: event.font_weight || config.preview.textConfig.defaultWeight,
textAlign: 'center',
},
bindKey: bindKey,
onImageInfoCached: (imageInfo: any) => {
cachedImageInfo.value = imageInfo;
}
});
const checkAndUpdate = (attempts = 0) => {
if (attempts > 10) return;
const currentConfig = isFront ? roundTextConfigFront.value : roundTextConfigBack.value;
if (frontKey.value == (isFront ? 1 : 2)) {
if (currentConfig && currentConfig.charList && currentConfig.charList.length > 0) {
// Vue 会自动响应式更新
} else if (attempts < 10) {
setTimeout(() => {
checkAndUpdate(attempts + 1);
}, 50);
}
}
};
setTimeout(() => {
checkAndUpdate(0);
}, 50);
}
}
const shapeText = ref('')
const changeShapeText = (e: any) => {
console.log('changeShapeText', e.target.value)
if (!validateInput(e.target.value)) {
showToast(config.preview.messages.invalidInput)
shapeText.value = e.target.value.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7a3\u00c0-\u017f\s~!@#¥%……&*()——+_)(*&^%$#@!~?><:"}{|、】【';、。,'.]/g, '');
return;
}
handleInput(e.target.value)
}
const topText = ref('')
const changeTopText = (e: any) => {
console.log('changeTopText', e.target.value)
if (!validateInput(e.target.value)) {
showToast(config.preview.messages.invalidInput)
topText.value = e.target.value.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7a3\u00c0-\u017f\s~!@#¥%……&*()——+_)(*&^%$#@!~?><:"}{|、】【';、。,'.]/g, '');
return;
}
handleInput(e.target.value, 'top')
const textPos = shapeList.value.find((item: any) => item.id === shapeId.value)?.text_pos?.find((d: any) => d.position_place == 'top')
if (textPos) {
textPos.frame_path = shapeImage.value;
textShow(textPos, 'top')
}
}
const bottomText = ref('')
const changeBottomText = (e: any) => {
console.log('changeBottomText', e.target.value)
if (!validateInput(e.target.value)) {
showToast(config.preview.messages.invalidInput)
bottomText.value = e.target.value.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7a3\u00c0-\u017f\s~!@#¥%……&*()——+_)(*&^%$#@!~?><:"}{|、】【';、。,'.]/g, '');
return;
}
handleInput(e.target.value, 'bottom')
const textPos = shapeList.value.find((item: any) => item.id === shapeId.value)?.text_pos?.find((d: any) => d.position_place == 'bottom')
if (textPos) {
textPos.frame_path = shapeImage.value;
textShow(textPos, 'bottom')
}
}
const frontSaveText = ref('')
const changeFrontText = (e: any) => {
const inputValue = e?.target?.value ?? frontText.value
console.log('changeFrontText', inputValue)
frontText.value = inputValue
frontSaveText.value = inputValue
handleInput(inputValue)
if (typeId.value == 6 && shapeList.value.length > 0) {
const currentShape = shapeList.value.find((item: any) => item.id === shapeId.value)
if (currentShape && currentShape.axisz == 1) {
frontTextShow(currentShape, true)
}
}
}
const backSaveText = ref('')
const changeBackText = (e: any) => {
const inputValue = e?.target?.value ?? backText.value
console.log('changeBackText', inputValue)
backText.value = inputValue
backSaveText.value = inputValue
handleInput(inputValue)
if (typeId.value == 6 && shapeList.value.length > 0) {
const currentShape = shapeList.value.find((item: any) => item.id === shapeId.value)
if (currentShape && currentShape.axisz == -1) {
frontTextShow(currentShape, false)
}
}
}
const validateInput = (input: string) => {
const regex = /^[a-zA-Z0-9\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7a3\u00c0-\u017f\s~!@#¥%……&*()——+_)(*&^%$#@!~?><:"}{|、】【';、。,'.]+$/;
return regex.test(input);
}
const handleInput = (value: string, type: string = 'shape') => {
let byteLength = calculateByteLength(value);
console.log('byteLength', byteLength, limitCount.value)
if (byteLength > limitCount.value) {
let validValue = '';
let currentBytes = 0;
for (let i = 0; i < value.length; i++) {
const char = value.charAt(i);
const code = char.charCodeAt(0);
let charBytes = 0;
if (code === 32) {
charBytes = 1;
} else {
charBytes = code > 255 ? 2 : 1;
}
if (currentBytes + charBytes <= limitCount.value) {
validValue += char;
currentBytes += charBytes;
} else {
break;
}
}
if (typeId.value == 2) {
if (config.preview.shapeIds.topBottomText.includes(shapeId.value)) {
type == 'top' ? topText.value = validValue : bottomText.value = validValue;
} else {
shapeText.value = validValue;
}
} else {
if (axisz.value == 1) {
frontText.value = validValue;
frontSaveText.value = validValue;
} else {
backText.value = validValue;
backSaveText.value = validValue;
}
}
if (typeId.value == 6 && shapeList.value.length > 0) {
const currentShape = shapeList.value.find((item: any) => item.id === shapeId.value)
if (currentShape) {
if (currentShape.axisz == 1) {
frontTextShow(currentShape, true)
} else if (currentShape.axisz == -1) {
frontTextShow(currentShape, false)
}
}
}
return;
}
if (typeId.value == 2) {
if (config.preview.shapeIds.topBottomText.includes(shapeId.value)) {
type == 'top' ? topText.value = value : bottomText.value = value;
} else {
shapeText.value = value;
}
} else {
if (axisz.value == 1) {
frontText.value = value;
frontSaveText.value = value;
} else {
backText.value = value;
backSaveText.value = value;
}
}
if (typeId.value == 6 && shapeList.value.length > 0) {
const currentShape = shapeList.value.find((item: any) => item.id === shapeId.value)
if (currentShape) {
if (currentShape.axisz == 1) {
frontTextShow(currentShape, true)
} else if (currentShape.axisz == -1) {
frontTextShow(currentShape, false)
}
}
}
}
const calculateByteLength = (str: string) => {
let len = 0;
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
if (code === 32) {
len += 1;
} else {
len += code > 255 ? 2 : 1;
}
}
return len;
}
const shapeChange = (item: any) => {
console.log('shapeChange', item)
sizeList.value.map((item: any) => {
if (item.shape_id !== item.id) {
item.count = 0
}
})
shapeText.value = '';
shapeId.value = item.id
shapeImage.value = item.frame_path
custom_switch.value = item.custom_switch
limitCount.value = item.text_limit_max
if (typeId.value == 3) {
imageUrl.value = config.preview.shapeIds.frame.includes(item.id) ? originUrl.value : frameUrl.value
} else {
imageUrl.value = originUrl.value
}
ImageShow(item)
if (custom_switch.value == 1 && typeId.value == 2) {
shapeText.value = ''
topText.value = ''
bottomText.value = ''
topTextStyle.value = ''
bottomTextStyle.value = ''
shapeTextStyle.value = ''
textShow(item)
}
if (custom_switch.value == 1 && typeId.value == 6) {
if (item.axisz == 1) {
axisz.value = 1
frontKey.value = 1
frontText.value = frontSaveText.value ? frontSaveText.value : ''
backText.value = backSaveText.value ? backSaveText.value : ''
frontTextShow(item, true)
} else if (item.axisz == -1) {
axisz.value = -1
frontKey.value = 2
backText.value = backSaveText.value ? backSaveText.value : ''
frontText.value = frontSaveText.value ? frontSaveText.value : ''
frontTextShow(item, false)
}
}
}
const orderStat = ref({})
const typeId = ref(0)
const subjectId = ref(0)
const getOrderStat = () => {
badgeApi.getOrderStat({}).then((res: any) => {
console.log('getOrderStat', res)
orderStat.value = res
typeId.value = res.type_id
subjectId.value = res.subject_id
badgeTypeId.value = res.type_id
getSizeList()
})
}
function handleBeforeUnload(_event: BeforeUnloadEvent) {
// code 的清除已在 main.ts 中统一处理,这里只处理页面特定的清理逻辑
// 清除轮询和进度条计时器
clearInterval(timer.value)
clearInterval(progressTimer.value)
// userId 的清除由 main.ts 统一处理
}
const pid = ref(0)
const route = useRoute()
const prodId = ref(0)
const kindId = ref(0)
onMounted(() => {
pid.value = route.query.pid
group.value = route.query.group
if (route.query.key) {
imgKey.value = route.query.key
}
if (route.query.prod_id) {
prodId.value = route.query.prod_id
}
if (route.query.kind_id) {
kindId.value = Number(route.query.kind_id)
}
getOrderStat()
getImageList()
progressList()
setTimeout(() => {
getCompareImage()
}, config.preview.polling.compareImageDelay)
timer.value = setInterval(() => {
getImageList()
}, config.preview.polling.interval)
window.addEventListener('beforeunload', handleBeforeUnload);
})
onUnmounted(() => {
clearInterval(timer.value)
clearInterval(progressTimer.value)
})
const searchResult = ref([])
</script>
<style scoped>
.preview-container {
min-height: 100vh;
font-family: 'PingFang SC', Arial, sans-serif;
color: #222;
}
.backstyle_1 {
position: fixed;
left: -110px;
top: -40px;
width: 328px;
height: 216px;
opacity: 0.2;
transform: rotate(-13.72deg);
border-radius: 50%;
background-image: radial-gradient( rgba(250, 182, 55, 0.726), #ebd5a09c, #d4cab100);
z-index: -10;
}
.backstyle_2 {
position: fixed;
left: 97px;
top: -95px;
width: 370px;
height: 374px;
opacity: 0.1;
border-radius: 50%;
background-image: radial-gradient( rgb(69, 255, 63), #90f2729a, #90f27200);
z-index: -10;
}
.header {
display: flex;
align-items: center;
height: 40px;
font-size: 14px;
color: #999;
padding: 16px 16px 0 16px;
}
.back-icon {
margin-right: 8px;
color: #222;
font-size: 16px;
cursor: pointer;
}
.step-container {
display: flex;
align-items: flex-start;
padding: 16px 16px 16px 0;
}
.step-item {
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.step-item.active {
flex: 2.5;
}
.step-num {
font-size: 48px;
line-height: 1;
position: relative;
margin-right: 8px;
text-shadow: 0 2px 8px #e6f7e6;
color: #CCCCCC;
}
.step-item.active .step-num {
color: #fff;
text-shadow:
1px 1px 0 #000,
-1px -1px 0 #000,
-1px 1px 0 #000,
1px -1px 0 #000,
0 1px 0 #000,
1px 0 0 #000,
0 -1px 0 #000,
-1px 0 0 #000;
}
.step-item.active .step-num::after {
content: '';
position: absolute;
top: 20px;
right: 0;
width: 15px;
height: 15px;
background-color: #50cf54;
opacity: 0.5;
border-radius: 50%;
}
.step-content {
display: flex;
flex-direction: column;
height: 48px;
}
.step-item.active .step-title {
color: #000;
font-weight: bold;
font-size: 16px;
}
.step-item.active .step-desc {
color: #808080;
font-size: 13px;
}
.step-title {
font-size: 16px;
color: #808080;;
}
.step-desc {
font-size: 12px;
color: #808080;
margin-top: 2px;
}
.progress-section {
margin: 0 auto;
text-align: center;
width: 100vw;
height: 100vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.progress-section-picture {
width: 320px;
height: 320px;
position: relative;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.progress-section-img {
position: relative;
display: flex;
justify-content: center;
}
.box1-back-image {
position: absolute;
top: 0;
left: 0;
z-index: 2;
object-fit: cover;
border-radius: 10px;
}
.box1-front-box {
position: absolute;
z-index: 1;
}
.box1-front-image {
width: 100%;
height: auto;
border-radius: 10px;
background: rgba(0, 0, 0, 0.3);
}
.progress-bar-bg {
width: 70%;
height: 10px;
background: #eee;
border-radius: 5px;
margin: 0 auto 8px auto;
position: relative;
overflow: hidden;
}
.progress-bar-fg {
height: 100%;
background: #6fdc8c;
border-radius: 5px;
transition: width 0.3s;
}
.progress-text {
font-size: 15px;
color: #222;
margin-bottom: 2px;
}
.progress-desc {
font-size: 12px;
color: #999;
}
.info-section {
display: flex;
justify-content: flex-start;
padding: 16px;
}
.info-item {
flex: 1;
}
.info-title {
font-size: 13px;
color: #222;
font-weight: 600;
margin-bottom: 2px;
}
.info-content {
font-size: 12px;
color: #999;
}
.order-section {
border-radius: 10px;
padding: 0 16px;
margin-bottom: 18px;
}
.order-title {
font-size: 14px;
color: #222;
font-weight: 600;
margin-bottom: 10px;
}
.order-item {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.order-size {
font-size: 15px;
color: #222;
margin-right: 8px;
}
.order-free {
font-size: 12px;
color: #999;
margin-right: 16px;
}
.order-ctrl {
display: flex;
align-items: center;
margin-left: auto;
}
.confirm-box {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
z-index: 10;
padding: 0 16px 16px 16px;
border-top: 1px solid #e6e6e6;
}
.action-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding: 0 16px;
}
.action-btn {
flex: 1;
margin: 0 4px;
border: none;
border-radius: 8px;
color: #222;
font-size: 13px;
padding: 8px 0;
cursor: pointer;
transition: background 0.2s;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
}
.action-img {
width: 20px;
height: 20px;
margin-right: 5px;
}
.btn-box {
display: flex;
justify-content: center;
align-items: center;
}
.confirm-btn {
width: 80vw;
background: linear-gradient(90deg, #D1ED8E 0%, #55E668 100%);
color: #000;
font-size: 17px;
font-weight: 600;
border: none;
border-radius: 24px;
padding: 12px 0;
cursor: pointer;
box-shadow: 0 2px 8px rgba(111,220,140,0.08);
letter-spacing: 2px;
}
.confirm-btn:active {
background: linear-gradient(90deg, #5ccf7a 0%, #8fdca0 100%);
}
.preview-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.preview-mask img {
max-width: 80%;
max-height: 80%;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.preview-mask p {
margin-top: 16px;
color: #fff;
font-size: 16px;
text-align: center;
}
.progress-image-item-shadow {
position: absolute;
top: 0;
left: 0;
width: 80vw;
height: 80vw;
object-fit: cover;
box-shadow: inset 2px 2px 4px rgba(255, 255, 255, 0.5),inset -2px -2px 4px rgba(0, 0, 0, 0.2);
border-radius: 50%;
}
.image-list-box {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 16px 16px 0 16px;
}
.image-list-item {
width: 20vw;
height: 20vw;
}
.image-list-item-img {
width: 20vw;
height: 20vw;
border-radius: 12px;
border: 3px solid #fff;
background: rgba(0, 0, 0, 0.3);
}
.image-list-item-img.imgActive {
border: 3px solid #50cf54;
}
.image-list-item-loading {
width: 20vw;
height: 20vw;
border-radius: 12px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #999;
}
.shape-body {
background: #fff;
border-radius: 8px;
padding: 0 20px;
}
.shape-box {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
justify-content: flex-start;
}
.shape-title {
padding-bottom: 12px;
font-size: 14px;
font-weight: bold;
}
.shape-item {
position: relative;
flex: 0 0 24%;
/* margin-right: calc(4% / 3); */
margin-bottom: calc(4% / 2);
margin-right: 8px!important;
}
.shape-item:nth-child(4n){
margin-right: 0;
}
.shape-item:last-child{
margin-right: auto;
}
.shape-item-image {
width: 20vw;
height: 20vw;
border-radius: 8px;
}
.shape-icon {
position: absolute;
right: 6px;
top: 14vw;
}
.shape-item-text {
font-size: 12px;
text-align: center;
max-width: 20vw;
}
.shape-type {
display: -webkit-box;
overflow-x: scroll;
scrollbar-width: none;
-ms-overflow-style: none;
padding-bottom: 16px;
}
.shape-type-item {
background: #F0F2F5;
font-size: 12px;
height: 7vw;
padding: 0px 16px;
border-radius: 4px;
margin-right: 8px;
display: flex;
justify-content: center;
align-items: center;
}
.shape-active {
background: #15CF5F;
color: #fff;
}
.shape-box-input {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.shape-box-input-box {
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
gap: 10px;
flex-direction: column;
}
.shape-box-input-text {
width: 100%;
height: 40px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 0 10px;
}
.shape-text {
position: absolute;
z-index: 2;
font-family: chinese;
overflow: hidden;
text-shadow: 1px 0px 1px #000000a1;
white-space: pre-wrap;
}
.font-chinese {
font-family: chinese;
}
.shape-item-image-round {
border-radius: 50%;
}
.van-address-edit {
padding: 0!important;
}
</style>