479 lines
12 KiB
Plaintext
479 lines
12 KiB
Plaintext
<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> |