feat: ui update

This commit is contained in:
2026-09-15 16:01:59 +08:00
parent 1a11816839
commit 26c392d612
89 changed files with 10377 additions and 4861 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
{
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
"navigation-bar": "/components/navigation-bar/navigation-bar",
"v2-overlay": "/components/v2-overlay/index"
}
}
+73 -45
View File
@@ -1,5 +1,6 @@
import { request, uploadFile, NOT_LOGIN_CODE } from '../../utils/request'
import { ProcessStatus } from '../../utils/config'
import { navPageScroll } from '../../utils/nav'
import { logger } from '../../utils/logger'
// 日志模块名
@@ -12,17 +13,42 @@ Page({
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() {
wx.showActionSheet({
itemList: ['拍照', '从相册选择'],
success: (res) => {
const sourceType: Array<'camera' | 'album'> = res.tapIndex === 0 ? ['camera'] : ['album']
this.chooseImage(sourceType)
},
fail: (e) => logger.debug(LOG, '取消选择图片, err=', e && e.errMsg),
})
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'>) {
@@ -66,33 +92,36 @@ Page({
// 后台继续轮询,仅用于更新步骤进度(不再自动跳详情)
this.pollStatus(this.data.clothesUuid)
// 立即弹窗让用户选择下一步,避免干等
wx.showModal({
title: '衣物已上传',
content: 'AI 正在识别中,可在衣橱查看进度',
confirmText: '继续上传',
cancelText: '回到衣橱',
success: (res) => {
this.clearTimer()
if (res.confirm) {
logger.debug(LOG, '选择继续上传,重置状态')
this.setData({ state: 'idle', imagePath: '', clothesUuid: '', step: 0, failReason: '' })
} else {
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),
})
},
})
}
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),
})
},
fail: (e) => logger.warn(LOG, '上传完成弹窗失败, err=', e && e.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(() => {
@@ -139,20 +168,19 @@ Page({
},
showQuotaModal() {
wx.showModal({
title: '上传额度不足',
content: '当前剩余 0 次,购买额度包后可继续上传',
confirmText: '去购买',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
wx.navigateTo({
url: '/pages/mall/mall',
fail: (e) => logger.warn(LOG, 'navigateTo 商城失败, err=', e && e.errMsg),
})
}
},
fail: (e) => logger.warn(LOG, '额度弹窗失败, err=', e && e.errMsg),
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),
})
},
+80 -35
View File
@@ -1,48 +1,93 @@
<view class="page upload-page">
<navigation-bar title="上传衣物" />
<view class="page up-page">
<navigation-bar title="上传衣物" scrolled="{{navScrolled}}" />
<!-- 初始态 -->
<view wx:if="{{state === 'idle'}}" class="idle">
<view class="upload-zone" bindtap="onChoose">
<view class="icon icon-camera upload-camera"></view>
<view class="upload-title">拍照或从相册选择</view>
<view class="upload-desc">支持 JPG / PNG,单张不超过 20MB</view>
<view class="upload-desc">上传消耗 1 次上传额度</view>
<!-- 初始态:巨大虚线发丝线方框 + 细描边相机 -->
<view wx:if="{{state === 'idle'}}">
<view class="head">
<view class="eyebrow">Upload</view>
<view class="display">上传衣物</view>
</view>
<view class="upload-tip">AI 将自动抠图并识别 15 项属性</view>
<view class="up-drop" hover-class="up-drop-press" bindtap="onChoose">
<view class="icon icon-camera up-drop-ic"></view>
<view class="up-drop-t">拍照或从相册选择</view>
<view class="up-drop-d">支持 JPG / PNG,单张不超过 20MB</view>
<view class="up-drop-d">上传消耗 1 次上传额度</view>
</view>
<view class="up-note">AI 将自动抠图并识别 15 项属性</view>
</view>
<!-- 处理中 -->
<view wx:elif="{{state === 'processing'}}" class="processing">
<image class="proc-img" src="{{imagePath}}" mode="aspectFit" />
<view class="steps">
<view class="step">
<view class="step-dot {{step >= 0 ? 'active' : ''}}"></view>
<text class="{{step >= 0 ? 'active-text' : ''}}">排队中</text>
<!-- 处理中:实际图片 + 三段发丝线进度轨(排队中 / AI 抠图 / 智能分类) -->
<view wx:elif="{{state === 'processing'}}">
<view class="head">
<view class="eyebrow">Processing</view>
<view class="display">AI 正在识别你的衣物…</view>
<view class="up-note-left">通常需要 10 秒左右,请稍候</view>
</view>
<view class="up-preview">
<image wx:if="{{imagePath}}" class="pic" src="{{imagePath}}" mode="aspectFit" />
<view wx:else class="ph icon icon-hanger"></view>
</view>
<view class="up-steps">
<view class="up-step {{step > 0 ? 'is-done' : ''}} {{step === 0 ? 'is-on' : ''}}">
<view class="up-dot"></view>
<text class="up-lb">排队中</text>
</view>
<view class="step-line {{step >= 1 ? 'active' : ''}}"></view>
<view class="step">
<view class="step-dot {{step >= 1 ? 'active' : ''}}"></view>
<text class="{{step >= 1 ? 'active-text' : ''}}">AI 抠图</text>
<view class="up-step {{step > 1 ? 'is-done' : ''}} {{step === 1 ? 'is-on' : ''}}">
<view class="up-dot"></view>
<text class="up-lb">AI 抠图</text>
</view>
<view class="step-line {{step >= 2 ? 'active' : ''}}"></view>
<view class="step">
<view class="step-dot {{step >= 2 ? 'active' : ''}}"></view>
<text class="{{step >= 2 ? 'active-text' : ''}}">智能分类</text>
<view class="up-step {{step === 2 ? 'is-on' : ''}}">
<view class="up-dot"></view>
<text class="up-lb">智能分类</text>
</view>
</view>
<view class="proc-text">AI 正在识别你的衣物…</view>
<view class="proc-desc">通常需要 10 秒左右,请稍候</view>
</view>
<!-- 失败态 -->
<view wx:else class="failed">
<view class="fail-icon">!</view>
<view class="fail-title">识别失败:{{failReason}}</view>
<view class="fail-desc">本次上传额度已自动退回</view>
<view class="fail-btns">
<view class="btn btn-primary" bindtap="onRetry">重新上传</view>
<view class="btn btn-outline" bindtap="onManualEdit">手动填写信息</view>
<!-- 失败态:极简「!」+ 衬线标题 + 双按钮 -->
<view wx:else class="up-failed">
<view class="up-bang">!</view>
<view class="head">
<view class="display">识别失败</view>
<view class="up-note-left">{{failReason || '未知原因'}} · 本次上传额度已自动退回</view>
</view>
<view class="up-duo">
<view class="btn btn--ghost" bindtap="onRetry">重新上传</view>
<view class="btn btn--ink" bindtap="onManualEdit">手动填写信息</view>
</view>
</view>
<!-- 图片来源选择 -->
<v2-overlay
visible="{{srcVisible}}"
type="actionsheet"
items="{{srcItems}}"
bind:select="onSourceSelect"
bind:cancel="onSourceCancel"
/>
<!-- 上传完成 -->
<v2-overlay
visible="{{doneVisible}}"
type="modal"
title="衣物已上传"
content="AI 正在识别中,可在衣橱查看进度"
buttons="{{doneButtons}}"
bind:confirm="onDoneConfirm"
bind:cancel="onDoneCancel"
/>
<!-- 额度不足 -->
<v2-overlay
visible="{{quotaVisible}}"
type="modal"
title="上传额度不足"
content="当前剩余 0 次,购买额度包后可继续上传"
buttons="{{quotaButtons}}"
bind:confirm="onQuotaConfirm"
bind:cancel="onQuotaCancel"
/>
</view>
+43 -131
View File
@@ -1,156 +1,68 @@
.upload-page {
padding: 32rpx;
box-sizing: border-box;
/** 上传衣物:虚线投放框 / 处理中三步进度轨 / 失败双按钮 **/
.up-page {
min-height: 100vh;
padding-bottom: calc(60rpx + env(safe-area-inset-bottom));
}
/* 初始态 */
.upload-zone {
margin-top: 40rpx;
height: 520rpx;
border: 3rpx dashed #C7C7CC;
border-radius: 32rpx;
background: #fff;
.up-drop {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: calc(100% - var(--pad) * 2);
margin: 56rpx var(--pad) 0;
padding: 108rpx 48rpx;
border: 1rpx dashed var(--hairline-2);
border-radius: var(--r-img);
transition: background .2s var(--ease), border-color .2s var(--ease);
}
.upload-camera {
width: 100rpx;
height: 100rpx;
color: #007AFF;
margin-bottom: 24rpx;
.up-drop-press {
background: var(--fill);
border-color: var(--ink-4);
}
.upload-title {
font-size: 30rpx;
color: #000;
.up-drop-ic {
width: 68rpx;
height: 68rpx;
color: var(--ink-3);
margin-bottom: 36rpx;
}
.up-drop-t {
font-family: var(--serif);
font-size: 34rpx;
font-weight: 500;
}
.upload-desc {
font-size: 22rpx;
color: #8E8E93;
margin-top: 12rpx;
.up-drop-d {
margin-top: 18rpx;
font-size: 23rpx;
color: var(--ink-3);
letter-spacing: .01em;
}
.upload-tip {
.up-note {
margin-top: 44rpx;
text-align: center;
font-size: 24rpx;
color: #8E8E93;
margin-top: 24rpx;
font-size: 23rpx;
color: var(--ink-3);
letter-spacing: .01em;
}
/* 处理中 */
.processing {
margin-top: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.proc-img {
width: 480rpx;
height: 480rpx;
border-radius: 32rpx;
background: #F2F2F7;
}
.steps {
display: flex;
align-items: center;
margin-top: 56rpx;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
font-size: 22rpx;
color: #C7C7CC;
}
.step-dot {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background: #E5E5EA;
margin-bottom: 12rpx;
}
.step-dot.active {
background: #007AFF;
}
.active-text {
color: #007AFF;
}
.step-line {
width: 100rpx;
height: 2rpx;
background: #E5E5EA;
margin: 0 16rpx 34rpx;
}
.step-line.active {
background: #007AFF;
}
.proc-text {
margin-top: 56rpx;
font-size: 30rpx;
font-weight: 500;
color: #000;
}
.proc-desc {
font-size: 24rpx;
color: #8E8E93;
margin-top: 12rpx;
.up-note-left {
margin-top: 20rpx;
font-size: 25rpx;
color: var(--ink-2);
line-height: 1.7;
}
/* 失败态 */
.failed {
margin-top: 120rpx;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 40rpx;
.up-failed {
padding-top: 40rpx;
}
.fail-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: #FF3B30;
color: #fff;
font-size: 72rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.fail-title {
margin-top: 40rpx;
font-size: 32rpx;
font-weight: 600;
color: #000;
text-align: center;
}
.fail-desc {
font-size: 24rpx;
color: #8E8E93;
margin-top: 16rpx;
}
.fail-btns {
width: 100%;
margin-top: 56rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
.up-failed .up-bang {
margin-left: var(--pad);
}