import { request } from '../../utils/request' import * as auth from '../../utils/auth' import { PagedResult, ClosetShareItem, OutfitShareItem } from '../../types/models' import { ShareStatus, ShareType } from '../../utils/config' import { normalizePaged } from '../../utils/paged' import { setTabBarSelected } from '../../utils/tabbar' import { navScroll } from '../../utils/nav' import { GUEST_SHARES, goLogin } from '../../utils/guest' import { logger } from '../../utils/logger' // 日志模块名 const LOG = 'closet-share' // 统一的分享列表项:衣柜分享与穿搭分享合并展示 interface ShareListItem { kind: 'closet' | 'outfit' share_uuid: string title: string typeText: string expireText: string status: number created_at: number owner_name?: string owner_avatar?: string cover?: string _invalid: boolean } // 游客态示例列表:与真实列表同构,复用同一套模板渲染,仅补「示例」标注 function guestShareList(): ShareListItem[] { return GUEST_SHARES.map((it) => ({ kind: it.kind, share_uuid: it.key, title: it.title, typeText: it.typeText, expireText: it.expireText, status: ShareStatus.NORMAL, created_at: 0, _invalid: false, })) } Page({ data: { tabs: [ { name: '我的分享' }, { name: '分享给我的' }, ], activeTab: 0, // 0 我的分享 1 分享给我的 list: [] as ShareListItem[], loading: true, loadingMore: false, page: 1, pageSize: 20, hasMore: true, // 滚动收缩:由 utils/nav.ts 的共享 handler 驱动 navScrolled: false, // 游客态(未登录):展示本地示例分享,不发需要授权的请求 guest: false, }, // scroll-view 滚动 → 导航收缩为玻璃小标题 onNavScroll: navScroll, onLoad() { // 首帧渲染前判定游客态:未登录直接渲染示例列表,避免首帧 setData 冲突 if (!auth.isLoggedIn()) { this.setData({ guest: true, list: guestShareList(), loading: false, hasMore: false }) logger.info(LOG, '未登录进入分享页,渲染游客态示例内容') } }, onShow() { this.syncTabBar() // 未登录:游客态浏览,零请求,不再强制跳登录(微信审核合规要求) if (!auth.isLoggedIn()) { if (!this.data.guest) { this.setData({ guest: true, list: guestShareList(), loading: false, hasMore: false }) } return } // 登录成功回到本页:清掉示例数据,切回真实列表 if (this.data.guest) { logger.info(LOG, '检测到登录态,从游客态切回真实分享列表') ;(this as any).firstShown = true this.setData({ guest: false, list: [], loading: true, hasMore: true }) this.refresh() return } // 首帧渲染未结束就 setData 会触发「Expected updated data but get first rendering data」, // 整个 webview 渲染失败。首次 onShow 的刷新延迟到下一帧 if (!(this as any).firstShown) { ;(this as any).firstShown = true wx.nextTick(() => this.refresh()) return } this.refresh() }, syncTabBar() { setTabBarSelected(3) }, onTabTap(e: WechatMiniprogram.TouchEvent) { const index = Number(e.currentTarget.dataset.index) if (index === this.data.activeTab) return // 游客态:分段仍可切换,示例内容用于体验浏览,不发请求 if (this.data.guest) { this.setData({ activeTab: index }) return } this.setData({ activeTab: index, list: [], loading: true }) this.refresh() }, // 游客态点击示例分享 / 受限入口:统一进入登录页,由用户自行选择是否登录 onGuestLogin() { logger.debug(LOG, '游客态点击受限入口,引导登录') goLogin() }, refresh() { this.setData({ page: 1, hasMore: true, loading: this.data.list.length === 0 }) this.fetchList(1) }, onReachBottom() { if (this.data.guest) return if (!this.data.hasMore || this.data.loading || this.data.loadingMore) return this.fetchList(this.data.page + 1) }, fetchList(page: number) { if (this.data.activeTab === 1) return this.fetchReceived(page) return this.fetchMine(page) }, // 我的分享:衣柜分享 + 穿搭分享并行拉取后按时间倒序合并 fetchMine(page: number) { // 请求序号:切 tab 时旧响应可能后到,序号不匹配直接丢弃 const seq = ((this as any).reqSeq || 0) + 1 ;(this as any).reqSeq = seq if (page > 1) this.setData({ loadingMore: true }) Promise.all([ request>({ url: '/closet-share', data: { page, page_size: this.data.pageSize }, }), request>({ url: '/outfit-share', data: { page, page_size: this.data.pageSize }, }), ]) .then(([closetRes, outfitRes]) => { if ((this as any).reqSeq !== seq) return const closetList = normalizePaged(closetRes).list.map((it) => this.decorateCloset(it)) const outfitList = normalizePaged(outfitRes).list.map((it) => this.decorateOutfit(it)) logger.debug(LOG, '我的分享加载成功, page=', page, '衣柜=', closetList.length, '穿搭=', outfitList.length) const prev = page === 1 ? [] : this.data.list const merged = prev .concat(closetList, outfitList) .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) // 任一侧仍返回满页则认为还有更多 const hasMore = closetList.length >= this.data.pageSize || outfitList.length >= this.data.pageSize this.setData({ list: merged, hasMore, loading: false, loadingMore: false, page }) }) .catch((e: any) => { if ((this as any).reqSeq !== seq) return logger.warn(LOG, '我的分享加载失败, page=', page, 'code=', e && e.code) this.setData({ loading: false, loadingMore: false }) }) }, // 分享给我的:衣柜分享 + 穿搭分享并行拉取后按时间倒序合并 fetchReceived(page: number) { const seq = ((this as any).reqSeq || 0) + 1 ;(this as any).reqSeq = seq if (page > 1) this.setData({ loadingMore: true }) Promise.all([ request>({ url: '/closet-share/received', data: { page, page_size: this.data.pageSize }, }), request>({ url: '/outfit-share/received', data: { page, page_size: this.data.pageSize }, }), ]) .then(([closetRes, outfitRes]) => { if ((this as any).reqSeq !== seq) return const closetList = normalizePaged(closetRes).list.map((it) => this.decorateCloset(it)) const outfitList = normalizePaged(outfitRes).list.map((it) => this.decorateOutfit(it)) logger.debug(LOG, '分享给我的加载成功, page=', page, '衣柜=', closetList.length, '穿搭=', outfitList.length) const prev = page === 1 ? [] : this.data.list const merged = prev .concat(closetList, outfitList) .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) // 任一侧仍返回满页则认为还有更多 const hasMore = closetList.length >= this.data.pageSize || outfitList.length >= this.data.pageSize this.setData({ list: merged, hasMore, loading: false, loadingMore: false, page }) }) .catch((e: any) => { if ((this as any).reqSeq !== seq) return logger.warn(LOG, '分享给我的加载失败, page=', page, 'code=', e && e.code) this.setData({ loading: false, loadingMore: false }) }) }, // 衣柜分享项:本地惰性过期 + 状态文案 decorateCloset(it: ClosetShareItem): ShareListItem { const now = Math.floor(Date.now() / 1000) let status = it.status if (status === ShareStatus.NORMAL && it.expire_time > 0 && it.expire_time < now) { status = ShareStatus.EXPIRED } let expireText = '长期有效' if (status === ShareStatus.CANCELLED) expireText = '已取消' else if (status === ShareStatus.EXPIRED) expireText = '已过期' else if (status === ShareStatus.BANNED) expireText = '已封禁' else if (it.expire_time > 0) { const days = Math.ceil((it.expire_time - now) / 86400) expireText = days > 0 ? `${days} 天后过期` : '已过期' } return { kind: 'closet', share_uuid: it.share_uuid, title: `衣柜分享 · ${it.clothes_count} 件`, typeText: it.share_type === ShareType.PARTIAL ? '部分衣物' : '全量衣橱', expireText, status, created_at: it.created_at || 0, owner_name: it.owner_name, owner_avatar: it.owner_avatar, _invalid: status === ShareStatus.CANCELLED || status === ShareStatus.EXPIRED || status === ShareStatus.BANNED, } }, // 穿搭分享项 decorateOutfit(it: OutfitShareItem): ShareListItem { const cancelled = it.status === ShareStatus.CANCELLED return { kind: 'outfit', share_uuid: it.share_uuid, title: it.outfit_name || '未命名穿搭', typeText: `穿搭分享 · ${it.items_count} 件`, expireText: cancelled ? '已取消' : '有效', status: it.status, created_at: it.created_at || 0, owner_name: it.owner_name, owner_avatar: it.owner_avatar, cover: it.cover_image, _invalid: cancelled, } }, onCreate() { // 新建分享需登录:游客态先引导登录,不直接进入创建页 if (this.data.guest) { this.onGuestLogin() return } wx.navigateTo({ url: '/pages/closet-share-create/closet-share-create', fail: (e) => logger.warn(LOG, 'navigateTo 创建分享失败, err=', e && e.errMsg), }) }, onItemTap(e: WechatMiniprogram.TouchEvent) { // 游客态示例条目仅作视觉展示:浏览路径上不要求登录(登录入口在页脚) if (this.data.guest) { logger.debug(LOG, '游客态点击示例分享条目,仅展示不跳转') return } const uuid = e.currentTarget.dataset.uuid as string const kind = e.currentTarget.dataset.kind as string logger.debug(LOG, '点击分享项, kind=', kind, 'uuid=', uuid) const url = kind === 'outfit' ? `/pages/outfit-share-detail/outfit-share-detail?uuid=${uuid}` : `/pages/closet-share-detail/closet-share-detail?uuid=${uuid}` wx.navigateTo({ url, fail: (err) => logger.warn(LOG, 'navigateTo 分享详情失败, err=', err && err.errMsg), }) }, })