consumer模块完成度95%,优化安卓端界面和小程序测试2
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"version" : "1.0",
|
||||
"configurations" : [
|
||||
{
|
||||
"customPlaygroundType" : "local",
|
||||
"customPlaygroundType" : "device",
|
||||
"localRepoPath" : "D:/companyproject/mall",
|
||||
"packageName" : "com.huawei.hisuite",
|
||||
"playground" : "standard",
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
unpackage/cache/.app-android/class/META-INF/main-1773220378663.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773220378663.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773220805552.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773220805552.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773221180327.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773221180327.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773221313747.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773221313747.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773277785288.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773277785288.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773278310518.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773278310518.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773278877919.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773278877919.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773278960314.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773278960314.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773279883162.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773279883162.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773279913669.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773279913669.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773279990478.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773279990478.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773281318383.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773281318383.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773281761772.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773281761772.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773284934718.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773284934718.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773285006797.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773285006797.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773285624654.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773285624654.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773285698015.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773285698015.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773298280394.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773298280394.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773298944933.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773298944933.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773298973496.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773298973496.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/META-INF/main-1773299706099.kotlin_module
vendored
Normal file
BIN
unpackage/cache/.app-android/class/META-INF/main-1773299706099.kotlin_module
vendored
Normal file
Binary file not shown.
BIN
unpackage/cache/.app-android/class/ktClasss.ser
vendored
BIN
unpackage/cache/.app-android/class/ktClasss.ser
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user