import { request } from '../../utils/request' import { PagedResult, OutfitItem } from '../../types/models' import { OutfitSource } from '../../utils/config' import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged' import { CoverCell, outfitCover } from '../../utils/outfit-cover' import { OUTFIT_STATUS, OutfitStatusPill, isPendingSave, outfitStatusPill } from '../../utils/outfit-status' import { navScroll } from '../../utils/nav' import { logger } from '../../utils/logger' // 日志模块名 const LOG = 'ai-pending' // 列表行:AI 生成结果 + 预计算的封面与状态胶囊(WXML 不能调用方法) type AiRow = OutfitItem & { _status: OutfitStatusPill _covers: CoverCell[] _collageCls: string _moreCount: number } Page({ data: { // 0 待保存(status=3) / 1 已保存(status=1) activeTab: 0, pendingList: [] as AiRow[], savedList: [] as AiRow[], // 当前分段渲染的数据(切段时同步切换,避免模板里写两套列表) list: [] as AiRow[], loading: true, refreshing: false, loadingMore: false, page: 1, pageSize: 20, hasMore: true, // 保存/删除防抖:同一时刻只允许一个写操作 saving: false, // 正在保存的那一条:按钮显示「保存中…」,避免重复点击 busyUuid: '', // 滚动收缩:由 utils/nav.ts 的共享 handler 驱动 navScrolled: false, // 删除确认(v2 玻璃弹窗) delVisible: false, delText: '', delUuid: '', delButtons: [ { text: '取消', value: '' }, { text: '确认删除', value: 'ok', type: 'main' }, ], // 轻量反馈(v2 深色玻璃 toast) toastVisible: false, toastText: '', }, // scroll-view 滚动 → 导航收缩为玻璃小标题 onNavScroll: navScroll, // 操作行空白处吞掉点击,避免穿透到卡片进详情 noop() {}, onLoad() { // 首帧渲染后再发请求,避免 setData 早于首次渲染导致 webview 渲染失败 wx.nextTick(() => this.fetchList(1)) }, onShow() { // 从详情页返回(可能在详情页保存过)需要重取,保证分段与状态一致 if ((this as any).loaded) this.fetchList(1) ;(this as any).loaded = true }, onTabChange(e: WechatMiniprogram.TouchEvent) { const index = Number(e.currentTarget.dataset.index) if (index === this.data.activeTab) return logger.debug(LOG, '切换分段, index=', index) this.setData({ activeTab: index, list: index === 0 ? this.data.pendingList : this.data.savedList }) }, onRefresh() { this.setData({ refreshing: true }) this.fetchList(1).finally(() => this.setData({ refreshing: false })) }, onReachBottom() { if (!this.data.hasMore || this.data.loading || this.data.loadingMore) return this.fetchList(this.data.page + 1) }, // 拉取 AI 穿搭:后端 GET /outfit 只支持 source 过滤(没有 status), // 因此一次拉全量再本地按 status 拆成「待保存 / 已保存」两段,分页仍按原始数据推进 fetchList(page: number): Promise { const seq = ((this as any).reqSeq || 0) + 1 ;(this as any).reqSeq = seq if (page > 1) { this.setData({ loadingMore: true }) } else { this.setData({ loading: this.data.pendingList.length === 0 && this.data.savedList.length === 0 }) } return request>({ url: '/outfit', data: { page, page_size: this.data.pageSize, source: OutfitSource.AI }, }) .then((res) => { if ((this as any).reqSeq !== seq) return const { list: raw, total } = normalizePaged(res) const rows: AiRow[] = raw.map((it) => ({ ...it, ...outfitCover(it.items), _status: outfitStatusPill(it), })) // 累积数组放实例而非 data:避免每次 setData 传输全量原始数据 const all = mergePaged(((this as any).allRows || []) as AiRow[], rows, page === 1) ;(this as any).allRows = all logger.debug(LOG, 'AI 穿搭加载成功, page=', page, '条数=', rows.length, '累计=', all.length) this.setData({ hasMore: hasMorePaged(all, total, this.data.pageSize), loading: false, loadingMore: false, page, }) this.applySplit(all) }) .catch((e: any) => { if ((this as any).reqSeq !== seq) return logger.warn(LOG, 'AI 穿搭加载失败, page=', page, 'code=', e && e.code) this.setData({ loading: false, loadingMore: false }) }) }, // 按 status 拆段并同步当前分段渲染数据 applySplit(all: AiRow[]) { const pendingList = all.filter((it) => isPendingSave(it)) const savedList = all.filter((it) => !isPendingSave(it)) this.setData({ pendingList, savedList, list: this.data.activeTab === 0 ? pendingList : savedList, }) }, // 保存为正式穿搭(status 3 → 1):成功后本地迁移段落,不重新请求以免闪屏 onSave(e: WechatMiniprogram.TouchEvent) { const uuid = e.currentTarget.dataset.uuid as string if (this.data.saving) return this.setData({ saving: true, busyUuid: uuid }) logger.info(LOG, '保存 AI 穿搭, uuid=', uuid) request({ url: '/ai-recommend/save', method: 'POST', data: { outfit_uuid: uuid }, showError: false }) .then(() => { const all = (((this as any).allRows || []) as AiRow[]).map((it) => it.outfit_uuid === uuid ? { ...it, status: OUTFIT_STATUS.NORMAL, _status: outfitStatusPill({ ...it, status: OUTFIT_STATUS.NORMAL }) } : it ) ;(this as any).allRows = all this.applySplit(all) this.setData({ saving: false, busyUuid: '' }) this.toast('已保存,可在「已保存」查看') }) .catch((err: any) => { this.setData({ saving: false, busyUuid: '' }) const code = err && err.code logger.warn(LOG, '保存 AI 穿搭失败, uuid=', uuid, 'code=', code) if (code === 40000) { // 超过 7 天被后端定时清理:本地同步移除,避免用户反复点必然失败的按钮 this.removeLocal(uuid) this.toast('这套搭配已被清理,请重新生成') return } const detail = (err && err.details && err.details[0]) || (err && err.message) || '' this.toast(detail || '保存失败,请稍后重试') }) }, // 删除入口:弹 v2 玻璃确认层 onDelete(e: WechatMiniprogram.TouchEvent) { const uuid = e.currentTarget.dataset.uuid as string this.setData({ delVisible: true, delUuid: uuid, delText: '删除后不可恢复,这套 AI 生成的搭配会从列表中移除。', }) }, onDelCancel() { this.setData({ delVisible: false }) }, onDelConfirm(e: WechatMiniprogram.CustomEvent) { this.setData({ delVisible: false }) if (e.detail.value !== 'ok') return const uuid = this.data.delUuid if (!uuid || this.data.saving) return this.setData({ saving: true }) request({ url: `/outfit/${uuid}`, method: 'DELETE', showError: false }) .then(() => { logger.info(LOG, '删除 AI 穿搭成功, uuid=', uuid) this.setData({ saving: false }) this.removeLocal(uuid) this.toast('已删除') }) .catch((err: any) => { this.setData({ saving: false }) const code = err && err.code logger.warn(LOG, '删除 AI 穿搭失败, uuid=', uuid, 'code=', code) if (code === 40000) { this.removeLocal(uuid) this.toast('这套搭配已不存在') return } const detail = (err && err.details && err.details[0]) || (err && err.message) || '' this.toast(detail || '删除失败,请稍后重试') }) }, // 本地移除(删除成功,或后端已清理) removeLocal(uuid: string) { const all = (((this as any).allRows || []) as AiRow[]).filter((it) => it.outfit_uuid !== uuid) ;(this as any).allRows = all this.applySplit(all) }, // 重新生成:回到 AI 推荐流程(会再消耗 1 次推荐额度) onRegen() { wx.navigateTo({ url: '/pages/ai-recommend/ai-recommend', fail: (e) => logger.warn(LOG, 'navigateTo AI 推荐失败, err=', e && e.errMsg), }) }, onDetail(e: WechatMiniprogram.TouchEvent) { const uuid = e.currentTarget.dataset.uuid as string logger.debug(LOG, '点击 AI 穿搭, uuid=', uuid) wx.navigateTo({ url: `/pages/outfit-detail/outfit-detail?uuid=${uuid}&owner=1`, fail: (err) => logger.warn(LOG, 'navigateTo 穿搭详情失败, err=', err && err.errMsg), }) }, onGoGenerate() { this.onRegen() }, // 轻量反馈:先置 false 再置 true,确保连续提示也能重新触发自动关闭 toast(text: string) { this.setData({ toastVisible: false, toastText: text }) wx.nextTick(() => this.setData({ toastVisible: true })) }, onToastClose() { this.setData({ toastVisible: false }) }, })