完善下单逻辑及其ui展示,修复支付倒计时显示错误bug

This commit is contained in:
2026-05-25 15:35:41 +08:00
parent d25f80ccdd
commit cecb51a8e2
40 changed files with 13040 additions and 3217 deletions

View File

@@ -436,68 +436,6 @@ function buildServiceImageText(categoryId: string): string {
return '服'
}
function buildMockServiceProducts(): Array<HomeCareServiceProductType> {
// TODO: 后续替换为服务首页专用接口,当前仅在真实服务目录为空时兜底。
return [
{
id: 'svc-001',
title: '基础上门照护',
subtitle: '协助起居、日常陪护、健康观察',
categoryId: 'basic_care',
price: 99,
unit: '次',
tags: ['平台认证', '可预约'],
salesText: '已服务230+',
imageText: '护',
coverGradient: getServiceGradient('basic_care'),
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-001',
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-001&mode=booking'
},
{
id: 'svc-002',
title: '居家康复指导',
subtitle: '术后恢复、动作训练、康复评估',
categoryId: 'rehab',
price: 129,
unit: '次',
tags: ['康复指导', '上门服务'],
salesText: '已服务180+',
imageText: '康',
coverGradient: getServiceGradient('rehab'),
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-002',
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-002&mode=booking'
},
{
id: 'svc-mock-escort',
title: '陪诊陪护服务',
subtitle: '挂号陪同、检查陪同、取药协助',
categoryId: 'escort',
price: 168,
unit: '次',
tags: ['陪诊服务', '安心陪护'],
salesText: '已服务320+',
imageText: '陪',
coverGradient: getServiceGradient('escort'),
detailPath: '',
bookingPath: ''
},
{
id: 'svc-003',
title: '慢病随访服务',
subtitle: '血压血糖记录、健康建议、定期回访',
categoryId: 'chronic',
price: 79,
unit: '次',
tags: ['慢病管理', '健康随访'],
salesText: '已服务150+',
imageText: '访',
coverGradient: getServiceGradient('chronic'),
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-003',
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-003&mode=booking'
}
]
}
function buildServiceProductsFromCatalog(catalog: Array<HomeServiceCatalogType>): Array<HomeCareServiceProductType> {
const result: Array<HomeCareServiceProductType> = []
for (let i = 0; i < catalog.length; i++) {
@@ -525,14 +463,10 @@ async function loadServiceHomeData(): Promise<void> {
serviceLoading.value = true
try {
const catalog = await fetchHomeServiceCatalog()
if (catalog.length > 0) {
allServiceProducts.value = buildServiceProductsFromCatalog(catalog)
} else {
allServiceProducts.value = buildMockServiceProducts()
}
allServiceProducts.value = buildServiceProductsFromCatalog(catalog)
} catch (error) {
console.error('加载服务首页数据失败', error)
allServiceProducts.value = buildMockServiceProducts()
allServiceProducts.value = [] as Array<HomeCareServiceProductType>
} finally {
serviceLoading.value = false
}
@@ -620,6 +554,8 @@ const hotProducts = ref<Product[]>([])
const recommendedProducts = ref<Product[]>([])
const hotKeywords = ref<string[]>([])
const defaultLoadLimit: number = 6
const recommendChannelLoadLimit: number = 16
const categoryChannelLoadLimit: number = 12
// 屏幕尺寸检测
const isMobile = ref(false)
@@ -922,10 +858,376 @@ function buildSimpleChannelCoverImages(startIndex: number): string[] {
return covers
}
function buildSimpleCategoryChannels(categoryId: string): SimpleCategoryChannel[] {
return []
function getRealProductImage(product: Product): string {
if (product.main_image_url != null && product.main_image_url !== '') {
return product.main_image_url
}
if (product.images != null && product.images.length > 0 && product.images[0] !== '') {
return product.images[0]
}
if (product.image_url != null && product.image_url !== '') {
return product.image_url
}
return '/static/images/default.png'
}
function getRealSalePrice(product: Product): number {
return product.base_price ?? product.price ?? 0
}
function getRealMarketPrice(product: Product): number {
return product.market_price ?? product.original_price ?? 0
}
function toChannelProduct(product: Product, labelPrefix: string): ChannelProduct {
const salePrice = getRealSalePrice(product)
const marketPrice = getRealMarketPrice(product)
const shortName = product.short_title != null && product.short_title !== ''
? product.short_title
: (product.name != null && product.name !== '' ? product.name : product.id)
return {
id: product.id,
name: product.name != null && product.name !== '' ? product.name : product.id,
shortName,
image: getRealProductImage(product),
price: salePrice,
marketPrice,
tag: labelPrefix
} as ChannelProduct
}
function getProductDiscountScore(product: Product): number {
const salePrice = getRealSalePrice(product)
const marketPrice = getRealMarketPrice(product)
if (marketPrice <= salePrice || marketPrice <= 0) {
return 0
}
const discountValue = marketPrice - salePrice
const discountRate = discountValue / marketPrice
return discountRate * 100000 + discountValue
}
function getProductQualityScore(product: Product): number {
let score = 0
if (product.is_featured == true) {
score = score + 100000
}
if (product.is_hot == true) {
score = score + 50000
}
score = score + (product.sale_count ?? 0)
return score
}
function getProductHotScore(product: Product): number {
let score = product.sale_count ?? 0
if (product.is_hot == true) {
score = score + 100000
}
if (product.is_featured == true) {
score = score + 50000
}
score = score + getProductDiscountScore(product)
return score
}
function cloneProductArray(source: Array<Product>): Array<Product> {
const result: Array<Product> = []
for (let i = 0; i < source.length; i++) {
result.push(source[i])
}
return result
}
function sortProductsByScoreDesc(source: Array<Product>, scoreType: string): Array<Product> {
const result = cloneProductArray(source)
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
let leftScore = 0
let rightScore = 0
if (scoreType == 'discount') {
leftScore = getProductDiscountScore(result[i])
rightScore = getProductDiscountScore(result[j])
} else if (scoreType == 'quality') {
leftScore = getProductQualityScore(result[i])
rightScore = getProductQualityScore(result[j])
} else {
leftScore = getProductHotScore(result[i])
rightScore = getProductHotScore(result[j])
}
if (rightScore > leftScore) {
const temp = result[i]
result[i] = result[j]
result[j] = temp
}
}
}
return result
}
function sortProductsByPriceAsc(source: Array<Product>): Array<Product> {
const result = cloneProductArray(source)
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const leftPrice = getRealSalePrice(result[i])
const rightPrice = getRealSalePrice(result[j])
if (rightPrice < leftPrice) {
const temp = result[i]
result[i] = result[j]
result[j] = temp
}
}
}
return result
}
function filterProductsByMode(source: Array<Product>, mode: string): Array<Product> {
const result: Array<Product> = []
for (let i = 0; i < source.length; i++) {
const item = source[i]
const salePrice = getRealSalePrice(item)
const marketPrice = getRealMarketPrice(item)
if (mode == 'discount' && marketPrice > salePrice) {
result.push(item)
continue
}
if (mode == 'quality' && (item.is_featured == true || item.is_hot == true)) {
result.push(item)
continue
}
if (mode == 'cheap-9' && salePrice > 0 && salePrice <= 9.9) {
result.push(item)
continue
}
if (mode == 'cheap-19' && salePrice > 0 && salePrice <= 19.9) {
result.push(item)
continue
}
if (mode == 'live' && (item.is_hot == true || (item.sale_count ?? 0) > 0)) {
result.push(item)
}
}
return result
}
function mergeUniqueProductLists(first: Array<Product>, second: Array<Product>, third: Array<Product>): Array<Product> {
const result: Array<Product> = []
const seenIds: Array<string> = []
const sources: Array<Array<Product>> = [first, second, third]
for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) {
const source = sources[sourceIndex]
for (let i = 0; i < source.length; i++) {
const item = source[i]
const productId = item.id ?? ''
if (productId != '' && seenIds.indexOf(productId) != -1) {
continue
}
if (productId != '') {
seenIds.push(productId)
}
result.push(item)
}
}
return result
}
function appendChannelProducts(source: Array<Product>, result: Array<Product>, selectedIds: Array<string>, desiredCount: number, allowRepeat: boolean): void {
for (let i = 0; i < source.length; i++) {
if (result.length >= desiredCount) {
return
}
const item = source[i]
const productId = item.id ?? ''
let existsInResult = false
for (let j = 0; j < result.length; j++) {
if (result[j].id == productId) {
existsInResult = true
break
}
}
if (existsInResult) {
continue
}
if (!allowRepeat && productId != '' && selectedIds.indexOf(productId) != -1) {
continue
}
result.push(item)
if (!allowRepeat && productId != '') {
selectedIds.push(productId)
}
}
}
function selectChannelProducts(primary: Array<Product>, secondary: Array<Product>, fallback: Array<Product>, selectedIds: Array<string>, desiredCount: number): Array<Product> {
const result: Array<Product> = []
appendChannelProducts(primary, result, selectedIds, desiredCount, false)
appendChannelProducts(secondary, result, selectedIds, desiredCount, false)
appendChannelProducts(fallback, result, selectedIds, desiredCount, false)
appendChannelProducts(primary, result, selectedIds, desiredCount, true)
appendChannelProducts(secondary, result, selectedIds, desiredCount, true)
appendChannelProducts(fallback, result, selectedIds, desiredCount, true)
return result
}
function buildChannelFromTemplate(template: MarketingChannel, products: Array<Product>, labelPrefix: string): MarketingChannel {
const mappedProducts: Array<ChannelProduct> = []
for (let i = 0; i < products.length; i++) {
mappedProducts.push(toChannelProduct(products[i], labelPrefix))
}
return {
id: template.id,
title: template.title,
subtitle: template.subtitle,
badge: template.badge,
themeColor: template.themeColor,
bgColor: template.bgColor,
routeType: template.routeType,
layoutType: template.layoutType,
products: mappedProducts,
moreProducts: mappedProducts
} as MarketingChannel
}
function logChannelProducts(channelTitle: string, products: Array<Product>): void {
for (let i = 0; i < products.length; i++) {
const item = products[i]
console.log('[home-channel] ' + channelTitle + ' product:', item.id, item.name ?? '', getRealProductImage(item), getRealSalePrice(item), getRealMarketPrice(item))
}
}
function buildRealRecommendMarketingChannels(products: Array<Product>): MarketingChannel[] {
console.log('[home-channel] buildRealRecommendMarketingChannels input count:', products.length)
const templates = getRecommendMarketingChannels()
if (products.length == 0 || templates.length == 0) {
console.log('[home-channel] fallback to mock channel data')
return templates
}
const uniqueProducts = dedupeProducts(products)
if (uniqueProducts.length == 0) {
console.log('[home-channel] fallback to mock channel data')
return templates
}
const selectedIds: Array<string> = []
const discountCandidates = sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'discount'), 'discount')
const qualityCandidates = sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'quality'), 'quality')
const cheapCandidates = mergeUniqueProductLists(
sortProductsByPriceAsc(filterProductsByMode(uniqueProducts, 'cheap-9')),
sortProductsByPriceAsc(filterProductsByMode(uniqueProducts, 'cheap-19')),
sortProductsByPriceAsc(uniqueProducts)
)
const liveCandidates = mergeUniqueProductLists(
sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'live'), 'hot'),
sortProductsByScoreDesc(discountCandidates, 'discount'),
sortProductsByScoreDesc(uniqueProducts, 'hot')
)
const hotFallback = sortProductsByScoreDesc(uniqueProducts, 'hot')
const cheapFallback = sortProductsByPriceAsc(uniqueProducts)
const subsidyProducts = selectChannelProducts(discountCandidates, hotFallback, hotFallback, selectedIds, 2)
const qualityProducts = selectChannelProducts(qualityCandidates, hotFallback, hotFallback, selectedIds, 2)
const cheapProducts = selectChannelProducts(cheapCandidates, cheapFallback, hotFallback, selectedIds, 2)
const liveProducts = selectChannelProducts(liveCandidates, discountCandidates, hotFallback, selectedIds, 2)
logChannelProducts('百亿补贴', subsidyProducts)
logChannelProducts('品质生活', qualityProducts)
logChannelProducts('9.9包邮', cheapProducts)
logChannelProducts('直播低价', liveProducts)
const mappedChannels: Array<MarketingChannel> = []
for (let i = 0; i < templates.length; i++) {
const template = templates[i]
if (template.id == 'subsidy') {
mappedChannels.push(buildChannelFromTemplate(template, subsidyProducts, '补贴价'))
continue
}
if (template.id == 'quality-life') {
mappedChannels.push(buildChannelFromTemplate(template, qualityProducts, '实惠'))
continue
}
if (template.id == 'cheap-mail') {
const cheapMappedProducts: Array<ChannelProduct> = []
for (let j = 0; j < cheapProducts.length; j++) {
const cheapProduct = cheapProducts[j]
const label = getRealSalePrice(cheapProduct) <= 9.9 ? '9.9包邮' : '特价'
cheapMappedProducts.push(toChannelProduct(cheapProduct, label))
}
mappedChannels.push({
id: template.id,
title: template.title,
subtitle: template.subtitle,
badge: template.badge,
themeColor: template.themeColor,
bgColor: template.bgColor,
routeType: template.routeType,
layoutType: template.layoutType,
products: cheapMappedProducts,
moreProducts: cheapMappedProducts
} as MarketingChannel)
continue
}
if (template.id == 'live-low-price') {
mappedChannels.push(buildChannelFromTemplate(template, liveProducts, '直播价'))
continue
}
mappedChannels.push(template)
}
return mappedChannels
}
function buildSimpleCategoryChannels(categoryId: string, products: Array<Product> = []): SimpleCategoryChannel[] {
const dedupedProducts = dedupeProducts(products)
if (dedupedProducts.length == 0) {
return [] as Array<SimpleCategoryChannel>
}
const hotProductsForCategory = sortProductsByScoreDesc(dedupedProducts, 'hot')
const qualityProductsForCategory = sortProductsByScoreDesc(dedupedProducts, 'quality')
const firstChannelCovers: Array<string> = []
const secondChannelCovers: Array<string> = []
for (let i = 0; i < hotProductsForCategory.length && firstChannelCovers.length < 2; i++) {
firstChannelCovers.push(getRealProductImage(hotProductsForCategory[i]))
}
for (let i = 0; i < qualityProductsForCategory.length && secondChannelCovers.length < 2; i++) {
secondChannelCovers.push(getRealProductImage(qualityProductsForCategory[i]))
}
while (firstChannelCovers.length < 2) {
firstChannelCovers.push('/static/images/default.png')
}
while (secondChannelCovers.length < 2) {
secondChannelCovers.push('/static/images/default.png')
}
return [
{
id: categoryId + '-rank',
title: '热销榜',
subtitle: '真实商品热度精选',
routeType: 'rank',
icon: '热',
coverImages: firstChannelCovers,
categoryId
} as SimpleCategoryChannel,
{
id: categoryId + '-quality',
title: '品质优选',
subtitle: '真实好物口碑推荐',
routeType: 'quality',
icon: '精',
coverImages: secondChannelCovers,
categoryId
} as SimpleCategoryChannel
]
}
async function loadCategoryChannelCards(categoryId: string): Promise<void> {
try {
const channelResult = await supabaseService.getMedicalMallProductsByCategory(categoryId, 1, categoryChannelLoadLimit)
categorySimpleChannels.value = buildSimpleCategoryChannels(categoryId, channelResult.data)
} catch (error) {
console.error('[home-channel] 加载分类频道卡片失败', categoryId, error)
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
}
}
function buildVisibleRecommendChannels(): MarketingChannel[] {
const source = getRecommendMarketingChannels()
const visible: MarketingChannel[] = []
@@ -937,16 +1239,16 @@ function buildVisibleRecommendChannels(): MarketingChannel[] {
visible.push(channel)
}
return visible
}
}
function applyChannelDisplay(categoryId: string): void {
if (categoryId === 'recommend') {
marketingChannels.value = buildVisibleRecommendChannels()
categorySimpleChannels.value = []
marketingChannels.value = [] as Array<MarketingChannel>
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
return
}
marketingChannels.value = []
categorySimpleChannels.value = buildSimpleCategoryChannels(categoryId)
marketingChannels.value = [] as Array<MarketingChannel>
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
}
function buildChannelDetailUrl(channelId: string, routeType: string, categoryId: string): string {
@@ -1440,11 +1742,18 @@ async function loadHotProducts(page: number, limit: number): Promise<void> {
}
}
setHotProducts(products)
if (currentFeedCategoryId.value === 'recommend' && page <= 1) {
marketingChannels.value = buildRealRecommendMarketingChannels(products)
}
hasMore.value = result.hasmore
currentPage.value = page
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
if (currentFeedCategoryId.value === 'recommend') {
console.log('[home-channel] fallback to mock channel data')
marketingChannels.value = buildVisibleRecommendChannels()
}
hasMore.value = false
}
}
@@ -1476,14 +1785,18 @@ async function loadCategoryGoods(categoryId: string): Promise<void> {
await syncCategoryLayout(categoryId)
if (categoryId === 'recommend') {
try {
const result = await supabaseService.getMedicalMallSmartRecommendations(1, defaultLoadLimit)
const result = await supabaseService.getMedicalMallSmartRecommendations(1, recommendChannelLoadLimit)
console.log('[home-channel] 推荐商品接口返回数量:', result.data.length)
failedProductImageIds.value = []
setHotProducts(result.data)
marketingChannels.value = buildRealRecommendMarketingChannels(result.data)
hasMore.value = result.hasmore
currentPage.value = 1
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
console.log('[home-channel] fallback to mock channel data')
marketingChannels.value = buildVisibleRecommendChannels()
hasMore.value = false
}
} else {
@@ -1492,10 +1805,12 @@ async function loadCategoryGoods(categoryId: string): Promise<void> {
const result = await supabaseService.getMedicalMallProductsByCategory(categoryId, 1, defaultLoadLimit)
failedProductImageIds.value = []
setHotProducts(result.data)
await loadCategoryChannelCards(categoryId)
hasMore.value = result.hasmore
} catch (e) {
console.error('分类商品加载失败', e)
hotProducts.value = []
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
hasMore.value = false
} finally {
loading.value = false
@@ -1520,12 +1835,16 @@ async function refreshHomeCategory(item: CategoryItem): Promise<void> {
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
applyChannelDisplay(item.id)
try {
const result = await supabaseService.getMedicalMallSmartRecommendations(1, defaultLoadLimit)
const result = await supabaseService.getMedicalMallSmartRecommendations(1, recommendChannelLoadLimit)
console.log('[home-channel] 推荐商品接口返回数量:', result.data.length)
setHotProducts(result.data)
marketingChannels.value = buildRealRecommendMarketingChannels(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('加载推荐商品失败:', error)
hotProducts.value = []
console.log('[home-channel] fallback to mock channel data')
marketingChannels.value = buildVisibleRecommendChannels()
hasMore.value = false
} finally {
loading.value = false
@@ -1545,10 +1864,12 @@ async function refreshHomeCategory(item: CategoryItem): Promise<void> {
try {
const result = await supabaseService.getMedicalMallProductsByCategory(item.id, 1, defaultLoadLimit)
setHotProducts(result.data)
await loadCategoryChannelCards(item.id)
hasMore.value = result.hasmore
} catch (error) {
console.error('分类商品加载失败', error)
hotProducts.value = []
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
hasMore.value = false
} finally {
loading.value = false
@@ -1857,7 +2178,8 @@ const switchSort = (sortId: string) => {
}
hasMore.value = true // 重置加载更多状态
// 重新加载热销商品,排序由 Supabase 服务处理
loadHotProducts(1, defaultLoadLimit)
const nextLimit = currentFeedCategoryId.value === 'recommend' ? recommendChannelLoadLimit : defaultLoadLimit
loadHotProducts(1, nextLimit)
}
// 切换筛选器
@@ -1909,12 +2231,13 @@ const loadMore = async () => {
showLoadMore.value = true
loading.value = true
try {
const pageLimit = currentFeedCategoryId.value === 'recommend' ? recommendChannelLoadLimit : defaultLoadLimit
const nextPage = currentPage.value + 1
const currentCount = hotProducts.value.length
console.log('开始加载更多,当前数量:', currentCount, '页码:', nextPage, '分类:', currentFeedCategoryId.value)
if (currentFeedCategoryId.value === 'recommend') {
const result = await fetchSortedProductsPage(nextPage, defaultLoadLimit)
const result = await fetchSortedProductsPage(nextPage, pageLimit)
const newProducts = result.data
if (newProducts.length == 0) {
@@ -2025,7 +2348,7 @@ const onScan = (): void => {
})
},
fail: (err) => {
console.error('扫码失败:', err)
console.error('扫码失败:', err)
}
})
}