first commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"empty-state": "/components/empty-state/empty-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { PagedResult, OutfitItem } from '../../types/models'
|
||||
import { OutfitSource, IsPublic } from '../../utils/config'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activeTab: 0,
|
||||
list: [] as OutfitItem[],
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.syncTabBar()
|
||||
const openCollect = wx.getStorageSync('zt_open_collect')
|
||||
if (openCollect) {
|
||||
wx.removeStorageSync('zt_open_collect')
|
||||
if (this.data.activeTab !== 1) {
|
||||
this.setData({ activeTab: 1, list: [], loading: true })
|
||||
}
|
||||
}
|
||||
this.refresh()
|
||||
},
|
||||
|
||||
syncTabBar() {
|
||||
const tabBar = (this as any).getTabBar && (this as any).getTabBar()
|
||||
if (tabBar) tabBar.setData({ selected: 1 })
|
||||
},
|
||||
|
||||
onTabChange(e: WechatMiniprogram.TouchEvent) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
if (index === this.data.activeTab) return
|
||||
this.setData({ activeTab: index, list: [], loading: true })
|
||||
this.refresh()
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.setData({ page: 1, hasMore: true, loading: this.data.list.length === 0 })
|
||||
this.fetchList(1)
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.setData({ refreshing: true })
|
||||
this.fetchList(1).finally(() => this.setData({ refreshing: false }))
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (!this.data.hasMore || this.data.loading) return
|
||||
this.fetchList(this.data.page + 1)
|
||||
},
|
||||
|
||||
fetchList(page: number): Promise<void> {
|
||||
const isCollect = this.data.activeTab === 1
|
||||
const url = isCollect ? '/collect' : '/outfit'
|
||||
return request<PagedResult<OutfitItem>>({ url, data: { page, page_size: this.data.pageSize } })
|
||||
.then(async (res) => {
|
||||
const items = await this.decorateList(res.list)
|
||||
const merged = page === 1 ? items : this.data.list.concat(items)
|
||||
this.setData({
|
||||
list: merged,
|
||||
hasMore: merged.length < res.total,
|
||||
loading: false,
|
||||
page,
|
||||
})
|
||||
})
|
||||
.catch(() => this.setData({ loading: false }))
|
||||
},
|
||||
|
||||
decorateList(list: OutfitItem[]): Promise<OutfitItem[]> {
|
||||
return Promise.all(
|
||||
list.map(async (it) => {
|
||||
let likeCount = 0
|
||||
try {
|
||||
const like = await request<{ is_liked: boolean; like_count: number }>({
|
||||
url: `/like/${it.outfit_uuid}`,
|
||||
showError: false,
|
||||
})
|
||||
likeCount = like.like_count
|
||||
} catch (e) {}
|
||||
return { ...it, _likeCount: likeCount, _status: this.getStatusText(it) }
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
getStatusText(it: OutfitItem): { text: string; type: string } {
|
||||
if (it.source === OutfitSource.AI) return { text: 'AI 推荐', type: 'indigo' }
|
||||
if (it.is_public === IsPublic.PUBLIC) return { text: '已公开', type: 'blue' }
|
||||
return { text: '仅自己可见', type: 'gray' }
|
||||
},
|
||||
|
||||
onCreate() {
|
||||
wx.navigateTo({ url: '/pages/outfit-edit/outfit-edit' })
|
||||
},
|
||||
|
||||
onAiRecommend() {
|
||||
wx.navigateTo({ url: '/pages/ai-recommend/ai-recommend' })
|
||||
},
|
||||
|
||||
onOutfitTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const uuid = e.currentTarget.dataset.uuid as string
|
||||
const owner = this.data.activeTab === 0 ? '1' : '0'
|
||||
wx.navigateTo({ url: `/pages/outfit-detail/outfit-detail?uuid=${uuid}&owner=${owner}` })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="穿搭" back="{{false}}" />
|
||||
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="scroll-body"
|
||||
refresher-enabled
|
||||
refresher-triggered="{{refreshing}}"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
bindscrolltolower="onReachBottom"
|
||||
>
|
||||
<!-- 分段控件 -->
|
||||
<view class="topbar">
|
||||
<view class="segment">
|
||||
<view class="segment-item {{activeTab === 0 ? 'active' : ''}}" data-index="0" bindtap="onTabChange">我的穿搭</view>
|
||||
<view class="segment-item {{activeTab === 1 ? 'active' : ''}}" data-index="1" bindtap="onTabChange">已收藏</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 入口按钮 -->
|
||||
<view class="entry-row">
|
||||
<view class="entry-btn entry-outline" bindtap="onCreate">+ 创建穿搭</view>
|
||||
<view class="entry-btn entry-primary" bindtap="onAiRecommend">AI 推荐</view>
|
||||
</view>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view wx:if="{{loading}}" class="list">
|
||||
<view class="skeleton sk-card" wx:for="{{[1, 2, 3]}}" wx:key="*this"></view>
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<empty-state
|
||||
wx:elif="{{!list.length}}"
|
||||
icon="icon-shirt"
|
||||
title="{{activeTab === 0 ? '还没有穿搭' : '还没有收藏的穿搭'}}"
|
||||
desc="{{activeTab === 0 ? '从衣橱挑几件,搭一套吧' : ''}}"
|
||||
buttonText="{{activeTab === 0 ? '创建穿搭' : ''}}"
|
||||
bind:action="onCreate"
|
||||
/>
|
||||
|
||||
<!-- 卡片流 -->
|
||||
<view wx:else class="list">
|
||||
<view
|
||||
class="outfit-card card"
|
||||
wx:for="{{list}}"
|
||||
wx:key="outfit_uuid"
|
||||
data-uuid="{{item.outfit_uuid}}"
|
||||
bindtap="onOutfitTap"
|
||||
>
|
||||
<view class="cover">
|
||||
<view
|
||||
class="cover-item cover-{{index}}"
|
||||
wx:for="{{item.items}}"
|
||||
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-ph"></view>
|
||||
</view>
|
||||
<view wx:if="{{!item.items.length}}" class="icon icon-hanger"></view>
|
||||
<text class="cover-count">{{item.items.length}} 件</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="title-row">
|
||||
<text class="title">{{item.outfit_name}}</text>
|
||||
<text class="tag tag-{{item._status.type}}">{{item._status.text}}</text>
|
||||
</view>
|
||||
<view class="desc" wx:if="{{item.outfit_desc}}">{{item.outfit_desc}}</view>
|
||||
<view class="meta-row">
|
||||
<text class="tag tag-gray" wx:if="{{item.scene}}">{{item.scene}}</text>
|
||||
<text class="meta">{{item.items.length}} 件衣物</text>
|
||||
<view class="spacer"></view>
|
||||
<view class="meta-icon">
|
||||
<view class="icon icon-heart heart"></view>
|
||||
<text>{{item._likeCount || 0}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{list.length && !hasMore}}" class="no-more">没有更多了</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -0,0 +1,208 @@
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
background: #fff;
|
||||
padding: 20rpx 32rpx;
|
||||
}
|
||||
|
||||
.segment {
|
||||
display: flex;
|
||||
background: #F2F2F7;
|
||||
border-radius: 20rpx;
|
||||
padding: 6rpx;
|
||||
}
|
||||
|
||||
.segment-item {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #3C3C43;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.segment-item.active {
|
||||
background: #fff;
|
||||
color: #007AFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.entry-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.entry-btn {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.entry-outline {
|
||||
border: 1rpx solid #007AFF;
|
||||
color: #007AFF;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.entry-primary {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.sk-card {
|
||||
height: 360rpx;
|
||||
border-radius: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.outfit-card {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.cover {
|
||||
height: 280rpx;
|
||||
background: #F8F8FA;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover .icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.cover-item {
|
||||
position: absolute;
|
||||
width: 150rpx;
|
||||
height: 190rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-0 { left: 16rpx; top: 40rpx; transform: rotate(-6deg); }
|
||||
.cover-1 { left: 140rpx; top: 82rpx; transform: rotate(3deg); }
|
||||
.cover-2 { left: 268rpx; top: 32rpx; transform: rotate(5deg); }
|
||||
.cover-3 { left: 392rpx; top: 84rpx; transform: rotate(-3deg); }
|
||||
.cover-4 { left: 480rpx; top: 40rpx; transform: rotate(6deg); }
|
||||
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cover-ph {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.cover-count {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
font-size: 22rpx;
|
||||
color: #007AFF;
|
||||
background: rgba(0, 122, 255, 0.1);
|
||||
padding: 4rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 12rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.heart {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
color: #C7C7CC;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #C7C7CC;
|
||||
padding: 24rpx 0 40rpx;
|
||||
}
|
||||
Reference in New Issue
Block a user