feat: ui update
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar"
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"empty-state": "/components/empty-state/empty-state",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +1,225 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { request, AUTH_EXPIRED_CODES } from '../../utils/request'
|
||||
import { OutfitItem, ClothesItem } from '../../types/models'
|
||||
import { IsPublic } from '../../utils/config'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
const LOG = 'outfit-detail'
|
||||
|
||||
// 接收者打开「分享者额度不足」链接后的自动返回倒计时(秒)
|
||||
const SHARE_FAIL_BACK_SECONDS = 10
|
||||
|
||||
Page({
|
||||
data: {
|
||||
uuid: '',
|
||||
isOwner: true,
|
||||
// 仅显式 owner=1 视为属主:无参数链接(如微信默认分享路径)安全降级为非属主视角
|
||||
isOwner: false,
|
||||
outfit: {} as OutfitItem,
|
||||
clothes: [] as ClothesItem[],
|
||||
isLiked: false,
|
||||
likeCount: 0,
|
||||
isCollected: false,
|
||||
loading: true,
|
||||
// 页面状态:normal 正常;quota 接收者命中「分享者额度不足」;invalid 加载失败
|
||||
pageState: 'normal' as 'normal' | 'quota' | 'invalid',
|
||||
invalidTitle: '',
|
||||
invalidDesc: '',
|
||||
needLogin: false,
|
||||
countdown: SHARE_FAIL_BACK_SECONDS,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// v2 玻璃弹层
|
||||
tipVisible: false,
|
||||
tipTitle: '',
|
||||
tipText: '',
|
||||
tipButtons: [{ text: '我知道了', value: 'ok', type: 'main' }],
|
||||
pubVisible: false,
|
||||
pubButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '确认公开', value: 'ok', type: 'main' },
|
||||
],
|
||||
delVisible: false,
|
||||
delButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '确认删除', value: 'ok', type: 'main' },
|
||||
],
|
||||
moreVisible: false,
|
||||
moreItems: [
|
||||
{ text: '编辑穿搭', value: 'edit' },
|
||||
{ text: '删除穿搭', value: 'delete' },
|
||||
],
|
||||
// 额度不足引导:v2 玻璃弹窗
|
||||
quotaVisible: false,
|
||||
quotaTitle: '',
|
||||
quotaText: '',
|
||||
quotaButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '去购买', value: 'ok', type: 'main' },
|
||||
],
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
const uuid = query.uuid || ''
|
||||
const isOwner = query.owner !== '0'
|
||||
logger.debug(LOG, 'onLoad, uuid=', uuid, 'isOwner=', isOwner)
|
||||
const isOwner = query.owner === '1'
|
||||
logger.debug(LOG, 'onLoad, uuid=', uuid, 'isOwner=', isOwner, 'shareFail=', query.share_fail)
|
||||
this.setData({ uuid, isOwner })
|
||||
if (uuid) this.fetchDetail()
|
||||
else logger.warn(LOG, 'onLoad 缺少 uuid 参数')
|
||||
|
||||
// 分享者额度不足时分享出去的链接:不请求后端(也不依赖登录),直接进入提示态
|
||||
if (query.share_fail === 'quota') {
|
||||
this.showShareFailQuota()
|
||||
return
|
||||
}
|
||||
|
||||
if (!uuid) {
|
||||
logger.warn(LOG, 'onLoad 缺少 uuid 参数')
|
||||
this.setInvalid('链接不完整', '缺少穿搭标识,无法打开')
|
||||
return
|
||||
}
|
||||
this.fetchDetail()
|
||||
},
|
||||
|
||||
// 接收者命中「分享者额度不足」:弹提醒 + 倒计时后返回「我的」页
|
||||
showShareFailQuota() {
|
||||
this.setData({
|
||||
pageState: 'quota',
|
||||
loading: false,
|
||||
countdown: SHARE_FAIL_BACK_SECONDS,
|
||||
tipVisible: true,
|
||||
tipTitle: '提示',
|
||||
tipText: '给您分享的好友额度不足哦',
|
||||
})
|
||||
this.startBackCountdown()
|
||||
},
|
||||
|
||||
onTipConfirm() {
|
||||
this.setData({ tipVisible: false })
|
||||
},
|
||||
|
||||
startBackCountdown() {
|
||||
this.clearBackTimer()
|
||||
;(this as any).backTimer = setInterval(() => {
|
||||
const next = this.data.countdown - 1
|
||||
if (next > 0) {
|
||||
this.setData({ countdown: next })
|
||||
return
|
||||
}
|
||||
this.clearBackTimer()
|
||||
this.onBackProfile()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
clearBackTimer() {
|
||||
const timer = (this as any).backTimer
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
;(this as any).backTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
onBackProfile() {
|
||||
logger.info(LOG, '额度不足提示结束,返回「我的」页')
|
||||
wx.switchTab({
|
||||
url: '/pages/profile/profile',
|
||||
fail: (e) => logger.warn(LOG, 'switchTab 我的失败, err=', e && e.errMsg),
|
||||
})
|
||||
},
|
||||
|
||||
onBackHome() {
|
||||
wx.switchTab({
|
||||
url: '/pages/wardrobe/wardrobe',
|
||||
fail: (e) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg),
|
||||
})
|
||||
},
|
||||
|
||||
onGoLogin() {
|
||||
// 透传视角参数:属主登录失效后重登,仍回到属主视角
|
||||
const owner = this.data.isOwner ? '1' : '0'
|
||||
const redirect = `/pages/outfit-detail/outfit-detail?uuid=${this.data.uuid}&owner=${owner}`
|
||||
const loginUrl = `/pages/login/login?redirect=${encodeURIComponent(redirect)}`
|
||||
wx.navigateTo({
|
||||
url: loginUrl,
|
||||
fail: () => {
|
||||
wx.redirectTo({
|
||||
url: loginUrl,
|
||||
fail: () => wx.switchTab({ url: '/pages/profile/profile' }),
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
setInvalid(title: string, desc: string, needLogin = false) {
|
||||
this.setData({
|
||||
loading: false,
|
||||
pageState: 'invalid',
|
||||
invalidTitle: title,
|
||||
invalidDesc: desc,
|
||||
needLogin,
|
||||
})
|
||||
},
|
||||
|
||||
async fetchDetail() {
|
||||
let outfit: OutfitItem
|
||||
try {
|
||||
const outfit = await request<OutfitItem>({ url: `/outfit/${this.data.uuid}` })
|
||||
logger.debug(LOG, '穿搭详情加载成功, 单品数=', outfit.items && outfit.items.length)
|
||||
this.setData({ outfit })
|
||||
|
||||
// 封面与衣物缩略图直接使用穿搭明细里的抠图(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: baseClothes })
|
||||
|
||||
// 属主可进一步拉取衣物详情补充名称;非属主无权限,跳过。
|
||||
if (this.data.isOwner) {
|
||||
const clothes = await Promise.all(
|
||||
(outfit.items || []).map(async (it) => {
|
||||
try {
|
||||
return await request<ClothesItem>({ 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}`,
|
||||
showError: false,
|
||||
})
|
||||
this.setData({ isLiked: like.is_liked, likeCount: like.like_count, loading: false })
|
||||
|
||||
// 收藏状态:调用后端 /collect/:targetUUID 初始化,失败静默保持未收藏
|
||||
request<{ is_collected: boolean; collect_count: number }>({
|
||||
url: `/collect/${this.data.uuid}`,
|
||||
showError: false,
|
||||
})
|
||||
.then((collect) => this.setData({ isCollected: collect.is_collected }))
|
||||
.catch((e: any) => logger.warn(LOG, '收藏状态拉取失败(保持未收藏), code=', e && e.code))
|
||||
outfit = await request<OutfitItem>({ url: `/outfit/${this.data.uuid}`, showError: false })
|
||||
} catch (e: any) {
|
||||
logger.warn(LOG, '穿搭详情加载失败, code=', e && e.code)
|
||||
this.setData({ loading: false })
|
||||
const code = e && e.code
|
||||
logger.warn(LOG, '穿搭详情加载失败, code=', code)
|
||||
if (AUTH_EXPIRED_CODES.indexOf(code) >= 0) {
|
||||
// 未登录:给「去登录」出口,登录成功后回到本页
|
||||
this.setInvalid('请先登录', '登录后即可查看该穿搭', true)
|
||||
} else if (code === 40004 || code === 40000) {
|
||||
// 他人未公开 / 已删除:不暴露具体原因,统一提示内容不可见
|
||||
this.setInvalid('内容不可见', '该穿搭可能已被删除或未公开')
|
||||
} else {
|
||||
this.setInvalid('加载失败', '请稍后重试')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug(LOG, '穿搭详情加载成功, 单品数=', outfit.items && outfit.items.length)
|
||||
this.setData({ outfit, loading: false })
|
||||
|
||||
// 封面与衣物缩略图直接使用穿搭明细里的抠图(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: baseClothes })
|
||||
|
||||
// 属主可进一步拉取衣物详情补充名称;非属主无权限,跳过。
|
||||
if (this.data.isOwner) {
|
||||
const clothes = await Promise.all(
|
||||
(outfit.items || []).map(async (it) => {
|
||||
try {
|
||||
return await request<ClothesItem>({ 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 })
|
||||
}
|
||||
|
||||
// 点赞状态:失败静默保持默认,不影响详情展示
|
||||
request<{ is_liked: boolean; like_count: number }>({
|
||||
url: `/like/${this.data.uuid}`,
|
||||
showError: false,
|
||||
})
|
||||
.then((like) => this.setData({ isLiked: like.is_liked, likeCount: like.like_count }))
|
||||
.catch((e: any) => logger.warn(LOG, '点赞状态拉取失败(保持默认), code=', e && e.code))
|
||||
|
||||
// 收藏状态:调用后端 /collect/:targetUUID 初始化,失败静默保持未收藏
|
||||
request<{ is_collected: boolean; collect_count: number }>({
|
||||
url: `/collect/${this.data.uuid}`,
|
||||
showError: false,
|
||||
})
|
||||
.then((collect) => this.setData({ isCollected: collect.is_collected }))
|
||||
.catch((e: any) => logger.warn(LOG, '收藏状态拉取失败(保持未收藏), code=', e && e.code))
|
||||
},
|
||||
|
||||
onLike() {
|
||||
@@ -99,18 +247,20 @@ Page({
|
||||
if (isPublic) {
|
||||
this.doPublic(IsPublic.PRIVATE)
|
||||
} else {
|
||||
wx.showModal({
|
||||
title: '公开到社区',
|
||||
content: '公开后将展示在社区供他人点赞收藏,并消耗 1 次社区分享额度',
|
||||
confirmText: '确认公开',
|
||||
success: (res) => {
|
||||
if (res.confirm) this.doPublic(IsPublic.PUBLIC)
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '公开弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
this.setData({ pubVisible: true })
|
||||
}
|
||||
},
|
||||
|
||||
onPubCancel() {
|
||||
this.setData({ pubVisible: false })
|
||||
},
|
||||
|
||||
onPubConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ pubVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
this.doPublic(IsPublic.PUBLIC)
|
||||
},
|
||||
|
||||
doPublic(isPublic: number) {
|
||||
// 后端 OutfitPublicRequest.OutfitUUID 为必填(binding required),请求体需携带
|
||||
request({
|
||||
@@ -123,18 +273,32 @@ Page({
|
||||
this.setData({ 'outfit.is_public': isPublic })
|
||||
wx.showToast({ title: isPublic === IsPublic.PUBLIC ? '已公开' : '已取消公开', icon: 'none' })
|
||||
})
|
||||
.catch((e: any) => logger.warn(LOG, '公开状态变更失败, code=', e && e.code))
|
||||
.catch((e: any) => {
|
||||
logger.warn(LOG, '公开状态变更失败, code=', e && e.code)
|
||||
// 社区分享额度不足(后端已回滚为私有):弹窗引导购买
|
||||
if (e && e.code === 80000) {
|
||||
this.setData({
|
||||
quotaVisible: true,
|
||||
quotaTitle: '社区分享额度不足',
|
||||
quotaText: '购买额度包后可公开到社区',
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onMore() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['编辑穿搭', '删除穿搭'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) this.onEdit()
|
||||
else this.onDelete()
|
||||
},
|
||||
fail: (e) => logger.debug(LOG, '取消更多操作, err=', e && e.errMsg),
|
||||
})
|
||||
this.setData({ moreVisible: true })
|
||||
},
|
||||
|
||||
onMoreCancel() {
|
||||
this.setData({ moreVisible: false })
|
||||
},
|
||||
|
||||
onMoreSelect(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ moreVisible: false })
|
||||
const value = e.detail.value
|
||||
if (value === 'edit') this.onEdit()
|
||||
else if (value === 'delete') this.onDelete()
|
||||
},
|
||||
|
||||
onEdit() {
|
||||
@@ -145,29 +309,29 @@ Page({
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
wx.showModal({
|
||||
title: '确认删除这套穿搭?',
|
||||
content: '删除后不可恢复,但不会影响衣橱内的单品。',
|
||||
confirmText: '确认删除',
|
||||
confirmColor: '#FF3B30',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE' })
|
||||
.then(() => {
|
||||
logger.info(LOG, '穿搭删除成功')
|
||||
wx.showToast({ title: '已删除', icon: 'success' })
|
||||
setTimeout(
|
||||
() =>
|
||||
wx.navigateBack({
|
||||
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
|
||||
}),
|
||||
500,
|
||||
)
|
||||
})
|
||||
.catch((e: any) => logger.warn(LOG, '穿搭删除失败, code=', e && e.code))
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '删除弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
this.setData({ delVisible: true })
|
||||
},
|
||||
|
||||
onDelCancel() {
|
||||
this.setData({ delVisible: false })
|
||||
},
|
||||
|
||||
onDelConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ delVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE' })
|
||||
.then(() => {
|
||||
logger.info(LOG, '穿搭删除成功')
|
||||
wx.showToast({ title: '已删除', icon: 'success' })
|
||||
setTimeout(
|
||||
() =>
|
||||
wx.navigateBack({
|
||||
fail: (e2) => logger.warn(LOG, 'navigateBack 失败, err=', e2 && e2.errMsg),
|
||||
}),
|
||||
500,
|
||||
)
|
||||
})
|
||||
.catch((e2: any) => logger.warn(LOG, '穿搭删除失败, code=', e2 && e2.code))
|
||||
},
|
||||
|
||||
onItemTap(e: WechatMiniprogram.TouchEvent) {
|
||||
@@ -184,14 +348,21 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 分享前先创建穿搭分享记录拿到 share_uuid,链接指向分享详情页;
|
||||
// 失败(如穿搭未公开)则回退到普通穿搭详情链接,保证分享动作仍可用。
|
||||
// 分享采用微信官方「同步返回 + promise 字段」异步分享模式(基础库 2.12.0+):
|
||||
// 同步返回兜底链接(3 秒内未 resolve 时生效),promise resolve 后以结果为准。
|
||||
// 非属主不创建分享记录(对他人穿搭创建会被拒),直接转发普通详情链接。
|
||||
onShareAppMessage(): any {
|
||||
const name = this.data.outfit.outfit_name || '我的穿搭'
|
||||
return request<{ share_uuid: string }>({
|
||||
const fallback = `/pages/outfit-detail/outfit-detail?uuid=${this.data.uuid}&owner=0`
|
||||
if (!this.data.isOwner) {
|
||||
return { title: name, path: fallback }
|
||||
}
|
||||
|
||||
const promise = request<{ share_uuid: string }>({
|
||||
url: '/outfit-share',
|
||||
method: 'POST',
|
||||
data: { outfit_uuid: this.data.uuid },
|
||||
showError: false,
|
||||
})
|
||||
.then((res) => {
|
||||
logger.info(LOG, '穿搭分享创建成功, share_uuid=', res.share_uuid)
|
||||
@@ -201,11 +372,37 @@ Page({
|
||||
}
|
||||
})
|
||||
.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`,
|
||||
logger.warn(LOG, '穿搭分享创建失败, code=', e && e.code)
|
||||
if (e && e.code === 80000) {
|
||||
// 额度不足:提示去购买,链接指向「额度不足」提示页(接收者侧提示后自动返回)
|
||||
this.setData({
|
||||
quotaVisible: true,
|
||||
quotaTitle: '分享额度不足',
|
||||
quotaText: '购买额度包后可继续分享',
|
||||
})
|
||||
return { title: name, path: `${fallback}&share_fail=quota` }
|
||||
}
|
||||
// 其他失败:回退普通详情链接(接收者侧展示通用「内容不可见」空态)
|
||||
return { title: name, path: fallback }
|
||||
})
|
||||
|
||||
return { title: name, path: fallback, promise }
|
||||
},
|
||||
|
||||
onQuotaCancel() {
|
||||
this.setData({ quotaVisible: false })
|
||||
},
|
||||
|
||||
onQuotaConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ quotaVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
wx.navigateTo({
|
||||
url: '/pages/mall/mall',
|
||||
fail: (err) => logger.warn(LOG, 'navigateTo 商城失败, err=', err && err.errMsg),
|
||||
})
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this.clearBackTimer()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,79 +1,179 @@
|
||||
<view class="page detail-page">
|
||||
<navigation-bar title="{{outfit.outfit_name || '穿搭详情'}}">
|
||||
<view wx:if="{{isOwner}}" slot="right" class="nav-more" bindtap="onMore">
|
||||
<wxs module="cl">
|
||||
function cls(n) {
|
||||
if (n >= 4) return ''
|
||||
if (n === 3) return 'collage--3'
|
||||
if (n === 2) return 'collage--2'
|
||||
return 'collage--1'
|
||||
}
|
||||
module.exports = { cls: cls }
|
||||
</wxs>
|
||||
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="{{outfit.outfit_name || '穿搭详情'}}" scrolled="{{navScrolled}}">
|
||||
<view
|
||||
wx:if="{{isOwner && pageState === 'normal' && !loading}}"
|
||||
slot="right"
|
||||
class="nav-more"
|
||||
bindtap="onMore"
|
||||
>
|
||||
<view class="icon icon-ellipsis"></view>
|
||||
</view>
|
||||
</navigation-bar>
|
||||
|
||||
<scroll-view scroll-y class="scroll-body">
|
||||
<!-- 拼图封面 -->
|
||||
<view class="cover">
|
||||
<view
|
||||
class="cover-item cover-{{index}}"
|
||||
wx:for="{{clothes}}"
|
||||
wx:key="clothes_uuid"
|
||||
wx:if="{{index < 5}}"
|
||||
>
|
||||
<image wx:if="{{item.processed_image}}" class="cover-img" src="{{item.processed_image}}" mode="aspectFit" />
|
||||
<view wx:else class="icon icon-hanger cover-placeholder"></view>
|
||||
</view>
|
||||
<view wx:if="{{!clothes.length}}" class="cover-empty">
|
||||
<view class="icon icon-hanger"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 接收者:分享者额度不足(不请求后端,倒计时后返回「我的」) -->
|
||||
<empty-state
|
||||
wx:if="{{pageState === 'quota'}}"
|
||||
icon="icon-user"
|
||||
title="好友分享额度不足"
|
||||
desc="给您分享的好友额度不足哦,{{countdown}} 秒后自动返回"
|
||||
buttonText="立即返回"
|
||||
bind:action="onBackProfile"
|
||||
/>
|
||||
|
||||
<!-- 信息卡片 -->
|
||||
<view class="card info-card">
|
||||
<view class="title-row">
|
||||
<text class="title">{{outfit.outfit_name}}</text>
|
||||
</view>
|
||||
<view class="tag-row">
|
||||
<text wx:if="{{outfit.scene}}" class="tag tag-gray">{{outfit.scene}}</text>
|
||||
<text class="tag {{outfit.source === 2 ? 'tag-indigo' : 'tag-gray'}}">{{outfit.source === 2 ? 'AI 推荐' : '自建'}}</text>
|
||||
<text class="tag {{outfit.is_public === 2 ? 'tag-blue' : 'tag-gray'}}">{{outfit.is_public === 2 ? '已公开' : '仅自己可见'}}</text>
|
||||
</view>
|
||||
<view class="desc" wx:if="{{outfit.outfit_desc}}">{{outfit.outfit_desc}}</view>
|
||||
<!-- 未登录:去登录后回本页 -->
|
||||
<empty-state
|
||||
wx:elif="{{pageState === 'invalid' && needLogin}}"
|
||||
icon="icon-user"
|
||||
title="{{invalidTitle}}"
|
||||
desc="{{invalidDesc}}"
|
||||
buttonText="去登录"
|
||||
bind:action="onGoLogin"
|
||||
/>
|
||||
|
||||
<!-- 包含衣物 -->
|
||||
<view class="items-title">包含衣物</view>
|
||||
<scroll-view scroll-x class="items-scroll" show-scrollbar="{{false}}">
|
||||
<view class="items-row">
|
||||
<view
|
||||
class="item-chip"
|
||||
wx:for="{{clothes}}"
|
||||
wx:key="clothes_uuid"
|
||||
data-uuid="{{item.clothes_uuid}}"
|
||||
bindtap="onItemTap"
|
||||
>
|
||||
<image wx:if="{{item.processed_image}}" class="item-img" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="item-img item-img-empty"><view class="icon icon-hanger"></view></view>
|
||||
<text class="item-name">{{item.name || '未命名'}}</text>
|
||||
<!-- 其他加载失败:内容不可见 / 加载失败 -->
|
||||
<empty-state
|
||||
wx:elif="{{pageState === 'invalid'}}"
|
||||
icon="icon-shirt"
|
||||
title="{{invalidTitle}}"
|
||||
desc="{{invalidDesc}}"
|
||||
buttonText="返回首页"
|
||||
bind:action="onBackHome"
|
||||
/>
|
||||
|
||||
<block wx:else>
|
||||
<scroll-view scroll-y class="scroll-body pb-dock" bindscroll="onNavScroll">
|
||||
<!-- 无缝拼图封面 -->
|
||||
<view class="od-cover">
|
||||
<view class="collage {{cl.cls(clothes.length)}}">
|
||||
<view class="cg" wx:for="{{clothes}}" wx:key="clothes_uuid" wx:if="{{index < 4}}">
|
||||
<image wx:if="{{item.processed_image}}" class="pic" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<view wx:if="{{clothes.length > 4}}" class="cg-more">+{{clothes.length - 4}}</view>
|
||||
<text class="cg-badge num">{{clothes.length}} 件</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-placeholder"></view>
|
||||
</scroll-view>
|
||||
<!-- 信息区 -->
|
||||
<view class="od-info">
|
||||
<view class="eyebrow">{{outfit.source === 2 ? 'AI Stylist' : 'My Look'}}</view>
|
||||
<view class="t-xl od-title">{{outfit.outfit_name}}</view>
|
||||
<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>
|
||||
</view>
|
||||
<view class="t-bd od-desc" wx:if="{{outfit.outfit_desc}}">{{outfit.outfit_desc}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="footer">
|
||||
<view class="footer-left">
|
||||
<view class="fb-btn" bindtap="onLike">
|
||||
<view class="icon icon-heart {{isLiked ? 'liked' : ''}}"></view>
|
||||
<text class="{{isLiked ? 'liked' : ''}}">{{likeCount}}</text>
|
||||
<!-- 包含衣物:横滑单品 -->
|
||||
<view class="od-items">
|
||||
<view class="grp-hd">
|
||||
<text class="gh-cn">包含衣物</text>
|
||||
<text class="gh-en">Items</text>
|
||||
</view>
|
||||
<scroll-view scroll-x class="od-items-scroll" show-scrollbar="{{false}}">
|
||||
<view class="od-items-row">
|
||||
<view
|
||||
class="od-chip"
|
||||
wx:for="{{clothes}}"
|
||||
wx:key="clothes_uuid"
|
||||
data-uuid="{{item.clothes_uuid}}"
|
||||
bindtap="onItemTap"
|
||||
>
|
||||
<view class="od-thumb">
|
||||
<image wx:if="{{item.processed_image}}" class="pic" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<text class="od-chip-n">{{item.name || '未命名'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="fb-btn" wx:if="{{!isOwner}}" bindtap="onCollect">
|
||||
<view class="icon {{isCollected ? 'icon-star-fill collected' : 'icon-star'}}"></view>
|
||||
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 悬浮玻璃操作条 -->
|
||||
<view class="dock">
|
||||
<view class="dock-row">
|
||||
<view class="dock-ic {{isLiked ? 'is-on' : ''}}" bindtap="onLike">
|
||||
<view class="icon icon-heart"></view>
|
||||
<text class="num">{{likeCount}}</text>
|
||||
</view>
|
||||
<view class="dock-ic" wx:if="{{!isOwner}}" bindtap="onCollect">
|
||||
<view class="icon {{isCollected ? 'icon-star-fill' : 'icon-star'}}"></view>
|
||||
</view>
|
||||
<view class="spacer"></view>
|
||||
<block wx:if="{{isOwner}}">
|
||||
<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>
|
||||
</button>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer-right" wx:if="{{isOwner}}">
|
||||
<view class="fb-public {{outfit.is_public === 2 ? 'public' : ''}}" bindtap="onTogglePublic">
|
||||
{{outfit.is_public === 2 ? '取消公开' : '公开到社区'}}
|
||||
</view>
|
||||
<button class="fb-share" open-type="share">
|
||||
<view class="icon icon-share"></view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 提示(分享者额度不足) -->
|
||||
<v2-overlay
|
||||
visible="{{tipVisible}}"
|
||||
type="modal"
|
||||
title="{{tipTitle}}"
|
||||
content="{{tipText}}"
|
||||
buttons="{{tipButtons}}"
|
||||
bind:confirm="onTipConfirm"
|
||||
/>
|
||||
|
||||
<!-- 公开到社区确认 -->
|
||||
<v2-overlay
|
||||
visible="{{pubVisible}}"
|
||||
type="modal"
|
||||
title="公开到社区"
|
||||
content="公开后将展示在社区供他人点赞收藏,并消耗 1 次社区分享额度"
|
||||
buttons="{{pubButtons}}"
|
||||
bind:confirm="onPubConfirm"
|
||||
bind:cancel="onPubCancel"
|
||||
/>
|
||||
|
||||
<!-- 删除确认 -->
|
||||
<v2-overlay
|
||||
visible="{{delVisible}}"
|
||||
type="modal"
|
||||
title="确认删除这套穿搭?"
|
||||
content="删除后不可恢复,但不会影响衣橱内的单品。"
|
||||
buttons="{{delButtons}}"
|
||||
bind:confirm="onDelConfirm"
|
||||
bind:cancel="onDelCancel"
|
||||
/>
|
||||
|
||||
<!-- 更多操作 -->
|
||||
<v2-overlay
|
||||
visible="{{moreVisible}}"
|
||||
type="actionsheet"
|
||||
items="{{moreItems}}"
|
||||
bind:select="onMoreSelect"
|
||||
bind:cancel="onMoreCancel"
|
||||
/>
|
||||
|
||||
<!-- 额度不足 -->
|
||||
<v2-overlay
|
||||
visible="{{quotaVisible}}"
|
||||
type="modal"
|
||||
title="{{quotaTitle}}"
|
||||
content="{{quotaText}}"
|
||||
buttons="{{quotaButtons}}"
|
||||
bind:confirm="onQuotaConfirm"
|
||||
bind:cancel="onQuotaCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.detail-page {
|
||||
/** 穿搭详情:无缝拼图封面 + 衬线大标题 + 横滑单品 + 悬浮玻璃操作条 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
@@ -9,233 +11,101 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.nav-more {
|
||||
padding: 0 8rpx;
|
||||
color: #000;
|
||||
.od-cover {
|
||||
padding: 32rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
/* 封面 */
|
||||
.cover {
|
||||
margin: 24rpx;
|
||||
height: 480rpx;
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
.od-info {
|
||||
padding: 48rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.od-title {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.od-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 26rpx;
|
||||
}
|
||||
|
||||
.od-desc {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.od-items {
|
||||
margin-top: 64rpx;
|
||||
}
|
||||
|
||||
.od-items-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.od-items-row {
|
||||
display: inline-flex;
|
||||
gap: 28rpx;
|
||||
padding: 0 var(--pad);
|
||||
}
|
||||
|
||||
.od-chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 160rpx;
|
||||
}
|
||||
|
||||
.od-thumb {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: var(--paper-2);
|
||||
border-radius: var(--r-img);
|
||||
overflow: hidden;
|
||||
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;
|
||||
.od-thumb .ph {
|
||||
width: 52%;
|
||||
height: 52%;
|
||||
color: var(--art);
|
||||
}
|
||||
|
||||
.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 {
|
||||
.od-thumb .pic {
|
||||
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;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 20rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 8rpx;
|
||||
max-width: 140rpx;
|
||||
.od-chip-n {
|
||||
margin-top: 14rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--ink-3);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer-placeholder {
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid #E5E5EA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 32rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.fb-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.fb-btn .icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.liked {
|
||||
color: #FF2D55;
|
||||
}
|
||||
|
||||
.collected {
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.fb-public {
|
||||
height: 76rpx;
|
||||
padding: 0 40rpx;
|
||||
border-radius: 38rpx;
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fb-public.public {
|
||||
background: #fff;
|
||||
border: 1rpx solid #007AFF;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.fb-share {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
/* 分享:原生 button 去默认外观 */
|
||||
.od-share {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
border: 0;
|
||||
background: var(--fill);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #007AFF;
|
||||
color: var(--ink);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.fb-share::after {
|
||||
.od-share::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fb-share .icon {
|
||||
.od-share .icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user