first commit

This commit is contained in:
2026-09-11 01:45:04 +08:00
commit 0a8d60d87f
145 changed files with 40978 additions and 0 deletions
@@ -0,0 +1,5 @@
{
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}
@@ -0,0 +1,145 @@
import { request } from '../../utils/request'
import { OutfitItem, ClothesItem } from '../../types/models'
import { OutfitSource, IsPublic } from '../../utils/config'
Page({
data: {
uuid: '',
isOwner: true,
outfit: {} as OutfitItem,
clothes: [] as ClothesItem[],
isLiked: false,
likeCount: 0,
isCollected: false,
loading: true,
},
onLoad(query: Record<string, string>) {
const uuid = query.uuid || ''
const isOwner = query.owner !== '0'
this.setData({ uuid, isOwner })
if (uuid) this.fetchDetail()
},
async fetchDetail() {
try {
const outfit = await request<OutfitItem>({ url: `/outfit/${this.data.uuid}` })
this.setData({ outfit })
const clothes = await Promise.all(
outfit.items.map(async (it) => {
try {
return await request<ClothesItem>({ url: `/clothes/${it.clothes_uuid}`, showError: false })
} catch (e) {
return { clothes_uuid: it.clothes_uuid } 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(() => {})
} catch (e) {
this.setData({ loading: false })
}
},
onLike() {
const action = this.data.isLiked ? 2 : 1
request({ url: '/like', method: 'POST', data: { target_uuid: this.data.uuid, action } })
.then(() => {
this.setData({
isLiked: action === 1,
likeCount: this.data.likeCount + (action === 1 ? 1 : -1),
})
})
.catch(() => {})
},
onCollect() {
const action = this.data.isCollected ? 2 : 1
request({ url: '/collect', method: 'POST', data: { target_uuid: this.data.uuid, action } })
.then(() => this.setData({ isCollected: action === 1 }))
.catch(() => {})
},
onTogglePublic() {
const isPublic = this.data.outfit.is_public === IsPublic.PUBLIC
if (isPublic) {
this.doPublic(IsPublic.PRIVATE)
} else {
wx.showModal({
title: '公开到社区',
content: '公开后将展示在社区供他人点赞收藏,并消耗 1 次社区分享额度',
confirmText: '确认公开',
success: (res) => {
if (res.confirm) this.doPublic(IsPublic.PUBLIC)
},
})
}
},
doPublic(isPublic: number) {
request({ url: `/outfit/${this.data.uuid}/public`, method: 'PUT', data: { is_public: isPublic } })
.then(() => {
this.setData({ 'outfit.is_public': isPublic })
wx.showToast({ title: isPublic === IsPublic.PUBLIC ? '已公开' : '已取消公开', icon: 'none' })
})
.catch(() => {})
},
onMore() {
wx.showActionSheet({
itemList: ['编辑穿搭', '删除穿搭'],
success: (res) => {
if (res.tapIndex === 0) this.onEdit()
else this.onDelete()
},
})
},
onEdit() {
wx.navigateTo({ url: `/pages/outfit-edit/outfit-edit?uuid=${this.data.uuid}` })
},
onDelete() {
wx.showModal({
title: '确认删除这套穿搭?',
content: '删除后不可恢复,但不会影响衣橱内的单品。',
confirmText: '确认删除',
confirmColor: '#FF3B30',
success: (res) => {
if (!res.confirm) return
request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE' })
.then(() => {
wx.showToast({ title: '已删除', icon: 'success' })
setTimeout(() => wx.navigateBack(), 500)
})
.catch(() => {})
},
})
},
onItemTap(e: WechatMiniprogram.TouchEvent) {
const uuid = e.currentTarget.dataset.uuid as string
wx.navigateTo({ url: `/pages/clothes-detail/clothes-detail?uuid=${uuid}` })
},
onShareAppMessage() {
return {
title: this.data.outfit.outfit_name || '我的穿搭',
path: `/pages/outfit-detail/outfit-detail?uuid=${this.data.uuid}&owner=0`,
}
},
})
@@ -0,0 +1,79 @@
<view class="page detail-page">
<navigation-bar title="{{outfit.outfit_name || '穿搭详情'}}">
<view wx:if="{{isOwner}}" 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>
<!-- 信息卡片 -->
<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>
<!-- 包含衣物 -->
<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>
</view>
</view>
</scroll-view>
</view>
<view class="footer-placeholder"></view>
</scroll-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>
<view class="fb-btn" wx:if="{{!isOwner}}" bindtap="onCollect">
<view class="icon {{isCollected ? 'icon-star-fill collected' : 'icon-star'}}"></view>
</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>
</view>
@@ -0,0 +1,241 @@
.detail-page {
height: 100vh;
display: flex;
flex-direction: column;
}
.scroll-body {
flex: 1;
height: 0;
}
.nav-more {
padding: 0 8rpx;
color: #000;
}
/* 封面 */
.cover {
margin: 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;
}
.item-name {
font-size: 20rpx;
color: #8E8E93;
margin-top: 8rpx;
max-width: 140rpx;
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;
padding: 0;
margin: 0;
background: #fff;
border: 1rpx solid #E5E5EA;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #007AFF;
line-height: 1;
}
.fb-share::after {
border: none;
}
.fb-share .icon {
width: 40rpx;
height: 40rpx;
}