first commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { ClothesItem } from '../../types/models'
|
||||
import { ProcessStatus } from '../../utils/config'
|
||||
|
||||
const ATTRS = [
|
||||
{ key: 'category_main', label: '大类', type: 'text' },
|
||||
{ key: 'category_sub', label: '小类', type: 'text' },
|
||||
{ key: 'style', label: '风格', type: 'text' },
|
||||
{ key: 'color_main', label: '主色', type: 'color' },
|
||||
{ key: 'color_sub', label: '副色', type: 'text' },
|
||||
{ key: 'thickness', label: '厚度', type: 'text' },
|
||||
{ key: 'version', label: '版型', type: 'text' },
|
||||
{ key: 'material', label: '材质', type: 'text' },
|
||||
{ key: 'pattern', label: '花纹', type: 'text' },
|
||||
{ key: 'sleeve_type', label: '袖型', type: 'text' },
|
||||
{ key: 'clothes_type', label: '衣长', type: 'text' },
|
||||
{ key: 'neckline_type', label: '领型', type: 'text' },
|
||||
{ key: 'formality', label: '正式程度', type: 'score' },
|
||||
{ key: 'occasions', label: '适用场合', type: 'text' },
|
||||
{ key: 'visual_impact', label: '视觉冲击力', type: 'score' },
|
||||
]
|
||||
|
||||
// 将后端 image_process_status 映射为前端展示步骤:0=排队中 1=AI 抠图 2=智能分类
|
||||
function statusToStep(status: number): number {
|
||||
if (status === ProcessStatus.QUEUED) return 0
|
||||
if (status === ProcessStatus.MATTING) return 1
|
||||
if (status === ProcessStatus.CLASSIFYING) return 2
|
||||
return 2
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
uuid: '',
|
||||
attrs: ATTRS,
|
||||
detail: {} as ClothesItem,
|
||||
formData: {} as ClothesItem,
|
||||
imageStatus: 0,
|
||||
step: 0,
|
||||
failReason: '',
|
||||
loading: true,
|
||||
editMode: false,
|
||||
},
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
const uuid = query.uuid || ''
|
||||
this.setData({ uuid })
|
||||
if (uuid) {
|
||||
this.fetchDetail()
|
||||
this.pollStatus()
|
||||
}
|
||||
},
|
||||
|
||||
fetchDetail(): Promise<void> {
|
||||
return request<ClothesItem>({ url: `/clothes/${this.data.uuid}` })
|
||||
.then((detail) => this.setData({ detail, loading: false }))
|
||||
.catch(() => this.setData({ loading: false }))
|
||||
},
|
||||
|
||||
pollStatus() {
|
||||
let count = 0
|
||||
;(this as any).pollTimer = setInterval(() => {
|
||||
count++
|
||||
request<{ image_process_status: number; fail_reason?: string }>({
|
||||
url: `/clothes/${this.data.uuid}/status`,
|
||||
showError: false,
|
||||
})
|
||||
.then((res) => {
|
||||
const status = res.image_process_status
|
||||
if (status === ProcessStatus.SUCCESS) {
|
||||
// 成功:先拉最新详情,再切换为详情视图,避免空属性闪烁
|
||||
this.clearTimer()
|
||||
this.fetchDetail().then(() => this.setData({ imageStatus: status, step: 2 }))
|
||||
} else if (status === ProcessStatus.FAILED) {
|
||||
this.clearTimer()
|
||||
this.setData({ imageStatus: status, failReason: res.fail_reason || '识别失败' })
|
||||
} else {
|
||||
this.setData({ imageStatus: status, step: statusToStep(status) })
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
if (count >= 120) {
|
||||
this.clearTimer()
|
||||
}
|
||||
}, 2000)
|
||||
},
|
||||
|
||||
clearTimer() {
|
||||
const t = (this as any).pollTimer
|
||||
if (t) {
|
||||
clearInterval(t)
|
||||
;(this as any).pollTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this.clearTimer()
|
||||
},
|
||||
|
||||
onToggleEdit() {
|
||||
if (this.data.editMode) {
|
||||
this.setData({ editMode: false })
|
||||
} else {
|
||||
this.setData({ editMode: true, formData: { ...this.data.detail } })
|
||||
}
|
||||
},
|
||||
|
||||
onNameInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ 'formData.name': e.detail.value })
|
||||
},
|
||||
|
||||
onRemarkInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ 'formData.remark': e.detail.value })
|
||||
},
|
||||
|
||||
onAttrTap(e: WechatMiniprogram.TouchEvent) {
|
||||
if (!this.data.editMode) return
|
||||
const key = e.currentTarget.dataset.key as string
|
||||
const attr = ATTRS.find((a) => a.key === key)
|
||||
if (!attr) return
|
||||
const current = (this.data.formData as any)[key] || ''
|
||||
|
||||
if (attr.type === 'score') {
|
||||
wx.showActionSheet({
|
||||
itemList: ['1', '2', '3', '4', '5'],
|
||||
success: (res) => {
|
||||
this.setData({ [`formData.${key}`]: String(res.tapIndex + 1) })
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.showModal({
|
||||
title: `编辑${attr.label}`,
|
||||
editable: true,
|
||||
placeholderText: `请输入${attr.label}`,
|
||||
content: current,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.setData({ [`formData.${key}`]: res.content })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
onSave() {
|
||||
const fd = this.data.formData as any
|
||||
const body: Record<string, any> = {}
|
||||
const fields: Array<[string, string]> = [
|
||||
['name', 'name'],
|
||||
['remark', 'remark'],
|
||||
['category_main', 'category_main'],
|
||||
['category_sub', 'category_sub'],
|
||||
['style', 'style'],
|
||||
['color_main', 'color'], // 主色请求字段名是 color
|
||||
['color_sub', 'color_sub'],
|
||||
['thickness', 'thickness'],
|
||||
['version', 'version'],
|
||||
['material', 'material'],
|
||||
['pattern', 'pattern'],
|
||||
['sleeve_type', 'sleeve_type'],
|
||||
['clothes_type', 'clothes_type'],
|
||||
['neckline_type', 'neckline_type'],
|
||||
['formality', 'formality'],
|
||||
['occasions', 'occasions'],
|
||||
['visual_impact', 'visual_impact'],
|
||||
]
|
||||
fields.forEach(([src, dest]) => {
|
||||
if (fd[src]) body[dest] = fd[src]
|
||||
})
|
||||
|
||||
request({ url: `/clothes/${this.data.uuid}`, method: 'PUT', data: body })
|
||||
.then(() => {
|
||||
wx.showToast({ title: '已保存', icon: 'success' })
|
||||
this.setData({ editMode: false })
|
||||
this.fetchDetail()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
const canRefund = this.data.imageStatus !== ProcessStatus.FAILED
|
||||
const content = canRefund ? '删除后将退回 1 次上传额度,确认删除?' : '确认删除?'
|
||||
wx.showModal({
|
||||
title: '删除这件衣物',
|
||||
content,
|
||||
confirmText: '确认删除',
|
||||
confirmColor: '#FF3B30',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
request({ url: `/clothes/${this.data.uuid}`, method: 'DELETE' })
|
||||
.then(() => {
|
||||
wx.showToast({ title: '已删除', icon: 'success' })
|
||||
setTimeout(() => wx.navigateBack(), 500)
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
<view class="page detail-page">
|
||||
<navigation-bar title="衣物详情">
|
||||
<view
|
||||
wx:if="{{imageStatus !== 1 && imageStatus !== 2 && imageStatus !== 3}}"
|
||||
slot="right"
|
||||
class="nav-action"
|
||||
bindtap="onToggleEdit"
|
||||
>{{editMode ? '取消' : '编辑'}}</view>
|
||||
</navigation-bar>
|
||||
|
||||
<scroll-view scroll-y class="scroll-body">
|
||||
<!-- 处理中:显示步骤进度 -->
|
||||
<block wx:if="{{imageStatus === 1 || imageStatus === 2 || imageStatus === 3}}">
|
||||
<view class="card processing-card">
|
||||
<view class="proc-icon"><view class="icon icon-hanger"></view></view>
|
||||
<view class="steps">
|
||||
<view class="step">
|
||||
<view class="step-dot {{step >= 0 ? 'active' : ''}}"></view>
|
||||
<text class="step-text {{step >= 0 ? 'active-text' : ''}}">排队中</text>
|
||||
</view>
|
||||
<view class="step-line {{step >= 1 ? 'active' : ''}}"></view>
|
||||
<view class="step">
|
||||
<view class="step-dot {{step >= 1 ? 'active' : ''}}"></view>
|
||||
<text class="step-text {{step >= 1 ? 'active-text' : ''}}">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-text {{step >= 2 ? 'active-text' : ''}}">智能分类</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="proc-text">AI 正在识别你的衣物…</view>
|
||||
<view class="proc-desc">通常需要 10 秒左右,请稍候</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 识别失败 -->
|
||||
<block wx:elif="{{imageStatus === 5}}">
|
||||
<view class="img-box failed-box">
|
||||
<view class="fail-big-icon">!</view>
|
||||
</view>
|
||||
<view class="card name-card">
|
||||
<view class="name-text">{{detail.name || '未命名衣物'}}</view>
|
||||
</view>
|
||||
<view class="card fail-card">
|
||||
<view class="fail-title">识别失败:{{failReason || '未知原因'}}</view>
|
||||
<view class="fail-desc">本次上传额度已自动退回,你可以删除或重新上传</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 正常详情 -->
|
||||
<block wx:else>
|
||||
<!-- 大图区 -->
|
||||
<view class="img-box">
|
||||
<image
|
||||
wx:if="{{detail.processed_image}}"
|
||||
class="img"
|
||||
src="{{detail.processed_image}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view wx:else class="img-placeholder"><view class="icon icon-hanger"></view></view>
|
||||
</view>
|
||||
|
||||
<!-- 名称 -->
|
||||
<view class="card name-card">
|
||||
<input
|
||||
wx:if="{{editMode}}"
|
||||
class="input"
|
||||
value="{{formData.name}}"
|
||||
bindinput="onNameInput"
|
||||
placeholder="衣物名称"
|
||||
/>
|
||||
<view wx:else class="name-text">{{detail.name || '未命名衣物'}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 属性列表 -->
|
||||
<view class="card attr-card">
|
||||
<view
|
||||
class="attr-row hairline"
|
||||
wx:for="{{attrs}}"
|
||||
wx:key="key"
|
||||
data-key="{{item.key}}"
|
||||
bindtap="onAttrTap"
|
||||
>
|
||||
<view class="attr-label">{{item.label}}</view>
|
||||
<view class="attr-value">
|
||||
<block wx:if="{{item.type === 'score'}}">
|
||||
<view class="score-bar">
|
||||
<view class="score-fill" style="width: {{(detail[item.key] || 0) * 20}}%"></view>
|
||||
</view>
|
||||
<text class="score-num">{{detail[item.key] || 0}}/5</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<text class="attr-text">{{detail[item.key] || '—'}}</text>
|
||||
<view
|
||||
wx:if="{{item.key === 'color_main' && detail.color_main}}"
|
||||
class="color-dot"
|
||||
style="background: {{detail.color_main}}"
|
||||
></view>
|
||||
</block>
|
||||
</view>
|
||||
<view wx:if="{{editMode}}" class="icon icon-chevron-right edit-arrow"></view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="remark-row">
|
||||
<textarea
|
||||
wx:if="{{editMode}}"
|
||||
class="textarea"
|
||||
value="{{formData.remark}}"
|
||||
bindinput="onRemarkInput"
|
||||
placeholder="备注"
|
||||
/>
|
||||
<view wx:elif="{{detail.remark}}" class="remark-text">备注:{{detail.remark}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 底部操作:处理中隐藏,其他显示 -->
|
||||
<view
|
||||
wx:if="{{imageStatus !== 1 && imageStatus !== 2 && imageStatus !== 3}}"
|
||||
class="footer"
|
||||
>
|
||||
<block wx:if="{{editMode}}">
|
||||
<view class="btn btn-primary" bindtap="onSave">保存</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="btn-danger-text delete-btn" bindtap="onDelete">删除这件衣物</view>
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -0,0 +1,257 @@
|
||||
.detail-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.nav-action {
|
||||
font-size: 30rpx;
|
||||
color: #007AFF;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.img-box {
|
||||
margin: 24rpx;
|
||||
height: 640rpx;
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.img-placeholder {
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.img-placeholder .icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
.name-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.name-text {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.attr-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.attr-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 88rpx;
|
||||
}
|
||||
|
||||
.attr-label {
|
||||
width: 200rpx;
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.attr-value {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.attr-text {
|
||||
font-size: 28rpx;
|
||||
color: #3C3C43;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
margin-left: 16rpx;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
}
|
||||
|
||||
.score-bar {
|
||||
width: 160rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 4rpx;
|
||||
background: #F2F2F7;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.score-fill {
|
||||
height: 100%;
|
||||
background: #007AFF;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.score-num {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
margin-left: 16rpx;
|
||||
min-width: 60rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.edit-arrow {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: #C7C7CC;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.remark-row {
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
.remark-text {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 16rpx 24rpx 48rpx;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
/* 处理中 */
|
||||
.processing-card {
|
||||
margin: 24rpx;
|
||||
padding: 64rpx 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.proc-icon {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
background: #F2F2F7;
|
||||
color: #C7C7CC;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.proc-icon .icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.step-text.active-text {
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
background: #E5E5EA;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.step-dot.active {
|
||||
background: #007AFF;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
width: 100rpx;
|
||||
height: 2rpx;
|
||||
background: #E5E5EA;
|
||||
margin: 0 16rpx 34rpx;
|
||||
}
|
||||
|
||||
.step-line.active {
|
||||
background: #007AFF;
|
||||
}
|
||||
|
||||
.proc-text {
|
||||
margin-top: 48rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.proc-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* 识别失败 */
|
||||
.failed-box {
|
||||
background: #FFF3F3;
|
||||
}
|
||||
|
||||
.fail-big-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-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.fail-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.fail-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 12rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
Reference in New Issue
Block a user