Browse Source

代码提交

main
Linzm 3 weeks ago
parent
commit
b52dcc0842
  1. 6
      .env.development
  2. 493
      src/config/cartoon.ts
  3. 33
      src/main.ts
  4. 60
      src/router/cartoon.ts
  5. 36
      src/router/index.ts
  6. 14
      src/utils/request.ts
  7. 9
      src/views/badge/index.vue
  8. 5
      src/views/badge/preview.vue
  9. 1165
      src/views/cartoon/index.vue
  10. 156
      src/views/cartoon/myOrder.vue
  11. 271
      src/views/cartoon/orderDetail.vue
  12. 229
      src/views/cartoon/photoAlbum.vue
  13. 1551
      src/views/cartoon/previewOrder.vue
  14. 1
      vite.config.ts

6
.env.development

@ -1,7 +1,9 @@
VITE_APP_PREVIEW=true VITE_APP_PREVIEW=true
# VITE_APP_API_BASE_URL=http://172.16.20.21:8166/web/ # VITE_APP_API_BASE_URL=http://172.16.20.21:20001/api/web/
# VITE_APP_API_BASE_URL=http://172.16.0.40:20001/api/web/
VITE_APP_API_BASE_URL=https://shop.api.suwa3d.com/api/web/
# VITE_APP_API_BASE_URL=https://wechat-test.api.puabadge.com/web/ # VITE_APP_API_BASE_URL=https://wechat-test.api.puabadge.com/web/
VITE_APP_API_BASE_URL=https://wechat.api.puabadge.com/web/ # VITE_APP_API_BASE_URL=https://wechat.api.puabadge.com/web/
# VITE_APP_API_WX_URL=https://wechat.api.puabadge.com/web/ # VITE_APP_API_WX_URL=https://wechat.api.puabadge.com/web/
# http://web.suwa3d.dev:28499/ # http://web.suwa3d.dev:28499/
VITE_HTTP_MOCK=true VITE_HTTP_MOCK=true

493
src/config/cartoon.ts

@ -0,0 +1,493 @@
// 卡通手办页面配置
export const cartoonConfig = {
// 路由配置
routes: {
record: '/cartoon/photoAlbum',
myOrder: '/cartoon/myOrder',
preview: '/cartoon/previewOrder',
orderDetail: '/cartoon/orderDetail',
home: '/cartoon'
},
// 产品类型配置
productTypes: {
1: { name: '立体徽章', key: 'badge_3d' },
2: { name: '浮雕相框', key: 'relief_frame' },
3: { name: '3D冰箱贴', key: 'fridge_magnet' },
4: { name: '3D卡通手办', key: 'cartoon_figure' },
6: { name: '浮雕挂饰', key: 'relief_pendant' }
},
// 主体类型配置
subjectTypes: {
person: {
id: 7,
name: '人物主体',
prodIds: [7, 1, 25], // 不同产品类型对应的人物产品ID
group: 'gp',
supportSubject: 1
},
pet: {
id: 8,
name: '宠物主体',
prodIds: [8, 2, 19, 24], // 不同产品类型对应的宠物产品ID
group: 'pet',
supportSubject: 2
}
},
// 产品ID映射配置(根据typeId和subjectId映射到实际产品ID)
productIdMapping: {
4: { // typeId == 4 (3D卡通手办)
person: 1,
pet: 19
},
6: { // typeId == 6 (浮雕挂饰)
person: 25,
pet: 24
},
3: { // typeId == 3 (3D冰箱贴)
person: 7,
pet: 8
},
default: { // 其他类型
person: 7,
pet: 8
}
},
// 产品限制配置
productLimits: {
// 根据产品ID配置是否需要人脸检测
faceCheckRequired: [7, 1, 25],
// 根据产品ID配置是否支持多视角参考图
multiViewSupported: [19],
// 根据产品ID配置是否支持风格类型
kindListSupported: [4],
// 根据typeId配置是否显示照片示例
photoExampleTypes: ['3D真人肖像']
},
// 上传配置
upload: {
minWidth: 300,
minHeight: 300,
maxSize: 10 * 1024 * 1024, // 10MB
maxReferCount: 3,
cropOptions: {
canScale: true,
autoCropWidth: 1024,
autoCropHeight: 1024,
ceilbutton: true,
infoTrue: true,
fixed: false,
fixedNumber: [3, 4],
fixedBox: false,
canMoveBox: true,
enlarge: 2
}
},
// 图片资源路径配置(在组件中通过 import 导入)
assets: {
sheji: '@/assets/badge/sheji.png',
order: '@/assets/badge/order.png'
},
// 提示信息配置
messages: {
personPhotoTip: {
single: '1',
multiple: '1-3',
unit: '人'
},
petPhotoTip: {
count: '1-3',
unit: '只'
}
},
// 业务规则配置
businessRules: {
// 禁止的产品ID组合(typeId, subjectId)
disabled: [
{ typeId: 3, subjectId: 7, message: '人物3D冰箱贴暂未开放' }
],
// 订单类型限制(typeId, currentProdId, targetSubjectId, message)
orderRestrictions: [
{ typeId: 4, prodId: 19, targetSubjectId: 8, message: '该订单属于宠物卡通手办' },
{ typeId: 4, prodId: 1, targetSubjectId: 7, message: '该订单属于人物卡通手办' }
]
},
// 样式配置
styles: {
colors: {
primary: '#50cf54',
primaryGradient: 'linear-gradient(90deg, #D1ED8E 0%, #55E668 100%)',
textPrimary: '#222',
textSecondary: '#333',
textTertiary: '#888',
textDisabled: '#b0b0b0',
background: '#F0F2F5',
backgroundSecondary: '#F2F2F2',
border: '#E5E5E5',
success: '#15CF5F',
progressBar: '#6fdc8c'
},
spacing: {
pagePadding: '16px',
sectionMargin: '16px',
itemGap: '8px'
}
},
// Preview页面配置
preview: {
// 图片尺寸配置
imageSize: {
previewWidth: 320,
previewHeight: 320,
compareImageSize: 120
},
// 进度条配置
progress: {
maxProgress: 90,
interval: 300, // ms
thresholds: {
stage1: 25,
stage2: 50,
stage3: 75
},
stages: {
stage1: '照片数据分析中',
stage2: '提炼图像关键词',
stage3: '图像生成中',
stage4: '图像后处理与优化'
},
totalTime: 90 // 秒
},
// 形状ID配置
shapeIds: {
round: [1, 2], // 圆形形状
specialFrame: [15, 16], // 特殊相框
frame: [17, 18], // 相框
topBottomText: [27, 28] // 支持上下文字的形状
},
// 文字配置默认值
textConfig: {
front: {
x: 163,
y: 222,
width: 700,
height: 700,
radius: 350,
fontSize: 72,
maxLength: 18
},
back: {
x: 238,
y: 297,
width: 550,
height: 550,
radius: 275,
fontSize: 60,
maxLength: 56
},
defaultColor: '#000000',
defaultWeight: 'normal'
},
// 图片缩放比例
imageScale: 0.6,
// 默认值配置
defaults: {
group: 1,
imgKey: 101,
countryId: 45,
addrId: 0
},
// 轮询配置
polling: {
interval: 10000, // 10秒
compareImageDelay: 1000 // 1秒
},
// 特殊code配置(用于地址识别功能)
specialCodes: {
addressRecognition: 'ACAYOAAC3PFCPO3CD6DVM1'
},
// 产品名称映射(根据typeId和prodId)
productNames: {
1: { // typeId == 1 (立体徽章)
7: '人物立体徽章',
8: '宠物立体徽章'
},
2: { // typeId == 2 (浮雕相框)
7: '人物浮雕相框',
8: '宠物浮雕相框'
},
3: { // typeId == 3 (3D冰箱贴)
7: '人物3D冰箱贴',
8: '宠物3D冰箱贴'
},
4: { // typeId == 4 (3D卡通手办)
1: '人物卡通手办',
19: '宠物卡通手办'
},
6: { // typeId == 6 (浮雕挂饰)
25: '人物浮雕挂饰',
24: '宠物浮雕挂饰'
}
},
// 提示信息
messages: {
invalidInput: '输入的文字不能包含特殊符号!',
selectQuantity: '请先选择下单数量',
fillContactName: '请先填写收货人信息',
fillContactMobile: '请先填写收货人手机号',
selectArea: '请先选择地区',
fillAddress: '请先填写详细地址',
orderSuccess: '下单成功',
noRemainingCount: '当前次数已用完,请重新购买',
confirmRegenerate: '请确认是否重新生成。',
confirmOrder: '请再次确认是否选择这个模型下单,下单后预计需要10天完成生产发货。如对产品有疑问,可以联系客服咨询。',
addressPlaceholder: '请输入姓名、手机号、详细地址(如:张三 13800138000 广东省深圳市南山区科技园1号)',
addressRecognitionResult: '识别结果',
compareTip: '温馨提示:AI设计的徽章效果图是根据用户提供的照片机器学习生成的,多试几次就能找到你满意的徽章~',
saveImageTip: '长按图片 → 保存到相册',
designing: '设计中',
designFailed: '设计失败',
remainingExchange: '剩余兑换',
process: '工艺',
productType: '产品类型',
orderQuantity: '订购数量',
addressRecognition: '地址识别',
shippingAddress: '收货地址',
regenerate: '再次生成',
compare: '前后对比',
saveImage: '保存图片',
confirmSelect: '确认选择',
originalImage: '原图',
previewImage: '效果图',
badgePreview: '徽章预览'
}
},
// PhotoAlbum页面配置
photoAlbum: {
// 分页配置
pagination: {
pageSize: 20,
initialPage: 1
},
// 状态配置
status: {
success: 1, // 设计成功
designing: 0, // 设计中
failed: 2 // 设计失败
},
// 图片资源路径(在组件中通过 import 导入)
assets: {
nullData: '@/assets/badge/null-data.png'
},
// 样式配置
styles: {
gridColumns: 2, // 网格列数
gap: 16, // 间距
borderRadius: 12 // 圆角
},
// 提示信息
messages: {
title: '设计记录',
note: '注:可点击效果图进行下单打印',
noMore: '没有更多了',
loading: '加载中...',
noData: '暂无数据',
designing: '设计中',
designFailed: '设计失败',
getDataFailed: '获取数据失败'
}
},
// MyOrder页面配置
myOrder: {
// 分页配置
pagination: {
pageSize: 30,
initialPage: 1,
defaultStatus: 0 // 默认订单状态
},
// 订单状态配置
orderStatus: {
pending: 1, // 待支付
cancelled: 2, // 已取消
producing: 100, // 生产中
shipping: 200, // 待收货
completed: 300 // 已完成
},
// 订单状态文本映射
statusText: {
1: '待支付',
2: '已取消',
100: '生产中',
200: '待收货',
300: '已完成'
},
// 订单状态样式配置
statusStyles: {
processing: {
background: '#bdbdbd' // 处理中状态背景色
},
finished: {
background: '#8bc34a' // 完成状态背景色
},
default: {
background: '#bdbdbd',
color: '#fff',
opacity: 0.9
}
},
// 图片资源路径(在组件中通过 import 导入)
assets: {
nullData: '@/assets/badge/null-data.png'
},
// 样式配置
styles: {
gap: 8, // 间距
itemWidth: 'calc(50% - 4px)', // 订单项宽度
itemHeight: '43vw', // 订单项高度
borderRadius: 12, // 圆角
padding: {
horizontal: 16,
vertical: 24
},
status: {
bottom: 5,
right: 5,
padding: {
vertical: 2,
horizontal: 12
},
borderRadius: 12,
fontSize: 12
},
shadow: '0 2px 8px rgba(0,0,0,0.08)' // 阴影
},
// 提示信息
messages: {
title: '我的订单',
noData: '暂无数据'
}
},
// OrderDetail页面配置
orderDetail: {
// 订单类型配置
orderTypes: {
shipping: 2 // 需要收货地址的订单类型
},
// 产品类型配置(需要特殊显示的产品类型)
productTypes: {
reliefPendant: 6 // 浮雕挂饰(需要显示双面)
},
// 图片尺寸配置
imageSize: {
width: 320,
height: 320,
borderRadius: 10
},
// 图片缩放比例
imageScale: 0.6,
// 样式配置
styles: {
padding: {
horizontal: 16,
vertical: 0
},
statusInfo: {
padding: {
horizontal: 16,
vertical: 0
},
title: {
fontSize: 18,
fontWeight: 'bold',
color: '#222',
marginBottom: 8
},
desc: {
fontSize: 10,
color: '#888'
}
},
productInfo: {
padding: {
horizontal: 16,
vertical: 0
},
title: {
fontSize: 15,
fontWeight: 'bold',
color: '#111',
marginBottom: 10
},
details: {
gap: 8
},
label: {
color: '#bdbdbd',
fontSize: 14
},
value: {
color: '#222',
fontSize: 14
}
},
avatar: {
gap: 10,
borderRadius: 10
},
bottomSpacing: 30
},
// 提示信息
messages: {
title: '订单详情',
statusDesc: '下单后预计需要10天完成生产发货。如对产品有疑问,可以联系客服咨询。',
shippingInfo: '收货信息',
contactInfo: '收货人信息',
shippingAddress: '收货地址',
productInfo: '产品信息',
productName: '产品名称',
size: '尺寸',
shape: '形状',
quantity: '购买数量',
orderInfo: '订单信息',
orderId: '购买订单id',
orderTime: '购买时间'
}
}
}

33
src/main.ts

@ -1,4 +1,4 @@
import { createApp, onMounted } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createRouterScroller } from 'vue-router-better-scroller' import { createRouterScroller } from 'vue-router-better-scroller'
@ -105,13 +105,34 @@ app.use(createRouterScroller({
}, },
})) }))
onMounted(() => { // 处理页面隐藏事件(可以区分刷新和关闭)
window.addEventListener('beforeunload', handleBeforeUnload); function handlePageHide(event: PageTransitionEvent) {
}); // event.persisted 为 true 表示页面被缓存(刷新),保留 code
// event.persisted 为 false 表示页面未被缓存(关闭),清除 code
if (!event.persisted) {
// 页面关闭(非刷新),清除 code 和 userId,避免下一个用户使用上一个用户的 code
localStorage.remove('code')
localStorage.remove('userId')
console.log('页面关闭,已清除 code 和 userId')
} else {
// 页面刷新(persisted === true),保留 code
console.log('页面刷新,保留 code')
}
}
// 处理页面卸载事件(作为备用方案,某些浏览器可能不支持 pagehide)
function handleBeforeUnload(_event: BeforeUnloadEvent) { function handleBeforeUnload(_event: BeforeUnloadEvent) {
localStorage.remove('code') // 注意:beforeunload 无法区分刷新和关闭
localStorage.remove('trialCode') // 主要依赖 pagehide 事件来处理
// 这里不做任何操作,避免误清除刷新时的 code
} }
// 在应用挂载前设置事件监听器
// 优先使用 pagehide 事件(可以区分刷新和关闭)
// 在支持的浏览器中,这是最可靠的方法
window.addEventListener('pagehide', handlePageHide)
// 监听 beforeunload 作为备用(某些旧浏览器可能不支持 pagehide)
window.addEventListener('beforeunload', handleBeforeUnload)
app.mount('#app') app.mount('#app')

60
src/router/cartoon.ts

@ -0,0 +1,60 @@
// https://router.vuejs.org/zh/
import 'nprogress/nprogress.css'
import cartoon from '@/views/cartoon/index.vue'
import previewOrder from '@/views/cartoon/previewOrder.vue'
import photoAlbum from '@/views/cartoon/photoAlbum.vue'
import myOrder from '@/views/cartoon/myOrder.vue'
import orderDetail from '@/views/cartoon/orderDetail.vue'
// 定义路由,每个路由都需要映射到一个组件
export const routes = [
{
path: '/cartoon',
name: 'cartoon',
component: cartoon,
meta: {
needGuard: true,
title: '卡通手办',
},
},
{
path: '/cartoon/previewOrder',
name: 'previewOrder',
component: previewOrder,
meta: {
needGuard: true,
title: '预览效果图',
},
},
{
path: '/cartoon/photoAlbum',
name: 'photoAlbum',
component: photoAlbum,
meta: {
needGuard: true,
title: '设计记录',
},
},
{
path: '/cartoon/myOrder',
name: 'myOrder',
component: myOrder,
meta: {
needGuard: true,
title: '我的订单',
},
},
{
path: '/cartoon/orderDetail',
name: 'orderDetail',
component: orderDetail,
meta: {
needGuard: true,
title: '订单详情',
},
}
]
export function mergeRoutes(allRoutes: any[]) {
allRoutes.push(...routes)
}

36
src/router/index.ts

@ -3,8 +3,10 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import 'nprogress/nprogress.css' import 'nprogress/nprogress.css'
// 导入路由组件 // 导入路由组件
import * as badgeRouter from './badge' // import * as badgeRouter from './badge'
import badge from '@/views/badge/index.vue' import * as cartoonRouter from './cartoon'
// import badge from '@/views/badge/index.vue'
import cartoon from '@/views/cartoon/index.vue'
import {useStore} from '@/stores' import {useStore} from '@/stores'
import { config } from '@/config/config' import { config } from '@/config/config'
import { localStorage } from '@/utils/local-storage' import { localStorage } from '@/utils/local-storage'
@ -15,8 +17,8 @@ NProgress.configure({ showSpinner: true, parent: '#app' })
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: 'badge', name: 'cartoon',
component: badge, component: cartoon,
meta: { meta: {
needGuard: true, needGuard: true,
title: '首页', title: '首页',
@ -24,7 +26,8 @@ const routes = [
}, },
] ]
badgeRouter.mergeRoutes(routes) // badgeRouter.mergeRoutes(routes)
cartoonRouter.mergeRoutes(routes)
// 创建路由实例并传递 `routes` 配置 // 创建路由实例并传递 `routes` 配置
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(process.env.VUE_APP_PUBLIC_PATH), history: createWebHashHistory(process.env.VUE_APP_PUBLIC_PATH),
@ -35,18 +38,25 @@ router.beforeEach((_to, _from, next) => {
// const tokenStore = useTokenStore() // const tokenStore = useTokenStore()
const store = useStore() const store = useStore()
const code = _to.query.code const code = _to.query.code
if (code) {
localStorage.set('code', code as string); // 如果 URL 中有 code 参数,保存到 localStorage(用于刷新时保留)
if (code && typeof code === 'string') {
localStorage.set('code', code)
console.log('保存 code 到 localStorage:', code)
} }
const trialCode = _to.query.trialCode
if (trialCode) { // 如果 URL 中没有 code,尝试从 localStorage 读取(用于刷新时恢复)
localStorage.set('trialCode', trialCode as string); const savedCode = localStorage.get('code')
if (!code && savedCode) {
console.log('从 localStorage 恢复 code:', savedCode)
} }
console.log('code===>', code);
if (code || trialCode) { console.log('当前 code===>', code || savedCode)
if (code) {
let url = store.redirectUrl() let url = store.redirectUrl()
if (_to.path !== '/badge') { if (_to.path !== '/cartoon') {
if (url) { if (url) {
store.setRedirect('') store.setRedirect('')
next(url) next(url)

14
src/utils/request.ts

@ -50,18 +50,18 @@ const errorHandler = (error: RequestError): Promise<any> => {
// 请求拦截器 // 请求拦截器
const requestHandler = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig> => { const requestHandler = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig> => {
// const savedToken = localStorage.get(STORAGE_TOKEN_KEY) // 从 localStorage 读取 code(刷新时也能获取到)
// 如果 token 存在
const code = localStorage.get('code') const code = localStorage.get('code')
const trialCode = localStorage.get('trialCode')
const userId = localStorage.get('userId') const userId = localStorage.get('userId')
// 让每个请求携带自定义 token, 请根据实际情况修改 // 让每个请求携带自定义 token, 请根据实际情况修改
if (code) if (code) {
config.headers['Authorization'] = code config.headers['Authorization'] = code
if (trialCode) }
config.headers['Authorization'] = trialCode if (userId) {
if (userId)
config.headers['Userid'] = userId config.headers['Userid'] = userId
}
return config return config
} }

9
src/views/badge/index.vue

@ -710,7 +710,7 @@ const getPid = async () => {
try { try {
const uploadTasks = [ const uploadTasks = [
sendToOss(imgurl.value, res.url), sendToOss(imgurl.value, res.url),
sendToOss(imgurl.value, res.oss_url) // sendToOss(imgurl.value, res.oss_url)
] ]
await Promise.all(uploadTasks) await Promise.all(uploadTasks)
if (typeId.value == 4 && prodId.value == 19 && referPicture.value.length > 0) { if (typeId.value == 4 && prodId.value == 19 && referPicture.value.length > 0) {
@ -898,9 +898,10 @@ function Encrypt(word: string): string {
return encrypted.ciphertext.toString(); return encrypted.ciphertext.toString();
} }
function handleBeforeUnload(_event: BeforeUnloadEvent) { function handleBeforeUnload(_event: BeforeUnloadEvent) {
localStorage.remove('code') // code main.ts userId
localStorage.remove('trialCode') // userId
localStorage.remove('userId') // main.ts
// localStorage.remove('userId')
} }
onMounted(() => { onMounted(() => {
getSundryList() getSundryList()

5
src/views/badge/preview.vue

@ -1216,12 +1216,11 @@ const getOrderStat = () => {
} }
function handleBeforeUnload(_event: BeforeUnloadEvent) { function handleBeforeUnload(_event: BeforeUnloadEvent) {
localStorage.remove('code') // code main.ts
localStorage.remove('trialCode')
localStorage.remove('userId')
// //
clearInterval(timer.value) clearInterval(timer.value)
clearInterval(progressTimer.value) clearInterval(progressTimer.value)
// trialCode userId main.ts
} }
const pid = ref(0) const pid = ref(0)

1165
src/views/cartoon/index.vue

File diff suppressed because it is too large Load Diff

156
src/views/cartoon/myOrder.vue

@ -0,0 +1,156 @@
<template>
<div class="my-order">
<div class="header" @click="goBack">
<span class="back-icon"><van-icon name="arrow-left" size="16px" color="#333" /> {{ toValueWithout(config.myOrder.messages.title) }}</span>
</div>
<div class="order-list" v-if="orders.length > 0">
<div class="order-item" @click="OrderDetail(item.id)" v-for="item in orders" :key="item.id">
<img class="order-img" :src="item.path" alt="order1" />
<span class="order-status status-finished">{{ getStatus(item.status) }}</span>
</div>
</div>
<div v-else class="record-image-item-null">
<img :src="nullDataImage" :alt="toValueWithout(config.myOrder.messages.noData)">
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import * as badgeApi from '@/api/badge'
import { onMounted, ref } from 'vue'
import { toValueWithout } from '@/lang/utils'
import { cartoonConfig as config } from '@/config/cartoon'
import nullDataImage from '@/assets/badge/null-data.png'
const router = useRouter()
const page = ref(config.myOrder.pagination.initialPage)
const loading = ref(false)
const finished = ref(false)
const orders = ref([])
onMounted(() => {
getOrder()
})
function getOrder() {
if(loading.value) return
loading.value = true
badgeApi.getMyOrder({
page: page.value,
size: config.myOrder.pagination.pageSize,
status: config.myOrder.pagination.defaultStatus,
}).then(res => {
if(page.value === config.myOrder.pagination.initialPage) {
orders.value = res.list
} else {
orders.value.push(...res.list)
}
if(res.total <= orders.value.length) {
finished.value = true
} else {
page.value++
}
}).finally(() => {
loading.value = false
})
}
function goBack() {
router.push(config.routes.home)
}
function OrderDetail(id: number) {
router.push({
path: config.routes.orderDetail,
query: {
id: id,
},
})
}
function getStatus(status: number) {
const statusText = config.myOrder.statusText[status]
if (statusText) {
return toValueWithout(statusText)
}
return ''
}
</script>
<style scoped>
.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;
}
.order-list {
display: flex;
flex-wrap: wrap;
gap: v-bind('config.myOrder.styles.gap + "px"');
padding: v-bind('config.myOrder.styles.padding.vertical + "px"') v-bind('config.myOrder.styles.padding.horizontal + "px"');
}
.order-item {
position: relative;
width: v-bind('config.myOrder.styles.itemWidth');
height: v-bind('config.myOrder.styles.itemHeight');
box-shadow: v-bind('config.myOrder.styles.shadow');
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.order-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: v-bind('config.myOrder.styles.borderRadius + "px"');
}
.order-status {
position: absolute;
bottom: v-bind('config.myOrder.styles.status.bottom + "px"');
right: v-bind('config.myOrder.styles.status.right + "px"');
padding: v-bind('config.myOrder.styles.status.padding.vertical + "px"') v-bind('config.myOrder.styles.status.padding.horizontal + "px"');
border-radius: v-bind('config.myOrder.styles.status.borderRadius + "px"');
font-size: v-bind('config.myOrder.styles.status.fontSize + "px"');
color: v-bind('config.myOrder.statusStyles.default.color');
background: v-bind('config.myOrder.statusStyles.default.background');
opacity: v-bind('config.myOrder.statusStyles.default.opacity');
pointer-events: none;
}
.status-processing {
background: v-bind('config.myOrder.statusStyles.processing.background');
}
.status-finished {
background: v-bind('config.myOrder.statusStyles.finished.background');
}
.record-image-item-null {
height: 50vh;
display: flex;
justify-content: center;
align-items: center;
}
.record-image-item-null img {
width: 80vw;
height: 200px;
}
</style>

271
src/views/cartoon/orderDetail.vue

@ -0,0 +1,271 @@
<template>
<div class="my-order">
<div class="header" @click="goBack">
<span class="back-icon"><van-icon name="arrow-left" size="16px" color="#333" /> {{ toValueWithout(config.orderDetail.messages.title) }}</span>
</div>
<div class="order-status">
<div class="avatar-wrapper">
<div class="avatar-front-box" v-if="order.type_id != config.orderDetail.productTypes.reliefPendant">
<img class="avatar-font-img" :src="imageUrl" :alt="order.kind_name" />
</div>
<div class="avatar-front-box-double" v-if="order.type_id == config.orderDetail.productTypes.reliefPendant">
<img class="avatar-font-img" :src="imageUrl" />
<img class="avatar-font-img" :src="backImageUrl" />
</div>
</div>
</div>
<div class="status-info">
<div class="status-title">{{ getStatus(order.status) }}</div>
<div class="status-desc">
{{ toValueWithout(config.orderDetail.messages.statusDesc) }}
</div>
</div>
<div :style="`padding: 0 ${config.orderDetail.styles.padding.horizontal}px;`">
<van-divider />
</div>
<div class="product-info" v-if="order.use_type == config.orderDetail.orderTypes.shipping">
<div class="product-title">{{ toValueWithout(config.orderDetail.messages.shippingInfo) }}</div>
<div class="product-details">
<div class="product-row">
<span class="product-label">{{ toValueWithout(config.orderDetail.messages.contactInfo) }}</span>
<span class="product-value">{{order.contact_name}} {{order.contact_mobile}}</span>
</div>
<div class="product-row">
<span class="product-label">{{ toValueWithout(config.orderDetail.messages.shippingAddress) }}</span>
<span class="product-value">{{order.province_name}} {{order.city_name}} {{order.county_name}} {{order.address}}</span>
</div>
</div>
</div>
<div :style="`padding: 0 ${config.orderDetail.styles.padding.horizontal}px;`" v-if="order.use_type == config.orderDetail.orderTypes.shipping">
<van-divider />
</div>
<div class="product-info">
<div class="product-title">{{ toValueWithout(config.orderDetail.messages.productInfo) }}</div>
<div class="product-details">
<div class="product-row">
<span class="product-label">{{ toValueWithout(config.orderDetail.messages.productName) }}</span>
<span class="product-value" v-if="order.type_id && config.preview.productNames[order.type_id]">
{{ toValueWithout(config.preview.productNames[order.type_id][order.prod_id] || '') }}
</span>
</div>
<div class="product-row">
<span class="product-label">{{ toValueWithout(config.orderDetail.messages.size) }}</span>
<span class="product-value">{{order.model_size}}</span>
</div>
<div class="product-row" v-if="shape_name && order.type_id != config.orderDetail.productTypes.reliefPendant">
<span class="product-label">{{ toValueWithout(config.orderDetail.messages.shape) }}</span>
<span class="product-value">{{ toValueWithout(shape_name) }}</span>
</div>
<div class="product-row">
<span class="product-label">{{ toValueWithout(config.orderDetail.messages.quantity) }}</span>
<span class="product-value">{{order.count}}</span>
</div>
</div>
</div>
<div :style="`padding: 0 ${config.orderDetail.styles.padding.horizontal}px;`">
<van-divider />
</div>
<div class="product-info">
<div class="product-title">{{ toValueWithout(config.orderDetail.messages.orderInfo) }}</div>
<div class="product-details">
<div class="product-row">
<span class="product-label">{{ toValueWithout(config.orderDetail.messages.orderId) }}</span>
<span class="product-value">{{order.sw_oid}}</span>
</div>
<div class="product-row">
<span class="product-label">{{ toValueWithout(config.orderDetail.messages.orderTime) }}</span>
<span class="product-value">{{order.pay_at}}</span>
</div>
</div>
</div>
<div :style="`height: ${config.orderDetail.styles.bottomSpacing}px;`"></div>
</div>
</template>
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router';
import * as badgeApi from '@/api/badge';
import { onMounted, ref } from 'vue';
import { toValueWithout } from '@/lang/utils';
import { cartoonConfig as config } from '@/config/cartoon';
const route = useRoute();
const router = useRouter();
const order = ref({})
const id = ref(0)
onMounted(() => {
id.value = route.query.id as any
getOrderDetail()
})
const shapeImage = ref('')
const imageUrl = ref('')
const backImageUrl = ref('')
const getStyle = ref('')
const shape_name = ref('')
const imageList = ref([])
function getOrderDetail() {
badgeApi.getOrderDetail({
id: id.value,
}).then(res => {
order.value = res;
imageUrl.value = res.path;
backImageUrl.value = res.back_path;
shapeImage.value = res.shape_details?.frame_path || '';
shape_name.value = res.shape_details?.name || '';
imageList.value = [
res.path,
res.back_path,
];
})
}
const ImageShow = (item: any) => {
const scale = config.orderDetail.imageScale
const img = new Image()
img.src = item.frame_path
img.onload = () => {
console.log('img', img)
const ratioWidth = config.orderDetail.imageSize.width / img.width;
const ratioHeight = config.orderDetail.imageSize.height / 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;transform: rotate3D(1, 1, 0, 0deg)`
}
}
function goBack() {
router.back()
}
function getStatus(status: number) {
const statusText = config.myOrder.statusText[status]
if (statusText) {
return toValueWithout(statusText)
}
return ''
}
</script>
<style scoped>
.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;
}
.order-status {
margin: 0 auto;
width: 100vw;
height: 100vw;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.avatar-wrapper {
position: relative;
width: v-bind('config.orderDetail.imageSize.width + "px"');
height: v-bind('config.orderDetail.imageSize.height + "px"');
display: flex;
justify-content: center;
}
.avatar-back-img {
position: absolute;
width: v-bind('config.orderDetail.imageSize.width + "px"');
height: v-bind('config.orderDetail.imageSize.height + "px"');
top: 0;
left: 0;
z-index: 2;
}
.avatar-front-box {
position: absolute;
z-index: 1;
}
.avatar-font-img {
width: 100%;
height: auto;
max-height: v-bind('config.orderDetail.imageSize.height + "px"');
border-radius: v-bind('config.orderDetail.imageSize.borderRadius + "px"');
}
.avatar-front-box-double {
display: flex;
flex-direction: row;
gap: v-bind('config.orderDetail.styles.avatar.gap + "px"');
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.status-info {
display: flex;
flex-direction: column;
padding: v-bind('config.orderDetail.styles.statusInfo.padding.vertical + "px"') v-bind('config.orderDetail.styles.statusInfo.padding.horizontal + "px"');
}
.status-title {
font-size: v-bind('config.orderDetail.styles.statusInfo.title.fontSize + "px"');
font-weight: v-bind('config.orderDetail.styles.statusInfo.title.fontWeight');
color: v-bind('config.orderDetail.styles.statusInfo.title.color');
margin-bottom: v-bind('config.orderDetail.styles.statusInfo.title.marginBottom + "px"');
}
.status-desc {
font-size: v-bind('config.orderDetail.styles.statusInfo.desc.fontSize + "px"');
color: v-bind('config.orderDetail.styles.statusInfo.desc.color');
}
.product-info {
width: 100%;
padding: v-bind('config.orderDetail.styles.productInfo.padding.vertical + "px"') v-bind('config.orderDetail.styles.productInfo.padding.horizontal + "px"');
}
.product-title {
font-size: v-bind('config.orderDetail.styles.productInfo.title.fontSize + "px"');
font-weight: v-bind('config.orderDetail.styles.productInfo.title.fontWeight');
color: v-bind('config.orderDetail.styles.productInfo.title.color');
margin-bottom: v-bind('config.orderDetail.styles.productInfo.title.marginBottom + "px"');
}
.product-details {
display: flex;
flex-direction: column;
gap: v-bind('config.orderDetail.styles.productInfo.details.gap + "px"');
}
.product-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-label {
color: v-bind('config.orderDetail.styles.productInfo.label.color');
font-size: v-bind('config.orderDetail.styles.productInfo.label.fontSize + "px"');
}
.product-value {
color: v-bind('config.orderDetail.styles.productInfo.value.color');
font-size: v-bind('config.orderDetail.styles.productInfo.value.fontSize + "px"');
}
</style>

229
src/views/cartoon/photoAlbum.vue

@ -0,0 +1,229 @@
<template>
<div>
<div class="header" @click="goBack" style="text-align: center;">
<span class="back-icon"><van-icon name="arrow-left" size="16px" color="#333" /> {{ toValueWithout(config.photoAlbum.messages.title) }}</span>
</div>
<div class="record-container">
<div class="record-note">{{ toValueWithout(config.photoAlbum.messages.note) }}</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
:finished-text="toValueWithout(config.photoAlbum.messages.noMore)"
@load="onLoad"
>
<div class="record-images" v-if="images.length > 0">
<div class="record-image-item" v-for="item in images" :key="item.id">
<block v-if="item.status == config.photoAlbum.status.success" @click="goPreview(item)">
<img :src="item.path" :alt="item.name" class="record-image">
<div class="record-image-item-shadow"></div>
</block>
<div v-else-if="item.status == config.photoAlbum.status.designing || item.reason" class="record-image-item-loading">
<div style="font-size: 12px;color: #999;text-align: center;">ID: {{ item.sw_pid }}</div>
<div style="width: 70%;font-size: 12px;">{{ item.reason || toValueWithout(config.photoAlbum.messages.designing) }}</div>
</div>
<div v-else-if="item.status == config.photoAlbum.status.failed || item.reason" class="record-image-item-loading">
<div style="font-size: 12px;color: #999;text-align: center;">ID: {{ item.sw_pid }}</div>
<div style="width: 70%;font-size: 12px;">{{ item.reason || toValueWithout(config.photoAlbum.messages.designFailed) }}</div>
</div>
</div>
</div>
<div v-else-if="loading && page.value === config.photoAlbum.pagination.initialPage" class="record-image-item-loading">
{{ toValueWithout(config.photoAlbum.messages.loading) }}
</div>
<div v-else class="record-image-item-null">
<img :src="nullDataImage" :alt="toValueWithout(config.photoAlbum.messages.noData)">
</div>
</van-list>
</van-pull-refresh>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import * as badgeApi from '@/api/badge';
import { onMounted, ref, onUnmounted } from 'vue';
import { toValueWithout } from '@/lang/utils';
import { cartoonConfig as config } from '@/config/cartoon';
import nullDataImage from '@/assets/badge/null-data.png';
const router = useRouter();
const images = ref([]);
const page = ref(config.photoAlbum.pagination.initialPage);
const loading = ref(false);
const finished = ref(false);
const error = ref(null);
const refreshing = ref(false);
//
async function getList() {
//
console.log(loading.value, finished.value);
if (loading.value || finished.value) return;
loading.value = true;
error.value = null;
try {
const res = await badgeApi.getLogOage({
page: page.value,
size: config.photoAlbum.pagination.pageSize,
});
if (page.value === config.photoAlbum.pagination.initialPage) {
images.value = res.list || [];
} else {
images.value.push(...(res.list || []));
}
//
if (!res.list || res.list.length < config.photoAlbum.pagination.pageSize) {
finished.value = true;
} else {
page.value++;
}
} catch (err: any) {
error.value = err.message || toValueWithout(config.photoAlbum.messages.getDataFailed);
console.error('获取设计记录失败', err);
} finally {
loading.value = false;
}
}
//
function goPreview(item) {
router.push({
path: config.routes.preview,
query: {
pid: item.sw_pid,
group: item.group_count,
key: item.num,
prod_id: item.prod_id
},
});
}
//
function onLoad() {
loading.value = false;
getList();
}
//
async function onRefresh() {
refreshing.value = true;
page.value = config.photoAlbum.pagination.initialPage;
finished.value = false;
await getList();
refreshing.value = false;
}
//
function goBack() {
router.push('/cartoon');
}
//
onMounted(() => {
getList();
});
//
onUnmounted(() => {
loading.value = false;
finished.value = false;
page.value = config.photoAlbum.pagination.initialPage;
});
</script>
<style scoped>
.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;
}
.record-container {
padding: 16px;
}
.record-note {
font-size: 12px;
color: #999;
margin-bottom: 16px;
}
.record-images {
display: grid;
grid-template-columns: repeat(v-bind('config.photoAlbum.styles.gridColumns'), 1fr);
gap: v-bind('config.photoAlbum.styles.gap + "px"');
}
.record-image-item {
width: 100%;
padding-bottom: 100%;
position: relative;
overflow: hidden;
cursor: pointer;
border-radius: v-bind('config.photoAlbum.styles.borderRadius + "px"');
}
.record-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.record-image-item-shadow {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
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);
}
.record-image-item-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: #f5f5f5;
font-size: 16px;
color: #999;
text-align: center;
}
.record-image-item-null {
height: 50vh;
display: flex;
justify-content: center;
align-items: center;
}
.record-image-item-null img {
width: 80vw;
height: 200px;
}
</style>

1551
src/views/cartoon/previewOrder.vue

File diff suppressed because it is too large Load Diff

1
vite.config.ts

@ -144,6 +144,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
server: { server: {
host: true, host: true,
port: 80, port: 80,
// https: false,
https: true, https: true,
proxy, proxy,
// proxy: env.VITE_HTTP_MOCK === 'true' // proxy: env.VITE_HTTP_MOCK === 'true'

Loading…
Cancel
Save