Files
zt-closet-wxmini-frontend/miniprogram/pages/mall/mall.ts
T
2026-09-11 01:45:04 +08:00

217 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { request } from '../../utils/request'
import { QuotaItem, UserInfo, GoodsItem, QuotaFlowItem, PagedResult, VirtualPayResult, OrderItem } from '../../types/models'
import { QUOTA_NAMES, GoodsType, GOODS_TYPE_NAMES } from '../../utils/config'
import { fenToYuan, formatDate, formatDateTime } from '../../utils/format'
const GOODS_ICONS: Record<number, string> = {
[GoodsType.MEMBER]: 'icon-crown',
[GoodsType.UPLOAD]: 'icon-camera',
[GoodsType.AI]: 'icon-sparkles',
[GoodsType.CLOSET_SHARE]: 'icon-hanger',
[GoodsType.OUTFIT_SHARE]: 'icon-shirt',
}
const GOODS_FILTERS = [
{ name: '全部', value: 0 },
{ name: '会员包', value: 1 },
{ name: '上传包', value: 2 },
{ name: 'AI 推荐包', value: 3 },
{ name: '衣柜分享包', value: 4 },
{ name: '搭配分享包', value: 5 },
]
type PayPlatform = 'android' | 'ios' | 'harmony' | 'windows'
// 归一化平台:后端仅接受 android/ios/harmony/windows,devtools/mac 等调试环境默认按 android 处理
function normalizePlatform(): PayPlatform {
let p = 'android'
try {
p = wx.getSystemInfoSync().platform
} catch (e) {
// 获取失败时回退 android,避免请求体缺失导致参数校验失败
}
if (p === 'ios' || p === 'harmony' || p === 'windows') return p
return 'android'
}
Page({
data: {
view: 'mall' as 'mall' | 'flow',
userInfo: {} as UserInfo,
isVip: false,
vipExpireText: '',
quotas: [] as QuotaItem[],
goodsFilters: GOODS_FILTERS,
activeFilter: 0,
goods: [] as GoodsItem[],
flowList: [] as QuotaFlowItem[],
goodsIcons: GOODS_ICONS,
orderVisible: false,
orderGoods: null as GoodsItem | null,
loading: true,
},
onLoad(query: Record<string, string>) {
if (query.tab === 'flow') this.setData({ view: 'flow' })
},
onShow() {
this.fetchUser()
this.fetchQuota()
this.fetchGoods()
if (this.data.view === 'flow') this.fetchFlow()
},
fetchUser() {
request<UserInfo>({ url: '/user/info', showError: false })
.then((user) => {
const isVip = user.vip_type !== 1
this.setData({
userInfo: user,
isVip,
vipExpireText: isVip && user.vip_expire ? `有效期至 ${formatDate(user.vip_expire)}` : '',
})
})
.catch(() => {})
},
fetchQuota() {
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false })
.then((res) => {
const quotas = res.quotas.map((q) => ({
...q,
_name: QUOTA_NAMES[q.quota_type] || '额度',
_percent: q.total > 0 ? Math.round((q.used / q.total) * 100) : 0,
}))
this.setData({ quotas })
})
.catch(() => {})
},
fetchGoods() {
const params: Record<string, any> = { page: 1, page_size: 50 }
const filter = this.data.activeFilter
if (filter > 0) params.goods_type = filter
request<PagedResult<GoodsItem>>({ url: '/goods', data: params, auth: false })
.then((res) => {
const goods = res.list.map((g) => ({
...g,
_icon: GOODS_ICONS[g.goods_type] || 'icon-crown',
_typeName: GOODS_TYPE_NAMES[g.goods_type] || '',
_priceText: fenToYuan(g.discount_price),
_originText: fenToYuan(g.price),
}))
this.setData({ goods, loading: false })
})
.catch(() => this.setData({ loading: false }))
},
fetchFlow() {
request<PagedResult<QuotaFlowItem>>({ url: '/quota/flow', data: { page: 1, page_size: 50 } })
.then((res) => {
const list = res.list.map((it) => ({
...it,
_time: formatDateTime(it.created_at),
_sign: it.change_num > 0 ? `+${it.change_num}` : `${it.change_num}`,
_name: QUOTA_NAMES[it.quota_type] || '额度',
}))
this.setData({ flowList: list })
})
.catch(() => {})
},
onGoodsFilter(e: WechatMiniprogram.TouchEvent) {
const index = Number(e.currentTarget.dataset.index)
if (index === this.data.activeFilter) return
this.setData({ activeFilter: index, loading: true })
this.fetchGoods()
},
onTapMember() {
this.setData({ activeFilter: 1, loading: true })
this.fetchGoods()
},
onSwitchMall() {
this.setData({ view: 'mall' })
},
onSwitchFlow() {
this.setData({ view: 'flow' })
this.fetchFlow()
},
onBuy(e: WechatMiniprogram.TouchEvent) {
const goods = e.currentTarget.dataset.goods as GoodsItem
this.setData({ orderGoods: goods, orderVisible: true })
},
onCloseOrder() {
this.setData({ orderVisible: false })
},
noop() {},
confirmPay() {
if (!this.data.orderGoods) return
request<VirtualPayResult>({
url: '/order/virtual',
method: 'POST',
data: {
goods_code: this.data.orderGoods.goods_code,
platform: normalizePlatform(),
},
})
.then((res) => {
this.setData({ orderVisible: false })
this.doPayment(res)
})
.catch(() => {})
},
doPayment(res: VirtualPayResult) {
wx.requestVirtualPayment({
signData: res.sign_data,
paySig: res.pay_sig,
signature: res.signature,
mode: 'short_series_goods',
success: () => {
// 拉起成功仅代表进入支付页,最终到账以后端发货推送为准,轮询订单确认
this.pollOrder(res.order_no)
},
fail: () => {},
})
},
pollOrder(orderNo: string) {
let count = 0
const timer = setInterval(() => {
count++
request<PagedResult<OrderItem>>({ url: '/order', data: { page: 1, page_size: 50 }, showError: false })
.then((res) => {
const order = res.list.find((it) => it.order_no === orderNo)
if (order && order.order_status === 2) {
clearInterval(timer)
wx.showToast({ title: '支付成功', icon: 'success' })
this.fetchQuota()
this.fetchUser()
} else if (order && (order.order_status === 3 || order.order_status === 4 || order.order_status === 5)) {
clearInterval(timer)
const msg = order.order_status === 5 ? '订单已退款' : '订单未支付成功'
wx.showToast({ title: msg, icon: 'none' })
} else if (count >= 10) {
this.finishPayTimeout(timer)
}
})
.catch(() => {
if (count >= 10) this.finishPayTimeout(timer)
})
}, 1500)
},
finishPayTimeout(timer: ReturnType<typeof setInterval>) {
clearInterval(timer)
wx.showToast({ title: '到账可能有延迟,请稍后查看', icon: 'none' })
this.fetchQuota()
this.fetchUser()
},
})