forked from natuka/web.puabadge.com
1286 lines
33 KiB
Vue
1286 lines
33 KiB
Vue
<template>
|
||
<div class="photo-upload-page">
|
||
<div class="badge-size">
|
||
<div class="order-type">
|
||
<div class="order-type-item" v-if="orderStat.order_no">
|
||
<div class="order-type-item-title">
|
||
{{ toValueWithout("订单编号") }}
|
||
</div>
|
||
<div class="order-type-item-title">
|
||
{{ orderStat.order_no }}
|
||
</div>
|
||
</div>
|
||
<div class="order-type-item" v-if="prodId">
|
||
<div class="order-type-item-title">
|
||
{{ toValueWithout("产品类型") }}
|
||
</div>
|
||
<div class="order-type-item-title" v-if="typeName">
|
||
{{ toValueWithout(typeName) }}
|
||
</div>
|
||
</div>
|
||
<div class="order-type-item" v-if="prodId">
|
||
<div class="order-type-item-title">
|
||
{{ toValueWithout("主体类型") }}
|
||
</div>
|
||
<div class="order-type-item-title" v-if="subjectName">
|
||
{{ toValueWithout(subjectName) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="size-title">
|
||
{{ toValueWithout("剩余兑换数量") }}
|
||
</div>
|
||
<div class="size-options">
|
||
<div class="size-item" v-for="item in sizeList" :key="item.id">
|
||
<div class="size-text">
|
||
{{ item.size }}
|
||
</div>
|
||
<div class="size-count">
|
||
({{ toValueWithout("剩余兑换") }}:{{ item.remaining }})
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="badge-info">
|
||
<div class="badge-item" @click="goToRecord">
|
||
<div class="badge-title">
|
||
{{ toValueWithout("设计图集") }}
|
||
</div>
|
||
<div class="badge-count">
|
||
{{ orderStat.create_count || 0 }}{{ toValueWithout("张") }}
|
||
</div>
|
||
</div>
|
||
<div class="badge-item" @click="goToMyOrder">
|
||
<div class="badge-title">
|
||
{{ toValueWithout("我的订单") }}
|
||
</div>
|
||
<div class="badge-count">
|
||
{{ orderStat.order_count || 0 }}{{ toValueWithout("笔") }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div :style="`height: ${config.styles.spacing.itemGap};background: ${config.styles.colors.backgroundSecondary};`"></div>
|
||
<div class="step-container">
|
||
<div class="step-item active">
|
||
<div class="step-num">
|
||
1
|
||
</div>
|
||
<div class="step-content">
|
||
<div class="step-title">
|
||
{{ toValueWithout("上传正面照片") }}
|
||
</div>
|
||
<div class="step-desc">
|
||
{{ toValueWithout("1张五官清晰的正面照片") }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="step-item">
|
||
<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="shouldShowKindList">
|
||
<div :style="`font-size: 16px; color: ${config.styles.colors.textPrimary}; font-weight: bold; margin-top: ${config.styles.spacing.sectionMargin};padding: 0 ${config.styles.spacing.pagePadding};`">
|
||
{{ toValueWithout("风格类型") }}
|
||
</div>
|
||
<div class="kind-box">
|
||
<div class="kind-box-item" v-for="item in kindList" :key="item.id" :class="{ styleActive: kindId == item.id }">
|
||
<div class="kind-item-title" @click="kindChange(item)">
|
||
{{ toValueWithout(item.name) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="typeId == 11 && count_prop > 0">
|
||
<div :style="`font-size: 16px; color: ${config.styles.colors.textPrimary}; font-weight: bold; margin-top: ${config.styles.spacing.sectionMargin};padding: 0 ${config.styles.spacing.pagePadding};`">
|
||
{{ toValueWithout("相框标题") }}
|
||
</div>
|
||
<div :style="`padding: 10px ${config.styles.spacing.pagePadding};`">
|
||
<el-input type="text" v-model="scene_prop" :placeholder="toValueWithout('请输入相框标题')" @input="scenePropInputText" />
|
||
<div class="photo-upload-tip" style="color: red;">
|
||
*{{ toValueWithout("相框标题将用于生成图片,请输入简洁明了的相框标题") }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="isViews == 1">
|
||
<div :style="`font-size: 16px; color: ${config.styles.colors.textPrimary}; font-weight: bold; margin-top: ${config.styles.spacing.sectionMargin};padding: 0 ${config.styles.spacing.pagePadding};`">
|
||
{{ toValueWithout("多视角参考图") }}
|
||
</div>
|
||
<div :style="`font-size: 12px; color: ${config.styles.colors.textTertiary}; margin-top: ${config.styles.spacing.itemGap};padding: 0 ${config.styles.spacing.pagePadding};`">
|
||
{{ toValueWithout("AI建模具有随机性,上传参考图有助于更好的还原主体特征") }}
|
||
</div>
|
||
<div class="photo-upload-box-refer">
|
||
<van-uploader
|
||
v-model="referPicture"
|
||
multiple
|
||
:max-count="config.upload.maxReferCount"
|
||
:max-size="config.upload.maxSize"
|
||
@oversize="onOversize"
|
||
:deletable="true"
|
||
:show-preview="true"
|
||
:before-read="beforeRead"
|
||
upload-icon="plus" />
|
||
</div>
|
||
</div>
|
||
<div v-if="typeId == 8 && mountsList.length > 0">
|
||
<div :style="`font-size: 16px; color: ${config.styles.colors.textPrimary}; font-weight: bold; margin-top: ${config.styles.spacing.sectionMargin};padding: 0 ${config.styles.spacing.pagePadding};`">
|
||
{{ toValueWithout("坐骑类型") }}
|
||
</div>
|
||
<div class="mounts-list-container" :style="`padding: 0 ${config.styles.spacing.pagePadding};`">
|
||
<div v-for="item in mountsList" :key="item.id" class="mounts-item" @click="rideChange(item)">
|
||
<div class="mounts-item-list">
|
||
<img
|
||
class="mounts-item-image"
|
||
:src="item.image"
|
||
:alt="toValueWithout(item.name)"
|
||
/>
|
||
<van-icon
|
||
v-if="item.id == mountsId"
|
||
class="mounts-icon"
|
||
:color="config.styles.colors.success"
|
||
name="checked"
|
||
size="18px"
|
||
/>
|
||
</div>
|
||
<div class="mounts-item-text">{{ toValueWithout(item.name) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="typeId == 9 && count_prop > 0">
|
||
<div :style="`font-size: 16px; color: ${config.styles.colors.textPrimary}; font-weight: bold; margin-top: ${config.styles.spacing.sectionMargin};padding: 0 ${config.styles.spacing.pagePadding};`">
|
||
{{ toValueWithout("场景道具") }}
|
||
</div>
|
||
<div :style="`padding: 10px ${config.styles.spacing.pagePadding};`">
|
||
<div :style="`margin-top: 16px;`">
|
||
<div
|
||
v-for="(_, index) in 4"
|
||
:key="index"
|
||
:style="`display: flex; align-items: center; margin-bottom: 12px;`"
|
||
>
|
||
<el-input
|
||
v-model="scenePropsInputs[index]"
|
||
:placeholder="toValueWithout('请输入场景道具')"
|
||
:style="`flex: 1; margin-right: 8px;`"
|
||
@input="updateScenePropFromInputs"
|
||
clearable
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="photo-upload-body" @click="triggerPick">
|
||
<div v-if="!picture" class="photo-upload-box">
|
||
<div class="photo-upload-area">
|
||
<div class="photo-upload-plus">
|
||
+
|
||
</div>
|
||
<div class="photo-upload-text">
|
||
{{ toValueWithout("点击上传照片") }}
|
||
</div>
|
||
<div class="photo-upload-tip">
|
||
*{{ toValueWithout("上传照片时建议勾选[原图]") }}
|
||
</div>
|
||
</div>
|
||
<div class="photo-upload-footer">
|
||
{{ toValueWithout("请确定您对上传的照片拥有合法使用权利或已取得他人合法授权,且同意本平台分析图片信息以提供生成服务") }}
|
||
</div>
|
||
</div>
|
||
<img class="photo-upload-img" v-if="picture" :src="picture" alt="" srcset="">
|
||
<input
|
||
ref="fileInput"
|
||
type="file"
|
||
accept="image/*"
|
||
class="photo-upload-input"
|
||
@change="onFileChange"
|
||
@click.stop
|
||
>
|
||
</div>
|
||
|
||
<Teleport to="body">
|
||
<div v-if="cropping" class="ct-cropper">
|
||
<div class="ct-cropper__stage">
|
||
<VueCropper
|
||
ref="cropperRef"
|
||
:img="cropImg"
|
||
:auto-crop="true"
|
||
:auto-crop-width="9999"
|
||
:auto-crop-height="9999"
|
||
:fixed="true"
|
||
:fixed-number="config.upload.crop.ratio"
|
||
:fixed-box="false"
|
||
:center-box="false"
|
||
:can-move="true"
|
||
:can-move-box="true"
|
||
:can-scale="true"
|
||
:info="true"
|
||
:info-true="true"
|
||
:full="false"
|
||
:high="true"
|
||
:enlarge="config.upload.crop.enlarge"
|
||
:output-type="config.upload.crop.outputType"
|
||
:output-size="1"
|
||
:fill-color="config.upload.crop.fillColor"
|
||
mode="contain"
|
||
/>
|
||
</div>
|
||
<div class="ct-cropper__bar">
|
||
<button class="ct-cropper__btn" type="button" @click="onCropCancel">{{ toValueWithout("取消") }}</button>
|
||
<div class="ct-cropper__zoom">
|
||
<button class="ct-cropper__zoom-btn" type="button" aria-label="缩小" @click="onCropZoom(-1)">-</button>
|
||
<button class="ct-cropper__zoom-btn" type="button" aria-label="放大" @click="onCropZoom(1)">+</button>
|
||
</div>
|
||
<button
|
||
class="ct-cropper__btn ct-cropper__btn--ok"
|
||
type="button"
|
||
:disabled="cropConfirming"
|
||
@click="onCropConfirm"
|
||
>
|
||
{{ toValueWithout("确定") }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
<div :style="`height: 120px;`"></div>
|
||
<div class="design-action-bar">
|
||
<div class="design-left">
|
||
<img class="design-leaf-icon" width="18" height="18" :src="leafIcon" alt="">
|
||
<span class="design-remaining">{{ toValueWithout("剩余") }}{{ orderStat.remain_count || 0 }}{{ toValueWithout("次") }}</span>
|
||
</div>
|
||
<button class="design-btn" @click="goToPreview">
|
||
<span>{{ toValueWithout("开始设计") }}</span>
|
||
<img class="design-arrow" :src="arrowIcon" alt="">
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { VueCropper } from 'vue-cropper'
|
||
import 'vue-cropper/dist/index.css'
|
||
import { showLoadingToast, showToast, closeToast, showFailToast, showSuccessToast } from 'vant';
|
||
import type { UploaderFileListItem } from 'vant'
|
||
import * as badgeApi from '@/api/badge'
|
||
import { useTranslation } from "i18next-vue";
|
||
import * as i18n from '@/lang/utils'
|
||
import { toValueWithout } from '@/lang/utils'
|
||
import { cartoonConfig } from '@/config/cartoon'
|
||
import leafIcon from '@/assets/badge/leaf.png'
|
||
import arrowIcon from '@/assets/badge/arrow.png'
|
||
|
||
// 初始化翻译功能
|
||
const { t } = useTranslation();
|
||
i18n.set(t);
|
||
|
||
// 使用配置
|
||
const config = cartoonConfig
|
||
|
||
const referPicture = ref<UploaderFileListItem[]>([])
|
||
const router = useRouter()
|
||
|
||
const fileInput = ref<HTMLInputElement | null>(null)
|
||
const cropperRef = ref<any>(null)
|
||
const cropping = ref(false)
|
||
const cropImg = ref('')
|
||
const cropConfirming = ref(false)
|
||
|
||
const imgurl = ref('')
|
||
|
||
// 跳转设计图集
|
||
function goToRecord() {
|
||
router.push(config.routes.record)
|
||
}
|
||
|
||
// 跳转我的订单
|
||
function goToMyOrder() {
|
||
router.push(config.routes.myOrder)
|
||
}
|
||
|
||
// 获取兑换数量
|
||
const sizeList = ref([])
|
||
const getSizeList = () => {
|
||
badgeApi.getOrderPrice({}).then((res: any) => {
|
||
sizeList.value = res
|
||
})
|
||
}
|
||
|
||
// 获取首页显示的数量和类型
|
||
const orderStat = ref({})
|
||
const prodId = ref(0)
|
||
const prop = ref('')
|
||
const typeId = ref(0)
|
||
const subjectId = ref(0)
|
||
const typeName = ref('')
|
||
const subjectName = ref('')
|
||
const isViews = ref(0)
|
||
const getOrderStat = () => {
|
||
badgeApi.getOrderStat({}).then((res: any) => {
|
||
orderStat.value = res
|
||
prodId.value = res.prod_id
|
||
prop.value = res.prop
|
||
typeId.value = res.type_id
|
||
typeName.value = res.type_name
|
||
subjectId.value = res.subject_id
|
||
subjectName.value = res.subject_name
|
||
isViews.value = res.is_views
|
||
getKindList()
|
||
if (res.type_id === 8) {
|
||
mountsId.value = 0
|
||
getmountsList()
|
||
} else if (res.type_id === 9) {
|
||
scenePropsInputs.value = ['', '', '', '']
|
||
scene_prop.value = ''
|
||
}
|
||
})
|
||
}
|
||
|
||
// 计算属性:是否显示照片示例
|
||
const shouldShowPhotoExample = computed(() => {
|
||
return config.productLimits.photoExampleTypes.includes(prop.value)
|
||
})
|
||
|
||
// 计算属性:是否显示风格类型
|
||
const shouldShowKindList = computed(() => {
|
||
return kindList.value.length > 0
|
||
})
|
||
|
||
const kindList = ref([])
|
||
const getKindList = () => {
|
||
badgeApi.getKindList({
|
||
prod_id: prodId.value
|
||
}).then((res: any) => {
|
||
kindList.value = res.list
|
||
kindId.value = res.list && res.list[0]?.id
|
||
count_prop.value = res.list && res.list[0]?.count_prop
|
||
})
|
||
}
|
||
|
||
|
||
|
||
const kindId = ref(0)
|
||
const count_prop = ref(0)
|
||
const kindChange = (item: any) => {
|
||
kindId.value = item.id
|
||
count_prop.value = item.count_prop
|
||
}
|
||
|
||
// 计算字符单位:中文占2个,英文/数字/空格/符号占1个
|
||
const getCharUnits = (str: string) => {
|
||
let units = 0
|
||
for (const char of str) {
|
||
units += /[\u4e00-\u9fff\u3400-\u4dbf]/.test(char) ? 2 : 1
|
||
}
|
||
return units
|
||
}
|
||
|
||
// 限制最多 12 个字符单位:中文占2个,英文/数字/空格/符号占1个
|
||
const scenePropInputText = (val: string) => {
|
||
if (getCharUnits(val) > 18) {
|
||
let units = 0
|
||
let i = 0
|
||
for (; i < val.length; i++) {
|
||
const add = /[\u4e00-\u9fff\u3400-\u4dbf]/.test(val[i]) ? 2 : 1
|
||
if (units + add > 18) break
|
||
units += add
|
||
}
|
||
val = val.slice(0, i)
|
||
}
|
||
scene_prop.value = val
|
||
}
|
||
|
||
// 形状列表相关
|
||
const mountsList = ref<any[]>([])
|
||
const mountsId = ref(0)
|
||
|
||
// 获取图片URL
|
||
const getImageUrl = (path: string) => {
|
||
if (!path) return ''
|
||
return 'https://suwa3d-3dview.oss-cn-shanghai.aliyuncs.com/' + path
|
||
}
|
||
|
||
// 获取形状列表
|
||
const getmountsList = () => {
|
||
if (!prodId.value || !typeId.value) {
|
||
return
|
||
}
|
||
badgeApi.getMountsList({
|
||
prod_id: prodId.value,
|
||
type_id: typeId.value,
|
||
status: 1,
|
||
page: 1,
|
||
size: 1000
|
||
}).then((res: any) => {
|
||
if (res && res.list && res.list.length > 0) {
|
||
mountsList.value = res.list
|
||
// 默认选中第一个
|
||
if (!mountsId.value) {
|
||
mountsId.value = res.list[0].id
|
||
}
|
||
}
|
||
}).catch((err: any) => {
|
||
console.error('getShapeList', err)
|
||
})
|
||
}
|
||
|
||
// 切换形状
|
||
const rideChange = (item: any) => {
|
||
mountsId.value = item.id
|
||
}
|
||
|
||
const scene_prop = ref('') // 四个输入框的值用;拼接后提交接口
|
||
|
||
// 场景道具四个输入框
|
||
const scenePropsInputs = ref<string[]>(['', '', '', ''])
|
||
|
||
// 四个输入框有值时,用;拼接更新到 scene_prop
|
||
const updateScenePropFromInputs = () => {
|
||
scene_prop.value = scenePropsInputs.value
|
||
.map(v => (v || '').trim())
|
||
.filter(v => v)
|
||
.join(';')
|
||
}
|
||
|
||
const isLoading = ref(false)
|
||
|
||
// 生成图片
|
||
function goToPreview() {
|
||
if (isLoading.value) {
|
||
return
|
||
}
|
||
if (!imgurl.value) {
|
||
showToast(toValueWithout('请先上传照片'))
|
||
return
|
||
}
|
||
if (orderStat.value.remain_count <= 0) {
|
||
showToast(toValueWithout('剩余次数不足'))
|
||
return
|
||
}
|
||
isLoading.value = true
|
||
getPid()
|
||
}
|
||
|
||
const picture = ref(null)
|
||
|
||
const imageWidth = ref(0);
|
||
const imageHeight = ref(0);
|
||
|
||
let lockedScrollY = 0
|
||
|
||
function preventTouchScroll(e: TouchEvent) {
|
||
const host = document.querySelector('.ct-cropper')
|
||
if (host && e.target instanceof Node && host.contains(e.target)) return
|
||
e.preventDefault()
|
||
}
|
||
|
||
function setBodyScrollLocked(locked: boolean) {
|
||
const html = document.documentElement
|
||
const body = document.body
|
||
if (locked) {
|
||
lockedScrollY = window.scrollY || window.pageYOffset || 0
|
||
html.style.overflow = 'hidden'
|
||
body.style.overflow = 'hidden'
|
||
body.style.position = 'fixed'
|
||
body.style.top = `-${lockedScrollY}px`
|
||
body.style.left = '0'
|
||
body.style.right = '0'
|
||
body.style.width = '100%'
|
||
document.addEventListener('touchmove', preventTouchScroll, { passive: false })
|
||
} else {
|
||
html.style.overflow = ''
|
||
body.style.overflow = ''
|
||
body.style.position = ''
|
||
body.style.top = ''
|
||
body.style.left = ''
|
||
body.style.right = ''
|
||
body.style.width = ''
|
||
document.removeEventListener('touchmove', preventTouchScroll)
|
||
window.scrollTo(0, lockedScrollY)
|
||
}
|
||
}
|
||
|
||
function openCropping(dataUrl: string) {
|
||
cropImg.value = dataUrl
|
||
cropping.value = true
|
||
setBodyScrollLocked(true)
|
||
}
|
||
|
||
function closeCropping() {
|
||
cropping.value = false
|
||
cropImg.value = ''
|
||
cropConfirming.value = false
|
||
setBodyScrollLocked(false)
|
||
}
|
||
|
||
function triggerPick() {
|
||
fileInput.value?.click()
|
||
}
|
||
|
||
function base64ToBlob(base64: string) {
|
||
const arr = base64.split(',');
|
||
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/jpeg';
|
||
const bstr = atob(arr[1]);
|
||
const n = bstr.length;
|
||
const u8arr = new Uint8Array(n);
|
||
for (let i = 0; i < n; i++) {
|
||
u8arr[i] = bstr.charCodeAt(i);
|
||
}
|
||
return new Blob([u8arr], {
|
||
type: mime
|
||
});
|
||
}
|
||
|
||
function applyCroppedData(data: string) {
|
||
picture.value = data
|
||
const img = new Image()
|
||
img.src = data
|
||
img.onload = () => {
|
||
imageWidth.value = img.naturalWidth
|
||
imageHeight.value = img.naturalHeight
|
||
if (imageWidth.value < config.upload.minWidth || imageHeight.value < config.upload.minHeight) {
|
||
showToast(toValueWithout(`请上传尺寸大于${config.upload.minWidth}*${config.upload.minHeight}像素的照片`))
|
||
imgurl.value = null
|
||
picture.value = null
|
||
return
|
||
}
|
||
const blob = base64ToBlob(data)
|
||
if (blob.size > config.upload.maxSize) {
|
||
showToast(toValueWithout(`照片大小不能超过${config.upload.maxSize / 1024 / 1024}M`))
|
||
imgurl.value = null
|
||
picture.value = null
|
||
return
|
||
}
|
||
imgurl.value = blob
|
||
}
|
||
img.onerror = () => {
|
||
showToast(toValueWithout('裁剪图片加载失败'))
|
||
imgurl.value = null
|
||
picture.value = null
|
||
}
|
||
}
|
||
|
||
function onCropCancel() {
|
||
closeCropping()
|
||
}
|
||
|
||
/** 裁剪页缩放:正数放大、负数缩小;也支持双指捏合 */
|
||
function onCropZoom(step: number) {
|
||
const cropper = cropperRef.value as any
|
||
cropper?.changeScale?.(step)
|
||
}
|
||
|
||
function onCropConfirm() {
|
||
if (cropConfirming.value) return
|
||
const cropper = cropperRef.value as any
|
||
if (!cropper?.getCropData) {
|
||
showToast(toValueWithout('裁剪组件未就绪'))
|
||
return
|
||
}
|
||
cropConfirming.value = true
|
||
cropper.getCropData((data: string) => {
|
||
closeCropping()
|
||
if (!data) {
|
||
showToast(toValueWithout('裁剪失败'))
|
||
return
|
||
}
|
||
applyCroppedData(data)
|
||
})
|
||
}
|
||
|
||
function onFileChange(e: Event) {
|
||
const input = e.target as HTMLInputElement
|
||
const file = input.files?.[0]
|
||
input.value = ''
|
||
if (!file) return
|
||
if (file.size > config.upload.maxSize) {
|
||
showToast(toValueWithout(`照片大小不能超过${config.upload.maxSize / 1024 / 1024}M`))
|
||
return
|
||
}
|
||
const objectUrl = URL.createObjectURL(file)
|
||
const img = new Image()
|
||
img.src = objectUrl
|
||
img.onload = () => {
|
||
imageWidth.value = img.naturalWidth
|
||
imageHeight.value = img.naturalHeight
|
||
URL.revokeObjectURL(objectUrl)
|
||
if (imageWidth.value < config.upload.minWidth || imageHeight.value < config.upload.minHeight) {
|
||
showToast(toValueWithout(`请上传尺寸大于${config.upload.minWidth}*${config.upload.minHeight}像素的照片`))
|
||
return
|
||
}
|
||
const reader = new FileReader()
|
||
reader.onload = (ev) => {
|
||
const dataUrl = String(ev.target?.result || '')
|
||
if (!dataUrl) {
|
||
showToast(toValueWithout('图片读取失败'))
|
||
return
|
||
}
|
||
nextTick(() => openCropping(dataUrl))
|
||
}
|
||
reader.onerror = () => showToast(toValueWithout('图片读取失败'))
|
||
reader.readAsDataURL(file)
|
||
}
|
||
img.onerror = () => {
|
||
URL.revokeObjectURL(objectUrl)
|
||
showToast(toValueWithout('图片加载失败'))
|
||
}
|
||
}
|
||
|
||
const onOversize = (file: any) => {
|
||
if (file.size > config.upload.maxSize) {
|
||
showToast(toValueWithout(`照片大小不能超过${config.upload.maxSize / 1024 / 1024}M`))
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
const beforeRead = (file: any) => {
|
||
const toPreviewAndCheck = (input: any): Promise<UploaderFileListItem> => {
|
||
return new Promise((resolve, reject) => {
|
||
const raw: File = input.file || input
|
||
if (raw.size > config.upload.maxSize) {
|
||
showToast(toValueWithout(`照片大小不能超过${config.upload.maxSize / 1024 / 1024}M`))
|
||
reject(false)
|
||
return
|
||
}
|
||
const objectUrl = URL.createObjectURL(raw)
|
||
const img = new Image()
|
||
img.src = objectUrl
|
||
img.onload = () => {
|
||
const width = img.naturalWidth
|
||
const height = img.naturalHeight
|
||
imageWidth.value = width
|
||
imageHeight.value = height
|
||
URL.revokeObjectURL(objectUrl)
|
||
if (width < config.upload.minWidth || height < config.upload.minHeight) {
|
||
showToast(toValueWithout(`请上传尺寸大于${config.upload.minWidth}×${config.upload.minHeight}像素的照片`))
|
||
reject(false)
|
||
return
|
||
}
|
||
const reader = new FileReader()
|
||
reader.onload = (e) => {
|
||
const url = e.target?.result as string
|
||
const out: any = input
|
||
out.url = url
|
||
resolve(out)
|
||
}
|
||
reader.onerror = () => {
|
||
showToast(toValueWithout('图片读取失败'))
|
||
reject(false)
|
||
}
|
||
reader.readAsDataURL(raw)
|
||
}
|
||
img.onerror = () => {
|
||
showToast(toValueWithout('图片加载失败'))
|
||
URL.revokeObjectURL(objectUrl)
|
||
reject(false)
|
||
}
|
||
})
|
||
}
|
||
|
||
if (Array.isArray(file)) {
|
||
return Promise.allSettled(file.map(toPreviewAndCheck)).then((results) => {
|
||
const passed = results
|
||
.filter(r => r.status === 'fulfilled')
|
||
.map((r: any) => r.value)
|
||
if (passed.length === 0) return Promise.reject(false)
|
||
return passed
|
||
})
|
||
}
|
||
return toPreviewAndCheck(file)
|
||
}
|
||
|
||
// 获取Pid
|
||
const pid = ref('')
|
||
const getPid = async () => {
|
||
showLoadingToast({
|
||
message: toValueWithout('上传中...'),
|
||
forbidClick: true,
|
||
loadingType: 'spinner',
|
||
duration: 0,
|
||
})
|
||
const params = {
|
||
prod_id: prodId.value,
|
||
type_id: typeId.value,
|
||
subject_id: subjectId.value
|
||
}
|
||
try {
|
||
const res = await badgeApi.getPid(params) as any
|
||
isLoading.value = false
|
||
pid.value = res.pid
|
||
try {
|
||
const uploadTasks = [
|
||
sendToOss(imgurl.value, res.url),
|
||
]
|
||
await Promise.all(uploadTasks)
|
||
if (isViews.value == 1 && referPicture.value.length > 0) {
|
||
const multiUrlTasks = referPicture.value.map((item) => {
|
||
return badgeApi.getMultiUrl({
|
||
pid: pid.value,
|
||
}).then((res: any) => {
|
||
return sendToOss(item.file, res.multi_url)
|
||
})
|
||
})
|
||
await Promise.all(multiUrlTasks)
|
||
}
|
||
} catch (err) {
|
||
closeToast()
|
||
console.error('上传失败:', err)
|
||
showFailToast({
|
||
message: toValueWithout('上传失败'),
|
||
duration: 2000,
|
||
})
|
||
isLoading.value = false
|
||
return
|
||
}
|
||
} catch (err: any) {
|
||
isLoading.value = false
|
||
closeToast()
|
||
showToast({
|
||
message: err.message,
|
||
duration: 2000,
|
||
})
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 上传到OSS
|
||
const pendingUploads = ref(0)
|
||
const isAnotherAPICalled = ref(false)
|
||
const sendToOss = async (src: string | Blob | File, url: string) => {
|
||
try {
|
||
pendingUploads.value++
|
||
const response = await fetch(url, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'image/jpeg'
|
||
},
|
||
body: src
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Upload failed')
|
||
}
|
||
|
||
if (--pendingUploads.value === 0 && !isAnotherAPICalled.value) {
|
||
isAnotherAPICalled.value = true
|
||
createLog()
|
||
}
|
||
|
||
} catch (err: any) {
|
||
closeToast()
|
||
pendingUploads.value--
|
||
showFailToast({
|
||
message: err.message,
|
||
duration: 2000,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 创建日志
|
||
const createLog = () => {
|
||
const params = {
|
||
pid: pid.value,
|
||
group: 1,
|
||
prod_id: prodId.value,
|
||
type_id: typeId.value,
|
||
kind_id: kindId.value,
|
||
subject_id: subjectId.value,
|
||
scene_prop: scene_prop.value ? scene_prop.value : undefined,
|
||
mounts_id: mountsId.value || undefined,
|
||
}
|
||
badgeApi.createLog(params).then(() => {
|
||
closeToast()
|
||
showSuccessToast({
|
||
message: toValueWithout('照片上传成功'),
|
||
duration: 2000,
|
||
})
|
||
setTimeout(() => {
|
||
closeToast()
|
||
router.push({
|
||
path: config.routes.preview,
|
||
query: {
|
||
pid: pid.value,
|
||
group: 1,
|
||
prod_id: prodId.value,
|
||
type_id: typeId.value,
|
||
kind_id: kindId.value,
|
||
subject_id: subjectId.value
|
||
},
|
||
})
|
||
}, 1000);
|
||
}).catch((err: any) => {
|
||
showFailToast({
|
||
message: err.message,
|
||
duration: 2000,
|
||
})
|
||
}).finally(() => {
|
||
closeToast()
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
getOrderStat()
|
||
getSizeList()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (cropping.value) closeCropping()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.photo-upload-page {
|
||
height: auto;
|
||
overflow-y: scroll!important;
|
||
min-height: 100vh;
|
||
background-color: #ffffff;
|
||
}
|
||
.badge-size {
|
||
padding: 16px;
|
||
.size-title {
|
||
font-size: 14px;
|
||
color: #000;
|
||
}
|
||
.size-options {
|
||
.size-item {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
.size-text {
|
||
font-size: 14px;
|
||
color: #333;
|
||
text-align: center;
|
||
padding: 4px 10px;
|
||
background: #F0F2F5;
|
||
border-radius: 4px;
|
||
margin-right: 5px;
|
||
display: inline-block;
|
||
}
|
||
.size-count {
|
||
font-size: 12px;
|
||
color: #999;
|
||
display: inline-block;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.badge-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 0 16px 16px 16px;
|
||
.badge-item:first-child {
|
||
background-image: url('@/assets/badge/sheji.png');
|
||
background-size: cover;
|
||
background-position: center;
|
||
margin-right: 5px;
|
||
}
|
||
.badge-item:last-child {
|
||
background-image: url('@/assets/badge/order.png');
|
||
background-size: cover;
|
||
background-position: center;
|
||
margin-left: 5px;
|
||
}
|
||
.badge-item {
|
||
padding: 18px 12px;
|
||
flex: 1;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
height: 88px;
|
||
.badge-title {
|
||
font-size: 16px;
|
||
color: #000;
|
||
font-weight: bold;
|
||
}
|
||
.badge-count {
|
||
font-size: 15px;
|
||
color: #333;
|
||
font-weight: bold;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
}
|
||
.step-container {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
padding: 16px 0 16px 16px;
|
||
}
|
||
.step-item {
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.step-item.active {
|
||
flex: 2;
|
||
margin-left: 50px;
|
||
}
|
||
.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;
|
||
}
|
||
.step-title {
|
||
font-size: 16px;
|
||
color: #808080;
|
||
}
|
||
.step-desc {
|
||
font-size: 12px;
|
||
color: #808080;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.photo-upload-body {
|
||
position: relative;
|
||
width: 80vw;
|
||
height: 80vw;
|
||
margin: 16px auto 0 auto;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
}
|
||
.photo-upload-box {
|
||
margin: 16px auto 0 auto;
|
||
width: 80vw;
|
||
height: 80vw;
|
||
border-radius: 12px;
|
||
background: #F0F2F5;
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
}
|
||
.photo-upload-input {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
.photo-upload-img {
|
||
max-width: 80vw;
|
||
max-height: 80vw;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
/* 全屏裁剪层:3:4 + 可缩放 */
|
||
.ct-cropper {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 2147483000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #000;
|
||
overscroll-behavior: none;
|
||
}
|
||
.ct-cropper__stage {
|
||
flex: 1;
|
||
min-height: 0;
|
||
width: 100%;
|
||
position: relative;
|
||
}
|
||
.ct-cropper__stage :deep(.vue-cropper) {
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
background: #111 !important;
|
||
touch-action: none;
|
||
}
|
||
.ct-cropper__stage :deep(.cropper-view-box) {
|
||
outline: 2px solid #fff !important;
|
||
}
|
||
.ct-cropper__stage :deep(.crop-point) {
|
||
background-color: #50cf54 !important;
|
||
opacity: 0.95;
|
||
}
|
||
.ct-cropper__bar {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
width: 100%;
|
||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||
box-sizing: border-box;
|
||
background: #111;
|
||
}
|
||
.ct-cropper__zoom {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.ct-cropper__zoom-btn {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: #fff;
|
||
font-size: 22px;
|
||
line-height: 1;
|
||
padding: 0;
|
||
}
|
||
.ct-cropper__btn {
|
||
height: 40px;
|
||
line-height: 40px;
|
||
padding: 0 20px;
|
||
border: none;
|
||
border-radius: 20px;
|
||
background: transparent;
|
||
color: #fff;
|
||
font-size: 15px;
|
||
}
|
||
.ct-cropper__btn--ok {
|
||
background: #50cf54;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
}
|
||
.ct-cropper__btn:disabled {
|
||
opacity: 0.6;
|
||
}
|
||
.photo-upload-header {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px 0 12px;
|
||
font-size: 14px;
|
||
color: #888;
|
||
justify-content: flex-end;
|
||
}
|
||
.photo-upload-guide-icon {
|
||
font-size: 16px;
|
||
margin-right: 4px;
|
||
}
|
||
.photo-upload-guide-text {
|
||
font-size: 13px;
|
||
}
|
||
.photo-upload-area {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
user-select: none;
|
||
}
|
||
.photo-upload-plus {
|
||
font-size: 48px;
|
||
color: #c2c2c2;
|
||
margin-bottom: 0;
|
||
}
|
||
.photo-upload-text {
|
||
font-size: 16px;
|
||
color: #888;
|
||
margin-bottom: 4px;
|
||
}
|
||
.photo-upload-tip {
|
||
font-size: 12px;
|
||
color: #b0b0b0;
|
||
}
|
||
.photo-upload-footer {
|
||
position: absolute;
|
||
bottom: 0px;
|
||
left: 0;
|
||
width: 100%;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: #b0b0b0;
|
||
padding: 0 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.step-line-item-desc {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
padding: 16px 16px 0 16px;
|
||
}
|
||
|
||
.design-action-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px;
|
||
background: #fff;
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 10;
|
||
}
|
||
.design-left {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.design-leaf-icon {
|
||
margin-right: 4px;
|
||
vertical-align: middle;
|
||
}
|
||
.design-remaining {
|
||
color: #222;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
.design-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
outline: none;
|
||
cursor: pointer;
|
||
width: 50vw;
|
||
height: 56px;
|
||
border-radius: 28px;
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
background: linear-gradient(90deg, #D1ED8E 0%, #55E668 100%);
|
||
color: #222;
|
||
box-shadow: 0 2px 8px rgba(80, 207, 84, 0.10);
|
||
transition: background 0.2s;
|
||
position: relative;
|
||
}
|
||
.design-btn .design-arrow {
|
||
margin-left: 20px;
|
||
vertical-align: middle;
|
||
position: absolute;
|
||
right: 30px;
|
||
transition: transform 0.2s;
|
||
width: 18px;
|
||
height: 12px;
|
||
}
|
||
.order-type-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
margin-bottom: 12px;
|
||
font-size: 14px;
|
||
}
|
||
.kind-box {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
flex-wrap: wrap;
|
||
padding: 16px;
|
||
}
|
||
.kind-box-item {
|
||
margin-right: 10px;
|
||
margin-bottom: 10px;
|
||
padding: 6px 10px;
|
||
background: #f5f5f5;
|
||
border-radius: 8px;
|
||
color: #333;
|
||
}
|
||
.kind-item-title {
|
||
font-size: 14px;
|
||
}
|
||
.kind-box-item.styleActive {
|
||
background: #50CF54;
|
||
color: #fff;
|
||
}
|
||
.photo-upload-box-refer {
|
||
margin: 16px;
|
||
}
|
||
.mounts-list-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-start;
|
||
margin-top: 12px;
|
||
}
|
||
.mounts-item {
|
||
position: relative;
|
||
flex: 0 0 calc(25% - 6px);
|
||
margin-right: 8px;
|
||
margin-bottom: 16px;
|
||
cursor: pointer;
|
||
}
|
||
.mounts-item:nth-child(4n) {
|
||
margin-right: 0;
|
||
}
|
||
.mounts-item-list {
|
||
position: relative;
|
||
width: 100%;
|
||
padding-bottom: 100%;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: #f5f5f5;
|
||
}
|
||
.mounts-item-image {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
border-radius: 8px;
|
||
transition: transform 0.2s;
|
||
}
|
||
.mounts-item-image-round {
|
||
border-radius: 50%;
|
||
}
|
||
.mounts-item:hover .mounts-item-image {
|
||
transform: scale(1.05);
|
||
}
|
||
.mounts-icon {
|
||
position: absolute;
|
||
right: 6px;
|
||
bottom: 6px;
|
||
z-index: 2;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-radius: 50%;
|
||
padding: 2px;
|
||
}
|
||
.mounts-item-text {
|
||
font-size: 12px;
|
||
text-align: center;
|
||
margin-top: 8px;
|
||
color: v-bind('config.styles.colors.textSecondary');
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
</style>
|
||
|