Files
medical-mall/pages/mall/consumer/channel-detail.uvue
2026-05-14 15:28:09 +08:00

479 lines
12 KiB
Plaintext
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.
<template>
<view class="channel-detail-page">
<view class="channel-topbar" :style="topbarStyle">
<view class="channel-topbar-row" :style="topbarRowStyle">
<view class="channel-topbar-back" @tap="goBack">
<text class="channel-topbar-back-text"></text>
</view>
<view class="channel-topbar-title-wrap" :style="titleWrapStyle">
<text class="channel-topbar-title">{{ pageTitle }}</text>
</view>
<view class="channel-topbar-space" :style="topbarSpaceStyle"></view>
</view>
</view>
<scroll-view class="channel-detail-scroll" direction="vertical">
<view v-if="channel != null" class="channel-detail-header" :style="{ backgroundColor: channelBgColor }">
<view class="channel-detail-title-row">
<text class="channel-detail-title">{{ channelTitle }}</text>
<text class="channel-detail-badge" :style="{ color: channelThemeColor, borderColor: channelThemeColor }">{{ channelBadge }}</text>
</view>
<text class="channel-detail-subtitle">{{ channelSubtitle }}</text>
</view>
<view v-if="channel != null" class="channel-section-card">
<text class="channel-section-title">频道精选</text>
<view class="channel-product-grid">
<view
v-for="(product, index) in seedProducts"
:key="buildChannelKey('seed', product.id, index)"
class="channel-product-card"
@tap="handleMockProductClick(product)"
>
<image class="channel-product-cover" :src="getChannelProductImage(product)" mode="aspectFill" />
<text class="channel-product-title">{{ product.name }}</text>
<view class="channel-product-price-row">
<text class="channel-product-tag" :style="{ color: channelThemeColor }">{{ product.tag }}</text>
<text class="channel-product-price" :style="{ color: channelThemeColor }">¥{{ formatChannelPrice(product.price) }}</text>
</view>
<text v-if="product.marketPrice > product.price" class="channel-product-market-price">¥{{ formatChannelPrice(product.marketPrice) }}</text>
</view>
</view>
</view>
<view v-if="channel != null && moreProducts.length > 0" class="channel-section-card">
<text class="channel-section-title">更多好物</text>
<view class="channel-product-grid">
<view
v-for="(product, index) in moreProducts"
:key="buildChannelKey('more', product.id, index)"
class="channel-product-card"
@tap="handleMockProductClick(product)"
>
<image class="channel-product-cover" :src="getChannelProductImage(product)" mode="aspectFill" />
<text class="channel-product-title">{{ product.name }}</text>
<view class="channel-product-price-row">
<text class="channel-product-tag" :style="{ color: channelThemeColor }">{{ product.tag }}</text>
<text class="channel-product-price" :style="{ color: channelThemeColor }">¥{{ formatChannelPrice(product.price) }}</text>
</view>
<text v-if="product.marketPrice > product.price" class="channel-product-market-price">¥{{ formatChannelPrice(product.marketPrice) }}</text>
</view>
</view>
</view>
<view v-if="channel == null" class="channel-empty-state">
<text class="channel-empty-title">频道信息暂不可用</text>
<text class="channel-empty-desc">请返回首页重新进入频道</text>
</view>
<view class="channel-safe-area"></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getChannelDetailData } from '@/utils/mockChannelData.uts'
import type { MarketingChannel, ChannelProduct } from '@/utils/mockChannelData.uts'
const channel = ref<MarketingChannel | null>(null)
const seedProducts = ref<ChannelProduct[]>([])
const moreProducts = ref<ChannelProduct[]>([])
const pageTitle = ref('频道详情')
const channelTitle = ref('频道详情')
const channelSubtitle = ref('')
const channelBadge = ref('')
const channelThemeColor = ref('#ff6b35')
const channelBgColor = ref('#f5f5f5')
const statusBarHeight = ref(0)
const navBarTop = ref(0)
const navBarHeight = ref(44)
const navRightReserve = ref(0)
const topbarStyle = ref('padding-top:0px;height:88rpx;')
const topbarRowStyle = ref('padding-left:20rpx;padding-right:12px;')
const titleWrapStyle = ref('left:0px;right:0px;')
const topbarSpaceStyle = ref('width:64rpx;height:64rpx;')
function syncTopbarStyle(): void {
const top = navBarTop.value > 0 ? navBarTop.value : statusBarHeight.value
const height = navBarHeight.value > 0 ? navBarHeight.value : 44
const rightReserve = navRightReserve.value > 0 ? navRightReserve.value : 12
topbarStyle.value = 'padding-top:' + top + 'px;height:' + (top + height + 12) + 'px;'
topbarRowStyle.value = 'padding-left:20rpx;padding-right:' + rightReserve + 'px;'
titleWrapStyle.value = 'left:0px;right:0px;'
topbarSpaceStyle.value = 'width:' + rightReserve + 'px;height:64rpx;'
}
function initPageLayout(): void {
try {
const windowInfo = uni.getWindowInfo()
statusBarHeight.value = windowInfo.statusBarHeight ?? 0
navBarTop.value = statusBarHeight.value
navBarHeight.value = 44
navRightReserve.value = 12
// #ifdef MP-WEIXIN
try {
const menuBtn = uni.getMenuButtonBoundingClientRect()
if (menuBtn != null && menuBtn.top != null && menuBtn.top > 0) {
navBarTop.value = menuBtn.top
navBarHeight.value = menuBtn.height
navRightReserve.value = (windowInfo.windowWidth - menuBtn.left) + 8
}
} catch (e) {
navBarTop.value = statusBarHeight.value
navBarHeight.value = 44
navRightReserve.value = 12
}
// #endif
} catch (e) {
statusBarHeight.value = 0
navBarTop.value = 0
navBarHeight.value = 44
navRightReserve.value = 12
}
syncTopbarStyle()
}
function buildChannelKey(prefix: string, id: string, index: number): string {
const normalizedId = id !== '' ? id : 'empty'
return prefix + '-' + normalizedId + '-' + index.toString()
}
function formatChannelPrice(price: number): string {
const rounded = Math.round(price)
if (Math.abs(price - rounded) < 0.001) {
return rounded.toString()
}
return price.toFixed(1)
}
function getChannelProductImage(product: ChannelProduct): string {
if (product.image != null && product.image !== '') {
return product.image
}
return '/static/images/default.png'
}
function applyChannel(channelData: MarketingChannel | null): void {
channel.value = channelData
if (channelData == null) {
seedProducts.value = []
moreProducts.value = []
pageTitle.value = '频道详情'
channelTitle.value = '频道详情'
channelSubtitle.value = ''
channelBadge.value = ''
channelThemeColor.value = '#ff6b35'
channelBgColor.value = '#f5f5f5'
return
}
pageTitle.value = channelData.title
channelTitle.value = channelData.title
channelSubtitle.value = channelData.subtitle
channelBadge.value = channelData.badge
channelThemeColor.value = channelData.themeColor
channelBgColor.value = channelData.bgColor
seedProducts.value = channelData.products
moreProducts.value = []
setTimeout(() => {
moreProducts.value = channelData.moreProducts
}, 30)
}
function handleMockProductClick(product: ChannelProduct): void {
uni.showToast({
title: product.shortName + ' 为 mock 商品',
icon: 'none'
})
}
function decodeQueryValue(value: string, fallbackValue: string): string {
if (value === '') {
return fallbackValue
}
try {
const decodedValue = decodeURIComponent(value)
if (decodedValue != null && decodedValue !== '') {
return decodedValue
}
return fallbackValue
} catch (e) {
return value !== '' ? value : fallbackValue
}
}
function normalizeLoadOptions(options: UTSJSONObject | null): UTSJSONObject {
if (options == null) {
return new UTSJSONObject()
}
if (options instanceof UTSJSONObject) {
return options
}
try {
return JSON.parse(JSON.stringify(options)) as UTSJSONObject
} catch (e) {
return new UTSJSONObject()
}
}
function resolveQueryValue(options: UTSJSONObject, key: string): string {
const value = options.getString(key) ?? ''
if (value !== '') {
return value
}
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const rawPageOptions = currentPage.options ?? null
const pageOptions = normalizeLoadOptions(rawPageOptions as UTSJSONObject | null)
return pageOptions.getString(key) ?? ''
}
return ''
}
function goBack(): void {
uni.navigateBack({ delta: 1 })
}
onLoad((options) => {
initPageLayout()
let channelId = 'subsidy'
let routeType = 'subsidy'
let categoryId = 'recommend'
try {
const optionObj = normalizeLoadOptions(options as UTSJSONObject | null)
channelId = decodeQueryValue(resolveQueryValue(optionObj, 'channelId'), 'subsidy')
routeType = decodeQueryValue(resolveQueryValue(optionObj, 'routeType'), 'subsidy')
categoryId = decodeQueryValue(resolveQueryValue(optionObj, 'categoryId'), 'recommend')
} catch (e) {
channelId = 'subsidy'
routeType = 'subsidy'
categoryId = 'recommend'
}
applyChannel(getChannelDetailData(channelId, routeType, categoryId))
})
</script>
<style>
.channel-detail-page {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f5f5f5;
display: flex;
flex-direction: column;
}
.channel-topbar {
box-sizing: border-box;
background: #f5f5f5;
flex-shrink: 0;
}
.channel-topbar-row {
box-sizing: border-box;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
position: relative;
}
.channel-topbar-back {
flex: 0 0 64rpx;
height: 64rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.channel-topbar-back-text {
font-size: 52rpx;
line-height: 1;
color: #202020;
transform: translateY(-3rpx);
}
.channel-topbar-title-wrap {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
height: 64rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
pointer-events: none;
}
.channel-topbar-title {
font-size: 28rpx;
font-weight: 700;
color: #202020;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.channel-topbar-space {
flex-shrink: 0;
height: 64rpx;
}
.channel-detail-scroll {
flex: 1;
min-height: 0;
width: 100%;
padding: 20rpx 20rpx 0;
box-sizing: border-box;
}
.channel-detail-header {
border-radius: 28rpx;
padding: 28rpx 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.05);
}
.channel-detail-title-row {
display: flex;
flex-direction: row;
align-items: center;
}
.channel-detail-title {
font-size: 36rpx;
font-weight: 800;
color: #202020;
line-height: 1.2;
margin-right: 10rpx;
}
.channel-detail-badge {
font-size: 20rpx;
line-height: 1.2;
border-width: 1rpx;
border-style: solid;
border-radius: 8rpx;
padding: 4rpx 8rpx;
background: rgba(255, 255, 255, 0.72);
}
.channel-detail-subtitle {
font-size: 24rpx;
color: #666666;
line-height: 1.4;
margin-top: 10rpx;
}
.channel-section-card {
background: #ffffff;
border-radius: 24rpx;
padding: 22rpx;
margin-bottom: 20rpx;
box-shadow: 0 3rpx 10rpx rgba(0, 0, 0, 0.04);
}
.channel-section-title {
font-size: 28rpx;
font-weight: 700;
color: #202020;
line-height: 1.2;
margin-bottom: 16rpx;
}
.channel-product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.channel-product-card {
width: 48.5%;
background: #fafafa;
border-radius: 18rpx;
padding: 14rpx;
box-sizing: border-box;
margin-bottom: 16rpx;
display: flex;
flex-direction: column;
}
.channel-product-cover {
width: 100%;
height: 220rpx;
border-radius: 16rpx;
background: #ffffff;
}
.channel-product-title {
font-size: 24rpx;
line-height: 1.35;
color: #333333;
height: 66rpx;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 10rpx;
}
.channel-product-price-row {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 8rpx;
}
.channel-product-tag {
font-size: 19rpx;
margin-right: 6rpx;
}
.channel-product-price {
font-size: 28rpx;
font-weight: 800;
line-height: 1.2;
}
.channel-product-market-price {
font-size: 20rpx;
color: #999999;
text-decoration: line-through;
margin-top: 6rpx;
}
.channel-empty-state {
background: #ffffff;
border-radius: 24rpx;
padding: 50rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 30rpx;
}
.channel-empty-title {
font-size: 28rpx;
font-weight: 700;
color: #333333;
margin-bottom: 10rpx;
}
.channel-empty-desc {
font-size: 22rpx;
color: #888888;
}
.channel-safe-area {
height: 40rpx;
width: 100%;
}
</style>