This commit is contained in:
2026-09-15 23:04:38 +08:00
parent 6be3f083cc
commit 592fa9be64
21 changed files with 1323 additions and 77 deletions
@@ -1,6 +1,7 @@
import { request, AUTH_EXPIRED_CODES } from '../../utils/request'
import { OutfitItem, ClothesItem } from '../../types/models'
import { IsPublic } from '../../utils/config'
import { OUTFIT_STATUS, OutfitStatusPill, isBanned, isPendingSave, outfitStatusPill } from '../../utils/outfit-status'
import { navScroll } from '../../utils/nav'
import { logger } from '../../utils/logger'
@@ -10,6 +11,14 @@ const LOG = 'outfit-detail'
// 接收者打开「分享者额度不足」链接后的自动返回倒计时(秒)
const SHARE_FAIL_BACK_SECONDS = 10
// 提示弹层按钮(type 缺省为非主按钮)
type OverlayButton = { text: string; value: string; type?: string }
const TIP_BUTTONS_ACK: OverlayButton[] = [{ text: '我知道了', value: 'ok', type: 'main' }]
const TIP_BUTTONS_TO_PENDING: OverlayButton[] = [
{ text: '知道了', value: '' },
{ text: '去 AI 穿搭', value: 'pending', type: 'main' },
]
Page({
data: {
uuid: '',
@@ -21,6 +30,12 @@ Page({
likeCount: 0,
isCollected: false,
loading: true,
// 状态派生标记(WXML 不能调用方法,取数后一次算好):
// pendingSave=待保存的 AI 推荐(status=3,可直接保存);banned=已封禁(status=2,只读)
pendingSave: false,
banned: false,
statePill: { text: '仅自己可见', cls: '' } as OutfitStatusPill,
saving: false,
// 页面状态:normal 正常;quota 接收者命中「分享者额度不足」;invalid 加载失败
pageState: 'normal' as 'normal' | 'quota' | 'invalid',
invalidTitle: '',
@@ -33,7 +48,7 @@ Page({
tipVisible: false,
tipTitle: '',
tipText: '',
tipButtons: [{ text: '我知道了', value: 'ok', type: 'main' }],
tipButtons: TIP_BUTTONS_ACK,
pubVisible: false,
pubButtons: [
{ text: '取消', value: '' },
@@ -88,15 +103,31 @@ Page({
pageState: 'quota',
loading: false,
countdown: SHARE_FAIL_BACK_SECONDS,
tipVisible: true,
tipTitle: '提示',
tipText: '给您分享的好友额度不足哦',
})
this.showTip('提示', '给您分享的好友额度不足哦')
this.startBackCountdown()
},
onTipConfirm() {
// 统一提示出口:标题 + 文案 +(可选)去 AI 穿搭的出口
showTip(title: string, text: string, toPending = false) {
this.setData({
tipVisible: true,
tipTitle: title,
tipText: text,
tipButtons: toPending ? TIP_BUTTONS_TO_PENDING : TIP_BUTTONS_ACK,
})
},
onTipConfirm(e: WechatMiniprogram.CustomEvent) {
this.setData({ tipVisible: false })
if (e && e.detail && e.detail.value === 'pending') this.onAiPending()
},
onAiPending() {
wx.navigateTo({
url: '/pages/ai-pending/ai-pending',
fail: (err) => logger.warn(LOG, 'navigateTo AI 穿搭失败, err=', err && err.errMsg),
})
},
startBackCountdown() {
@@ -180,8 +211,21 @@ Page({
return
}
logger.debug(LOG, '穿搭详情加载成功, 单品数=', outfit.items && outfit.items.length)
this.setData({ outfit, loading: false })
logger.debug(
LOG,
'穿搭详情加载成功, 单品数=',
outfit.items && outfit.items.length,
'status=',
outfit.status
)
// 状态口径统一由 utils/outfit-status.ts 决定:待保存可直接保存、封禁只读
this.setData({
outfit,
loading: false,
pendingSave: isPendingSave(outfit),
banned: isBanned(outfit),
statePill: outfitStatusPill(outfit),
})
// 封面与衣物缩略图直接使用穿搭明细里的抠图(processed_image),
// 避免非属主逐件请求 /clothes/:uuid 被拒(50005)导致图片与名称全部丢失。
@@ -242,6 +286,37 @@ Page({
.catch((e: any) => logger.warn(LOG, '收藏操作失败, action=', action, 'code=', e && e.code))
},
// 待保存的 AI 推荐:详情页直接保存为正式穿搭(status 3 → 1),不必回 AI 穿搭页
onSaveAsOutfit() {
if (this.data.saving) return
this.setData({ saving: true })
logger.info(LOG, '保存待保存穿搭, uuid=', this.data.uuid)
request({
url: '/ai-recommend/save',
method: 'POST',
data: { outfit_uuid: this.data.uuid },
showError: false,
})
.then(() => {
const outfit = { ...this.data.outfit, status: OUTFIT_STATUS.NORMAL }
logger.info(LOG, '待保存穿搭保存成功, uuid=', this.data.uuid)
this.setData({ saving: false, outfit, pendingSave: false, statePill: outfitStatusPill(outfit) })
wx.showToast({ title: '已保存', icon: 'success' })
})
.catch((e: any) => {
this.setData({ saving: false })
const code = e && e.code
logger.warn(LOG, '待保存穿搭保存失败, code=', code)
if (code === 40000) {
// 超过 7 天被后端定时清理:给出明确出口,不让用户反复点必然失败的按钮
this.showTip('保存失败', '这套搭配已被清理,请重新生成。', true)
return
}
const detail = (e && e.details && e.details[0]) || (e && e.message) || ''
this.showTip('保存失败', detail || '请稍后重试')
})
},
onTogglePublic() {
const isPublic = this.data.outfit.is_public === IsPublic.PUBLIC
if (isPublic) {
@@ -270,7 +345,8 @@ Page({
})
.then(() => {
logger.info(LOG, '公开状态变更成功, isPublic=', isPublic)
this.setData({ 'outfit.is_public': isPublic })
const outfit = { ...this.data.outfit, is_public: isPublic }
this.setData({ outfit, statePill: outfitStatusPill(outfit) })
wx.showToast({ title: isPublic === IsPublic.PUBLIC ? '已公开' : '已取消公开', icon: 'none' })
})
.catch((e: any) => {
@@ -319,7 +395,7 @@ Page({
onDelConfirm(e: WechatMiniprogram.CustomEvent) {
this.setData({ delVisible: false })
if (e.detail.value !== 'ok') return
request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE' })
request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE', showError: false })
.then(() => {
logger.info(LOG, '穿搭删除成功')
wx.showToast({ title: '已删除', icon: 'success' })
@@ -331,7 +407,11 @@ Page({
500,
)
})
.catch((e2: any) => logger.warn(LOG, '穿搭删除失败, code=', e2 && e2.code))
.catch((e2: any) => {
logger.warn(LOG, '穿搭删除失败, code=', e2 && e2.code)
const detail = (e2 && e2.details && e2.details[0]) || (e2 && e2.message) || ''
this.showTip('删除失败', e2 && e2.code === 40000 ? '这套搭配已不存在' : detail || '请稍后重试')
})
},
onItemTap(e: WechatMiniprogram.TouchEvent) {
@@ -10,8 +10,9 @@ module.exports = { cls: cls }
<view class="page page-flex">
<navigation-bar title="{{outfit.outfit_name || '穿搭详情'}}" scrolled="{{navScrolled}}">
<!-- 封禁态只读:不提供编辑/删除入口 -->
<view
wx:if="{{isOwner && pageState === 'normal' && !loading}}"
wx:if="{{isOwner && pageState === 'normal' && !loading && !banned}}"
slot="right"
class="nav-more"
bindtap="onMore"
@@ -71,9 +72,11 @@ module.exports = { cls: cls }
<view class="od-tags">
<text wx:if="{{outfit.scene}}" class="pill pill--line">{{outfit.scene}}</text>
<text class="pill {{outfit.source === 2 ? 'pill--ink' : ''}}">{{outfit.source === 2 ? 'AI 推荐' : '自建'}}</text>
<text class="pill {{outfit.is_public === 2 ? 'pill--line' : ''}}">{{outfit.is_public === 2 ? '已公开' : '仅自己可见'}}</text>
<text class="pill {{statePill.cls}}">{{statePill.text}}</text>
</view>
<view class="t-bd od-desc" wx:if="{{outfit.outfit_desc}}">{{outfit.outfit_desc}}</view>
<view class="od-note" wx:if="{{banned}}">该穿搭已被封禁,暂不支持编辑、公开与分享</view>
<view class="od-note" wx:elif="{{pendingSave}}">AI 生成的推荐,保存后归入「AI 穿搭」的「已保存」</view>
</view>
<!-- 包含衣物:横滑单品 -->
@@ -115,7 +118,11 @@ module.exports = { cls: cls }
<view class="icon {{isCollected ? 'icon-star-fill' : 'icon-star'}}"></view>
</view>
<view class="spacer"></view>
<block wx:if="{{isOwner}}">
<!-- 待保存的 AI 推荐:唯一操作是保存;封禁态只读,隐藏管理操作与分享 -->
<block wx:if="{{isOwner && pendingSave}}">
<view class="btn btn--ink btn--sm" bindtap="onSaveAsOutfit">{{saving ? '保存中…' : '保存'}}</view>
</block>
<block wx:elif="{{isOwner && !banned}}">
<view class="btn btn--ghost btn--sm" bindtap="onTogglePublic">{{outfit.is_public === 2 ? '取消公开' : '公开到社区'}}</view>
<button class="od-share" open-type="share">
<view class="icon icon-share"></view>
@@ -125,7 +132,7 @@ module.exports = { cls: cls }
</view>
</block>
<!-- 提示(分享者额度不足) -->
<!-- 提示(分享者额度不足 / 保存失败) -->
<v2-overlay
visible="{{tipVisible}}"
type="modal"
@@ -30,6 +30,14 @@
margin-top: 26rpx;
}
/* 状态说明行:待保存 / 已封禁 */
.od-note {
margin-top: 22rpx;
font-size: 23rpx;
line-height: 1.6;
color: var(--ink-3);
}
.od-desc {
margin-top: 28rpx;
}