完成consumer端同步
This commit is contained in:
479
pages/mall/consumer/channel-detail.uvue
Normal file
479
pages/mall/consumer/channel-detail.uvue
Normal file
@@ -0,0 +1,479 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user