feat(穿搭详情): 支持社区公开穿搭只读访问单品

非属主查看社区公开穿搭时携带 outfit_uuid 凭证请求单品详情,
放行后端只读读取,避免名称退化为「未命名」;单品详情页同步
支持 outfit_uuid 只读凭证。点赞增加防连点,来源胶囊与状态
胶囊文案重复时只保留一个。
This commit is contained in:
2026-09-16 02:28:58 +08:00
parent ac271493c0
commit fb92782c92
3 changed files with 49 additions and 32 deletions
@@ -104,8 +104,9 @@ function statusToStep(status: number): number {
Page({ Page({
data: { data: {
uuid: '', uuid: '',
// 来自穿搭分享的只读查看:携带 share_uuid,隐藏编辑/删除 // 只读查看的两个凭证:share_uuid(穿搭分享)/ outfit_uuid(社区公开穿搭),命中即隐藏编辑/删除
shareUUID: '', shareUUID: '',
outfitUUID: '',
readonly: false, readonly: false,
attrGroups: ATTR_GROUPS, attrGroups: ATTR_GROUPS,
colorPresets: COLOR_PRESETS, colorPresets: COLOR_PRESETS,
@@ -145,9 +146,10 @@ Page({
onLoad(query: Record<string, string>) { onLoad(query: Record<string, string>) {
const uuid = query.uuid || '' const uuid = query.uuid || ''
const shareUUID = query.share_uuid || '' const shareUUID = query.share_uuid || ''
const readonly = !!shareUUID const outfitUUID = query.outfit_uuid || ''
const readonly = !!(shareUUID || outfitUUID)
logger.debug(LOG, 'onLoad, uuid=', uuid, 'readonly=', readonly) logger.debug(LOG, 'onLoad, uuid=', uuid, 'readonly=', readonly)
this.setData({ uuid, shareUUID, readonly }) this.setData({ uuid, shareUUID, outfitUUID, readonly })
if (!uuid) { if (!uuid) {
// uuid 缺失时不请求不轮询,否则会停在空白页且无法退出 // uuid 缺失时不请求不轮询,否则会停在空白页且无法退出
logger.warn(LOG, 'onLoad 缺少 uuid 参数') logger.warn(LOG, 'onLoad 缺少 uuid 参数')
@@ -155,7 +157,7 @@ Page({
return return
} }
this.fetchDetail().then(() => { this.fetchDetail().then(() => {
// 只读(来自穿搭分享)场景无需轮询 AI 处理状态 // 只读(来自穿搭分享 / 社区公开穿搭)场景无需轮询 AI 处理状态
if (readonly) return if (readonly) return
// 详情响应已带处理状态:成功/失败等终态直接定态,仅处理中才轮询 // 详情响应已带处理状态:成功/失败等终态直接定态,仅处理中才轮询
if (isProcessTerminal(this.data.detail)) return if (isProcessTerminal(this.data.detail)) return
@@ -171,10 +173,10 @@ Page({
}, },
fetchDetail(): Promise<void> { fetchDetail(): Promise<void> {
// 只读场景携带 share_uuid,后端据此放行非属主读取衣物详情 // 只读场景需携带放行凭证:穿搭分享用 share_uuid,社区公开穿搭用 outfit_uuid
const url = this.data.readonly let url = `/clothes/${this.data.uuid}`
? `/clothes/${this.data.uuid}?share_uuid=${this.data.shareUUID}` if (this.data.shareUUID) url += `?share_uuid=${this.data.shareUUID}`
: `/clothes/${this.data.uuid}` else if (this.data.outfitUUID) url += `?outfit_uuid=${this.data.outfitUUID}`
return request<ClothesItem>({ url }) return request<ClothesItem>({ url })
.then((detail) => { .then((detail) => {
logger.debug(LOG, '详情加载成功, name=', detail && detail.name) logger.debug(LOG, '详情加载成功, name=', detail && detail.name)
@@ -227,27 +227,30 @@ Page({
statePill: outfitStatusPill(outfit), statePill: outfitStatusPill(outfit),
}) })
// 封面与衣物缩略图直接使用穿搭明细里的抠图(processed_image), // 封面与衣物缩略图先直接使用穿搭明细里的抠图(processed_image),
// 避免非属主逐件请求 /clothes/:uuid 被拒(50005)导致图片与名称全部丢失。 // 即使后续单品详情拉取失败,图片也不会丢。
const baseClothes = (outfit.items || []).map( const baseClothes = (outfit.items || []).map(
(it) => ({ clothes_uuid: it.clothes_uuid, processed_image: it.processed_image }) as ClothesItem, (it) => ({ clothes_uuid: it.clothes_uuid, processed_image: it.processed_image }) as ClothesItem,
) )
this.setData({ clothes: baseClothes }) this.setData({ clothes: baseClothes })
// 属主可进一步拉取衣物详情补充名称;非属主无权限,跳过。 // 拉取单品详情补充名称与属性:属主直接读;非属主(社区公开穿搭 / 他人分享)携带本穿搭 uuid,
if (this.data.isOwner) { // 由后端只读放行,避免被拒(50005)导致名称全部退化成「未命名」。
const clothes = await Promise.all( const clothesQuery = this.data.isOwner ? '' : `?outfit_uuid=${this.data.uuid}`
(outfit.items || []).map(async (it) => { const clothes = await Promise.all(
try { (outfit.items || []).map(async (it) => {
return await request<ClothesItem>({ url: `/clothes/${it.clothes_uuid}`, showError: false }) try {
} catch (e: any) { return await request<ClothesItem>({
logger.warn(LOG, '单品详情拉取失败, uuid=', it.clothes_uuid, 'code=', e && e.code) url: `/clothes/${it.clothes_uuid}${clothesQuery}`,
return { clothes_uuid: it.clothes_uuid, processed_image: it.processed_image } as ClothesItem showError: false,
} })
}) } catch (e: any) {
) logger.warn(LOG, '单品详情拉取失败, uuid=', it.clothes_uuid, 'code=', e && e.code)
this.setData({ clothes }) return { clothes_uuid: it.clothes_uuid, processed_image: it.processed_image } as ClothesItem
} }
})
)
this.setData({ clothes })
// 点赞状态:失败静默保持默认,不影响详情展示 // 点赞状态:失败静默保持默认,不影响详情展示
request<{ is_liked: boolean; like_count: number }>({ request<{ is_liked: boolean; like_count: number }>({
@@ -267,6 +270,12 @@ Page({
}, },
onLike() { onLike() {
// 防连点:请求进行中忽略后续点击,避免误触/连点并发发出多条点赞、取消请求
if ((this as any).liking) {
logger.debug(LOG, '点赞请求进行中,忽略重复点击')
return
}
;(this as any).liking = true
const action = this.data.isLiked ? 2 : 1 const action = this.data.isLiked ? 2 : 1
request({ url: '/like', method: 'POST', data: { target_uuid: this.data.uuid, action } }) request({ url: '/like', method: 'POST', data: { target_uuid: this.data.uuid, action } })
.then(() => { .then(() => {
@@ -277,6 +286,9 @@ Page({
}) })
}) })
.catch((e: any) => logger.warn(LOG, '点赞操作失败, action=', action, 'code=', e && e.code)) .catch((e: any) => logger.warn(LOG, '点赞操作失败, action=', action, 'code=', e && e.code))
.finally(() => {
;(this as any).liking = false
})
}, },
onCollect() { onCollect() {
@@ -416,14 +428,12 @@ Page({
onItemTap(e: WechatMiniprogram.TouchEvent) { onItemTap(e: WechatMiniprogram.TouchEvent) {
const uuid = e.currentTarget.dataset.uuid as string const uuid = e.currentTarget.dataset.uuid as string
// 非属主无权读取衣物详情(50005),点击仅提示;属主正常跳转。 // 非属主(社区公开穿搭 / 他人分享)携带本穿搭 uuid 进入只读详情,
if (!this.data.isOwner) { // 后端据此放行;不携带时后端会按无权限拒绝(50005)。
logger.debug(LOG, '非属主点击衣物,仅可查看穿搭, uuid=', uuid) const query = this.data.isOwner ? '' : `&outfit_uuid=${this.data.uuid}`
wx.showToast({ title: '仅可查看穿搭', icon: 'none' }) logger.debug(LOG, '点击单品, uuid=', uuid, 'isOwner=', this.data.isOwner)
return
}
wx.navigateTo({ wx.navigateTo({
url: `/pages/clothes-detail/clothes-detail?uuid=${uuid}`, url: `/pages/clothes-detail/clothes-detail?uuid=${uuid}${query}`,
fail: (err) => logger.warn(LOG, 'navigateTo 单品详情失败, err=', err && err.errMsg), fail: (err) => logger.warn(LOG, 'navigateTo 单品详情失败, err=', err && err.errMsg),
}) })
}, },
@@ -71,7 +71,12 @@ module.exports = { cls: cls }
<view class="t-xl od-title">{{outfit.outfit_name}}</view> <view class="t-xl od-title">{{outfit.outfit_name}}</view>
<view class="od-tags"> <view class="od-tags">
<text wx:if="{{outfit.scene}}" class="pill pill--line">{{outfit.scene}}</text> <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> <!-- 来源胶囊:AI 来源的文案已由状态胶囊承担(见 utils/outfit-status.ts),
两者相同时只保留一个,避免出现两个「AI 推荐」;待保存/已封禁等状态下仍保留来源标签 -->
<text
wx:if="{{statePill.text !== 'AI 推荐'}}"
class="pill {{outfit.source === 2 ? 'pill--ink' : ''}}"
>{{outfit.source === 2 ? 'AI 推荐' : '自建'}}</text>
<text class="pill {{statePill.cls}}">{{statePill.text}}</text> <text class="pill {{statePill.cls}}">{{statePill.text}}</text>
</view> </view>
<view class="t-bd od-desc" wx:if="{{outfit.outfit_desc}}">{{outfit.outfit_desc}}</view> <view class="t-bd od-desc" wx:if="{{outfit.outfit_desc}}">{{outfit.outfit_desc}}</view>