feat(guest): 支持游客态浏览示例衣物与搭配

游客可点开示例衣物详情与搭配抽屉,不再在浏览路径上要求登录。
This commit is contained in:
2026-09-17 11:41:42 +08:00
parent 45222317af
commit f0503c7ad7
30 changed files with 648 additions and 127 deletions
+36 -7
View File
@@ -95,9 +95,35 @@ Page({
this.setData({ scene: e.detail.value })
},
onSkipScene() {
this.setData({ showSceneSheet: false })
this.startGenerate(1, '')
// 取消场景选择:退出 AI 推荐页返回来源页(不再用「每日推荐」自动生成)
onCancelScene() {
logger.info(LOG, '取消场景选择,返回来源页')
this.backToSource()
},
// 取消的统一出口:优先返回来源页(从运势进回运势、从穿搭进回穿搭);
// 页面栈不足(分享卡片/深链直达)时兜底 switchTab 回运势页
backToSource() {
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
if (pages.length > 1) {
wx.navigateBack({
delta: 1,
fail: (e) => {
logger.warn(LOG, 'navigateBack 失败,兜底回运势页, err=', e && e.errMsg)
this.switchToHoroscope()
},
})
return
}
logger.warn(LOG, '页面栈不足,兜底回运势页, 栈深=', pages.length)
this.switchToHoroscope()
},
switchToHoroscope() {
wx.switchTab({
url: '/pages/horoscope/horoscope',
fail: (e) => logger.warn(LOG, 'switchTab 运势失败, err=', e && e.errMsg),
})
},
onStartGenerate() {
@@ -112,16 +138,19 @@ Page({
this.setData({ confirmVisible: true })
},
// 取消生成:回到场景选择抽屉,避免停在无任何可渲染内容的空页
// 取消生成:退出 AI 推荐页返回来源页(与抽屉「取消」语义一致,避免停在空页)
onConfirmCancel() {
this.setData({ confirmVisible: false, showSceneSheet: true })
logger.info(LOG, '确认弹窗取消,返回来源页')
this.setData({ confirmVisible: false })
this.backToSource()
},
onConfirmGenerate(e: WechatMiniprogram.CustomEvent) {
this.setData({ confirmVisible: false })
if (e.detail.value !== 'ok') {
// 兜底:非「开始生成」按钮同样回到场景抽屉(取消按钮已走 cancel 分支)
this.setData({ showSceneSheet: true })
// 兜底:非「开始生成」按钮同样按取消处理(取消按钮已走 cancel 分支)
logger.info(LOG, '确认弹窗非主按钮,按取消处理,返回来源页')
this.backToSource()
return
}
const pending = (this as any).pendingScene || { sceneType: 1, scene: '' }
@@ -95,13 +95,13 @@ module.exports = { cls: cls }
</view>
<!-- 场景选择抽屉 -->
<view wx:if="{{showSceneSheet}}" class="scrim" bindtap="onSkipScene"></view>
<view wx:if="{{showSceneSheet}}" class="scrim" bindtap="onCancelScene"></view>
<view wx:if="{{showSceneSheet}}" class="sheet-wrap">
<view class="sheet" catchtap="noop">
<view class="sheet-grab"></view>
<view class="sheet-hd">
<text class="sheet-t">选择推荐场景</text>
<text class="sheet-x" bindtap="onSkipScene">跳过</text>
<text class="sheet-x" bindtap="onCancelScene">取消</text>
</view>
<view class="sheet-body">
<view class="scene-card {{sceneType === 1 ? 'is-on' : ''}}" bindtap="selectDaily">
@@ -275,8 +275,9 @@ Page({
},
onItemTap(e: WechatMiniprogram.TouchEvent) {
// 游客态示例条目仅作视觉展示:浏览路径上不要求登录(登录入口在页脚)
if (this.data.guest) {
this.onGuestLogin()
logger.debug(LOG, '游客态点击示例分享条目,仅展示不跳转')
return
}
const uuid = e.currentTarget.dataset.uuid as string
@@ -3,6 +3,7 @@ import { ClothesItem } from '../../types/models'
import { ProcessStatus } from '../../utils/config'
import { isProcessTerminal } from '../../utils/clothes-status'
import { removePendingClothes } from '../../utils/pending-clothes'
import { findDamoClothes } from '../../damo/damo'
import { navScroll } from '../../utils/nav'
import { logger } from '../../utils/logger'
@@ -108,6 +109,8 @@ Page({
shareUUID: '',
outfitUUID: '',
readonly: false,
// 游客态示例:由本地 damo 素材渲染的只读详情,零请求
demo: false,
attrGroups: ATTR_GROUPS,
colorPresets: COLOR_PRESETS,
detail: {} as ClothesItem,
@@ -144,6 +147,25 @@ Page({
noop() {},
onLoad(query: Record<string, string>) {
const demo = query.demo || ''
// 示例模式:游客从衣橱示例卡片进入,本地数据只读渲染,不发请求、不轮询
if (demo) {
const detail = findDamoClothes(demo)
logger.info(LOG, '示例模式加载, demo=', demo, '命中=', !!detail)
if (!detail) {
this.setData({ demo: true, readonly: true, loading: false, notFound: true })
return
}
this.setData({
demo: true,
readonly: true,
uuid: detail.clothes_uuid,
detail,
loading: false,
})
return
}
const uuid = query.uuid || ''
const shareUUID = query.share_uuid || ''
const outfitUUID = query.outfit_uuid || ''
@@ -119,6 +119,12 @@ module.exports = { hex: hex, tags: tags }
<view wx:else class="ph icon icon-hanger"></view>
</view>
<!-- 示例内容说明:游客态只读浏览,不要求登录 -->
<view wx:if="{{demo}}" class="cd-demo">
<view class="icon icon-info"></view>
<text>示例内容 · 登录后可上传并识别你自己的衣物</text>
</view>
<!-- 名称:编辑态给标签 + 浅填充输入框,一眼看出可输入 -->
<view class="cd-name">
<block wx:if="{{editMode}}">
@@ -176,3 +176,22 @@
width: auto;
display: block;
}
/* 示例内容说明:游客态只读浏览(不引导、不打断) */
.cd-demo {
display: flex;
align-items: center;
gap: 12rpx;
margin: 28rpx var(--pad) 0;
font-size: 22rpx;
line-height: 1.5;
color: var(--ink-3);
letter-spacing: .01em;
}
.cd-demo .icon {
width: 28rpx;
height: 28rpx;
color: var(--ink-4);
flex-shrink: 0;
}
+27 -5
View File
@@ -8,7 +8,7 @@ import { OutfitStatusPill, isPendingSave, outfitStatusPill } from '../../utils/o
import { setTabBarSelected } from '../../utils/tabbar'
import { formatRelativeTime } from '../../utils/format'
import { navScroll } from '../../utils/nav'
import { GUEST_OUTFITS, goLogin } from '../../utils/guest'
import { GUEST_OUTFIT, goLogin } from '../../utils/guest'
import { logger } from '../../utils/logger'
// 日志模块名
@@ -45,9 +45,11 @@ Page({
pendingCount: 0,
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
navScrolled: false,
// 游客态(未登录):展示本地示例穿搭,不发需要授权的请求
// 游客态(未登录):只展示本地示例穿搭,不发需要授权的请求
guest: false,
guestList: GUEST_OUTFITS,
guestOutfit: GUEST_OUTFIT,
// 游客态示例搭配抽屉:可完整浏览示例单品,浏览路径上不要求登录
guestSheetVisible: false,
},
// scroll-view 滚动 → 导航收缩为玻璃小标题
@@ -72,7 +74,7 @@ Page({
if (this.data.guest) {
logger.info(LOG, '检测到登录态,从游客态切换为真实穿搭列表')
;(this as any).firstShown = true
this.setData({ guest: false, guestList: [], list: [], loading: true })
this.setData({ guest: false, guestSheetVisible: false, list: [], loading: true })
this.refresh()
this.fetchPendingCount()
return
@@ -116,12 +118,32 @@ Page({
this.refresh()
},
// 游客态点击示例穿搭 / 受限入口:统一进入登录页,由用户自行选择是否登录
// 游客态点击受限入口:进入登录页,由用户自行选择是否登录
onGuestLogin() {
logger.debug(LOG, '游客态点击受限入口,引导登录')
goLogin()
},
// 抽屉内容点击不穿透到遮罩
noop() {},
// 游客态点击示例搭配:打开只读示例抽屉(浏览路径上不要求登录)
onGuestOutfitTap() {
logger.debug(LOG, '游客态打开示例搭配抽屉')
this.setData({ guestSheetVisible: true })
},
onGuestSheetClose() {
this.setData({ guestSheetVisible: false })
},
// 抽屉底部入口:用户主动选择登录时才进入登录页
onGuestSheetLogin() {
logger.info(LOG, '示例搭配抽屉内主动登录')
this.setData({ guestSheetVisible: false })
goLogin()
},
refresh() {
this.setData({ page: 1, hasMore: true, loading: this.data.list.length === 0 })
this.fetchList(1)
+45 -12
View File
@@ -63,26 +63,25 @@
bind:action="onCreate"
/>
<!-- 游客态:本地示例穿搭(点击任一示例进入登录页) -->
<!-- 游客态:只展示一套本地示例搭配,点击可完整浏览(不要求登录) -->
<view wx:elif="{{guest}}" class="of-list">
<view class="of-card" wx:for="{{guestList}}" wx:key="key" bindtap="onGuestLogin">
<view class="collage collage--{{item.icons.length}}">
<view class="cg" wx:for="{{item.icons}}" wx:for-item="ic" wx:key="*this">
<view class="ph icon {{ic}}"></view>
<view class="of-card" bindtap="onGuestOutfitTap">
<view class="collage collage--2">
<view class="cg" wx:for="{{guestOutfit.items}}" wx:key="clothes_uuid">
<image class="pic" src="{{item.processed_image}}" mode="aspectFit" />
</view>
<view class="guest-flag">示例</view>
</view>
<view class="of-trow">
<text class="of-title">{{item.name}}</text>
<text class="pill pill--line">{{item.scene}}</text>
<text class="of-title">{{guestOutfit.name}}</text>
<text class="pill pill--line">{{guestOutfit.scene}}</text>
</view>
<view class="of-desc">{{item.desc}}</view>
<view class="of-desc">{{guestOutfit.desc}}</view>
<view class="of-meta">
<text class="of-m">{{item.icons.length}} 件衣物</text>
<text class="pill">示例</text>
<text class="of-m">{{guestOutfit.items.length}} 件衣物</text>
<view class="spacer"></view>
<view class="of-m">
<view class="icon icon-heart"></view>
<text class="num">-</text>
<view class="icon icon-chevron-right"></view>
</view>
</view>
</view>
@@ -151,4 +150,38 @@
<view class="btn btn--glass" bindtap="onCreate">创建穿搭</view>
<view class="btn btn--ink" bindtap="onAiRecommend">AI 推荐</view>
</view>
<!-- 游客态示例搭配抽屉:可完整浏览示例单品,登录仅由用户主动选择 -->
<view wx:if="{{guestSheetVisible}}" class="scrim" bindtap="onGuestSheetClose"></view>
<view wx:if="{{guestSheetVisible}}" class="sheet-wrap">
<view class="sheet" catchtap="noop">
<view class="sheet-grab"></view>
<view class="sheet-hd">
<text class="sheet-t">示例搭配</text>
<text class="sheet-x" bindtap="onGuestSheetClose">关闭</text>
</view>
<view class="sheet-body">
<view class="gd-hd">
<view class="t-lg">{{guestOutfit.name}}</view>
<view class="gd-tags">
<text class="pill pill--line">{{guestOutfit.scene}}</text>
<text class="pill">示例</text>
</view>
</view>
<view class="gd-desc">{{guestOutfit.desc}}</view>
<view class="gd-items">
<view class="gd-item" wx:for="{{guestOutfit.items}}" wx:key="clothes_uuid">
<view class="gd-item-img">
<image class="pic" src="{{item.processed_image}}" mode="aspectFit" />
</view>
<view class="tile-name">{{item.name}}</view>
<view class="tile-sub">{{item.category_main}}{{item.category_sub ? ' · ' + item.category_sub : ''}}</view>
</view>
</view>
</view>
<view class="sheet-foot">
<view class="btn btn--ghost btn--block" bindtap="onGuestSheetLogin">登录后可创建你的穿搭</view>
</view>
</view>
</view>
</view>
+42
View File
@@ -192,3 +192,45 @@
width: 26rpx;
height: 26rpx;
}
/* ===== 游客态:示例搭配抽屉(只读浏览,不要求登录) ===== */
.gd-hd {
padding: 8rpx var(--pad) 0;
}
.gd-tags {
display: flex;
gap: 12rpx;
margin-top: 18rpx;
}
.gd-desc {
margin-top: 18rpx;
padding: 0 var(--pad);
font-size: 26rpx;
line-height: 1.6;
color: var(--ink-2);
}
.gd-items {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32rpx 26rpx;
padding: 40rpx var(--pad) 0;
}
.gd-item-img {
width: 100%;
aspect-ratio: 1 / 1;
background: var(--paper-2);
border-radius: var(--r-img);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.gd-item-img .pic {
width: 100%;
height: 100%;
}
+20 -7
View File
@@ -7,7 +7,7 @@ import { clothesStatusFlag } from '../../utils/clothes-status'
import { resolvePendingClothes } from '../../utils/pending-clothes'
import { setTabBarSelected } from '../../utils/tabbar'
import { navScroll } from '../../utils/nav'
import { GUEST_WARDROBE, GuestClothes, goLogin } from '../../utils/guest'
import { GUEST_WARDROBE, goLogin } from '../../utils/guest'
import { logger } from '../../utils/logger'
// 日志模块名
@@ -24,9 +24,9 @@ const CATEGORIES = [
]
// 游客态分类筛选:与分类条 value 对齐,「全部」返回完整示例
function filterGuestClothes(index: number): GuestClothes[] {
function filterGuestClothes(index: number): ClothesItem[] {
const cat = CATEGORIES[index] ? CATEGORIES[index].value : ''
return cat ? GUEST_WARDROBE.filter((it) => it.category === cat) : GUEST_WARDROBE
return cat ? GUEST_WARDROBE.filter((it) => it.category_main === cat) : GUEST_WARDROBE
}
Page({
@@ -73,14 +73,22 @@ Page({
// 未登录:游客态浏览,零请求,不再强制跳登录(微信审核合规要求:
// 用户可先浏览体验功能服务,登录与否由用户自行选择)
if (!auth.isLoggedIn()) {
if (!this.data.guest) this.setData({ guest: true, list: [], loading: false })
if (!this.data.guest) {
// 重新回到游客态(如登录态失效后选择先逛逛):按当前分类还原示例数据
this.setData({
guest: true,
guestList: filterGuestClothes(this.data.activeCategory),
list: [],
loading: false,
})
}
return
}
// 登录成功回到首页:清空示例数据,切回真实衣橱
if (this.data.guest) {
logger.info(LOG, '检测到登录态,从游客态切换为真实衣橱')
;(this as any).firstShown = true
this.setData({ guest: false, guestList: [], list: [], total: 0, uploadRemain: 0, loading: true })
this.setData({ guest: false, list: [], total: 0, uploadRemain: 0, loading: true })
this.resolvePending()
this.refresh()
return
@@ -237,11 +245,16 @@ Page({
},
onClothesTap(e: WechatMiniprogram.CustomEvent) {
const item = e.detail.item as ClothesItem
// 游客态:打开本地示例衣物详情(只读、零请求),浏览路径上不要求登录
if (this.data.guest) {
this.onGuestLogin()
logger.debug(LOG, '游客态点击示例衣物, uuid=', item && item.clothes_uuid)
wx.navigateTo({
url: `/pages/clothes-detail/clothes-detail?demo=${item.clothes_uuid}`,
fail: (err) => logger.warn(LOG, 'navigateTo 示例衣物详情失败, err=', err && err.errMsg),
})
return
}
const item = e.detail.item as ClothesItem
logger.debug(LOG, '点击衣物, uuid=', item && item.clothes_uuid)
wx.navigateTo({
url: `/pages/clothes-detail/clothes-detail?uuid=${item.clothes_uuid}`,
+7 -8
View File
@@ -60,15 +60,14 @@
bind:action="onUpload"
/>
<!-- 游客态:本地示例衣物网格(可切换分类即时筛选) -->
<!-- 游客态:本地示例衣物网格(可切换分类筛选,点卡片可看示例详情,不要求登录) -->
<view wx:elif="{{guest}}" class="wd-grid">
<view class="wd-tile" wx:for="{{guestList}}" wx:key="key" bindtap="onGuestLogin">
<view class="gd-img">
<view class="icon {{item.icon}} gd-ph"></view>
<view class="guest-flag">示例</view>
</view>
<view class="tile-name">{{item.name}}</view>
<view class="tile-sub">{{item.category}} · {{item.color}}色</view>
<view class="wd-tile" wx:for="{{guestList}}" wx:key="clothes_uuid">
<clothes-card
item="{{item}}"
statusText="示例"
bind:select="onClothesTap"
/>
</view>
<view wx:if="{{!guestList.length}}" class="gd-none">该分类暂无示例内容</view>
</view>
+1 -20
View File
@@ -53,26 +53,7 @@
transform: scale(.975);
}
/* ===== 游客态:示例衣物瓦片(线稿占位 + 示例角标) ===== */
.gd-img {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
background: var(--paper-2);
border-radius: var(--r-img);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.gd-img .gd-ph {
width: 56%;
height: 56%;
color: var(--art);
}
/* 分类下无示例时的整行占位 */
/* ===== 游客态:示例衣物网格(复用 clothes-card,仅保留无示例时的占位) ===== */
.gd-none {
grid-column: 1 / -1;
padding: 72rpx 0;