refactor: 拆分小程序 vue 结构
This commit is contained in:
52
src/pages/menu/composables/menu-page/data-actions.ts
Normal file
52
src/pages/menu/composables/menu-page/data-actions.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type {
|
||||
FulfillmentScene,
|
||||
MiniCategory,
|
||||
MiniMenuSection
|
||||
} from '@/shared'
|
||||
import { getCategories, getMenu } from '@/services'
|
||||
import type { useAppStore } from '@/stores'
|
||||
|
||||
type AppStoreInstance = ReturnType<typeof useAppStore>
|
||||
|
||||
export function createMenuDataActions (payload: {
|
||||
appStore: AppStoreInstance
|
||||
categories: Ref<MiniCategory[]>
|
||||
errorMessage: Ref<string>
|
||||
loading: Ref<boolean>
|
||||
sections: Ref<MiniMenuSection[]>
|
||||
}) {
|
||||
const { appStore, categories, errorMessage, loading, sections } = payload
|
||||
|
||||
async function loadMenu () {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await appStore.initBootstrap()
|
||||
await appStore.initStores()
|
||||
|
||||
const [nextCategories, nextSections] = await Promise.all([
|
||||
getCategories(appStore.currentStore.id, appStore.scene, appStore.channel),
|
||||
getMenu(appStore.currentStore.id, appStore.scene, appStore.channel)
|
||||
])
|
||||
|
||||
categories.value = nextCategories
|
||||
sections.value = nextSections
|
||||
} catch (error: unknown) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '菜单加载失败,请检查接口是否可用'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSceneChange (value: string) {
|
||||
appStore.setScene(value as FulfillmentScene)
|
||||
await loadMenu()
|
||||
}
|
||||
|
||||
return {
|
||||
handleSceneChange,
|
||||
loadMenu
|
||||
}
|
||||
}
|
||||
183
src/pages/menu/composables/menu-page/detail-actions.ts
Normal file
183
src/pages/menu/composables/menu-page/detail-actions.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import type {
|
||||
MiniProductCard,
|
||||
MiniProductDetail,
|
||||
MiniProductOption,
|
||||
MiniProductOptionGroup
|
||||
} from '@/shared'
|
||||
import { showToast } from '@tarojs/taro'
|
||||
import { getProductDetail } from '@/services'
|
||||
import type { useAppStore, useCartStore } from '@/stores'
|
||||
import {
|
||||
buildSelectedNames,
|
||||
resolveSelectionError,
|
||||
resolveSkuId
|
||||
} from './selection-helpers'
|
||||
|
||||
type AppStoreInstance = ReturnType<typeof useAppStore>
|
||||
type CartStoreInstance = ReturnType<typeof useCartStore>
|
||||
|
||||
export function createMenuDetailActions (payload: {
|
||||
activeDetail: Ref<MiniProductDetail | null>
|
||||
addonSelections: Ref<Record<string, string[]>>
|
||||
appStore: AppStoreInstance
|
||||
canAddCurrentDetail: ComputedRef<boolean>
|
||||
cartStore: CartStoreInstance
|
||||
closeDetail: () => void
|
||||
currentDetailPrice: ComputedRef<number>
|
||||
currentSkuId: Ref<string>
|
||||
detailQuantity: Ref<number>
|
||||
detailVisible: Ref<boolean>
|
||||
specSelections: Ref<Record<string, string[]>>
|
||||
}) {
|
||||
const {
|
||||
activeDetail,
|
||||
addonSelections,
|
||||
appStore,
|
||||
canAddCurrentDetail,
|
||||
cartStore,
|
||||
closeDetail,
|
||||
currentDetailPrice,
|
||||
currentSkuId,
|
||||
detailQuantity,
|
||||
detailVisible,
|
||||
specSelections
|
||||
} = payload
|
||||
|
||||
async function openProductDetail (productId: string) {
|
||||
try {
|
||||
const detail = await getProductDetail(productId, appStore.scene, appStore.channel)
|
||||
activeDetail.value = detail
|
||||
detailQuantity.value = 1
|
||||
|
||||
const nextSpecSelections: Record<string, string[]> = {}
|
||||
const nextAddonSelections: Record<string, string[]> = {}
|
||||
const defaultSku = detail.skus.find((sku) => sku.id === detail.defaultSkuId) || detail.skus[0]
|
||||
|
||||
detail.optionGroups.forEach((group) => {
|
||||
if (group.groupType === 'addon') {
|
||||
const defaults = group.required
|
||||
? group.options
|
||||
.filter((option) => !option.soldOut)
|
||||
.slice(0, Math.max(group.minSelect, 1))
|
||||
.map((option) => option.id)
|
||||
: []
|
||||
nextAddonSelections[group.id] = defaults
|
||||
return
|
||||
}
|
||||
|
||||
const matched = group.options.find((option) => defaultSku?.selectedOptionIds.includes(option.id))
|
||||
|| group.options.find((option) => !option.soldOut)
|
||||
nextSpecSelections[group.id] = matched ? [matched.id] : []
|
||||
})
|
||||
|
||||
specSelections.value = nextSpecSelections
|
||||
addonSelections.value = nextAddonSelections
|
||||
currentSkuId.value = detail.skus.length ? resolveSkuId(detail, nextSpecSelections) : ''
|
||||
detailVisible.value = true
|
||||
} catch (error: unknown) {
|
||||
await showToast({
|
||||
title: error instanceof Error ? error.message : '商品详情加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function toggleOption (group: MiniProductOptionGroup, option: MiniProductOption) {
|
||||
if (option.soldOut) return
|
||||
|
||||
if (group.groupType === 'addon') {
|
||||
const current = addonSelections.value[group.id] || []
|
||||
|
||||
if (group.selectionType === 'single') {
|
||||
addonSelections.value = { ...addonSelections.value, [group.id]: [option.id] }
|
||||
return
|
||||
}
|
||||
|
||||
const hasSelected = current.includes(option.id)
|
||||
const next = hasSelected
|
||||
? current.filter((id) => id !== option.id)
|
||||
: [...current, option.id]
|
||||
addonSelections.value = { ...addonSelections.value, [group.id]: next }
|
||||
return
|
||||
}
|
||||
|
||||
specSelections.value = { ...specSelections.value, [group.id]: [option.id] }
|
||||
if (activeDetail.value) {
|
||||
currentSkuId.value = resolveSkuId(activeDetail.value, specSelections.value)
|
||||
}
|
||||
}
|
||||
|
||||
function changeDetailQuantity (delta: number) {
|
||||
detailQuantity.value = Math.max(1, detailQuantity.value + delta)
|
||||
}
|
||||
|
||||
async function confirmAddCurrentDetail () {
|
||||
if (!activeDetail.value || !canAddCurrentDetail.value) {
|
||||
const message = resolveSelectionError({
|
||||
activeDetail: activeDetail.value,
|
||||
addonSelections: addonSelections.value,
|
||||
currentSkuId: currentSkuId.value,
|
||||
specSelections: specSelections.value
|
||||
})
|
||||
|
||||
if (message) {
|
||||
await showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const specNames = buildSelectedNames(
|
||||
activeDetail.value.optionGroups.filter((group) => group.groupType !== 'addon'),
|
||||
specSelections.value
|
||||
)
|
||||
const addonNames = buildSelectedNames(
|
||||
activeDetail.value.optionGroups.filter((group) => group.groupType === 'addon'),
|
||||
addonSelections.value
|
||||
)
|
||||
const addonIds = activeDetail.value.optionGroups
|
||||
.filter((group) => group.groupType === 'addon')
|
||||
.flatMap((group) => addonSelections.value[group.id] || [])
|
||||
|
||||
cartStore.addItem({
|
||||
productId: activeDetail.value.id,
|
||||
name: activeDetail.value.name,
|
||||
unitPrice: currentDetailPrice.value / detailQuantity.value,
|
||||
quantity: detailQuantity.value,
|
||||
skuId: currentSkuId.value || undefined,
|
||||
skuName: specNames.join('/'),
|
||||
addonItemIds: addonIds,
|
||||
addonNames,
|
||||
coverImageUrl: activeDetail.value.coverImageUrl
|
||||
})
|
||||
|
||||
await showToast({ title: '已加入购物车', icon: 'success' })
|
||||
closeDetail()
|
||||
}
|
||||
|
||||
async function handleProductAction (product: MiniProductCard) {
|
||||
if (product.hasOptions) {
|
||||
await openProductDetail(product.id)
|
||||
return
|
||||
}
|
||||
|
||||
cartStore.addItem({
|
||||
productId: product.id,
|
||||
name: product.name,
|
||||
unitPrice: product.price,
|
||||
quantity: 1,
|
||||
coverImageUrl: product.coverImageUrl,
|
||||
addonItemIds: [],
|
||||
addonNames: []
|
||||
})
|
||||
|
||||
await showToast({ title: '已加入购物车', icon: 'success' })
|
||||
}
|
||||
|
||||
return {
|
||||
changeDetailQuantity,
|
||||
confirmAddCurrentDetail,
|
||||
handleProductAction,
|
||||
toggleOption
|
||||
}
|
||||
}
|
||||
61
src/pages/menu/composables/menu-page/selection-helpers.ts
Normal file
61
src/pages/menu/composables/menu-page/selection-helpers.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type {
|
||||
MiniProductDetail,
|
||||
MiniProductOptionGroup
|
||||
} from '@/shared'
|
||||
|
||||
export function getSelectedIds (
|
||||
addonSelections: Record<string, string[]>,
|
||||
specSelections: Record<string, string[]>,
|
||||
groupId: string
|
||||
) {
|
||||
return addonSelections[groupId] || specSelections[groupId] || []
|
||||
}
|
||||
|
||||
export function resolveSkuId (
|
||||
detail: MiniProductDetail,
|
||||
nextSelections: Record<string, string[]>
|
||||
) {
|
||||
const selectedOptionIds = detail.optionGroups
|
||||
.filter((group) => group.groupType !== 'addon')
|
||||
.flatMap((group) => nextSelections[group.id] || [])
|
||||
.sort()
|
||||
|
||||
if (!detail.skus.length) return ''
|
||||
|
||||
const matchedSku = detail.skus.find((sku) =>
|
||||
[...sku.selectedOptionIds].sort().join('|') === selectedOptionIds.join('|')
|
||||
)
|
||||
|
||||
return matchedSku?.id || ''
|
||||
}
|
||||
|
||||
export function resolveSelectionError (payload: {
|
||||
activeDetail: MiniProductDetail | null
|
||||
addonSelections: Record<string, string[]>
|
||||
currentSkuId: string
|
||||
specSelections: Record<string, string[]>
|
||||
}) {
|
||||
const { activeDetail, addonSelections, currentSkuId, specSelections } = payload
|
||||
if (!activeDetail) return ''
|
||||
|
||||
for (const group of activeDetail.optionGroups) {
|
||||
const selectedCount = getSelectedIds(addonSelections, specSelections, group.id).length
|
||||
const requiredCount = group.required ? Math.max(group.minSelect, 1) : group.minSelect
|
||||
|
||||
if (requiredCount > 0 && selectedCount < requiredCount) return `请选择${group.name}`
|
||||
if (group.selectionType === 'single' && selectedCount > 1) return `${group.name}只能选择一项`
|
||||
if (group.maxSelect > 0 && selectedCount > group.maxSelect) return `${group.name}超出可选上限`
|
||||
}
|
||||
|
||||
if (activeDetail.skus.length && !currentSkuId) return '请先选择完整规格'
|
||||
return ''
|
||||
}
|
||||
|
||||
export function buildSelectedNames (
|
||||
groups: MiniProductOptionGroup[],
|
||||
source: Record<string, string[]>
|
||||
) {
|
||||
return groups
|
||||
.flatMap((group) => group.options.filter((option) => (source[group.id] || []).includes(option.id)))
|
||||
.map((option) => option.name)
|
||||
}
|
||||
Reference in New Issue
Block a user