Files
zt-closet-wxmini-frontend/miniprogram/pages/upload/upload.ts
T
2026-09-15 16:01:59 +08:00

199 lines
5.9 KiB
TypeScript
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.
import { request, uploadFile, NOT_LOGIN_CODE } from '../../utils/request'
import { ProcessStatus } from '../../utils/config'
import { navPageScroll } from '../../utils/nav'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'upload'
Page({
data: {
state: 'idle' as 'idle' | 'processing' | 'failed',
imagePath: '',
clothesUuid: '',
failReason: '',
step: 0,
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
navScrolled: false,
// v2 玻璃弹层
srcVisible: false,
srcItems: [
{ text: '拍照', value: 'camera' },
{ text: '从相册选择', value: 'album' },
],
doneVisible: false,
doneButtons: [
{ text: '回到衣橱', value: 'back' },
{ text: '继续上传', value: 'again', type: 'main' },
],
quotaVisible: false,
quotaButtons: [
{ text: '取消', value: '' },
{ text: '去购买', value: 'ok', type: 'main' },
],
},
// 页面级滚动 → 导航收缩为玻璃小标题
onPageScroll: navPageScroll,
onChoose() {
this.setData({ srcVisible: true })
},
onSourceCancel() {
this.setData({ srcVisible: false })
},
onSourceSelect(e: WechatMiniprogram.CustomEvent) {
const value = e.detail.value as string
this.setData({ srcVisible: false })
if (value !== 'camera' && value !== 'album') return
this.chooseImage([value])
},
chooseImage(sourceType: Array<'camera' | 'album'>) {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType,
success: (res) => {
const file = res.tempFiles[0]
logger.debug(LOG, '已选择图片, 来源=', sourceType.join(','))
this.startUpload(file.tempFilePath)
},
fail: (e) => logger.warn(LOG, '选择图片失败, err=', e && e.errMsg),
})
},
startUpload(filePath: string) {
this.setData({ state: 'processing', imagePath: filePath, step: 0, failReason: '' })
uploadFile<{ clothes_uuid: string; image_process_status: number }>(filePath)
.then((res) => {
logger.info(LOG, '衣物上传成功, uuid=', res.clothes_uuid)
this.setData({ clothesUuid: res.clothes_uuid })
this.askNextAction()
})
.catch((e: any) => {
this.setData({ state: 'idle' })
logger.warn(LOG, '衣物上传失败, code=', e && e.code)
if (e && e.code === 80000) {
this.showQuotaModal()
return
}
// uploadFile 已统一 toast 业务错误与网络错误;未登录时请求并未发出,需补一次提示
if (e && e.code === NOT_LOGIN_CODE) {
wx.showToast({ title: '请先登录', icon: 'none' })
}
})
},
askNextAction() {
// 后台继续轮询,仅用于更新步骤进度(不再自动跳详情)
this.pollStatus(this.data.clothesUuid)
// 立即弹窗让用户选择下一步,避免干等
this.setData({ doneVisible: true })
},
onDoneCancel() {
// 回到衣橱
this.setData({ doneVisible: false })
this.clearTimer()
wx.navigateBack({
delta: 1,
fail: (e) => {
logger.warn(LOG, 'navigateBack 失败,改跳衣橱, err=', e && e.errMsg)
wx.switchTab({
url: '/pages/wardrobe/wardrobe',
fail: (e2) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e2 && e2.errMsg),
})
},
})
},
onDoneConfirm(e: WechatMiniprogram.CustomEvent) {
this.setData({ doneVisible: false })
this.clearTimer()
if (e.detail.value === 'again') {
logger.debug(LOG, '选择继续上传,重置状态')
this.setData({ state: 'idle', imagePath: '', clothesUuid: '', step: 0, failReason: '' })
return
}
this.onDoneCancel()
},
pollStatus(uuid: string) {
let count = 0
;(this as any).pollTimer = setInterval(() => {
count++
request<{ image_process_status: number; fail_reason: string }>({
url: `/clothes/${uuid}/status`,
showError: false,
})
.then((res) => {
const status = res.image_process_status
if (status === ProcessStatus.MATTING) {
this.setData({ step: 1 })
} else if (status === ProcessStatus.CLASSIFYING) {
this.setData({ step: 2 })
} else if (status === ProcessStatus.SUCCESS) {
// 处理成功不再自动跳转,由用户自行从衣橱进入详情
this.clearTimer()
} else if (status === ProcessStatus.FAILED) {
this.clearTimer()
logger.warn(LOG, 'AI 识别失败, 原因=', res.fail_reason)
this.setData({ state: 'failed', failReason: res.fail_reason || '识别失败' })
}
})
.catch((e: any) => logger.warn(LOG, '状态轮询失败, code=', e && e.code))
if (count >= 60) {
this.clearTimer()
logger.warn(LOG, 'AI 处理超时(90s)')
this.setData({ state: 'failed', failReason: '处理超时,请重试' })
}
}, 1500)
},
clearTimer() {
const t = (this as any).pollTimer
if (t) {
clearInterval(t)
;(this as any).pollTimer = null
}
},
onUnload() {
this.clearTimer()
},
showQuotaModal() {
this.setData({ quotaVisible: true })
},
onQuotaCancel() {
this.setData({ quotaVisible: false })
},
onQuotaConfirm(e: WechatMiniprogram.CustomEvent) {
this.setData({ quotaVisible: false })
if (e.detail.value !== 'ok') return
wx.navigateTo({
url: '/pages/mall/mall',
fail: (err) => logger.warn(LOG, 'navigateTo 商城失败, err=', err && err.errMsg),
})
},
onRetry() {
this.setData({ state: 'idle', imagePath: '', clothesUuid: '', failReason: '' })
},
onManualEdit() {
if (!this.data.clothesUuid) return
wx.redirectTo({
url: `/pages/clothes-detail/clothes-detail?uuid=${this.data.clothesUuid}`,
fail: (e) => logger.warn(LOG, 'redirectTo 衣物详情失败, err=', e && e.errMsg),
})
},
})