import { request } from '../../utils/request' import { OutfitItem, ClothesItem, PagedResult } from '../../types/models' import { OutfitSource } from '../../utils/config' import { normalizePaged, hasMorePaged } from '../../utils/paged' import { navScroll } from '../../utils/nav' import { logger } from '../../utils/logger' // 日志模块名 const LOG = 'outfit-edit' const CATEGORIES = [ { name: '全部', value: '' }, { name: '上装', value: '上装' }, { name: '下装', value: '下装' }, { name: '连衣裙', value: '连衣裙' }, { name: '外套', value: '外套' }, { name: '鞋', value: '鞋' }, { name: '配饰', value: '配饰' }, ] const PAGE_SIZE = 50 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, loadingMore: false, hasMore: true, page: 1, saving: false, // 滚动收缩:由 utils/nav.ts 的共享 handler 驱动 navScrolled: false, // 删除确认:v2 玻璃弹窗 delVisible: false, delButtons: [ { text: '取消', value: '' }, { text: '确认删除', value: 'ok', type: 'main' }, ], }, // scroll-view 滚动 → 导航收缩为玻璃小标题 onNavScroll: navScroll, onLoad(query: Record) { const uuid = query.uuid || '' logger.debug(LOG, 'onLoad, uuid=', uuid, '编辑态=', !!uuid) if (uuid) { this.setData({ uuid, isEdit: true }) this.fetchOutfit(uuid) } }, async fetchOutfit(uuid: string) { try { const outfit = await request({ 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({ url: `/clothes/${id}`, showError: false }) } catch (e: any) { logger.warn(LOG, '单品详情拉取失败, uuid=', id, 'code=', e && e.code) return { clothes_uuid: id } as ClothesItem } }) ) // 编辑态下也要填充已选快照,避免确认选衣时跨分类数据丢失 const map = this.getSelectedMap() clothes.forEach((it) => { map[it.clothes_uuid] = it }) this.setData({ outfitName: outfit.outfit_name, outfitDesc: outfit.outfit_desc, scene: outfit.scene, selectedUuids: uuids, selectedItems: clothes, }) } catch (e: any) { logger.warn(LOG, '穿搭详情加载失败, code=', e && e.code) } }, // 已选衣物快照:跨分类切换时列表只保留当前分类,快照保留全部已选项 getSelectedMap(): Record { if (!(this as any).selectedMap) (this as any).selectedMap = {} return (this as any).selectedMap }, 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, page: 1, hasMore: true }) this.fetchAllClothes(1) }, backToForm() { this.setData({ view: 'form' }) }, fetchAllClothes(page = 1) { const cat = CATEGORIES[this.data.activeCategory].value const params: Record = { page, page_size: PAGE_SIZE } if (cat) params.category_main = cat // 请求序号:切换分类时旧响应可能后到,序号不匹配直接丢弃 const seq = ((this as any).reqSeq || 0) + 1 ;(this as any).reqSeq = seq if (page > 1) this.setData({ loadingMore: true }) request>({ url: '/clothes', data: params }) .then((res) => { if ((this as any).reqSeq !== seq) return const { list: raw, total } = normalizePaged(res) logger.debug(LOG, '衣物列表加载成功, 分类=', cat || '全部', 'page=', page, '条数=', raw && raw.length) const list = raw.map((it) => ({ ...it, _selected: this.data.selectedUuids.indexOf(it.clothes_uuid) >= 0, })) const merged = page === 1 ? list : this.data.allClothes.concat(list) this.setData({ allClothes: merged, selectLoading: false, loadingMore: false, page, hasMore: hasMorePaged(merged, total, PAGE_SIZE), }) }) .catch((e: any) => { if ((this as any).reqSeq !== seq) return logger.warn(LOG, '衣物列表加载失败, page=', page, 'code=', e && e.code) this.setData({ selectLoading: false, loadingMore: false }) }) }, onCategoryTap(e: WechatMiniprogram.TouchEvent) { const index = Number(e.currentTarget.dataset.index) if (index === this.data.activeCategory) return this.setData({ activeCategory: index, selectLoading: true, page: 1, hasMore: true }) this.fetchAllClothes(1) }, onReachBottom() { if (this.data.view !== 'select') return if (!this.data.hasMore || this.data.selectLoading || this.data.loadingMore) return this.fetchAllClothes(this.data.page + 1) }, onToggleSelect(e: WechatMiniprogram.CustomEvent) { const item = e.detail.item as ClothesItem const uuids = this.data.selectedUuids const map = this.getSelectedMap() if (uuids.indexOf(item.clothes_uuid) >= 0) { delete map[item.clothes_uuid] this.syncSelect(uuids.filter((u) => u !== item.clothes_uuid)) return } if (uuids.length >= 10) { logger.debug(LOG, '已选 10 件,达到上限') wx.showToast({ title: '最多选择 10 件衣物', icon: 'none' }) return } map[item.clothes_uuid] = item 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 map = this.getSelectedMap() // 以快照为准:只从当前分类列表里过滤会丢掉其它分类已选的衣物 const selectedItems = this.data.selectedUuids.map( (u) => map[u] || ({ clothes_uuid: u } as ClothesItem) ) this.setData({ view: 'form', selectedItems }) }, onSave() { if (this.data.saving) return 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: Record = { outfit_name: name, outfit_desc: this.data.outfitDesc, scene: this.data.scene, items: this.data.selectedUuids.map((u) => ({ clothes_uuid: u })), } // 创建态:后端 OutfitCreateRequest.Source 为 oneof=1 2 必填校验(漏传会报校验失败) // 编辑态:OutfitUpdateByUUIDRequest 没有 source 字段,不能传,避免把 AI 来源改掉 if (!this.data.isEdit) body.source = OutfitSource.USER // 编辑态后端 OutfitUpdateByUUIDRequest.OutfitUUID 为必填(binding required),请求体需携带 if (this.data.isEdit) body.outfit_uuid = this.data.uuid this.setData({ saving: true }) const req = this.data.isEdit ? request({ url: `/outfit/${this.data.uuid}`, method: 'PUT', data: body }) : request({ url: '/outfit', method: 'POST', data: body }) req .then(() => { logger.info(LOG, '穿搭保存成功, 编辑态=', this.data.isEdit, '件数=', body.items.length) this.setData({ saving: false }) 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) this.setData({ saving: false }) }) }, onDelete() { 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)) }, })