consumer模块完成度95%,优化安卓端界面和小程序测试2

This commit is contained in:
cyh666666
2026-03-12 15:20:45 +08:00
parent 77f9968d18
commit b2a6e5a142
633 changed files with 4405 additions and 1651 deletions

View File

@@ -2,7 +2,7 @@
"version" : "1.0",
"configurations" : [
{
"customPlaygroundType" : "local",
"customPlaygroundType" : "device",
"localRepoPath" : "D:/companyproject/mall",
"packageName" : "com.huawei.hisuite",
"playground" : "standard",

View File

@@ -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,

View File

@@ -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;

View File

@@ -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}`)
}
// 重置导航栏显示状态(例如点击回到顶部时)

View File

@@ -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;

View File

@@ -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

View File

@@ -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 {

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -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 实现自适应高度 */
}
/* 模块通用头部 */

View File

@@ -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%;
}
}

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More