Files
zt-closet-wxmini-frontend/miniprogram/pages/upload/upload.ts
T

259 lines
7.8 KiB
TypeScript

import { request, uploadFile, NOT_LOGIN_CODE } from '../../utils/request'
import { ProcessStatus } from '../../utils/config'
import { addPendingClothes, removePendingClothes } from '../../utils/pending-clothes'
import { navPageScroll } from '../../utils/nav'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'upload'
// 状态轮询:2s 一次、最多 90 次 ≈ 3 分钟,与后端单任务 3 分钟上限对齐。
// 预算耗尽只停轮询并转中性提示,绝不判定失败:任务可能仍在排队/执行中,
// 由后端恢复调度器兜底,最终结果由衣橱页核对补齐。
const POLL_INTERVAL_MS = 2000
const POLL_MAX_COUNT = 90
Page({
data: {
state: 'idle' as 'idle' | 'processing' | 'failed',
imagePath: '',
clothesUuid: '',
failReason: '',
step: 0,
// 轮询预算耗尽:仍在处理中,只做中性提示,不算失败
slowHint: false,
// 失败原因是否伴随额度退回(仅后端确认失败才为真)
quotaRefunded: false,
// 滚动收缩:由 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: '',
slowHint: false,
quotaRefunded: false,
})
uploadFile<{ clothes_uuid: string; image_process_status: number }>(filePath)
.then((res) => {
logger.info(LOG, '衣物上传成功, uuid=', res.clothes_uuid)
// 登记待核对:即使用户立刻退出小程序,回到衣橱时也会补齐最终处理结果
addPendingClothes(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: '',
slowHint: false,
quotaRefunded: false,
})
return
}
this.onDoneCancel()
},
pollStatus(uuid: string) {
// 先清旧定时器:onShow 续接时避免叠加成多份轮询
this.clearTimer()
let count = 0
;(this as any).pollTimer = setInterval(() => {
count++
request<{ image_process_status: number; fail_reason: string }>({
url: `/clothes/${uuid}/status`,
showError: false,
silentAuth: true,
})
.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()
removePendingClothes(uuid)
} else if (status === ProcessStatus.FAILED) {
// 仅后端确认失败才进入失败态:该路径额度已由后端自动退回
this.clearTimer()
removePendingClothes(uuid)
logger.warn(LOG, 'AI 识别失败, 原因=', res.fail_reason)
this.setData({
state: 'failed',
failReason: res.fail_reason || '识别失败',
quotaRefunded: true,
})
}
})
.catch((e: any) => logger.warn(LOG, '状态轮询失败, code=', e && e.code))
if (count >= POLL_MAX_COUNT) {
// 只停轮询,不判失败:任务可能仍在队列/执行中,由后端兜底、衣橱页核对
this.clearTimer()
logger.warn(LOG, '状态轮询预算耗尽,转中性提示, uuid=', uuid)
this.setData({ slowHint: true })
}
}, POLL_INTERVAL_MS)
},
clearTimer() {
const t = (this as any).pollTimer
if (t) {
clearInterval(t)
;(this as any).pollTimer = null
}
},
// 离开页面只停本地轮询:后端处理不中断,最终结果由衣橱页核对补齐
onHide() {
this.clearTimer()
},
// 回到前台:仍在处理中则续接轮询,恢复进度展示
onShow() {
if (this.data.state === 'processing' && this.data.clothesUuid) {
logger.debug(LOG, 'onShow 续接状态轮询, uuid=', this.data.clothesUuid)
this.pollStatus(this.data.clothesUuid)
}
},
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: '',
step: 0,
slowHint: false,
quotaRefunded: false,
})
},
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),
})
},
})