first commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"clothes-card": "/components/clothes-card/clothes-card"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { OutfitItem, ClothesItem, PagedResult } from '../../types/models'
|
||||
|
||||
const CATEGORIES = [
|
||||
{ name: '全部', value: '' },
|
||||
{ name: '上装', value: '上装' },
|
||||
{ name: '下装', value: '下装' },
|
||||
{ name: '连衣裙', value: '连衣裙' },
|
||||
{ name: '外套', value: '外套' },
|
||||
{ name: '鞋', value: '鞋' },
|
||||
{ name: '配饰', value: '配饰' },
|
||||
]
|
||||
|
||||
Page({
|
||||
data: {
|
||||
uuid: '',
|
||||
isEdit: false,
|
||||
view: 'form' as 'form' | 'select',
|
||||
outfitName: '',
|
||||
outfitDesc: '',
|
||||
scene: '',
|
||||
selectedUuids: [] as string[],
|
||||
selectedItems: [] as ClothesItem[],
|
||||
categories: CATEGORIES,
|
||||
activeCategory: 0,
|
||||
allClothes: [] as ClothesItem[],
|
||||
selectLoading: true,
|
||||
},
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
const uuid = query.uuid || ''
|
||||
if (uuid) {
|
||||
this.setData({ uuid, isEdit: true })
|
||||
this.fetchOutfit(uuid)
|
||||
}
|
||||
},
|
||||
|
||||
async fetchOutfit(uuid: string) {
|
||||
try {
|
||||
const outfit = await request<OutfitItem>({ url: `/outfit/${uuid}` })
|
||||
const uuids = outfit.items.map((i) => i.clothes_uuid)
|
||||
const clothes = await Promise.all(
|
||||
uuids.map(async (id) => {
|
||||
try {
|
||||
return await request<ClothesItem>({ url: `/clothes/${id}`, showError: false })
|
||||
} catch (e) {
|
||||
return { clothes_uuid: id } as ClothesItem
|
||||
}
|
||||
})
|
||||
)
|
||||
this.setData({
|
||||
outfitName: outfit.outfit_name,
|
||||
outfitDesc: outfit.outfit_desc,
|
||||
scene: outfit.scene,
|
||||
selectedUuids: uuids,
|
||||
selectedItems: clothes,
|
||||
})
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
onNameInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ outfitName: e.detail.value })
|
||||
},
|
||||
onDescInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ outfitDesc: e.detail.value })
|
||||
},
|
||||
onSceneInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ scene: e.detail.value })
|
||||
},
|
||||
|
||||
openSelect() {
|
||||
this.setData({ view: 'select', selectLoading: true })
|
||||
this.fetchAllClothes()
|
||||
},
|
||||
|
||||
backToForm() {
|
||||
this.setData({ view: 'form' })
|
||||
},
|
||||
|
||||
fetchAllClothes() {
|
||||
const cat = CATEGORIES[this.data.activeCategory].value
|
||||
const params: Record<string, any> = { page: 1, page_size: 50 }
|
||||
if (cat) params.category_main = cat
|
||||
request<PagedResult<ClothesItem>>({ url: '/clothes', data: params })
|
||||
.then((res) => {
|
||||
const list = res.list.map((it) => ({
|
||||
...it,
|
||||
_selected: this.data.selectedUuids.indexOf(it.clothes_uuid) >= 0,
|
||||
}))
|
||||
this.setData({ allClothes: list, selectLoading: false })
|
||||
})
|
||||
.catch(() => this.setData({ selectLoading: false }))
|
||||
},
|
||||
|
||||
onCategoryTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
if (index === this.data.activeCategory) return
|
||||
this.setData({ activeCategory: index, selectLoading: true })
|
||||
this.fetchAllClothes()
|
||||
},
|
||||
|
||||
onToggleSelect(e: WechatMiniprogram.CustomEvent) {
|
||||
const item = e.detail.item as ClothesItem
|
||||
const uuids = this.data.selectedUuids
|
||||
const idx = uuids.indexOf(item.clothes_uuid)
|
||||
if (idx >= 0) {
|
||||
this.syncSelect(uuids.filter((u) => u !== item.clothes_uuid))
|
||||
} else {
|
||||
if (uuids.length >= 10) {
|
||||
wx.showToast({ title: '最多选择 10 件衣物', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.syncSelect([...uuids, item.clothes_uuid])
|
||||
}
|
||||
},
|
||||
|
||||
syncSelect(uuids: string[]) {
|
||||
this.setData({
|
||||
selectedUuids: uuids,
|
||||
allClothes: this.data.allClothes.map((it) => ({
|
||||
...it,
|
||||
_selected: uuids.indexOf(it.clothes_uuid) >= 0,
|
||||
})),
|
||||
})
|
||||
},
|
||||
|
||||
confirmSelect() {
|
||||
const selectedItems = this.data.allClothes.filter(
|
||||
(it) => this.data.selectedUuids.indexOf(it.clothes_uuid) >= 0
|
||||
)
|
||||
this.setData({ view: 'form', selectedItems })
|
||||
},
|
||||
|
||||
onSave() {
|
||||
const name = this.data.outfitName.trim()
|
||||
if (!name) {
|
||||
wx.showToast({ title: '请填写穿搭名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.data.selectedUuids.length) {
|
||||
wx.showToast({ title: '至少选择 1 件衣物', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const body = {
|
||||
outfit_name: name,
|
||||
outfit_desc: this.data.outfitDesc,
|
||||
scene: this.data.scene,
|
||||
items: this.data.selectedUuids.map((u) => ({ clothes_uuid: u })),
|
||||
}
|
||||
const req = this.data.isEdit
|
||||
? request({ url: `/outfit/${this.data.uuid}`, method: 'PUT', data: body })
|
||||
: request({ url: '/outfit', method: 'POST', data: body })
|
||||
req
|
||||
.then(() => {
|
||||
wx.showToast({ title: '已保存', icon: 'success' })
|
||||
setTimeout(() => wx.navigateBack(), 500)
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
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(() => {})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
<view class="page edit-page">
|
||||
<navigation-bar title="{{view === 'select' ? '选择衣橱单品' : (isEdit ? '编辑穿搭' : '创建穿搭')}}">
|
||||
<view wx:if="{{view === 'select'}}" slot="right" class="nav-cancel" bindtap="backToForm">取消</view>
|
||||
</navigation-bar>
|
||||
|
||||
<!-- 表单视图 -->
|
||||
<scroll-view wx:if="{{view === 'form'}}" scroll-y class="scroll-body">
|
||||
<!-- 拼图预览 -->
|
||||
<view class="card preview">
|
||||
<view wx:if="{{selectedItems.length}}" class="preview-items">
|
||||
<view
|
||||
class="preview-item preview-{{index}}"
|
||||
wx:for="{{selectedItems}}"
|
||||
wx:key="clothes_uuid"
|
||||
wx:if="{{index < 5}}"
|
||||
>
|
||||
<image wx:if="{{item.processed_image}}" class="preview-img" src="{{item.processed_image}}" mode="aspectFit" />
|
||||
<view wx:else class="icon icon-hanger preview-placeholder"></view>
|
||||
</view>
|
||||
<view wx:if="{{selectedItems.length > 5}}" class="preview-more">+{{selectedItems.length - 5}}</view>
|
||||
</view>
|
||||
<view wx:else class="preview-empty">
|
||||
<view class="icon icon-hanger"></view>
|
||||
<text class="pe-title">还没有选择衣物</text>
|
||||
<text class="pe-desc">点击下方「选择衣物」添加你的单品</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单 -->
|
||||
<view class="card form-card">
|
||||
<view class="field">
|
||||
<view class="field-label">
|
||||
<text>穿搭名称 <text class="req">*</text></text>
|
||||
<text class="count">{{outfitName.length}}/32</text>
|
||||
</view>
|
||||
<input class="input" value="{{outfitName}}" bindinput="onNameInput" placeholder="给这套穿搭起个名字" maxlength="32" />
|
||||
</view>
|
||||
<view class="field">
|
||||
<view class="field-label">
|
||||
<text>穿搭描述</text>
|
||||
<text class="count">{{outfitDesc.length}}/500</text>
|
||||
</view>
|
||||
<textarea class="textarea" value="{{outfitDesc}}" bindinput="onDescInput" placeholder="简单描述这套穿搭的灵感或亮点" maxlength="500" />
|
||||
</view>
|
||||
<view class="field">
|
||||
<view class="field-label">
|
||||
<text>适合场景</text>
|
||||
<text class="count">{{scene.length}}/100</text>
|
||||
</view>
|
||||
<input class="input" value="{{scene}}" bindinput="onSceneInput" placeholder="例如:通勤、约会、旅行" maxlength="100" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选择衣物入口 -->
|
||||
<view class="card select-entry" bindtap="openSelect">
|
||||
<view class="entry-left">
|
||||
<view class="entry-icon"><view class="icon icon-shirt"></view></view>
|
||||
<view>
|
||||
<view class="entry-title">选择衣橱单品</view>
|
||||
<view class="entry-desc">{{selectedUuids.length ? '点击调整当前穿搭包含的衣物' : '选择 1~10 件衣物合成画报'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="entry-right">
|
||||
<text class="entry-count {{selectedUuids.length ? 'on' : ''}}">{{selectedUuids.length ? '已选 ' + selectedUuids.length + ' 件' : '未选择'}}</text>
|
||||
<view class="icon icon-chevron-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="footer">
|
||||
<view wx:if="{{isEdit}}" class="btn-danger-text delete-link" bindtap="onDelete">删除穿搭</view>
|
||||
<view
|
||||
class="btn btn-primary {{(!outfitName || !selectedUuids.length) ? 'disabled' : ''}}"
|
||||
bindtap="onSave"
|
||||
>{{isEdit ? '保存修改' : '保存'}}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 多选视图 -->
|
||||
<view wx:else class="select-view">
|
||||
<scroll-view scroll-x class="cat-scroll" show-scrollbar="{{false}}">
|
||||
<view class="cat-row">
|
||||
<view
|
||||
wx:for="{{categories}}"
|
||||
wx:key="name"
|
||||
class="cat-item {{activeCategory === index ? 'active' : ''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="onCategoryTap"
|
||||
>{{item.name}}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<scroll-view scroll-y class="select-body">
|
||||
<view wx:if="{{selectLoading}}" class="grid">
|
||||
<view class="grid-item" wx:for="{{[1, 2, 3, 4]}}" wx:key="*this">
|
||||
<view class="skeleton sk-card"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="grid">
|
||||
<view class="grid-item" wx:for="{{allClothes}}" wx:key="clothes_uuid">
|
||||
<clothes-card item="{{item}}" selectable selected="{{item._selected}}" bind:select="onToggleSelect" />
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="select-footer">
|
||||
<view>
|
||||
<view class="sf-count">已选 <text class="t-primary">{{selectedUuids.length}}</text> 件</view>
|
||||
<view class="sf-desc">最多可选择 10 件单品</view>
|
||||
</view>
|
||||
<view class="sf-confirm btn btn-primary" bindtap="confirmSelect">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,296 @@
|
||||
.edit-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.nav-cancel {
|
||||
font-size: 30rpx;
|
||||
color: #007AFF;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
/* 预览区 */
|
||||
.preview {
|
||||
margin: 24rpx;
|
||||
height: 320rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.preview-empty .icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.pe-title {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.pe-desc {
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.preview-items {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
position: absolute;
|
||||
width: 140rpx;
|
||||
height: 180rpx;
|
||||
background: #F8F8FA;
|
||||
border-radius: 16rpx;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-0 { left: 30rpx; top: 60rpx; transform: rotate(-5deg); }
|
||||
.preview-1 { left: 120rpx; top: 100rpx; transform: rotate(3deg); }
|
||||
.preview-2 { left: 210rpx; top: 50rpx; transform: rotate(6deg); }
|
||||
.preview-3 { left: 300rpx; top: 110rpx; transform: rotate(-3deg); }
|
||||
.preview-4 { left: 390rpx; top: 70rpx; transform: rotate(8deg); }
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.preview-more {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 24rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 表单 */
|
||||
.form-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #1C1C1E;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.req {
|
||||
color: #FF3B30;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 选择衣物入口 */
|
||||
.select-entry {
|
||||
margin: 0 24rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.entry-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entry-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(0, 122, 255, 0.1);
|
||||
color: #007AFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.entry-icon .icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1C1C1E;
|
||||
}
|
||||
|
||||
.entry-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.entry-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.entry-count {
|
||||
font-size: 26rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.entry-count.on {
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.entry-right .icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.footer {
|
||||
padding: 16rpx 24rpx;
|
||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.delete-link {
|
||||
text-align: center;
|
||||
padding: 16rpx 0 24rpx;
|
||||
}
|
||||
|
||||
/* 多选视图 */
|
||||
.select-view {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cat-scroll {
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cat-row {
|
||||
display: inline-flex;
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.cat-item {
|
||||
flex-shrink: 0;
|
||||
padding: 0 32rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 26rpx;
|
||||
color: #3C3C43;
|
||||
background: #F2F2F7;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.cat-item.active {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.select-body {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 24rpx;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
width: calc(50% - 12rpx);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sk-card {
|
||||
height: 360rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.select-footer {
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #E5E5EA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 32rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.sf-count {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1C1C1E;
|
||||
}
|
||||
|
||||
.sf-desc {
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.sf-confirm {
|
||||
width: 200rpx;
|
||||
height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
Reference in New Issue
Block a user