consumer模块完成度95%,优化安卓端界面和小程序测试2
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<!-- pages/main/cart.uvue -->
|
||||
<!-- pages/main/cart.uvue -->
|
||||
<template>
|
||||
<view class="cart-page">
|
||||
<!-- 智能顶部导航栏 - 与消息页保持一致 -->
|
||||
@@ -103,6 +103,7 @@
|
||||
<view v-if="!isManageMode" class="total-info">
|
||||
<text class="total-text">合计:</text>
|
||||
<text class="total-price">¥{{ totalPrice }}</text>
|
||||
<text v-if="parseFloat(memberSavedAmount) > 0" class="member-saved">会员已省¥{{ memberSavedAmount }}</text>
|
||||
</view>
|
||||
<button v-if="!isManageMode" class="checkout-btn" @click="goToCheckout">
|
||||
去结算({{ selectedCount }})
|
||||
@@ -188,6 +189,8 @@ type LocalCartItem = {
|
||||
shopName: string
|
||||
name: string
|
||||
price: number
|
||||
originalPrice: number // 原价
|
||||
memberPrice: number // 会员价
|
||||
image: string
|
||||
spec: string
|
||||
quantity: number
|
||||
@@ -284,7 +287,19 @@ const selectedCount = computed(() => {
|
||||
const totalPrice = computed(() => {
|
||||
return cartItems.value
|
||||
.filter((item: LocalCartItem) => item.selected)
|
||||
.reduce((sum: number, item: LocalCartItem) => sum + item.price * item.quantity, 0)
|
||||
.reduce((sum: number, item: LocalCartItem) => {
|
||||
// 优先使用会员价,如果没有会员价则使用原价
|
||||
const finalPrice = item.memberPrice > 0 && item.memberPrice < item.price ? item.memberPrice : item.price
|
||||
return sum + finalPrice * item.quantity
|
||||
}, 0)
|
||||
.toFixed(2)
|
||||
})
|
||||
|
||||
// 计算会员节省金额
|
||||
const memberSavedAmount = computed(() => {
|
||||
return cartItems.value
|
||||
.filter((item: LocalCartItem) => item.selected && item.memberPrice > 0 && item.memberPrice < item.price)
|
||||
.reduce((sum: number, item: LocalCartItem) => sum + (item.price - item.memberPrice) * item.quantity, 0)
|
||||
.toFixed(2)
|
||||
})
|
||||
|
||||
@@ -393,6 +408,18 @@ const loadCartData = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 获取会员折扣信息
|
||||
let memberDiscount = 1.0
|
||||
try {
|
||||
const memberInfo = await supabaseService.getUserMemberInfo()
|
||||
const discountRaw = memberInfo.get('discount')
|
||||
if (discountRaw != null) {
|
||||
memberDiscount = discountRaw as number
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('获取会员信息失败,使用默认折扣:', e)
|
||||
}
|
||||
|
||||
// 从Supabase加载购物车数据
|
||||
const supabaseCartItems = await supabaseService.getCartItems()
|
||||
|
||||
@@ -405,12 +432,21 @@ const loadCartData = async () => {
|
||||
const shopId = (item.shop_id != null && item.shop_id !== '') ? item.shop_id : 'default_shop'
|
||||
const shopName = (item.shop_name != null && item.shop_name !== '') ? item.shop_name : '商城优选'
|
||||
|
||||
// 计算会员价
|
||||
const originalPrice = item.product_price != null ? item.product_price : 0
|
||||
let memberPrice = 0
|
||||
if (memberDiscount > 0 && memberDiscount < 1 && originalPrice > 0) {
|
||||
memberPrice = Math.round(originalPrice * memberDiscount * 100) / 100
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
shopId: shopId,
|
||||
shopName: shopName,
|
||||
name: item.product_name ?? '未知商品',
|
||||
price: item.product_price != null ? item.product_price : 0,
|
||||
price: originalPrice,
|
||||
originalPrice: originalPrice,
|
||||
memberPrice: memberPrice,
|
||||
image: item.product_image ?? '/static/images/default-product.png',
|
||||
spec: item.product_specification ?? '标准规格',
|
||||
quantity: item.quantity ?? 1,
|
||||
@@ -534,6 +570,8 @@ const toggleShopSelect = async (shopId: string) => {
|
||||
shopName: item.shopName,
|
||||
name: item.name,
|
||||
price: item.price,
|
||||
originalPrice: item.originalPrice,
|
||||
memberPrice: item.memberPrice,
|
||||
image: item.image,
|
||||
spec: item.spec,
|
||||
quantity: item.quantity,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<template>
|
||||
<view class="category-page">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<view class="search-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="search-container">
|
||||
<view class="search-box" @click="navigateToSearch" :style="{ height: '30px' }">
|
||||
<!-- 模拟输入框 -->
|
||||
<text class="search-placeholder">请输入药品名称、症状或品牌</text>
|
||||
<text class="search-placeholder">请输入商品名称、店铺</text>
|
||||
|
||||
<!-- 扫码图标 -->
|
||||
<view class="nav-icon-btn" @click.stop="onScan">
|
||||
@@ -100,10 +100,16 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<!-- 加载中状态 -->
|
||||
<view v-else-if="loading" class="loading-state">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 - 仅在非加载状态且无商品时显示 -->
|
||||
<view v-else class="empty-state">
|
||||
<text class="empty-icon">💊</text>
|
||||
<text class="empty-text">暂无相关药品</text>
|
||||
<text class="empty-icon"><EFBFBD></text>
|
||||
<text class="empty-text">暂无相关商品</text>
|
||||
<text class="empty-desc">该分类下暂无商品,敬请期待</text>
|
||||
</view>
|
||||
|
||||
@@ -1095,6 +1101,37 @@ function onScan(): void {
|
||||
margin-right: 6px; /* gap replacement */
|
||||
}
|
||||
|
||||
/* 加载中状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #f0f0f0;
|
||||
border-top-color: #ff5000;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- pages/main/index.uvue -->
|
||||
<!-- pages/main/index.uvue -->
|
||||
<template>
|
||||
<view class="medic-home">
|
||||
<!-- 智能顶部导航栏 - 添加滚动隐藏效果 -->
|
||||
@@ -12,7 +12,7 @@
|
||||
<view class="search-container">
|
||||
<view class="search-box" @click="navigateToSearch" :style="{ height: '30px' }">
|
||||
<!-- 模拟输入框 -->
|
||||
<text class="search-placeholder">请输入药品名称、症状或品牌</text>
|
||||
<text class="search-placeholder">请输入商品名称、店铺</text>
|
||||
|
||||
<!-- 扫码图标 -->
|
||||
<view class="nav-icon-btn" @click.stop="onScan">
|
||||
@@ -681,37 +681,43 @@ onShow(() => {
|
||||
|
||||
// 处理滚动事件
|
||||
const handleScroll = (event: any) => {
|
||||
const eventObj = event as UTSJSONObject
|
||||
const detail = eventObj.get('detail') as UTSJSONObject
|
||||
const scrollTop = detail.getNumber('scrollTop') ?? 0
|
||||
const currentTime = Date.now()
|
||||
|
||||
// 判断滚动方向
|
||||
if (scrollTop > lastScrollTop.value) {
|
||||
// 向下滚动
|
||||
scrollingUp.value = false
|
||||
// 向下滚动超过阈值时隐藏导航栏
|
||||
if (scrollTop > scrollThreshold && showNavbar.value) {
|
||||
showNavbar.value = false
|
||||
try {
|
||||
const eventObj = event as UTSJSONObject
|
||||
const detailRaw = eventObj.get('detail')
|
||||
if (detailRaw == null) return
|
||||
const detail = detailRaw as UTSJSONObject
|
||||
const scrollTop = detail.getNumber('scrollTop') ?? 0
|
||||
const currentTime = Date.now()
|
||||
|
||||
// 判断滚动方向
|
||||
if (scrollTop > lastScrollTop.value) {
|
||||
// 向下滚动
|
||||
scrollingUp.value = false
|
||||
// 向下滚动超过阈值时隐藏导航栏
|
||||
if (scrollTop > scrollThreshold && showNavbar.value) {
|
||||
showNavbar.value = false
|
||||
}
|
||||
} else if (scrollTop < lastScrollTop.value) {
|
||||
// 向上滚动
|
||||
scrollingUp.value = true
|
||||
// 向上滚动时显示导航栏
|
||||
if (!showNavbar.value) {
|
||||
showNavbar.value = true
|
||||
}
|
||||
}
|
||||
} else if (scrollTop < lastScrollTop.value) {
|
||||
// 向上滚动
|
||||
scrollingUp.value = true
|
||||
// 向上滚动时显示导航栏
|
||||
if (!showNavbar.value) {
|
||||
|
||||
// 滚动到顶部时强制显示导航栏
|
||||
if (scrollTop <= 10) {
|
||||
showNavbar.value = true
|
||||
}
|
||||
|
||||
lastScrollTop.value = scrollTop
|
||||
|
||||
// 调试信息(开发时可启用)
|
||||
// console.log(`Scroll: ${scrollTop}, ShowNavbar: ${showNavbar.value}, ScrollingUp: ${scrollingUp.value}`)
|
||||
} catch (e) {
|
||||
// 忽略滚动事件处理错误
|
||||
}
|
||||
|
||||
// 滚动到顶部时强制显示导航栏
|
||||
if (scrollTop <= 10) {
|
||||
showNavbar.value = true
|
||||
}
|
||||
|
||||
lastScrollTop.value = scrollTop
|
||||
|
||||
// 调试信息(开发时可启用)
|
||||
// console.log(`Scroll: ${scrollTop}, ShowNavbar: ${showNavbar.value}, ScrollingUp: ${scrollingUp.value}`)
|
||||
}
|
||||
|
||||
// 重置导航栏显示状态(例如点击回到顶部时)
|
||||
|
||||
@@ -58,7 +58,16 @@
|
||||
<view class="message-content-wrapper">
|
||||
<text class="sender-name">{{ headerTitle }}</text>
|
||||
<view class="message-bubble received-bubble">
|
||||
<text class="message-text">{{ message.content }}</text>
|
||||
<!-- 图片消息 -->
|
||||
<image
|
||||
v-if="message.msgType == 'image'"
|
||||
class="message-image"
|
||||
:src="message.content"
|
||||
mode="widthFix"
|
||||
@click="previewImage(message.content)"
|
||||
/>
|
||||
<!-- 文本消息 -->
|
||||
<text v-if="message.msgType != 'image'" class="message-text">{{ message.content }}</text>
|
||||
<text class="message-time">{{ message.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -68,7 +77,16 @@
|
||||
<view v-else class="message-wrapper me">
|
||||
<view class="message-content-wrapper">
|
||||
<view class="message-bubble me">
|
||||
<text class="message-text">{{ message.content }}</text>
|
||||
<!-- 图片消息 -->
|
||||
<image
|
||||
v-if="message.msgType == 'image'"
|
||||
class="message-image"
|
||||
:src="message.content"
|
||||
mode="widthFix"
|
||||
@click="previewImage(message.content)"
|
||||
/>
|
||||
<!-- 文本消息 -->
|
||||
<text v-if="message.msgType != 'image'" class="message-text">{{ message.content }}</text>
|
||||
<text class="message-time">{{ message.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -138,6 +156,7 @@ type UiChatMessage = {
|
||||
type: string
|
||||
content: string
|
||||
time: string
|
||||
msgType: string // 'text' | 'image'
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
@@ -224,6 +243,7 @@ function setupRealtimeSubscription(): void {
|
||||
const receiverId = newMsg.getString('receiver_id') ?? ''
|
||||
const msgId = newMsg.getString('id') ?? ''
|
||||
const content = newMsg.getString('content') ?? ''
|
||||
const msgType = newMsg.getString('msg_type') ?? 'text'
|
||||
|
||||
console.log('=== 消息详情 ===')
|
||||
console.log('消息ID:', msgId)
|
||||
@@ -232,6 +252,7 @@ function setupRealtimeSubscription(): void {
|
||||
console.log('当前用户ID:', currentUserId.value)
|
||||
console.log('商家ID:', merchantId.value)
|
||||
console.log('消息内容:', content)
|
||||
console.log('消息类型 msgType:', msgType)
|
||||
|
||||
// 检查消息是否已经在列表中(避免重复)
|
||||
for (let i = 0; i < messages.value.length; i++) {
|
||||
@@ -271,7 +292,8 @@ function setupRealtimeSubscription(): void {
|
||||
viewId: safeViewId,
|
||||
type: isMyMessage ? 'sent' : 'received',
|
||||
content: content,
|
||||
time: timeStr
|
||||
time: timeStr,
|
||||
msgType: msgType
|
||||
}
|
||||
|
||||
console.log('=== 添加新消息到列表 ===')
|
||||
@@ -326,7 +348,8 @@ async function loadChatHistory(): Promise<void> {
|
||||
viewId: safeViewId,
|
||||
type: msgType,
|
||||
content: m.content ?? '',
|
||||
time: timeStr
|
||||
time: timeStr,
|
||||
msgType: m.msg_type ?? 'text'
|
||||
}
|
||||
uiMessages.push(uiMsg)
|
||||
}
|
||||
@@ -476,17 +499,124 @@ function showEmojiPicker(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// 执行图片上传
|
||||
async function doUploadImage(filePath: string): Promise<void> {
|
||||
console.log('[doUploadImage] 开始上传图片:', filePath)
|
||||
|
||||
// 显示加载提示
|
||||
uni.showLoading({
|
||||
title: '发送中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
try {
|
||||
// 上传图片
|
||||
const imageUrl = await supabaseService.uploadChatImage(filePath)
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (imageUrl == '') {
|
||||
uni.showToast({
|
||||
title: '图片上传失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[doUploadImage] 图片上传成功:', imageUrl)
|
||||
|
||||
// 发送图片消息
|
||||
const success = await supabaseService.sendMessage(merchantId.value, imageUrl, 'image')
|
||||
if (!success) {
|
||||
uni.showToast({
|
||||
title: '发送失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('[doUploadImage] 上传异常:', e)
|
||||
uni.showToast({
|
||||
title: '上传失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 显示图片选择器
|
||||
function showImagePicker(): void {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
console.log('选择图片:', res.tempFilePaths)
|
||||
// 这里可以处理图片上传
|
||||
console.log('选择图片成功:', JSON.stringify(res))
|
||||
|
||||
// 处理 tempFilePaths,兼容不同平台
|
||||
let filePath: string = ''
|
||||
const tempFilePaths = res.tempFilePaths
|
||||
if (tempFilePaths != null) {
|
||||
if (Array.isArray(tempFilePaths)) {
|
||||
const arr = tempFilePaths as string[]
|
||||
if (arr.length > 0) {
|
||||
filePath = arr[0]
|
||||
}
|
||||
} else if (tempFilePaths instanceof UTSJSONObject) {
|
||||
const keys = UTSJSONObject.keys(tempFilePaths as UTSJSONObject)
|
||||
if (keys.length > 0) {
|
||||
filePath = (tempFilePaths as UTSJSONObject).getString(keys[0]) ?? ''
|
||||
}
|
||||
} else if (typeof tempFilePaths === 'string') {
|
||||
filePath = tempFilePaths as string
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从 tempFiles 获取
|
||||
if (filePath == '' && res.tempFiles != null) {
|
||||
const tempFiles = res.tempFiles
|
||||
if (Array.isArray(tempFiles)) {
|
||||
const files = tempFiles as any[]
|
||||
if (files.length > 0) {
|
||||
const firstFile = files[0]
|
||||
if (firstFile instanceof UTSJSONObject) {
|
||||
filePath = firstFile.getString('path') ?? ''
|
||||
} else if (typeof firstFile === 'object' && firstFile != null) {
|
||||
const fileObj = JSON.parse(JSON.stringify(firstFile)) as UTSJSONObject
|
||||
filePath = fileObj.getString('path') ?? ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[showImagePicker] 文件路径:', filePath)
|
||||
|
||||
if (filePath == '') {
|
||||
uni.showToast({
|
||||
title: '获取图片路径失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行上传
|
||||
doUploadImage(filePath)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('选择图片失败:', err)
|
||||
uni.showToast({
|
||||
title: '选择图片失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
function previewImage(url: string): void {
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
current: url
|
||||
})
|
||||
}
|
||||
|
||||
// 显示更多工具
|
||||
function showMoreTools(): void {
|
||||
uni.showActionSheet({
|
||||
@@ -717,6 +847,13 @@ const goBack = () => {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message-image {
|
||||
max-width: 200px;
|
||||
min-width: 100px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 结算页面 -->
|
||||
<!-- 结算页面 -->
|
||||
<template>
|
||||
<view class="checkout-page">
|
||||
<scroll-view class="checkout-content" direction="vertical">
|
||||
@@ -316,6 +316,8 @@ type CheckoutItemType = {
|
||||
product_image: string
|
||||
sku_specifications: any
|
||||
price: number
|
||||
original_price: number // 原价
|
||||
member_price: number // 会员价
|
||||
quantity: number
|
||||
shop_id?: string
|
||||
shop_name?: string
|
||||
@@ -474,7 +476,11 @@ const shopGroups = computed((): Array<ShopGroupType> => {
|
||||
const getGroupTotal = (group: ShopGroupType): string => {
|
||||
let sum = 0
|
||||
group.items.forEach((item) => {
|
||||
const price = item.price
|
||||
// 优先使用会员价,如果没有会员价则使用原价
|
||||
let price = item.price
|
||||
if (item.member_price != null && item.member_price > 0 && item.member_price < item.price) {
|
||||
price = item.member_price
|
||||
}
|
||||
const quantity = item.quantity
|
||||
if (isNaN(price) == false && isNaN(quantity) == false) {
|
||||
sum += (price * quantity)
|
||||
@@ -495,7 +501,11 @@ const totalAmount = computed(() => {
|
||||
// 确保item存在且包含必要的属性
|
||||
if (item == null) return sum
|
||||
|
||||
const price = item.price
|
||||
// 优先使用会员价,如果没有会员价则使用原价
|
||||
let price = item.price
|
||||
if (item.member_price != null && item.member_price > 0 && item.member_price < item.price) {
|
||||
price = item.member_price
|
||||
}
|
||||
const quantity = item.quantity
|
||||
|
||||
// 验证转换后的数字是否有效
|
||||
@@ -546,8 +556,20 @@ watch(checkoutItems, (newItems: Array<CheckoutItemType>) => {
|
||||
}, { deep: true })
|
||||
|
||||
// 处理商品数据清洗
|
||||
const processCheckoutItems = (items: any[]) => {
|
||||
// 数据清洗:确保价格和数量是数字类型
|
||||
const processCheckoutItems = async (items: any[]) => {
|
||||
// 获取会员折扣信息
|
||||
let memberDiscount = 1.0
|
||||
try {
|
||||
const memberInfo = await supabaseService.getUserMemberInfo()
|
||||
const discountRaw = memberInfo.get('discount')
|
||||
if (discountRaw != null) {
|
||||
memberDiscount = discountRaw as number
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('获取会员信息失败,使用默认折扣:', e)
|
||||
}
|
||||
|
||||
// 数据清洗:确保价格和数量是数字类型
|
||||
const converted: Array<CheckoutItemType> = []
|
||||
if (items != null && items.length > 0) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
@@ -584,6 +606,12 @@ const processCheckoutItems = (items: any[]) => {
|
||||
const shopId = obj.getString('shop_id') ?? obj.getString('shopId') ?? 'unknown'
|
||||
const shopName = obj.getString('shop_name') ?? obj.getString('shopName') ?? ''
|
||||
const merchantId = obj.getString('merchant_id') ?? obj.getString('merchantId') ?? ''
|
||||
|
||||
// 计算会员价
|
||||
let memberPrice = 0
|
||||
if (memberDiscount > 0 && memberDiscount < 1 && price > 0) {
|
||||
memberPrice = Math.round(price * memberDiscount * 100) / 100
|
||||
}
|
||||
|
||||
converted.push({
|
||||
id: id,
|
||||
@@ -593,6 +621,8 @@ const processCheckoutItems = (items: any[]) => {
|
||||
product_image: productImage,
|
||||
sku_specifications: specs,
|
||||
price: parseFloat(price.toFixed(2)),
|
||||
original_price: parseFloat(price.toFixed(2)),
|
||||
member_price: memberPrice,
|
||||
quantity: quantity,
|
||||
shop_id: shopId,
|
||||
shop_name: shopName,
|
||||
@@ -605,7 +635,7 @@ const processCheckoutItems = (items: any[]) => {
|
||||
if (checkoutItems.value.length > 0) {
|
||||
console.log('清洗后商品价格明细:')
|
||||
checkoutItems.value.forEach((item: CheckoutItemType, index: number) => {
|
||||
console.log(`商品${index}:`, item.product_name, '价格:', item.price, 'shop:', item.shop_id)
|
||||
console.log(`商品${index}:`, item.product_name, '原价:', item.price, '会员价:', item.member_price, 'shop:', item.shop_id)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -929,7 +959,7 @@ async function loadAddressList(): Promise<void> {
|
||||
}
|
||||
|
||||
// 从本地存储加载结算数据(例如从购物车进入)
|
||||
function loadFromLocalStorage(): void {
|
||||
async function loadFromLocalStorage(): Promise<void> {
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
const cartDataStr = cartData != null ? cartData.toString() : ''
|
||||
if (cartDataStr != '') {
|
||||
@@ -942,7 +972,7 @@ function loadFromLocalStorage(): void {
|
||||
if (selected) selectedCartItems.push(obj)
|
||||
}
|
||||
if (selectedCartItems.length > 0) {
|
||||
processCheckoutItems(selectedCartItems)
|
||||
await processCheckoutItems(selectedCartItems)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败:', e)
|
||||
@@ -956,7 +986,8 @@ function loadCheckoutData(): void {
|
||||
loadFromLocalStorage()
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
// 初始化加载数据
|
||||
async function initCheckoutData(): Promise<void> {
|
||||
let dataLoaded = false
|
||||
const checkoutTypeAny = uni.getStorageSync('checkout_type')
|
||||
const checkoutType = checkoutTypeAny != null ? checkoutTypeAny.toString() : ''
|
||||
@@ -969,7 +1000,7 @@ onLoad((options: any) => {
|
||||
const items = JSON.parse(itemsStr as string)
|
||||
console.log('从Storage加载的商品数据:', items)
|
||||
if (items != null && Array.isArray(items) && items.length > 0) {
|
||||
processCheckoutItems(items)
|
||||
await processCheckoutItems(items)
|
||||
dataLoaded = true
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -980,11 +1011,15 @@ onLoad((options: any) => {
|
||||
|
||||
if (dataLoaded == false) {
|
||||
console.log('未找到预结算数据,尝试从购物车本地存储加载')
|
||||
loadFromLocalStorage()
|
||||
await loadFromLocalStorage()
|
||||
}
|
||||
|
||||
loadDefaultAddress()
|
||||
loadAddressList()
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
initCheckoutData()
|
||||
})
|
||||
|
||||
// 页面显示时触发
|
||||
@@ -1337,6 +1372,7 @@ const submitOrder = async () => {
|
||||
sku_id: item.sku_id,
|
||||
quantity: item.quantity,
|
||||
price: item.price,
|
||||
member_price: item.member_price,
|
||||
product_name: item.product_name,
|
||||
product_image: item.product_image,
|
||||
specifications: item.sku_specifications
|
||||
|
||||
@@ -2,79 +2,96 @@
|
||||
<template>
|
||||
<view class="payment-page">
|
||||
<scroll-view class="payment-content" direction="vertical">
|
||||
<!-- 价格明细 -->
|
||||
<view class="price-detail-section">
|
||||
<text class="section-title">价格明细</text>
|
||||
<view class="price-detail">
|
||||
<view class="price-row">
|
||||
<text class="price-label">商品总价</text>
|
||||
<text class="price-value">¥{{ productAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="price-row">
|
||||
<text class="price-label">运费</text>
|
||||
<text class="price-value">+¥{{ deliveryFee.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view v-if="discountAmount > 0" class="price-row">
|
||||
<text class="price-label">优惠减免</text>
|
||||
<text class="price-value discount">-¥{{ discountAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view class="price-row total">
|
||||
<text class="price-label">应付金额</text>
|
||||
<text class="price-value total-price">¥{{ amount.toFixed(2) }}</text>
|
||||
</view>
|
||||
<!-- 支付成功样式头部 -->
|
||||
<view class="payment-amount-header">
|
||||
<text class="amount-label">支付金额</text>
|
||||
<view class="amount-value-row">
|
||||
<text class="amount-currency">¥</text>
|
||||
<text class="amount-number">{{ amount.toFixed(2) }}</text>
|
||||
</view>
|
||||
<text class="order-no">订单号: {{ orderNo }}</text>
|
||||
<text class="order-no-text">订单号: {{ orderNo }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 支付方式 -->
|
||||
<view class="methods-section">
|
||||
<text class="section-title">选择支付方式</text>
|
||||
<view class="methods-section-new">
|
||||
<view class="section-header">
|
||||
<text class="section-title">选择支付方式</text>
|
||||
</view>
|
||||
<view class="method-list">
|
||||
<view v-for="method in paymentMethods"
|
||||
:key="method.id"
|
||||
:class="['method-item', { selected: selectedMethod === method.id }]"
|
||||
class="method-item-modern"
|
||||
@click="selectMethod(method)">
|
||||
<view class="method-left">
|
||||
<text class="method-icon">{{ getMethodIcon(method.id) }}</text>
|
||||
<image class="method-img" :src="getMethodBrandIcon(method.id)" mode="aspectFit" />
|
||||
<view class="method-info">
|
||||
<text class="method-name">{{ method.name }}</text>
|
||||
<text class="method-desc">{{ method.description }}</text>
|
||||
<text class="method-desc" v-if="method.id === 'balance'">当前余额: ¥{{ userBalance.toFixed(2) }}</text>
|
||||
<text class="method-desc" v-else>{{ method.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="selectedMethod === method.id" class="method-selected">
|
||||
<text class="selected-icon">✓</text>
|
||||
<view class="method-right">
|
||||
<view :class="['radio-circle', { checked: selectedMethod === method.id }]">
|
||||
<view class="radio-inner" v-if="selectedMethod === method.id"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 余额支付 -->
|
||||
<view v-if="selectedMethod === 'balance' && userBalance > 0" class="balance-section">
|
||||
<view class="balance-info">
|
||||
<text class="balance-label">账户余额</text>
|
||||
<text class="balance-value">¥{{ userBalance.toFixed(2) }}</text>
|
||||
</view>
|
||||
<view v-if="userBalance < amount" class="balance-tip">
|
||||
<text class="tip-text">余额不足,请选择其他支付方式</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 弹窗式密码输入层 -->
|
||||
<view v-if="showPassword" class="password-popup-mask" @click="closePasswordPopup">
|
||||
<view class="password-popup-content" @click.stop="">
|
||||
<view class="popup-header">
|
||||
<text class="popup-close" @click="closePasswordPopup">✕</text>
|
||||
<text class="popup-title">请输入支付密码</text>
|
||||
<view class="popup-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<view class="popup-amount-info">
|
||||
<text class="popup-amount-label">支付金额</text>
|
||||
<view class="popup-amount-row">
|
||||
<text class="popup-currency">¥</text>
|
||||
<text class="popup-value">{{ amount.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<view v-if="showPassword" class="password-section">
|
||||
<text class="password-title">请输入支付密码</text>
|
||||
<view class="password-input">
|
||||
<view v-for="(_, index) in 6"
|
||||
:key="index"
|
||||
class="password-dot">
|
||||
<text v-if="password.length > index" class="password-dot-text">●</text>
|
||||
<view class="password-input-row">
|
||||
<view v-for="(_, index) in 6"
|
||||
:key="index"
|
||||
class="password-box">
|
||||
<view v-if="password.length > index" class="password-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="forgot-password-link" @click="forgotPassword">忘记密码?</text>
|
||||
|
||||
<!-- 这里的密码键盘会被放在页面底部,但我们可以通过 CSS 控制它 -->
|
||||
|
||||
<!-- 移动键盘到弹窗内部 -->
|
||||
<view class="password-keyboard-popup">
|
||||
<view class="keyboard-grid">
|
||||
<view v-for="num in 9"
|
||||
:key="num"
|
||||
class="keyboard-key"
|
||||
@click="inputPassword(num.toString())">
|
||||
<text class="key-text">{{ num }}</text>
|
||||
</view>
|
||||
<view class="keyboard-key"></view>
|
||||
<view class="keyboard-key" @click="inputPassword('0')">
|
||||
<text class="key-text">0</text>
|
||||
</view>
|
||||
<view class="keyboard-key" @click="deletePassword">
|
||||
<text class="key-text">⌫</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="forgot-password" @click="forgotPassword">忘记密码?</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部支付按钮 -->
|
||||
<view class="payment-bottom">
|
||||
<view class="payment-bottom" v-if="!showPassword">
|
||||
<view class="price-summary">
|
||||
<text class="summary-label">需支付:</text>
|
||||
<text class="summary-price">¥{{ amount.toFixed(2) }}</text>
|
||||
@@ -87,24 +104,6 @@
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 密码键盘 -->
|
||||
<view v-if="showPassword" class="password-keyboard">
|
||||
<view class="keyboard-grid">
|
||||
<view v-for="num in 9"
|
||||
:key="num"
|
||||
class="keyboard-key"
|
||||
@click="inputPassword(num.toString())">
|
||||
<text class="key-text">{{ num }}</text>
|
||||
</view>
|
||||
<view class="keyboard-key"></view>
|
||||
<view class="keyboard-key" @click="inputPassword('0')">
|
||||
<text class="key-text">0</text>
|
||||
</view>
|
||||
<view class="keyboard-key" @click="deletePassword">
|
||||
<text class="key-text">⌫</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -415,6 +414,20 @@ const getMethodIcon = (methodId: string): string => {
|
||||
return '💳'
|
||||
}
|
||||
|
||||
// 获取支付品牌图片
|
||||
const getMethodBrandIcon = (methodId: string): string => {
|
||||
if (methodId === 'wechat') {
|
||||
return '/static/logo.png' // 替换为真实的微信支付图标路径
|
||||
} else if (methodId === 'alipay') {
|
||||
return '/static/logo.png' // 替换为真实的支付宝图标路径
|
||||
} else if (methodId === 'balance') {
|
||||
return '/static/logo.png' // 替换为真实的余额支付图标路径
|
||||
} else if (methodId === 'bankcard') {
|
||||
return '/static/logo.png' // 替换为真实的银行卡支付图标路径
|
||||
}
|
||||
return '/static/logo.png'
|
||||
}
|
||||
|
||||
// 选择支付方式
|
||||
const selectMethod = (method: PaymentMethodType) => {
|
||||
if (!method.enabled) {
|
||||
@@ -426,10 +439,17 @@ const selectMethod = (method: PaymentMethodType) => {
|
||||
}
|
||||
|
||||
selectedMethod.value = method.id
|
||||
showPassword.value = method.id === 'balance' || method.id === 'bankcard'
|
||||
// 切换方式时,除非点击支付,否则不自动弹出密码
|
||||
showPassword.value = false
|
||||
password.value = '' // 清空密码
|
||||
}
|
||||
|
||||
// 关闭密码弹窗
|
||||
const closePasswordPopup = () => {
|
||||
showPassword.value = false
|
||||
password.value = ''
|
||||
}
|
||||
|
||||
// 获取支付按钮文本
|
||||
const getPayButtonText = (): string => {
|
||||
if (selectedMethod.value === 'balance' && userBalance.value < amount.value) {
|
||||
@@ -457,9 +477,9 @@ const getPayButtonText = (): string => {
|
||||
const confirmPayment = async () => {
|
||||
if (isPaying.value) return
|
||||
|
||||
// 余额支付检查
|
||||
if (selectedMethod.value === 'balance') {
|
||||
if (userBalance.value < amount.value) {
|
||||
// 余额支付或银行卡支付检查密码
|
||||
if (selectedMethod.value === 'balance' || selectedMethod.value === 'bankcard') {
|
||||
if (selectedMethod.value === 'balance' && userBalance.value < amount.value) {
|
||||
uni.showToast({
|
||||
title: '余额不足',
|
||||
icon: 'none'
|
||||
@@ -469,6 +489,7 @@ const confirmPayment = async () => {
|
||||
|
||||
if (!showPassword.value) {
|
||||
showPassword.value = true
|
||||
password.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
@@ -649,137 +670,133 @@ onUnmounted(() => {
|
||||
.payment-content {
|
||||
flex: 1;
|
||||
/* overflow-y: auto; */
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
/* 价格明细部分 */
|
||||
.price-detail-section {
|
||||
.payment-amount-header {
|
||||
background-color: #ffffff;
|
||||
padding: 20px 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.price-detail {
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
padding: 40px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.price-row.total {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
margin-top: 8px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
.amount-label {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.price-value.discount {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.price-value.total-price {
|
||||
font-size: 18px;
|
||||
color: #ff4757;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
/* display: block; */
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.methods-section {
|
||||
background-color: #ffffff;
|
||||
padding: 20px 15px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.amount-value-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.amount-currency {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.amount-number {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.order-no-text {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.methods-section-new {
|
||||
background-color: #ffffff;
|
||||
margin: 0 12px;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
/* display: block; */
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.method-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* gap: 10px; */
|
||||
}
|
||||
|
||||
.method-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
.method-item-modern {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 0.5px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.method-item.selected {
|
||||
border-color: #007aff;
|
||||
background-color: #f0f8ff;
|
||||
.method-item-modern:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.method-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 15px;
|
||||
.method-img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.method-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 5px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.method-desc {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.method-selected {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
background-color: #007aff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.radio-circle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ddd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.selected-icon {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
.radio-circle.checked {
|
||||
border-color: #ff5000;
|
||||
background-color: #ff5000;
|
||||
}
|
||||
|
||||
.radio-inner {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.balance-section {
|
||||
@@ -817,52 +834,186 @@ onUnmounted(() => {
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.password-section {
|
||||
background-color: #ffffff;
|
||||
padding: 30px 15px;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
/* 密码输入弹窗 */
|
||||
.password-popup-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.password-title {
|
||||
/* display: block; */
|
||||
.password-popup-content {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 20px 0; /* 减少左右内边距,让键盘撑满 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
font-size: 20px;
|
||||
color: #999;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.popup-placeholder {
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.popup-amount-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
.popup-amount-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.popup-amount-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.popup-currency {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.popup-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.password-input-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
/* gap: 15px; */
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.password-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background-color: #333333;
|
||||
.password-box {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border: 1px solid #ddd;
|
||||
border-right: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 7.5px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.password-dot-text {
|
||||
color: #ffffff;
|
||||
font-size: 8px;
|
||||
.password-box:first-child {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
color: #007aff;
|
||||
font-size: 14px;
|
||||
.password-box:last-child {
|
||||
border-right: 1px solid #ddd;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.password-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
font-size: 13px;
|
||||
color: #576b95;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.password-section {
|
||||
/* 移除旧的样式或保持隐藏 */
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 弹窗专用键盘样式 */
|
||||
.password-keyboard-popup {
|
||||
width: 100%;
|
||||
background-color: #f5f5f5;
|
||||
padding: 6px;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 键盘样式优化 */
|
||||
.password-keyboard {
|
||||
display: none; /* 隐藏独立键盘 */
|
||||
}
|
||||
|
||||
.keyboard-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.keyboard-key {
|
||||
width: 33.33%;
|
||||
background-color: #ffffff;
|
||||
height: 54px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3px solid #f5f5f5;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.keyboard-key:active {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.key-text {
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.payment-bottom {
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
padding: 15px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 12px 16px;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -870,30 +1021,33 @@ onUnmounted(() => {
|
||||
|
||||
.price-summary {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
margin-right: 5px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.summary-price {
|
||||
font-size: 20px;
|
||||
color: #ff4757;
|
||||
font-size: 24px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pay-btn {
|
||||
background-color: #007aff;
|
||||
background-color: #ff5000;
|
||||
color: #ffffff;
|
||||
padding: 0 40px;
|
||||
height: 45px;
|
||||
border-radius: 22.5px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
border-radius: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pay-btn.disabled {
|
||||
|
||||
1815
pages/mall/consumer/product-detail copy 2.uvue
Normal file
1815
pages/mall/consumer/product-detail copy 2.uvue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,16 +14,30 @@
|
||||
|
||||
<!-- 商品基本信息 -->
|
||||
<view class="product-info">
|
||||
<!-- 价格区域 -->
|
||||
<view class="price-section">
|
||||
<text class="current-price">¥{{ product.price }}</text>
|
||||
<!-- 会员价功能已禁用 -->
|
||||
<!-- <text v-if="memberPrice > 0 && memberPrice < product.price" class="member-price-tag">会员价 ¥{{ memberPrice }}</text> -->
|
||||
<text v-if="product.original_price" class="original-price">¥{{ product.original_price }}</text>
|
||||
<!-- 会员折扣标签行 -->
|
||||
<view v-if="memberDiscount > 0 && memberPrice > 0 && memberPrice < product.price" class="price-header">
|
||||
<view class="member-badge">
|
||||
<text class="member-badge-text">VIP</text>
|
||||
</view>
|
||||
<text class="member-discount-label">{{ memberDiscount }}折</text>
|
||||
</view>
|
||||
|
||||
<!-- 价格行 -->
|
||||
<view class="price-row">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-value">{{ memberPrice > 0 && memberPrice < product.price ? memberPrice : product.price }}</text>
|
||||
<text v-if="memberPrice > 0 && memberPrice < product.price" class="price-original">¥{{ product.price }}</text>
|
||||
<text v-else-if="product.original_price != null && product.original_price > product.price" class="price-original">¥{{ product.original_price }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 节省金额 -->
|
||||
<view v-if="memberPrice > 0 && memberPrice < product.price" class="save-row">
|
||||
<text class="save-text">已省 ¥{{ (product.price - memberPrice).toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 会员专享折扣功能已禁用 -->
|
||||
<!-- <view v-if="memberDiscount > 0" class="member-discount-row">
|
||||
<text class="member-discount-text">会员专享 {{ memberDiscount }}折优惠</text>
|
||||
</view> -->
|
||||
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<text class="sales-info">已售{{ product.sales }}件 · 库存{{ product.stock }}件</text>
|
||||
</view>
|
||||
@@ -137,9 +151,13 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 规格选择弹窗 (京东风格) -->
|
||||
<!-- 规格选择弹窗 (京东风格) -->
|
||||
<view v-if="showSpec" class="spec-modal" @click="hideSpecModal">
|
||||
<view class="spec-content" @click.stop>
|
||||
<!-- 强化提示语显示逻辑 -->
|
||||
<view v-if="selectedSkuId == ''" class="spec-error-tip">
|
||||
<text class="error-tip-text">请选择规格</text>
|
||||
</view>
|
||||
<view class="spec-header-jd">
|
||||
<image :src="getSelectedSkuImage()" class="spec-product-img" mode="aspectFill" />
|
||||
<view class="spec-info-jd">
|
||||
@@ -614,10 +632,14 @@ export default {
|
||||
}
|
||||
|
||||
if (discountRaw != null) {
|
||||
const discount = discountRaw as number
|
||||
if (discount > 0 && discount < 10) {
|
||||
this.memberDiscount = discount
|
||||
this.memberPrice = Math.round(this.product.price * discount) / 10
|
||||
const discountRate = discountRaw as number
|
||||
// discountRate 是折扣率,如 0.9 表示 9 折
|
||||
// 会员价 = 原价 × 折扣率
|
||||
if (discountRate > 0 && discountRate < 1) {
|
||||
// 计算折扣显示值:0.9 -> 9 折
|
||||
this.memberDiscount = Math.round(discountRate * 10) / 10 * 10
|
||||
// 计算会员价:原价 × 折扣率
|
||||
this.memberPrice = Math.round(this.product.price * discountRate * 100) / 100
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -804,11 +826,9 @@ export default {
|
||||
|
||||
async addToCart() {
|
||||
if (this.productSkus.length > 0 && (this.selectedSkuId == null || this.selectedSkuId === '')) {
|
||||
this.showSpecModal()
|
||||
uni.showToast({
|
||||
title: '请选择规格',
|
||||
icon: 'none'
|
||||
})
|
||||
if (!this.showSpec) {
|
||||
this.showSpecModal()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -839,11 +859,9 @@ export default {
|
||||
|
||||
buyNow() {
|
||||
if (this.productSkus.length > 0 && (this.selectedSkuId == null || this.selectedSkuId === '')) {
|
||||
this.showSpecModal()
|
||||
uni.showToast({
|
||||
title: '请选择规格',
|
||||
icon: 'none'
|
||||
})
|
||||
if (!this.showSpec) {
|
||||
this.showSpecModal()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1043,6 +1061,74 @@ export default {
|
||||
|
||||
.price-section {
|
||||
margin-bottom: 20rpx;
|
||||
background: linear-gradient(135deg, #fff5f0 0%, #ffffff 100%);
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
margin: -10rpx -10rpx 20rpx -10rpx;
|
||||
}
|
||||
|
||||
.price-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.member-badge {
|
||||
background: linear-gradient(135deg, #ff5000 0%, #ff7a00 100%);
|
||||
border-radius: 6rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.member-badge-text {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.member-discount-label {
|
||||
font-size: 26rpx;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.price-symbol {
|
||||
font-size: 32rpx;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 56rpx;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.price-original {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
text-decoration-line: line-through;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.save-row {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.save-text {
|
||||
font-size: 24rpx;
|
||||
color: #52c41a;
|
||||
background-color: #f6ffed;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
@@ -1063,14 +1149,39 @@ export default {
|
||||
}
|
||||
|
||||
.member-discount-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 15rpx;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.member-tag {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #ff5000 0%, #ff7a00 100%);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx;
|
||||
margin-right: 10rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.member-discount-text {
|
||||
font-size: 24rpx;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.member-save-text {
|
||||
font-size: 22rpx;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.current-price.has-discount {
|
||||
color: #ff5000;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
@@ -1451,6 +1562,7 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80vh;
|
||||
position: relative; /* 确保子元素绝对定位相对于此容器 */
|
||||
}
|
||||
|
||||
.spec-header-jd {
|
||||
@@ -1500,6 +1612,24 @@ export default {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 规格未选提示样式 */
|
||||
.spec-error-tip {
|
||||
position: absolute;
|
||||
top: -80rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 12rpx 30rpx;
|
||||
border-radius: 40rpx;
|
||||
z-index: 2000;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.error-tip-text {
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.spec-stock-jd {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
|
||||
@@ -48,10 +48,13 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主内容区域:改为普通 view,由页面整体滚动 -->
|
||||
<view
|
||||
<!-- 主内容区域:使用 scroll-view 支持安卓端滚动 -->
|
||||
<scroll-view
|
||||
v-else
|
||||
class="main-content"
|
||||
direction="vertical"
|
||||
:scroll-y="true"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<!-- 初始状态(无搜索词) -->
|
||||
<view v-if="searchKeyword == '' && showResults == false">
|
||||
@@ -230,7 +233,7 @@
|
||||
|
||||
<!-- 底部安全区域 -->
|
||||
<view class="safe-area"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1025,6 +1028,7 @@ const goBack = () => {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
height: 0; /* 配合 flex: 1 实现自适应高度 */
|
||||
}
|
||||
|
||||
/* 模块通用头部 */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<view class="shop-detail-page">
|
||||
<scroll-view class="page-scroll" scroll-y="true" @scrolltolower="onScrollToLower" refresher-enabled="true" @refresherrefresh="onRefresherRefresh" :refresher-triggered="isRefresherTriggered">
|
||||
<!-- 店铺头部信息 -->
|
||||
@@ -853,24 +853,15 @@ const goToProduct = (id: string) => {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
width: 48% !important;
|
||||
width: 48%;
|
||||
margin-bottom: 12px;
|
||||
margin-right: 0 !important;
|
||||
margin-right: 2%;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.result-item:nth-child(2n-1) {
|
||||
margin-right: 4% !important;
|
||||
}
|
||||
|
||||
.result-item:nth-child(2n) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
@@ -933,22 +924,22 @@ const goToProduct = (id: string) => {
|
||||
|
||||
.result-item {
|
||||
width: 23%;
|
||||
margin-right: 2% !important;
|
||||
margin-right: 2%;
|
||||
}
|
||||
|
||||
.result-item:nth-child(2n) {
|
||||
margin-right: 2% !important;
|
||||
margin-right: 2%;
|
||||
}
|
||||
|
||||
.result-item:nth-child(4n) {
|
||||
margin-right: 0 !important;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大桌面端 (1400px以上) */
|
||||
@media screen and (min-width: 1400px) {
|
||||
.result-item {
|
||||
width: 23.5%;
|
||||
width: 23%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user