diff --git a/miniprogram/app.json b/miniprogram/app.json index 0461dc2..eaedc70 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -15,6 +15,7 @@ "pages/closet-share/closet-share", "pages/closet-share-create/closet-share-create", "pages/closet-share-detail/closet-share-detail", + "pages/outfit-share-detail/outfit-share-detail", "pages/mall/mall", "pages/order/order" ], diff --git a/miniprogram/components/navigation-bar/navigation-bar.ts b/miniprogram/components/navigation-bar/navigation-bar.ts index 95e3ab4..f276a52 100644 --- a/miniprogram/components/navigation-bar/navigation-bar.ts +++ b/miniprogram/components/navigation-bar/navigation-bar.ts @@ -117,11 +117,22 @@ Component({ back() { const data = this.data if (data.delta) { - logger.debug(LOG, '返回上一页, delta=', data.delta) - wx.navigateBack({ - delta: data.delta, - fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg) - }) + // 从分享卡片等场景直接打开时页面栈仅一层,navigateBack 无上一页可退, + // 此时兜底 switchTab 回衣橱,避免点击返回毫无反应。 + const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : [] + if (pages.length > data.delta) { + logger.debug(LOG, '返回上一页, delta=', data.delta) + wx.navigateBack({ + delta: data.delta, + fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg) + }) + } else { + logger.warn(LOG, '页面栈不足,switchTab 回衣橱兜底, 栈深=', pages.length) + wx.switchTab({ + url: '/pages/wardrobe/wardrobe', + fail: (e) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg) + }) + } } this.triggerEvent('back', { delta: data.delta }, {}) } diff --git a/miniprogram/pages/closet-share/closet-share.ts b/miniprogram/pages/closet-share/closet-share.ts index 005ff46..a53dcfc 100644 --- a/miniprogram/pages/closet-share/closet-share.ts +++ b/miniprogram/pages/closet-share/closet-share.ts @@ -1,6 +1,6 @@ import { request, redirectToLogin } from '../../utils/request' import * as auth from '../../utils/auth' -import { PagedResult, ClosetShareItem } from '../../types/models' +import { PagedResult, ClosetShareItem, OutfitShareItem } from '../../types/models' import { ShareStatus, ShareType } from '../../utils/config' import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged' import { setTabBarSelected } from '../../utils/tabbar' @@ -9,6 +9,21 @@ 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 +} + Page({ data: { tabs: [ @@ -16,7 +31,7 @@ Page({ { name: '分享给我的' }, ], activeTab: 0, // 0 我的分享 1 分享给我的 - list: [] as ClosetShareItem[], + list: [] as ShareListItem[], loading: true, loadingMore: false, page: 1, @@ -63,19 +78,26 @@ Page({ }, fetchList(page: number) { - const url = this.data.activeTab === 0 ? '/closet-share' : '/closet-share/received' + 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 }) - request>({ url, data: { page, page_size: this.data.pageSize } }) + request>({ + url: '/closet-share', + data: { page, page_size: this.data.pageSize }, + }) .then((res) => { if ((this as any).reqSeq !== seq) return const { list: raw, total } = normalizePaged(res) - logger.debug(LOG, '列表加载成功, tab=', this.data.activeTab, 'page=', page, '条数=', raw && raw.length) - const list = raw.map((it) => this.decorate(it)) + logger.debug(LOG, '我的分享加载成功, page=', page, '条数=', raw && raw.length) + const list = raw.map((it) => this.decorateCloset(it)) const merged = mergePaged(this.data.list, list, page === 1) this.setData({ list: merged, @@ -87,12 +109,50 @@ Page({ }) .catch((e: any) => { if ((this as any).reqSeq !== seq) return - logger.warn(LOG, '列表加载失败, tab=', this.data.activeTab, 'page=', page, 'code=', e && e.code) + logger.warn(LOG, '我的分享加载失败, page=', page, 'code=', e && e.code) this.setData({ loading: false, loadingMore: false }) }) }, - decorate(it: ClosetShareItem) { + // 分享给我的:衣柜分享 + 穿搭分享并行拉取后按时间倒序合并 + 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) { @@ -109,10 +169,15 @@ Page({ } return { - ...it, - _status: status, - _expireText: expireText, - _typeText: it.share_type === ShareType.PARTIAL ? '部分衣物' : '全量衣橱', + 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 || @@ -120,6 +185,24 @@ Page({ } }, + // 穿搭分享项 + 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() { wx.navigateTo({ url: '/pages/closet-share-create/closet-share-create', @@ -129,9 +212,14 @@ Page({ onItemTap(e: WechatMiniprogram.TouchEvent) { const uuid = e.currentTarget.dataset.uuid as string - logger.debug(LOG, '点击分享项, uuid=', uuid) + 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: `/pages/closet-share-detail/closet-share-detail?uuid=${uuid}`, + url, fail: (err) => logger.warn(LOG, 'navigateTo 分享详情失败, err=', err && err.errMsg), }) }, diff --git a/miniprogram/pages/closet-share/closet-share.wxml b/miniprogram/pages/closet-share/closet-share.wxml index fc1db51..f0b850e 100644 --- a/miniprogram/pages/closet-share/closet-share.wxml +++ b/miniprogram/pages/closet-share/closet-share.wxml @@ -21,7 +21,7 @@ wx:if="{{!loading && !list.length}}" icon="icon-share" title="{{activeTab === 0 ? '还没有分享' : '还没有收到分享'}}" - desc="{{activeTab === 0 ? '把衣橱或精选衣物分享给好友' : '好友分享给你的衣橱会出现在这里'}}" + desc="{{activeTab === 0 ? '把衣橱或精选衣物分享给好友' : '好友分享给你的衣橱或穿搭会出现在这里'}}" buttonText="{{activeTab === 0 ? '新建分享' : ''}}" bind:action="onCreate" /> @@ -32,14 +32,22 @@ wx:for="{{list}}" wx:key="share_uuid" data-uuid="{{item.share_uuid}}" + data-kind="{{item.kind}}" bindtap="onItemTap" > - + + + + + + + + - - - + + + - + diff --git a/miniprogram/pages/closet-share/closet-share.wxss b/miniprogram/pages/closet-share/closet-share.wxss index c3552ac..0c4c855 100644 --- a/miniprogram/pages/closet-share/closet-share.wxss +++ b/miniprogram/pages/closet-share/closet-share.wxss @@ -85,6 +85,30 @@ height: 40rpx; } +/* 穿搭分享封面 */ +.preview-cover { + width: 160rpx; + height: 160rpx; + border-radius: 16rpx; + background: #F2F2F7; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + flex-shrink: 0; + color: #C7C7CC; +} + +.preview-cover-img { + width: 100%; + height: 100%; +} + +.preview-cover-ph { + width: 64rpx; + height: 64rpx; +} + .owner-row { display: flex; align-items: center; diff --git a/miniprogram/pages/clothes-detail/clothes-detail.ts b/miniprogram/pages/clothes-detail/clothes-detail.ts index 166e390..e6ad7a1 100644 --- a/miniprogram/pages/clothes-detail/clothes-detail.ts +++ b/miniprogram/pages/clothes-detail/clothes-detail.ts @@ -101,6 +101,9 @@ function statusToStep(status: number): number { Page({ data: { uuid: '', + // 来自穿搭分享的只读查看:携带 share_uuid,隐藏编辑/删除 + shareUUID: '', + readonly: false, attrGroups: ATTR_GROUPS, colorPresets: COLOR_PRESETS, detail: {} as ClothesItem, @@ -123,8 +126,10 @@ Page({ onLoad(query: Record) { const uuid = query.uuid || '' - logger.debug(LOG, 'onLoad, uuid=', uuid) - this.setData({ uuid }) + const shareUUID = query.share_uuid || '' + const readonly = !!shareUUID + logger.debug(LOG, 'onLoad, uuid=', uuid, 'readonly=', readonly) + this.setData({ uuid, shareUUID, readonly }) if (!uuid) { // uuid 缺失时不请求不轮询,否则会停在空白页且无法退出 logger.warn(LOG, 'onLoad 缺少 uuid 参数') @@ -132,7 +137,8 @@ Page({ return } this.fetchDetail() - this.pollStatus() + // 只读(来自穿搭分享)场景无需轮询 AI 处理状态 + if (!readonly) this.pollStatus() }, onBackWardrobe() { @@ -143,7 +149,11 @@ Page({ }, fetchDetail(): Promise { - return request({ url: `/clothes/${this.data.uuid}` }) + // 只读场景携带 share_uuid,后端据此放行非属主读取衣物详情 + const url = this.data.readonly + ? `/clothes/${this.data.uuid}?share_uuid=${this.data.shareUUID}` + : `/clothes/${this.data.uuid}` + return request({ url }) .then((detail) => { logger.debug(LOG, '详情加载成功, 处理状态=', detail && detail.image_process_status) this.setData({ detail, loading: false }) diff --git a/miniprogram/pages/clothes-detail/clothes-detail.wxml b/miniprogram/pages/clothes-detail/clothes-detail.wxml index 3c619f3..5355785 100644 --- a/miniprogram/pages/clothes-detail/clothes-detail.wxml +++ b/miniprogram/pages/clothes-detail/clothes-detail.wxml @@ -43,7 +43,7 @@ module.exports = { hex: hex, tags: tags } diff --git a/miniprogram/pages/outfit-detail/outfit-detail.ts b/miniprogram/pages/outfit-detail/outfit-detail.ts index 32a38e5..d74e2f0 100644 --- a/miniprogram/pages/outfit-detail/outfit-detail.ts +++ b/miniprogram/pages/outfit-detail/outfit-detail.ts @@ -33,17 +33,27 @@ Page({ logger.debug(LOG, '穿搭详情加载成功, 单品数=', outfit.items && outfit.items.length) this.setData({ outfit }) - const clothes = await Promise.all( - outfit.items.map(async (it) => { - try { - return await request({ url: `/clothes/${it.clothes_uuid}`, showError: false }) - } catch (e: any) { - logger.warn(LOG, '单品详情拉取失败, uuid=', it.clothes_uuid, 'code=', e && e.code) - return { clothes_uuid: it.clothes_uuid } as ClothesItem - } - }) + // 封面与衣物缩略图直接使用穿搭明细里的抠图(processed_image), + // 避免非属主逐件请求 /clothes/:uuid 被拒(50005)导致图片与名称全部丢失。 + const baseClothes = (outfit.items || []).map( + (it) => ({ clothes_uuid: it.clothes_uuid, processed_image: it.processed_image }) as ClothesItem, ) - this.setData({ clothes }) + this.setData({ clothes: baseClothes }) + + // 属主可进一步拉取衣物详情补充名称;非属主无权限,跳过。 + if (this.data.isOwner) { + const clothes = await Promise.all( + (outfit.items || []).map(async (it) => { + try { + return await request({ url: `/clothes/${it.clothes_uuid}`, showError: false }) + } catch (e: any) { + logger.warn(LOG, '单品详情拉取失败, uuid=', it.clothes_uuid, 'code=', e && e.code) + return { clothes_uuid: it.clothes_uuid, processed_image: it.processed_image } as ClothesItem + } + }) + ) + this.setData({ clothes }) + } const like = await request<{ is_liked: boolean; like_count: number }>({ url: `/like/${this.data.uuid}`, @@ -162,16 +172,40 @@ Page({ onItemTap(e: WechatMiniprogram.TouchEvent) { const uuid = e.currentTarget.dataset.uuid as string + // 非属主无权读取衣物详情(50005),点击仅提示;属主正常跳转。 + if (!this.data.isOwner) { + logger.debug(LOG, '非属主点击衣物,仅可查看穿搭, uuid=', uuid) + wx.showToast({ title: '仅可查看穿搭', icon: 'none' }) + return + } wx.navigateTo({ url: `/pages/clothes-detail/clothes-detail?uuid=${uuid}`, fail: (err) => logger.warn(LOG, 'navigateTo 单品详情失败, err=', err && err.errMsg), }) }, - onShareAppMessage() { - return { - title: this.data.outfit.outfit_name || '我的穿搭', - path: `/pages/outfit-detail/outfit-detail?uuid=${this.data.uuid}&owner=0`, - } + // 分享前先创建穿搭分享记录拿到 share_uuid,链接指向分享详情页; + // 失败(如穿搭未公开)则回退到普通穿搭详情链接,保证分享动作仍可用。 + onShareAppMessage(): any { + const name = this.data.outfit.outfit_name || '我的穿搭' + return request<{ share_uuid: string }>({ + url: '/outfit-share', + method: 'POST', + data: { outfit_uuid: this.data.uuid }, + }) + .then((res) => { + logger.info(LOG, '穿搭分享创建成功, share_uuid=', res.share_uuid) + return { + title: name, + path: `/pages/outfit-share-detail/outfit-share-detail?uuid=${res.share_uuid}`, + } + }) + .catch((e: any) => { + logger.warn(LOG, '穿搭分享创建失败,回退普通分享链接, code=', e && e.code) + return { + title: name, + path: `/pages/outfit-detail/outfit-detail?uuid=${this.data.uuid}&owner=0`, + } + }) }, }) diff --git a/miniprogram/pages/outfit-share-detail/outfit-share-detail.json b/miniprogram/pages/outfit-share-detail/outfit-share-detail.json new file mode 100644 index 0000000..aaf395a --- /dev/null +++ b/miniprogram/pages/outfit-share-detail/outfit-share-detail.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "navigation-bar": "/components/navigation-bar/navigation-bar", + "empty-state": "/components/empty-state/empty-state" + } +} diff --git a/miniprogram/pages/outfit-share-detail/outfit-share-detail.ts b/miniprogram/pages/outfit-share-detail/outfit-share-detail.ts new file mode 100644 index 0000000..699b827 --- /dev/null +++ b/miniprogram/pages/outfit-share-detail/outfit-share-detail.ts @@ -0,0 +1,159 @@ +import { request, AUTH_EXPIRED_CODES } from '../../utils/request' +import { OutfitShareDetail, OutfitItem } from '../../types/models' +import { ShareStatus } from '../../utils/config' +import { logger } from '../../utils/logger' + +// 日志模块名 +const LOG = 'outfit-share-detail' + +Page({ + data: { + uuid: '', + detail: null as OutfitShareDetail | null, + outfit: { items: [] } as OutfitItem, + loading: true, + invalid: false, + invalidTitle: '该分享已取消', + invalidDesc: '分享链接已失效', + needLogin: false, + }, + + onLoad(query: Record) { + const uuid = query.uuid || '' + logger.debug(LOG, 'onLoad, uuid=', uuid) + this.setData({ uuid }) + if (!uuid) { + // uuid 缺失时若不兜底,loading 会永久为 true,页面白屏且无法退出 + logger.warn(LOG, 'onLoad 缺少 uuid 参数') + this.setInvalid('链接不完整', '缺少分享标识,无法打开') + return + } + this.fetchDetail() + }, + + fetchDetail() { + request({ url: `/outfit-share/${this.data.uuid}`, showError: false }) + .then((detail) => { + logger.debug(LOG, '详情加载成功, status=', detail && detail.status) + if (this.checkInvalid(detail)) return + this.setData({ detail, outfit: detail.outfit, loading: false }) + }) + .catch((e: any) => { + const code = e && e.code + logger.warn(LOG, '详情加载失败, code=', code) + if (AUTH_EXPIRED_CODES.indexOf(code) >= 0) { + // 查看分享需登录:给「去登录」出口,登录成功后回到本页 + this.setData({ + loading: false, + invalid: true, + needLogin: true, + invalidTitle: '请先登录', + invalidDesc: '登录后即可查看好友分享的穿搭', + }) + return + } + if (code === 90104) { + this.setInvalid('分享已取消', '该分享已被创建者取消') + } else if (code === 90103) { + this.setInvalid('分享已失效', '该分享链接无效或对应穿搭已不可见') + } else { + // 兜底:任何未知错误都不能停在空白页 + this.setInvalid('加载失败', '请稍后重试') + } + }) + }, + + // 本地状态校验,返回 true 表示已置为失效态 + checkInvalid(detail: OutfitShareDetail): boolean { + if (detail.status === ShareStatus.CANCELLED) { + this.setInvalid('分享已取消', '该分享已被创建者取消') + return true + } + return false + }, + + setInvalid(title: string, desc: string) { + this.setData({ + loading: false, + invalid: true, + needLogin: false, + invalidTitle: title, + invalidDesc: desc, + }) + }, + + onItemTap(e: WechatMiniprogram.TouchEvent) { + const uuid = e.currentTarget.dataset.uuid as string + // 只读查看:携带 share_uuid,后端据此放行非属主读取衣物详情 + logger.debug(LOG, '点击衣物, uuid=', uuid) + wx.navigateTo({ + url: `/pages/clothes-detail/clothes-detail?uuid=${uuid}&share_uuid=${this.data.uuid}`, + fail: (err) => logger.warn(LOG, 'navigateTo 单品详情失败, err=', err && err.errMsg), + }) + }, + + onCancel() { + wx.showModal({ + title: '取消分享', + content: '取消后该分享链接将失效', + confirmText: '取消分享', + confirmColor: '#FF3B30', + success: (res) => { + if (!res.confirm) return + request({ url: `/outfit-share/${this.data.uuid}/cancel`, method: 'POST' }) + .then(() => { + logger.info(LOG, '取消分享成功') + wx.showToast({ title: '已取消', icon: 'success' }) + ;(this as any).backTimer = setTimeout( + () => + wx.navigateBack({ + fail: (e: any) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg), + }), + 500, + ) + }) + .catch((e: any) => logger.warn(LOG, '取消分享失败, code=', e && e.code)) + }, + fail: (e: any) => logger.warn(LOG, '取消分享弹窗失败, err=', e && e.errMsg), + }) + }, + + onBackHome() { + wx.switchTab({ + url: '/pages/wardrobe/wardrobe', + fail: (e: any) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg), + }) + }, + + onGoLogin() { + const redirect = `/pages/outfit-share-detail/outfit-share-detail?uuid=${this.data.uuid}` + const loginUrl = `/pages/login/login?redirect=${encodeURIComponent(redirect)}` + wx.navigateTo({ + url: loginUrl, + fail: () => { + wx.redirectTo({ + url: loginUrl, + fail: () => wx.switchTab({ url: '/pages/profile/profile' }), + }) + }, + }) + }, + + onShareAppMessage() { + const name = (this.data.outfit && this.data.outfit.outfit_name) || '好友的穿搭' + const owner = (this.data.detail && this.data.detail.owner_name) || '好友' + logger.debug(LOG, '转发分享, uuid=', this.data.uuid) + return { + title: `${owner} 向你分享了穿搭「${name}」`, + path: `/pages/outfit-share-detail/outfit-share-detail?uuid=${this.data.uuid}`, + } + }, + + onUnload() { + const timer = (this as any).backTimer + if (timer) { + clearTimeout(timer) + ;(this as any).backTimer = null + } + }, +}) diff --git a/miniprogram/pages/outfit-share-detail/outfit-share-detail.wxml b/miniprogram/pages/outfit-share-detail/outfit-share-detail.wxml new file mode 100644 index 0000000..4e77a46 --- /dev/null +++ b/miniprogram/pages/outfit-share-detail/outfit-share-detail.wxml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + {{detail.owner_name || '好友'}} 分享了穿搭 + 共 {{outfit.items.length}} 件单品 + + + + + + + + + + + + + + + + + + {{outfit.outfit_name}} + + + {{outfit.scene}} + {{outfit.source === 2 ? 'AI 推荐' : '自建'}} + + {{outfit.outfit_desc}} + + + 包含衣物 + + + + + + + + + + + + + + + 取消分享 + + + diff --git a/miniprogram/pages/outfit-share-detail/outfit-share-detail.wxss b/miniprogram/pages/outfit-share-detail/outfit-share-detail.wxss new file mode 100644 index 0000000..080d2c9 --- /dev/null +++ b/miniprogram/pages/outfit-share-detail/outfit-share-detail.wxss @@ -0,0 +1,201 @@ +.detail-page { + height: 100vh; + display: flex; + flex-direction: column; +} + +.scroll-body { + flex: 1; + height: 0; +} + +/* 分享者信息 */ +.owner-card { + margin: 24rpx; + display: flex; + align-items: center; +} + +.avatar { + width: 96rpx; + height: 96rpx; + border-radius: 50%; + background: #F2F2F7; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + color: #C7C7CC; + flex-shrink: 0; +} + +.avatar-img { + width: 100%; + height: 100%; +} + +.avatar-placeholder { + width: 56rpx; + height: 56rpx; +} + +.owner-info { + margin-left: 24rpx; +} + +.owner-name { + font-size: 30rpx; + font-weight: 600; + color: #000; +} + +.owner-expire { + font-size: 24rpx; + color: #8E8E93; + margin-top: 8rpx; +} + +/* 封面 */ +.cover { + margin: 0 24rpx 24rpx; + height: 480rpx; + background: #fff; + border-radius: 32rpx; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.cover-item { + position: absolute; + width: 200rpx; + height: 260rpx; + background: #F8F8FA; + border-radius: 20rpx; + border: 1rpx solid #E5E5EA; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.cover-0 { left: 30rpx; top: 80rpx; transform: rotate(-6deg); } +.cover-1 { left: 120rpx; top: 140rpx; transform: rotate(2deg); } +.cover-2 { left: 220rpx; top: 70rpx; transform: rotate(5deg); } +.cover-3 { left: 320rpx; top: 150rpx; transform: rotate(-2deg); } +.cover-4 { left: 420rpx; top: 90rpx; transform: rotate(7deg); } + +.cover-img { + width: 100%; + height: 100%; +} + +.cover-placeholder { + width: 80rpx; + height: 80rpx; + color: #C7C7CC; +} + +.cover-empty { + color: #C7C7CC; +} + +.cover-empty .icon { + width: 120rpx; + height: 120rpx; +} + +/* 信息卡 */ +.info-card { + margin: 0 24rpx 24rpx; +} + +.title { + font-size: 40rpx; + font-weight: 700; + color: #000; +} + +.tag-row { + display: flex; + flex-wrap: wrap; + gap: 12rpx; + margin-top: 20rpx; +} + +.desc { + font-size: 28rpx; + color: #3C3C43; + margin-top: 20rpx; + line-height: 1.6; +} + +.items-title { + font-size: 28rpx; + font-weight: 600; + color: #000; + margin-top: 32rpx; +} + +.items-scroll { + margin-top: 20rpx; + white-space: nowrap; +} + +.items-row { + display: inline-flex; +} + +.item-chip { + display: flex; + flex-direction: column; + align-items: center; + width: 140rpx; + margin-right: 20rpx; +} + +.item-img { + width: 120rpx; + height: 120rpx; + border-radius: 20rpx; + background: #F2F2F7; +} + +.item-img-empty { + display: flex; + align-items: center; + justify-content: center; + color: #C7C7CC; +} + +.item-img-empty .icon { + width: 56rpx; + height: 56rpx; +} + +/* 转发出口 */ +.share-btn { + margin: 32rpx 24rpx 0; + height: 88rpx; + line-height: 88rpx; + font-size: 30rpx; + font-weight: 500; + color: var(--color-primary, #000); + background: #fff; + border-radius: 24rpx; +} + +.share-btn::after { + border: none; +} + +.cancel-btn { + text-align: center; + padding: 32rpx 0; +} + +.footer-space { + height: 40rpx; +} diff --git a/miniprogram/types/models.ts b/miniprogram/types/models.ts index f82517d..60f8989 100644 --- a/miniprogram/types/models.ts +++ b/miniprogram/types/models.ts @@ -145,6 +145,7 @@ export interface ClosetShareItem { status: number owner_name?: string owner_avatar?: string + created_at: number } export interface ClosetShareDetail { @@ -157,6 +158,29 @@ export interface ClosetShareDetail { is_owner: boolean } +// 穿搭分享列表项(/outfit-share 与 /outfit-share/received 返回) +export interface OutfitShareItem { + share_uuid: string + outfit_uuid: string + outfit_name: string + cover_image: string + items_count: number + status: number + owner_name?: string + owner_avatar?: string + created_at: number +} + +// 穿搭分享详情(/outfit-share/:uuid 返回) +export interface OutfitShareDetail { + share_uuid: string + owner_name: string + owner_avatar: string + status: number + is_owner: boolean + outfit: OutfitItem +} + export interface AiRecommendResult { outfit_uuid: string outfit_name: string diff --git a/miniprogram/utils/config.ts b/miniprogram/utils/config.ts index a28c809..48a266f 100644 --- a/miniprogram/utils/config.ts +++ b/miniprogram/utils/config.ts @@ -148,6 +148,8 @@ export const ERROR_MESSAGES: Record = { 90100: '分享不存在', 90101: '分享已过期', 90102: '分享已取消', + 90103: '穿搭分享不存在', + 90104: '穿搭分享已取消', 90200: '目标内容不存在', 19000: '服务繁忙,请稍后重试', 19001: '服务繁忙,请稍后重试',