refactor: 拆分小程序 vue 结构

This commit is contained in:
2026-03-11 14:11:26 +08:00
commit b050c01a24
141 changed files with 24904 additions and 0 deletions

View 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
}
}

View 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
}
}

View 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)
}