Files
ece.suwa3d.com/src/views/badge/preview.vue
T
2026-01-16 10:48:19 +08:00

1833 lines
55 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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("总计大约需要90秒,请耐心等待...") }}</div>
</div>
<div v-else class="progress-section-picture" style="width: 320px;height: 320px;">
<div class="progress-section-img" style="width: 320px;height: 320px;">
<img v-if="shapeImage" class="box1-back-image" :src="shapeImage" style="width: 320px;height: 320px;" alt="">
<div class="box1-front-box" :style="getStyle">
<img class="box1-front-image" :src="imageUrl" alt="" style="max-height: 320px;">
<!-- <div class="box1-round-shadow" v-if="shapeId == 1 && shapeImage == ''" style="width: 320px;height: 320px;"></div> -->
</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("设计中") }}
</div>
<div v-else-if="item.status == 2" class="image-list-item-loading">
{{ toValueWithout("设计失败") }}
</div>
</div>
</div>
<div class="info-section">
<div class="info-item">
<div class="info-title" v-if="typeId == 1">{{ prodId == 7 ? toValueWithout('人物立体徽章') : toValueWithout('宠物立体徽章') }}</div>
<div class="info-title" v-if="typeId == 2">{{ prodId == 7 ? toValueWithout('人物浮雕相框') : toValueWithout('宠物浮雕相框') }}</div>
<div class="info-title" v-if="typeId == 3">{{ prodId == 7 ? toValueWithout('人物3D冰箱贴') : toValueWithout('宠物3D冰箱贴') }}</div>
<div class="info-title" v-if="typeId == 4">{{ prodId == 1 ? toValueWithout('人物卡通手办') : toValueWithout('宠物卡通手办') }}</div>
<div class="info-title" v-if="typeId == 6">{{ prodId == 25 ? toValueWithout('人物浮雕挂饰') : toValueWithout('宠物浮雕挂饰') }}</div>
<div class="info-content">{{ toValueWithout("产品类型") }}</div>
</div>
<div class="info-item">
<div class="info-title">{{ toValueWithout("3D全彩打印") }}</div>
<div class="info-content">{{ toValueWithout("工艺") }}</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': item.id == 1 || item.id == 2 }" :src="item.cover_path || imageUrl"/>
<van-icon v-if="item.id == shapeId" class="shape-icon" color="#15CF5F" 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="shapeId == 27 || shapeId == 28">
<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 class="info-desc">
下单后预计需要7天完成生产发货。<br>
实物3D打印色彩与图片有合理色差,如对产品有疑问,可以联系客服咨询。
</div> -->
<div style="padding: 0 16px;">
<van-divider />
</div>
<div class="order-section" v-if="trialCode == false && imageUrl">
<div class="order-title">{{ toValueWithout("订购数量") }}</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("剩余兑换") }}:{{ 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 && code == 'ACAYOAAC3PFCPO3CD6DVM1'">
<div class="order-title" style="padding: 0 16px;">{{ toValueWithout("地址识别") }}</div>
<div class="address-item">
<van-field
v-model="addressInput"
label=""
:placeholder="toValueWithout('请输入姓名、手机号、详细地址(如:张三 13800138000 广东省深圳市南山区科技园1号)')"
clearable
right-icon="search"
>
<template #button>
<van-button size="small" type="primary" @click="handleAddressInput" style="margin-left: 8px;">{{ toValueWithout("自动识别") }}</van-button>
</template>
</van-field>
</div>
<div class="address-item" style="padding: 0px 16px 16px 16px;" v-if="contact_name && contact_mobile && province_name && city_name && county_name && address">
<div style="font-size: 16px;color: #387aff;text-align: left;margin-bottom: 8px;">{{ toValueWithout("识别结果") }}</div>
<div style="font-size: 14px;text-align: left;">
{{ contact_name }} {{ contact_mobile }} {{ province_name }} {{ city_name }} {{ county_name }} {{ address }}
</div>
</div>
</div>
<div class="address-box" v-if="orderStat.use_type == 2 && imageUrl">
<div class="order-title" style="padding: 0 16px;">{{ toValueWithout("收货地址") }}</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 v-if="trialCode == false" @click="sureReload" :disabled="flag < 1" class="action-btn"><img class="action-img" src="@/assets/badge/reload.png" alt=""> {{ toValueWithout("再次生成") }}</button>
<button @click="compare" :disabled="flag < 1" class="action-btn"><img class="action-img" src="@/assets/badge/duibi.png" alt=""> {{ toValueWithout("前后对比") }}</button>
<button @click="save" :disabled="flag < 1" class="action-btn"><img class="action-img" src="@/assets/badge/down.png" alt=""> {{ toValueWithout("保存图片") }}</button>
</div>
<div class="btn-box" v-if="trialCode == false">
<button @click="confirm" :disabled="flag < 1" class="confirm-btn">{{ toValueWithout("确认选择") }}</button>
</div>
</div>
</div>
<van-action-sheet v-model:show="showCompare" :title="toValueWithout('前后对比')" :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('原图')" style="max-width: 120px; max-height: 120px; 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('效果图')" style="width: 120px; height: 120px; border-radius: 8px;">
</div>
</div>
<div style="text-align: center; margin-top: 16px; color: #999; font-size: 12px;">
{{ toValueWithout("温馨提示:AI设计的徽章效果图是根据用户提供的照片机器学习生成的,多试几次就能找到你满意的徽章~") }}
</div>
</div>
</van-action-sheet>
<div v-if="showPreview">
<div class="preview-mask" @click="showPreview = false">
<img :src="imageWater" :alt="toValueWithout('徽章预览')" />
<p>{{ toValueWithout("长按图片 → 保存到相册") }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, onUnmounted, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { showSuccessToast, showToast, showNotify, showConfirmDialog, AddressEdit, Area } 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'
const router = useRouter();
const value = ref(0);
const showReload = ref(false)
const showCompare = ref(false)
const showBgCompare = ref(false)
const currentIndex = ref(0)
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) => {
// 适配 Vue 的 setData,直接更新 ref 值
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 code = localStorage.get('code')
function compare() {
showCompare.value = true
}
function sureReload() {
console.log('orderStat', orderStat.value)
if (orderStat.value.remain_count <= 0) {
showToast(toValueWithout('当前次数已用完,请重新购买'))
return
}
isPreview.value = false
showConfirmDialog({
title: toValueWithout('再次生成'),
message:
toValueWithout('请确认是否重新生成。'),
})
.then(() => {
group.value = newGroup.value
const params = {
pid: pid.value,
group: 1,
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)
showReload.value = false
showToast({
message: err.message,
duration: 2000,
})
}).finally(() => {
showReload.value = false
})
})
.catch(() => {
showReload.value = false
});
}
const imgKey = ref(101)
const frameUrl = ref('')
const changeImage = (item: any) => {
console.log('changeImage', item)
imgKey.value = item.key
imageUrl.value = shapeId.value == 17 || shapeId.value == 18 ? item.frame_url : item.origin_url
frameUrl.value = item.frame_url
imageWater.value = item.image_url
}
const createLog = () => {
badgeApi.createLog({
pid: pid.value,
group: 1,
prod_id: prodId.value,
type_id: typeId.value,
kind_id: kindId.value
}).then((res: any) => {
console.log('createLog', res)
showReload.value = false
progressTimer.value = null
getImageList()
progressList()
getCompareImage()
timer.value = setInterval(() => {
getImageList()
}, 10000)
}).catch((err) => {
console.log('createLog', err)
showToast({
message: err.message,
duration: 2000,
})
}).finally((err) => {
showReload.value = false
})
}
const showPreview = ref(false)
function save() {
showPreview.value = true;
}
const compareList = ref({})
const style_name = ref('')
function getCompareImage() {
badgeApi.getCompareImage({
pid: pid.value,
group: 1,
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,
})
}).finally((err) => {
})
}
function goBack() {
router.back()
}
const sizeList = ref([])
const shapeItem = 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 addressInput = ref('')
const handleAddressInput = () => {
console.log('handleAddressInput', addressInput.value)
const text = addressInput.value;
if (!text) return;
// 识别手机号
const phoneRegex = /(1[3-9]\d{9})/g;
const phoneMatch = phoneRegex.exec(text);
if (phoneMatch) {
contact_mobile.value = phoneMatch[0];
}
// 简单识别姓名(2-4个中文字符)
const nameRegex = /([\u4e00-\u9fa5]{2,4})/g;
const nameMatches = text.match(nameRegex);
if (nameMatches && nameMatches.length > 0) {
// 假设第一个匹配的中文名是姓名
contact_name.value = nameMatches[0];
}
console.log('contact_name', contact_name.value)
console.log('contact_mobile', contact_mobile.value)
// 识别省份
let provinceFound = '';
for (const [provinceId, provinceName] of Object.entries(areaList.province_list)) {
if (text.includes(provinceName)) {
provinceFound = provinceName;
console.log('识别到省份ID:', provinceId);
province_id.value = provinceId;
break;
}
}
console.log('province_id', province_id.value)
console.log('provinceFound', provinceFound)
province_name.value = provinceFound;
// 识别城市
let cityFound = '';
for (const [cityId, cityName] of Object.entries(areaList.city_list)) {
if (text.includes(cityName) && cityId !== province_id.value) {
cityFound = cityName;
city_id.value = cityId;
break;
}
}
console.log('cityFound', cityFound)
city_name.value = cityFound;
// 提取详细地址(从文本中移除已识别的部分)
let detail = text
.replace(contact_name.value, '')
.replace(contact_mobile.value, '')
.replace(province_name.value, '')
.replace(city_name.value, '')
.trim();
// 移除可能的区级信息(简单处理)
if (detail.includes('区') || detail.includes('县')) {
const districtMatch = detail.match(/(.+?(区|县))/);
if (districtMatch) {
// 遍历区县列表,查找匹配的区县名,获取对应的区id
let foundCountyId = '';
for (const [cid, cname] of Object.entries(areaList.county_list)) {
if (districtMatch[0].includes(cname)) {
foundCountyId = cid;
county_id.value = cid;
county_name.value = cname;
break;
}
}
// 如果没有在区县列表中找到,仍然保留原有逻辑
if (!foundCountyId) {
county_id.value = 0;
county_name.value = '';
}
detail = detail.replace(county_name.value, '').trim();
}
}
console.log('county_id', county_id.value)
console.log('county_name', county_name.value)
console.log('detail', detail)
address.value = detail;
}
const key = ref(0)
const loading = ref(false)
const confirm = () => {
if (loading.value) return
console.log('confirm')
if (payAmount.value <= 0) {
showToast(toValueWithout('请先选择下单数量'))
return
}
if (orderStat.value.use_type == 2 && !contact_name.value) {
showToast(toValueWithout('请先填写收货人信息'))
return
}
if (orderStat.value.use_type == 2 && !contact_mobile.value) {
showToast(toValueWithout('请先填写收货人手机号'))
return
}
if (orderStat.value.use_type == 2 && !province_id.value) {
showToast(toValueWithout('请先选择地区'))
return
}
if (orderStat.value.use_type == 2 && !address.value) {
showToast(toValueWithout('请先填写详细地址'))
return
}
getPosition()
loading.value = true
showConfirmDialog({
title: toValueWithout('确认下单'),
message:
toValueWithout('请再次确认是否选择这个模型下单,下单后预计需要10天完成生产发货。如对产品有疑问,可以联系客服咨询。'),
})
.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 : '',
}
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 = 0
parms.country_id = 45
}
console.log('parms', parms)
badgeApi.creatOrder(parms).then((res: any) => {
console.log('creatOrder', res)
showSuccessToast({
message: toValueWithout('下单成功'),
duration: 2000,
})
router.push({
path: '/badge/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
// on cancel
});
}
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(1);
// 轮询获取图片
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 < 90) {
progress.value += 1
if (progress.value < 25) {
progressText.value = toValueWithout('照片数据分析中')
} else if (progress.value < 50) {
progressText.value = toValueWithout('提炼图像关键词')
} else if (progress.value < 75) {
progressText.value = toValueWithout('图像生成中')
} else {
progressText.value = toValueWithout('图像后处理与优化')
}
}
}, 300)
}
}
const newGroup = ref(0)
const isPreview = ref(false)
const getImageList = () => {
badgeApi.getImageList({
pid: pid.value,
group: 1,
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
}))
// 找到第一个状态为1(已生成)的图片
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)
// 进度条到100%
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) => {
// 清除轮询和进度条计时器
// clearInterval(timer.value)
// clearInterval(progressTimer.value)
// progress.value = 0
showToast({
message: err.message,
duration: 2000,
})
}).finally((err) => {
})
}
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)
// 根据第一个形状的 id 设置 axisz
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 getStyle = ref('')
const ImageShow = (item: any) => {
const scale = 0.6;
const img = new Image()
img.src = item.frame_path
img.onload = () => {
console.log('img', img)
const ratioWidth = 320 / img.width;
const ratioHeight = 320 / 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 = 0.6;
const img = new Image()
img.src = item.frame_path
img.onload = () => {
console.log('img', img)
const ratioWidth = 320 / img.width;
const ratioHeight = 320 / 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 frontTextStyle = ref('')
const backTextStyle = ref('')
const frontTextShow = (event: any, isFront: boolean | null = null) => {
// 如果没有传递 isFront,从 frontKey 获取
if (isFront === null) {
isFront = frontKey.value == 1;
}
// 确保 isFront 是布尔值
isFront = Boolean(isFront);
// 根据正面/背面设置不同的参数
let config: any = {};
if (isFront) {
// 正面文字参数(使用硬编码值或从 event 获取)
config = {
x: event.front_text_axisx || 163,
y: event.front_text_axisy || 222,
width: event.front_text_width || 700,
height: event.front_text_height || 700,
radius: event.front_text_radius || 350,
fontSize: event.front_font_size || 72,
maxLength: 18
};
} else {
// 背面文字参数(使用 event 中的背面专用参数,完全独立于正面参数)
// 优先使用 back_ 开头的参数,如果没有则使用默认值
config = {
x: event.back_text_axisx !== undefined ? event.back_text_axisx : 238,
y: event.back_text_axisy !== undefined ? event.back_text_axisy : 297,
width: event.back_text_width !== undefined ? event.back_text_width : 550,
height: event.back_text_height !== undefined ? event.back_text_height : 550,
radius: event.back_text_radius !== undefined ? event.back_text_radius : 275,
fontSize: event.back_font_size !== undefined ? event.back_font_size : 60,
maxLength: 56
};
}
const text = isFront ? frontText.value : backText.value;
// roundText 会计算字符位置并更新到 ref 中,Vue 会自动渲染
// 根据正面/背面存储到不同的配置键
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.x,
y: config.y,
width: config.width,
height: config.height,
radius: config.radius,
text: text,
isFront: isFront,
style: {
fontSize: config.fontSize,
color: event.font_color || '#000000',
fontWeight: event.font_weight || 'normal',
textAlign: 'center',
},
bindKey: bindKey,
cachedImageInfo: cachedInfo // 传入缓存的图片信息
});
// 立即更新显示(因为使用了缓存,计算是同步的)
nextTick(() => {
const currentConfig = isFront ? roundTextConfigFront.value : roundTextConfigBack.value;
if (frontKey.value == (isFront ? 1 : 2) && currentConfig && currentConfig.charList) {
// Vue 会自动响应式更新,无需手动 setData
}
});
} else {
// 需要获取图片信息
textHelper.roundText({
shapeImage: shapeImagePath,
x: config.x,
y: config.y,
width: config.width,
height: config.height,
radius: config.radius,
text: text,
isFront: isFront,
style: {
fontSize: config.fontSize,
color: event.font_color || '#000000',
fontWeight: event.font_weight || 'normal',
textAlign: 'center',
},
bindKey: bindKey,
onImageInfoCached: (imageInfo: any) => {
// 缓存图片信息,下次输入时可以直接使用
cachedImageInfo.value = imageInfo;
}
});
// 延迟更新当前显示的配置(根据 frontKey),确保数据已更新
const checkAndUpdate = (attempts = 0) => {
if (attempts > 10) return; // 最多尝试10次,避免无限循环
const currentConfig = isFront ? roundTextConfigFront.value : roundTextConfigBack.value;
// 如果当前显示的是对应的面,配置已自动更新(Vue 响应式)
if (frontKey.value == (isFront ? 1 : 2)) {
if (currentConfig && currentConfig.charList && currentConfig.charList.length > 0) {
// Vue 会自动响应式更新,无需手动 setData
} 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('输入的文字不能包含特殊符号!')
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('输入的文字不能包含特殊符号!')
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, '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('输入的文字不能包含特殊符号!')
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, '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) => {
// 由于使用了 v-model,frontText.value 已经自动更新
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) => {
// 由于使用了 v-model,backText.value 已经自动更新
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 (shapeId.value == 27 || shapeId.value == 28) {
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 (shapeId.value == 27 || shapeId.value == 28) {
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 = item.id == 15 || item.id == 16 ? 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 trialCode = ref(false)
const getTrialCode = () => {
const value = localStorage.get('trialCode')
if (value) {
trialCode.value = true
} else {
getOrderStat()
}
}
const orderStat = ref({})
const typeId = ref(0)
const getOrderStat = () => {
badgeApi.getOrderStat({}).then((res: any) => {
console.log('getOrderStat', res)
orderStat.value = res
typeId.value = res.type_id
// prodId.value = res.prod_id
badgeTypeId.value = res.type_id // 设置 badgeTypeId
getSizeList()
})
}
function handleBeforeUnload(_event: BeforeUnloadEvent) {
// code 的清除已在 main.ts 中统一处理,这里只处理页面特定的清理逻辑
// 清除轮询和进度条计时器
clearInterval(timer.value)
clearInterval(progressTimer.value)
// trialCode 和 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)
}
getTrialCode()
// pid.value = 281505;
// group.value = 1;
// prodId.value = 7;
// getTypesList()
getImageList()
progressList()
setTimeout(() => {
getCompareImage()
}, 1000)
timer.value = setInterval(() => {
getImageList()
}, 10000)
window.addEventListener('beforeunload', handleBeforeUnload);
})
onUnmounted(() => {
clearInterval(timer.value)
clearInterval(progressTimer.value)
})
</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: radial-gradient(48.48% 50% at 50% 50%, #EBD4A0 0%, #ebd5a000 81.94%); */
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: radial-gradient(59.73% 50% at 50% 50%, #90F272 0%, #90f27200 84.03%); */
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);
}
.box1-round-shadow {
position: absolute;
top: 0;
left: 0;
width: 320px;
height: 320px;
border-radius: 50%;
box-shadow: 2px 2px 4px #00000042,inset -2px -2px 4px #00000048,inset 2px 2px 4px #ffffffb4;
z-index: 10;
}
.box1-round-image {
border-radius: 50%;
}
.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;
}
.info-desc {
font-size: 12px;
color: #999;
margin-bottom: 18px;
line-height: 1.5;
text-align: left;
padding: 0 16px;
}
.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;
}
.order-btn {
width: 26px;
height: 26px;
border: none;
background: #e8e8e8;
color: #bbb;
font-size: 18px;
border-radius: 4px;
margin: 0 4px;
cursor: pointer;
}
.order-btn:disabled {
background: #f2f2f2;
color: #ddd;
cursor: not-allowed;
}
.order-count {
font-size: 15px;
color: #222;
width: 22px;
text-align: center;
}
.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%);
}
.style-selection {
padding: 16px;
}
.style-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.style-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
position: relative;
width: 28vw;
}
.style-img {
width: 28vw;
height: 28vw;
border-radius: 8px;
object-fit: cover;
background: #f5f5f5;
border: 2px solid transparent;
transition: border 0.2s;
}
.style-item .style-selected {
position: absolute;
right: 4px;
bottom: 25px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
.style-label {
font-size: 13px;
color: #333;
margin-top: 4px;
}
.confirm-wrapper {
margin-top: 24px;
}
.confirm-button {
width: 100%;
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;
}
.confirm-button:active {
background: linear-gradient(90deg, #5ccf7a 0%, #8fdca0 100%);
}
.style-item.active .style-img {
border: 2px solid #50cf54;
}
.style-item.active .style-label {
color: #50cf54;
}
.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>