继续补充功能页面,consumer模块完成度70%
This commit is contained in:
42
pages.json
42
pages.json
@@ -39,6 +39,48 @@
|
||||
"navigationBarTitleText": "搜索",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mall/consumer/product-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "商品详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mall/consumer/shop-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "店铺详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mall/consumer/coupons",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的优惠券"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mall/consumer/favorites",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的收藏"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mall/consumer/footprint",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的足迹"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mall/consumer/address-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "收货地址"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mall/consumer/address-edit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "编辑地址"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tabBar": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
221
pages/mall/consumer/address-list.uvue
Normal file
221
pages/mall/consumer/address-list.uvue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<view class="address-list-page">
|
||||
<view class="address-list">
|
||||
<view v-if="addresses.length === 0" class="empty-state">
|
||||
<text class="empty-icon">📍</text>
|
||||
<text class="empty-text">暂无收货地址</text>
|
||||
</view>
|
||||
|
||||
<view v-else v-for="(item, index) in addresses" :key="item.id" class="address-item" @click="selectAddress(item)">
|
||||
<view class="item-content">
|
||||
<view class="item-header">
|
||||
<text class="user-name">{{ item.name }}</text>
|
||||
<text class="user-phone">{{ item.phone }}</text>
|
||||
<text v-if="item.isDefault" class="default-tag">默认</text>
|
||||
<text v-if="item.label" class="label-tag">{{ item.label }}</text>
|
||||
</view>
|
||||
<text class="address-text">{{ getFullAddress(item) }}</text>
|
||||
</view>
|
||||
<view class="item-edit" @click.stop="editAddress(item.id)">
|
||||
<text class="edit-icon">📝</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-btn">
|
||||
<button class="add-btn" @click="addAddress">新建收货地址</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
|
||||
type Address = {
|
||||
id: string
|
||||
name: string
|
||||
phone: string
|
||||
province: string
|
||||
city: string
|
||||
district: string
|
||||
detail: string
|
||||
isDefault: boolean
|
||||
label?: string
|
||||
}
|
||||
|
||||
const addresses = ref<Address[]>([])
|
||||
|
||||
onShow(() => {
|
||||
loadAddresses()
|
||||
})
|
||||
|
||||
const loadAddresses = () => {
|
||||
const storedAddresses = uni.getStorageSync('addresses')
|
||||
if (storedAddresses) {
|
||||
try {
|
||||
addresses.value = JSON.parse(storedAddresses as string) as Address[]
|
||||
} catch (e) {
|
||||
console.error('Failed to parse addresses', e)
|
||||
addresses.value = []
|
||||
}
|
||||
} else {
|
||||
// 初始Mock数据
|
||||
addresses.value = [
|
||||
{
|
||||
id: 'addr_001',
|
||||
name: '张三',
|
||||
phone: '13800138000',
|
||||
province: '北京市',
|
||||
city: '北京市',
|
||||
district: '朝阳区',
|
||||
detail: '三里屯SOHO A座',
|
||||
isDefault: true,
|
||||
label: '公司'
|
||||
}
|
||||
]
|
||||
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
|
||||
}
|
||||
}
|
||||
|
||||
const getFullAddress = (item: Address): string => {
|
||||
return `${item.province}${item.city}${item.district} ${item.detail}`
|
||||
}
|
||||
|
||||
const addAddress = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/address-edit'
|
||||
})
|
||||
}
|
||||
|
||||
const editAddress = (id: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/address-edit?id=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
const selectAddress = (item: Address) => {
|
||||
// 如果是选择地址模式(例如从订单确认页过来),则返回并传递地址
|
||||
// 目前暂未实现选择模式,仅作为普通点击
|
||||
editAddress(item.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.address-list-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.address-list {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.address-item {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.user-phone {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.default-tag {
|
||||
background-color: #ff5000;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.label-tag {
|
||||
background-color: #e0f2f1;
|
||||
color: #00796b;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.address-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.item-edit {
|
||||
padding: 10px;
|
||||
border-left: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
font-size: 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
padding: 10px 15px;
|
||||
padding-bottom: calc(10px + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background-color: #ff5000;
|
||||
color: white;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,38 @@
|
||||
<!-- pages/mall/consumer/category.uvue -->
|
||||
<template>
|
||||
<view class="category-page">
|
||||
<!-- 顶部搜索栏 -->
|
||||
<view class="search-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="search-container">
|
||||
<view class="search-box" @click="navigateToSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
<text class="search-placeholder">症状/药品/品牌智能搜索</text>
|
||||
<view class="search-box" @click="navigateToSearch" :style="{ height: '30px' }">
|
||||
<!-- 模拟输入框 -->
|
||||
<text class="search-placeholder">请输入药品名称、症状或品牌</text>
|
||||
|
||||
<!-- 扫码图标 -->
|
||||
<view class="nav-icon-btn" @click.stop="onScan">
|
||||
<text class="nav-icon">🔳</text>
|
||||
</view>
|
||||
|
||||
<!-- 相机图标 -->
|
||||
<view class="nav-camera-btn" @click.stop="onCamera">
|
||||
<text class="nav-camera-icon">📷</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索按钮 -->
|
||||
<view class="nav-inner-search-btn" :style="{ height: '22px' }">
|
||||
<text class="nav-inner-search-text">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类内容区 -->
|
||||
<view class="category-content">
|
||||
<view
|
||||
class="category-content"
|
||||
:style="{
|
||||
marginTop: (statusBarHeight + headerHeight + 10) + 'px',
|
||||
height: `calc(100vh - ${statusBarHeight + headerHeight + 10}px)`
|
||||
}"
|
||||
>
|
||||
<!-- 左侧一级分类 -->
|
||||
<scroll-view scroll-y class="primary-category">
|
||||
<view
|
||||
@@ -94,6 +114,7 @@ import { ref, onMounted } from 'vue'
|
||||
|
||||
// 响应式数据
|
||||
const statusBarHeight = ref(0)
|
||||
const headerHeight = ref(44) // 默认头部高度
|
||||
const primaryCategories = ref<any[]>([])
|
||||
const productList = ref<any[]>([])
|
||||
const activePrimary = ref<string>('cold')
|
||||
@@ -126,6 +147,8 @@ const mockProducts = {
|
||||
cold: [
|
||||
{
|
||||
id: 'cold1',
|
||||
shopId: 'shop_001',
|
||||
shopName: '修正药业官方旗舰店',
|
||||
name: '布洛芬缓释胶囊',
|
||||
specification: '0.3g*24粒',
|
||||
price: 18.5,
|
||||
@@ -137,6 +160,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'cold2',
|
||||
shopId: 'shop_002',
|
||||
shopName: '白云山大药房',
|
||||
name: '板蓝根颗粒',
|
||||
specification: '10g*20袋',
|
||||
price: 22.8,
|
||||
@@ -148,6 +173,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'cold3',
|
||||
shopId: 'shop_003',
|
||||
shopName: '以岭药业自营店',
|
||||
name: '连花清瘟胶囊',
|
||||
specification: '0.35g*36粒',
|
||||
price: 42.8,
|
||||
@@ -159,6 +186,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'cold4',
|
||||
shopId: 'shop_004',
|
||||
shopName: '强生制药旗舰店',
|
||||
name: '对乙酰氨基酚片',
|
||||
specification: '0.5g*12片',
|
||||
price: 8.9,
|
||||
@@ -170,6 +199,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'cold5',
|
||||
shopId: 'shop_005',
|
||||
shopName: '同仁堂大药房',
|
||||
name: '感冒清热颗粒',
|
||||
specification: '3g*10袋',
|
||||
price: 16.5,
|
||||
@@ -181,6 +212,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'cold6',
|
||||
shopId: 'shop_006',
|
||||
shopName: '三九医药旗舰店',
|
||||
name: '复方氨酚烷胺片',
|
||||
specification: '10片/盒',
|
||||
price: 12.8,
|
||||
@@ -195,6 +228,8 @@ const mockProducts = {
|
||||
stomach: [
|
||||
{
|
||||
id: 'stomach1',
|
||||
shopId: 'shop_006',
|
||||
shopName: '三九医药旗舰店',
|
||||
name: '胃康灵胶囊',
|
||||
specification: '0.4g*24粒',
|
||||
price: 32.8,
|
||||
@@ -206,6 +241,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'stomach2',
|
||||
shopId: 'shop_007',
|
||||
shopName: '阿斯利康医药',
|
||||
name: '奥美拉唑肠溶胶囊',
|
||||
specification: '20mg*14粒',
|
||||
price: 28.5,
|
||||
@@ -217,6 +254,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'stomach3',
|
||||
shopId: 'shop_008',
|
||||
shopName: '江中制药旗舰店',
|
||||
name: '健胃消食片',
|
||||
specification: '0.8g*32片',
|
||||
price: 15.9,
|
||||
@@ -228,6 +267,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'stomach4',
|
||||
shopId: 'shop_009',
|
||||
shopName: '益普生大药房',
|
||||
name: '蒙脱石散',
|
||||
specification: '3g*10袋',
|
||||
price: 18.6,
|
||||
@@ -239,6 +280,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'stomach5',
|
||||
shopId: 'shop_010',
|
||||
shopName: '西安杨森旗舰店',
|
||||
name: '多潘立酮片',
|
||||
specification: '10mg*30片',
|
||||
price: 22.8,
|
||||
@@ -250,6 +293,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'stomach6',
|
||||
shopId: 'shop_011',
|
||||
shopName: '拜耳医药自营店',
|
||||
name: '铝碳酸镁咀嚼片',
|
||||
specification: '0.5g*20片',
|
||||
price: 25.9,
|
||||
@@ -264,6 +309,8 @@ const mockProducts = {
|
||||
pain: [
|
||||
{
|
||||
id: 'pain1',
|
||||
shopId: 'shop_012',
|
||||
shopName: '华北制药旗舰店',
|
||||
name: '阿莫西林胶囊',
|
||||
specification: '0.25g*24粒',
|
||||
price: 28.5,
|
||||
@@ -275,6 +322,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'pain2',
|
||||
shopId: 'shop_013',
|
||||
shopName: '诺华制药旗舰店',
|
||||
name: '双氯芬酸钠缓释片',
|
||||
specification: '75mg*10片',
|
||||
price: 19.8,
|
||||
@@ -286,6 +335,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'pain3',
|
||||
shopId: 'shop_014',
|
||||
shopName: '云南白药旗舰店',
|
||||
name: '云南白药胶囊',
|
||||
specification: '0.25g*32粒',
|
||||
price: 35.9,
|
||||
@@ -297,6 +348,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'pain4',
|
||||
shopId: 'shop_015',
|
||||
shopName: '辉瑞医药旗舰店',
|
||||
name: '塞来昔布胶囊',
|
||||
specification: '0.2g*10粒',
|
||||
price: 48.6,
|
||||
@@ -308,6 +361,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'pain5',
|
||||
shopId: 'shop_016',
|
||||
shopName: '中美史克大药房',
|
||||
name: '布洛芬片',
|
||||
specification: '0.1g*24片',
|
||||
price: 12.5,
|
||||
@@ -319,6 +374,8 @@ const mockProducts = {
|
||||
},
|
||||
{
|
||||
id: 'pain6',
|
||||
shopId: 'shop_002',
|
||||
shopName: '白云山大药房',
|
||||
name: '头孢克肟胶囊',
|
||||
specification: '0.1g*6粒',
|
||||
price: 32.8,
|
||||
@@ -344,6 +401,8 @@ const generateDefaultProducts = (categoryId: string) => {
|
||||
|
||||
return baseProducts.map((product, index) => ({
|
||||
id: `${categoryId}${index + 1}`,
|
||||
shopId: `shop_default_${categoryId}_${index}`, // 确保不同分类店铺ID不同
|
||||
shopName: '平台自营大药房',
|
||||
...product,
|
||||
specification: '规格待定',
|
||||
originalPrice: product.price * 1.2,
|
||||
@@ -419,8 +478,15 @@ onLoad((options: any) => {
|
||||
}, 100)
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ onLoad中未找到分类参数')
|
||||
console.log('保持当前分类显示:', activePrimary.value)
|
||||
console.log('⚠️ onLoad中未找到分类参数,使用默认分类')
|
||||
// 默认选中第一个分类
|
||||
const defaultCategory = 'cold'
|
||||
console.log('默认分类:', defaultCategory)
|
||||
|
||||
// 无论如何都重新加载一次默认分类的数据
|
||||
setTimeout(() => {
|
||||
selectPrimaryCategory(defaultCategory)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
console.log('=== category页面onLoad执行完成 ===')
|
||||
@@ -505,6 +571,10 @@ const initPage = () => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||
|
||||
// 保持与主页一致的固定高度计算,不进行动态调整
|
||||
// 这样在移动端会与主页的视觉体验保持一致(主页占位符固定为44px)
|
||||
headerHeight.value = 10
|
||||
|
||||
// 获取页面参数
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0) {
|
||||
@@ -585,6 +655,41 @@ const selectPrimaryCategory = (categoryId: string) => {
|
||||
|
||||
// 添加到购物车
|
||||
const addToCart = (product: any) => {
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let cartItems: any[] = []
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商品是否已存在
|
||||
const existingItem = cartItems.find((item: any) => item.id === product.id)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity++
|
||||
} else {
|
||||
// 添加新商品
|
||||
cartItems.push({
|
||||
id: product.id,
|
||||
shopId: product.shopId || 'shop_default',
|
||||
shopName: product.shopName || product.manufacturer || '自营店铺',
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
spec: product.specification || '默认规格',
|
||||
quantity: 1,
|
||||
selected: true
|
||||
})
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
@@ -597,7 +702,48 @@ const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/searc
|
||||
const navigateToCart = () => uni.navigateTo({ url: '/pages/medicine/cart' })
|
||||
const navigateToProduct = (product: any) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/medicine/detail?id=${product.id}`
|
||||
url: `/pages/mall/consumer/product-detail?productId=${product.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 相机功能
|
||||
const onCamera = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['camera'],
|
||||
success: (res) => {
|
||||
console.log('相机拍摄成功:', res.tempFilePaths[0])
|
||||
uni.showToast({
|
||||
title: '已拍摄,正在识别...',
|
||||
icon: 'loading'
|
||||
})
|
||||
// 这里可以添加后续的识别逻辑
|
||||
setTimeout(() => {
|
||||
uni.showToast({
|
||||
title: '识别成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('相机调用失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 扫码功能
|
||||
const onScan = () => {
|
||||
uni.scanCode({
|
||||
success: (res) => {
|
||||
console.log('扫码成功:', res)
|
||||
uni.showToast({
|
||||
title: '扫码成功: ' + res.result,
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('扫码失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -611,7 +757,6 @@ const navigateToProduct = (product: any) => {
|
||||
flex-direction: column;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
position: fixed;
|
||||
@@ -623,8 +768,10 @@ const navigateToProduct = (product: any) => {
|
||||
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
/* 导航栏搜索框容器内边距调整 */
|
||||
.search-container {
|
||||
height: 60px;
|
||||
height: 44px; /* 调整为与消息页一致的高度 */
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -634,44 +781,90 @@ const navigateToProduct = (product: any) => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 搜索框 hover 效果 */
|
||||
.search-box:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 导航栏搜索框容器内边距调整 */
|
||||
.search-box {
|
||||
flex: 1;
|
||||
max-width: 600px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 25px;
|
||||
padding: 10px 20px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 20px;
|
||||
padding: 0 4px 0 12px;
|
||||
display: flex;
|
||||
flex-direction: row; /* UVUE 显式设置 row */
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.search-box:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 18px;
|
||||
color: #4CAF50;
|
||||
margin-right: 12px;
|
||||
width: 100%;
|
||||
height: 32px; /* 减小高度,与顶部高度44px适配,略小于顶部高度 */
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
font-size: 15px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nav-inner-search-text {
|
||||
font-size: 12px; /* 字体稍微变小 */
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 22px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-icon-btn {
|
||||
padding: 4px 8px 4px 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid #ddd;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.nav-camera-btn {
|
||||
padding: 4px 8px 4px 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right-width: 1px;
|
||||
border-right-style: solid;
|
||||
border-right-color: #ddd;
|
||||
border-right: 1px solid #ddd; /* 修复UVUE样式 */
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.nav-camera-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* 搜索按钮高度微调 */
|
||||
.nav-inner-search-btn {
|
||||
padding: 0 12px; /* 减小内边距 */
|
||||
background-color: #87CEEB; /* 天空蓝 */
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px; /* 随搜索框高度减小而减小 */
|
||||
}
|
||||
|
||||
.cart-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
@@ -693,14 +886,14 @@ const navigateToProduct = (product: any) => {
|
||||
.category-content {
|
||||
display: flex;
|
||||
flex-direction: row; /* 强制水平排列 */
|
||||
margin-top: 60px;
|
||||
/* margin-top: 44px; 已通过 style 动态绑定 */
|
||||
padding: 0 16px;
|
||||
max-width: 1400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
height: calc(100vh - 60px); /* 设置固定高度,减去头部高度 */
|
||||
/* height: calc(100vh - 44px); 已通过 style 动态绑定 */
|
||||
overflow: hidden; /* 防止整体滚动 */
|
||||
}
|
||||
|
||||
@@ -996,11 +1189,59 @@ const navigateToProduct = (product: any) => {
|
||||
}
|
||||
|
||||
.product-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, 1fr); /* 改为双列显示 */
|
||||
gap: 8px;
|
||||
padding: 0 4px 20px 4px; /* 增加底部内边距 */
|
||||
}
|
||||
|
||||
/* 手机端商品卡片极简模式 - 仿照主页样式 */
|
||||
.product-spec,
|
||||
.manufacturer,
|
||||
.original-price,
|
||||
.sales-info,
|
||||
.product-badge { /* 分类页也隐藏角标,保持整洁 */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
height: 100px; /* 由于分类页右侧空间更窄,图片高度设得更小一点 */
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.price-section {
|
||||
margin-bottom: 0;
|
||||
margin-top: 4px;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price-symbol {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.product-meta {
|
||||
display: none; /* 隐藏整个元数据行 */
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: 0 12px;
|
||||
height: 55px;
|
||||
@@ -1175,4 +1416,4 @@ const navigateToProduct = (product: any) => {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,590 +1,149 @@
|
||||
<!-- 优惠券页面 -->
|
||||
<template>
|
||||
<view class="coupons-page">
|
||||
<!-- 顶部栏 -->
|
||||
<view class="coupons-header">
|
||||
<view class="header-tabs">
|
||||
<view :class="['header-tab', { active: activeTab === 'available' }]" @click="changeTab('available')">
|
||||
<text class="tab-text">可用券</text>
|
||||
</view>
|
||||
<view :class="['header-tab', { active: activeTab === 'unavailable' }]" @click="changeTab('unavailable')">
|
||||
<text class="tab-text">已失效</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 优惠券列表 -->
|
||||
<scroll-view class="coupons-list" scroll-y>
|
||||
<!-- 为空提示 -->
|
||||
<view v-if="coupons.length === 0" class="empty-coupons">
|
||||
<text class="empty-icon">🎫</text>
|
||||
<text class="empty-text">{{ getEmptyText() }}</text>
|
||||
<text class="empty-subtext">{{ getEmptySubtext() }}</text>
|
||||
<button v-if="activeTab === 'available'" class="get-coupons-btn" @click="goToCouponCenter">
|
||||
去领券中心
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 优惠券项 -->
|
||||
<view v-for="coupon in coupons" :key="coupon.id" class="coupon-item">
|
||||
<view class="coupon-left">
|
||||
<text class="coupon-value">{{ formatCouponValue(coupon) }}</text>
|
||||
<text class="coupon-condition">{{ formatCondition(coupon) }}</text>
|
||||
</view>
|
||||
<view class="coupon-right">
|
||||
<view class="coupon-info">
|
||||
<text class="coupon-name">{{ coupon.template?.name || coupon.name }}</text>
|
||||
<text class="coupon-desc">{{ coupon.template?.description || '' }}</text>
|
||||
<text class="coupon-time">{{ formatTimeRange(coupon) }}</text>
|
||||
</view>
|
||||
<view class="coupon-actions">
|
||||
<button v-if="coupon.status === 1 && coupon.is_valid"
|
||||
class="use-btn"
|
||||
@click="useCoupon(coupon)">
|
||||
立即使用
|
||||
</button>
|
||||
<button v-else-if="coupon.template_id && activeTab === 'available'"
|
||||
class="receive-btn"
|
||||
@click="receiveCoupon(coupon)">
|
||||
领取
|
||||
</button>
|
||||
<text v-else class="status-text">{{ getStatusText(coupon) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="coupons-page">
|
||||
<view class="coupon-list">
|
||||
<view v-if="coupons.length === 0" class="empty-state">
|
||||
<text class="empty-icon">🎫</text>
|
||||
<text class="empty-text">暂无优惠券</text>
|
||||
</view>
|
||||
|
||||
<view v-else v-for="(coupon, index) in coupons" :key="index" class="coupon-item">
|
||||
<view class="coupon-left">
|
||||
<text class="coupon-amount">{{ coupon.amount }}</text>
|
||||
<text class="coupon-type">优惠券</text>
|
||||
</view>
|
||||
<view class="coupon-right">
|
||||
<text class="coupon-title">{{ coupon.title }}</text>
|
||||
<text class="coupon-expiry">有效期至: {{ coupon.expiry }}</text>
|
||||
<button class="use-btn" @click="useCoupon(coupon)">去使用</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
type CouponTemplateType = {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
coupon_type: number
|
||||
discount_value: number
|
||||
min_order_amount: number
|
||||
start_time: string
|
||||
end_time: string
|
||||
per_user_limit: number
|
||||
total_quantity: number
|
||||
used_quantity: number
|
||||
status: number
|
||||
type Coupon = {
|
||||
title: string
|
||||
amount: string
|
||||
expiry: string
|
||||
id: string
|
||||
}
|
||||
|
||||
type UserCouponType = {
|
||||
id: string
|
||||
user_id: string
|
||||
template_id: string
|
||||
coupon_code: string
|
||||
status: number
|
||||
is_valid: boolean
|
||||
used_at: string | null
|
||||
expire_at: string
|
||||
created_at: string
|
||||
template: CouponTemplateType | null
|
||||
}
|
||||
const coupons = ref<Coupon[]>([])
|
||||
|
||||
const activeTab = ref<string>('available')
|
||||
const coupons = ref<Array<any>>([])
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
// 监听标签页变化
|
||||
watch(activeTab, () => {
|
||||
loadCoupons()
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadCoupons()
|
||||
loadCoupons()
|
||||
})
|
||||
|
||||
// 加载优惠券
|
||||
const loadCoupons = async () => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
if (activeTab.value === 'available') {
|
||||
await loadAvailableCoupons(userId)
|
||||
} else {
|
||||
await loadUnavailableCoupons(userId)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载优惠券异常:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
const loadCoupons = () => {
|
||||
// 从本地存储获取已领取的优惠券详情
|
||||
// 假设存储格式为 JSON 字符串数组
|
||||
const storedCoupons = uni.getStorageSync('myCoupons')
|
||||
if (storedCoupons) {
|
||||
try {
|
||||
coupons.value = JSON.parse(storedCoupons as string) as Coupon[]
|
||||
} catch (e) {
|
||||
console.error('Failed to parse coupons', e)
|
||||
coupons.value = []
|
||||
}
|
||||
} else {
|
||||
// 默认空或者是mock一些基础数据如果需要
|
||||
coupons.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 加载可用优惠券
|
||||
const loadAvailableCoupons = async (userId: string) => {
|
||||
try {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
// 加载用户已领取的优惠券
|
||||
const { data: userCoupons, error: userError } = await supa
|
||||
.from('user_coupons')
|
||||
.select(`
|
||||
*,
|
||||
template:coupon_templates(*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.eq('status', 1)
|
||||
.gte('expire_at', now)
|
||||
.order('expire_at', { ascending: true })
|
||||
|
||||
if (userError !== null) {
|
||||
console.error('加载用户优惠券失败:', userError)
|
||||
return
|
||||
}
|
||||
|
||||
// 加载可领取的优惠券
|
||||
const { data: availableCoupons, error: availableError } = await supa
|
||||
.from('coupon_templates')
|
||||
.select('*')
|
||||
.eq('status', 1)
|
||||
.gte('end_time', now)
|
||||
.lte('start_time', now)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (availableError !== null) {
|
||||
console.error('加载可领取优惠券失败:', availableError)
|
||||
return
|
||||
}
|
||||
|
||||
// 合并结果
|
||||
const allCoupons = [...(userCoupons || []), ...(availableCoupons || [])]
|
||||
coupons.value = allCoupons
|
||||
} catch (err) {
|
||||
console.error('加载可用优惠券异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载不可用优惠券
|
||||
const loadUnavailableCoupons = async (userId: string) => {
|
||||
try {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const { data, error } = await supa
|
||||
.from('user_coupons')
|
||||
.select(`
|
||||
*,
|
||||
template:coupon_templates(*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.or('status.eq.2,expire_at.lt.' + now)
|
||||
.order('used_at', { ascending: false })
|
||||
.order('expire_at', { ascending: false })
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载失效优惠券失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
coupons.value = data ?? []
|
||||
} catch (err) {
|
||||
console.error('加载失效优惠券异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string | null => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore?.id || null
|
||||
}
|
||||
|
||||
// 获取空状态文本
|
||||
const getEmptyText = (): string => {
|
||||
return activeTab.value === 'available' ? '暂无可用优惠券' : '暂无失效优惠券'
|
||||
}
|
||||
|
||||
// 获取空状态副文本
|
||||
const getEmptySubtext = (): string => {
|
||||
return activeTab.value === 'available' ? '去领券中心看看' : '努力使用优惠券吧'
|
||||
}
|
||||
|
||||
// 格式化优惠券价值
|
||||
const formatCouponValue = (coupon: any): string => {
|
||||
if (coupon.template_id) {
|
||||
// 用户优惠券
|
||||
const template = coupon.template
|
||||
if (!template) return '未知'
|
||||
|
||||
if (template.coupon_type === 1) {
|
||||
return `¥${template.discount_value}`
|
||||
} else if (template.coupon_type === 2) {
|
||||
return `${template.discount_value}折`
|
||||
} else {
|
||||
return '未知'
|
||||
}
|
||||
} else {
|
||||
// 优惠券模板
|
||||
if (coupon.coupon_type === 1) {
|
||||
return `¥${coupon.discount_value}`
|
||||
} else if (coupon.coupon_type === 2) {
|
||||
return `${coupon.discount_value}折`
|
||||
} else {
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化使用条件
|
||||
const formatCondition = (coupon: any): string => {
|
||||
const minAmount = coupon.template?.min_order_amount || coupon.min_order_amount
|
||||
if (minAmount > 0) {
|
||||
return `满${minAmount}元可用`
|
||||
}
|
||||
return '无门槛'
|
||||
}
|
||||
|
||||
// 格式化时间范围
|
||||
const formatTimeRange = (coupon: any): string => {
|
||||
const startTime = coupon.template?.start_time || coupon.start_time
|
||||
const endTime = coupon.template?.end_time || coupon.expire_at
|
||||
|
||||
if (startTime && endTime) {
|
||||
const start = new Date(startTime)
|
||||
const end = new Date(endTime)
|
||||
|
||||
const startStr = `${start.getMonth() + 1}月${start.getDate()}日`
|
||||
const endStr = `${end.getMonth() + 1}月${end.getDate()}日`
|
||||
|
||||
return `${startStr}-${endStr}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (coupon: any): string => {
|
||||
if (coupon.status === 2) {
|
||||
return '已使用'
|
||||
} else if (!coupon.is_valid) {
|
||||
return '已失效'
|
||||
} else if (new Date(coupon.expire_at) < new Date()) {
|
||||
return '已过期'
|
||||
}
|
||||
return '未知'
|
||||
}
|
||||
|
||||
// 切换标签页
|
||||
const changeTab = (tab: string) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
// 使用优惠券
|
||||
const useCoupon = (coupon: any) => {
|
||||
// 如果是从订单页面跳转过来的,返回选择的优惠券
|
||||
const pages = getCurrentPages()
|
||||
const prevPage = pages[pages.length - 2]
|
||||
|
||||
if (prevPage && prevPage.route === 'pages/mall/consumer/checkout') {
|
||||
uni.$emit('couponSelected', coupon)
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
|
||||
// 否则跳转到商品列表页
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/index'
|
||||
})
|
||||
}
|
||||
|
||||
// 领取优惠券
|
||||
const receiveCoupon = async (coupon: any) => {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
// 检查是否已领取
|
||||
const { data: existingCoupons, error: checkError } = await supa
|
||||
.from('user_coupons')
|
||||
.select('id')
|
||||
.eq('user_id', userId)
|
||||
.eq('template_id', coupon.id)
|
||||
|
||||
if (checkError !== null) {
|
||||
console.error('检查优惠券失败:', checkError)
|
||||
return
|
||||
}
|
||||
|
||||
if (existingCoupons && existingCoupons.length >= coupon.per_user_limit) {
|
||||
uni.showToast({
|
||||
title: '已达领取上限',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
if (coupon.used_quantity >= coupon.total_quantity) {
|
||||
uni.showToast({
|
||||
title: '优惠券已领完',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成优惠券码
|
||||
const couponCode = generateCouponCode()
|
||||
const expireAt = coupon.end_time
|
||||
|
||||
try {
|
||||
const { error: insertError } = await supa
|
||||
.from('user_coupons')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
template_id: coupon.id,
|
||||
coupon_code: couponCode,
|
||||
expire_at: expireAt,
|
||||
status: 1,
|
||||
is_valid: true
|
||||
})
|
||||
|
||||
if (insertError !== null) {
|
||||
console.error('领取优惠券失败:', insertError)
|
||||
uni.showToast({
|
||||
title: '领取失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新优惠券模板的已领取数量
|
||||
const { error: updateError } = await supa
|
||||
.from('coupon_templates')
|
||||
.update({ used_quantity: coupon.used_quantity + 1 })
|
||||
.eq('id', coupon.id)
|
||||
|
||||
if (updateError !== null) {
|
||||
console.error('更新优惠券数量失败:', updateError)
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '领取成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重新加载数据
|
||||
loadCoupons()
|
||||
} catch (err) {
|
||||
console.error('领取优惠券异常:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成优惠券码
|
||||
const generateCouponCode = (): string => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
let result = ''
|
||||
for (let i = 0; i < 12; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 跳转到领券中心
|
||||
const goToCouponCenter = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/coupon-center'
|
||||
})
|
||||
const useCoupon = (coupon: Coupon) => {
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/index'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
.coupons-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.coupons-header {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.header-tabs {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header-tab {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-tab.active {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.header-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #007aff;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.header-tab.active .tab-text {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.coupons-list {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.empty-coupons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.get-coupons-btn {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
padding: 10px 40px;
|
||||
border-radius: 25px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.coupon-item {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.coupon-left {
|
||||
width: 100px;
|
||||
background: linear-gradient(135deg, #ff6b6b, #ffa726);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 10px;
|
||||
color: #ffffff;
|
||||
width: 100px;
|
||||
background: linear-gradient(135deg, #FF9800, #FF5722);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.coupon-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
.coupon-amount {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.coupon-condition {
|
||||
font-size: 11px;
|
||||
opacity: 0.9;
|
||||
.coupon-type {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.coupon-right {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.coupon-info {
|
||||
flex: 1;
|
||||
.coupon-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.coupon-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.coupon-desc {
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.coupon-time {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.coupon-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.use-btn,
|
||||
.receive-btn {
|
||||
padding: 8px 20px;
|
||||
border-radius: 15px;
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
.coupon-expiry {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.use-btn {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.receive-btn {
|
||||
background-color: #ff4757;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
font-style: italic;
|
||||
align-self: flex-end;
|
||||
font-size: 12px;
|
||||
background-color: #FF5722;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,694 +1,269 @@
|
||||
<!-- 收藏页面 -->
|
||||
<template>
|
||||
<view class="favorites-page">
|
||||
<!-- 顶部栏 -->
|
||||
<view class="favorites-header">
|
||||
<view class="header-title">
|
||||
<text class="title-text">我的收藏</text>
|
||||
</view>
|
||||
<view v-if="favorites.length > 0" class="edit-btn" @click="toggleEditMode">
|
||||
<text class="edit-text">{{ isEditMode ? '完成' : '编辑' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<view class="favorites-tabs">
|
||||
<view :class="['favorites-tab', { active: activeTab === 'product' }]" @click="changeTab('product')">
|
||||
<text class="tab-text">商品</text>
|
||||
</view>
|
||||
<view :class="['favorites-tab', { active: activeTab === 'shop' }]" @click="changeTab('shop')">
|
||||
<text class="tab-text">店铺</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收藏内容 -->
|
||||
<scroll-view class="favorites-content" scroll-y @scrolltolower="loadMore">
|
||||
<!-- 空状态 -->
|
||||
<view v-if="favorites.length === 0 && !isLoading" class="empty-favorites">
|
||||
<text class="empty-icon">❤️</text>
|
||||
<text class="empty-text">{{ getEmptyText() }}</text>
|
||||
<text class="empty-subtext">{{ getEmptySubtext() }}</text>
|
||||
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
|
||||
</view>
|
||||
|
||||
<!-- 商品收藏 -->
|
||||
<view v-if="activeTab === 'product'" class="product-favorites">
|
||||
<view v-for="item in favorites" :key="item.id" class="product-item">
|
||||
<view v-if="isEditMode" class="item-selector" @click="toggleSelect(item)">
|
||||
<view :class="['select-icon', { selected: item.selected }]">
|
||||
<text v-if="item.selected" class="icon-text">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-content" @click="viewProduct(item)">
|
||||
<image class="product-image" :src="getProductImage(item)" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ item.product?.name || '商品已下架' }}</text>
|
||||
<text class="product-price">¥{{ item.product?.price || 0 }}</text>
|
||||
<view class="product-meta">
|
||||
<text class="meta-text">收藏时间: {{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 店铺收藏 -->
|
||||
<view v-if="activeTab === 'shop'" class="shop-favorites">
|
||||
<view v-for="item in favorites" :key="item.id" class="shop-item">
|
||||
<view v-if="isEditMode" class="item-selector" @click="toggleSelect(item)">
|
||||
<view :class="['select-icon', { selected: item.selected }]">
|
||||
<text v-if="item.selected" class="icon-text">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-content" @click="viewShop(item)">
|
||||
<image class="shop-logo" :src="item.shop?.shop_logo || '/static/default-shop.png'" />
|
||||
<view class="shop-info">
|
||||
<text class="shop-name">{{ item.shop?.shop_name || '店铺已关闭' }}</text>
|
||||
<view class="shop-rating">
|
||||
<text class="rating-text">评分: {{ item.shop?.rating?.toFixed(1) || '0.0' }}</text>
|
||||
<text class="sales-text">销量: {{ item.shop?.total_sales || 0 }}</text>
|
||||
</view>
|
||||
<view class="shop-meta">
|
||||
<text class="meta-text">收藏时间: {{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="isLoading" class="loading-more">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view v-if="!hasMore && favorites.length > 0" class="no-more">
|
||||
<text class="no-more-text">没有更多了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 编辑操作栏 -->
|
||||
<view v-if="isEditMode && favorites.length > 0" class="edit-bar">
|
||||
<view class="select-all" @click="toggleSelectAll">
|
||||
<view :class="['all-select-icon', { selected: isAllSelected }]">
|
||||
<text v-if="isAllSelected" class="icon-text">✓</text>
|
||||
</view>
|
||||
<text class="select-all-text">全选</text>
|
||||
</view>
|
||||
<view class="delete-btn" @click="deleteSelected">
|
||||
<text class="delete-text">删除({{ selectedCount }})</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="favorites-page">
|
||||
<view class="product-grid">
|
||||
<view v-if="favorites.length === 0" class="empty-state">
|
||||
<text class="empty-icon">❤️</text>
|
||||
<text class="empty-text">暂无收藏商品</text>
|
||||
<button class="go-shopping-btn" @click="goShopping">去逛逛</button>
|
||||
</view>
|
||||
|
||||
<view v-else v-for="(product, index) in favorites" :key="index" class="product-item" @click="goToDetail(product.id)">
|
||||
<image :src="product.image" class="product-image" mode="aspectFill" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<text class="product-price">¥{{ product.price }}</text>
|
||||
<view class="product-footer">
|
||||
<text class="product-sales">已售 {{ product.sales }}</text>
|
||||
<view class="action-btns">
|
||||
<view class="cart-btn" @click.stop="addToCart(product)">
|
||||
<text class="cart-icon">🛒</text>
|
||||
</view>
|
||||
<view class="remove-btn" @click.stop="removeFavorite(product.id)">
|
||||
<text class="remove-icon">🗑️</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
type FavoriteType = {
|
||||
id: string
|
||||
user_id: string
|
||||
product_id: string | null
|
||||
merchant_id: string | null
|
||||
type: string // 'product' | 'shop'
|
||||
created_at: string
|
||||
selected?: boolean
|
||||
product?: {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
images: string[]
|
||||
status: number
|
||||
}
|
||||
shop?: {
|
||||
id: string
|
||||
shop_name: string
|
||||
shop_logo: string
|
||||
rating: number
|
||||
total_sales: number
|
||||
shop_status: number
|
||||
}
|
||||
type Product = {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
image: string
|
||||
sales: number
|
||||
shopId?: string
|
||||
shopName?: string
|
||||
}
|
||||
|
||||
const activeTab = ref<string>('product')
|
||||
const favorites = ref<Array<FavoriteType>>([])
|
||||
const isEditMode = ref<boolean>(false)
|
||||
const isLoading = ref<boolean>(false)
|
||||
const currentPage = ref<number>(1)
|
||||
const pageSize = ref<number>(20)
|
||||
const hasMore = ref<boolean>(true)
|
||||
const favorites = ref<Product[]>([])
|
||||
|
||||
// 计算属性
|
||||
const selectedCount = computed(() => {
|
||||
return favorites.value.filter(item => item.selected).length
|
||||
})
|
||||
|
||||
const isAllSelected = computed(() => {
|
||||
return favorites.value.length > 0 && favorites.value.every(item => item.selected)
|
||||
})
|
||||
|
||||
// 监听标签页变化
|
||||
watch(activeTab, () => {
|
||||
resetData()
|
||||
loadFavorites()
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadFavorites()
|
||||
loadFavorites()
|
||||
})
|
||||
|
||||
// 重置数据
|
||||
const resetData = () => {
|
||||
favorites.value = []
|
||||
currentPage.value = 1
|
||||
hasMore.value = true
|
||||
isEditMode.value = false
|
||||
const addToCart = (product: Product) => {
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let cartItems: any[] = []
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商品是否已存在
|
||||
const existingItem = cartItems.find((item: any) => item.id === product.id)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity++
|
||||
} else {
|
||||
// 添加新商品
|
||||
cartItems.push({
|
||||
id: product.id,
|
||||
shopId: product.shopId || 'shop_favorite_default',
|
||||
shopName: product.shopName || '收藏店铺',
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
spec: '默认规格',
|
||||
quantity: 1,
|
||||
selected: true
|
||||
})
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
// 加载收藏数据
|
||||
const loadFavorites = async (loadMore: boolean = false) => {
|
||||
if (isLoading.value || (!hasMore.value && loadMore)) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const page = loadMore ? currentPage.value + 1 : 1
|
||||
|
||||
let query = supa
|
||||
.from('user_favorites')
|
||||
.select(`
|
||||
*,
|
||||
product:product_id(*),
|
||||
shop:merchant_id(*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.eq('type', activeTab.value)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
// 分页
|
||||
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载收藏失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
const newFavorites = (data || []).map((item: any) => ({
|
||||
...item,
|
||||
selected: false
|
||||
}))
|
||||
|
||||
if (loadMore) {
|
||||
favorites.value.push(...newFavorites)
|
||||
currentPage.value = page
|
||||
} else {
|
||||
favorites.value = newFavorites
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
hasMore.value = newFavorites.length === pageSize.value
|
||||
} catch (err) {
|
||||
console.error('加载收藏异常:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
const loadFavorites = () => {
|
||||
// 从本地存储获取收藏列表
|
||||
const storedFavorites = uni.getStorageSync('favorites')
|
||||
if (storedFavorites) {
|
||||
try {
|
||||
favorites.value = JSON.parse(storedFavorites as string) as Product[]
|
||||
} catch (e) {
|
||||
console.error('Failed to parse favorites', e)
|
||||
favorites.value = []
|
||||
}
|
||||
} else {
|
||||
favorites.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore?.id || ''
|
||||
}
|
||||
|
||||
// 获取商品图片
|
||||
const getProductImage = (item: FavoriteType): string => {
|
||||
if (!item.product?.images?.[0]) {
|
||||
return '/static/default-product.png'
|
||||
}
|
||||
return item.product.images[0]
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string): string => {
|
||||
const date = new Date(timeStr)
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
return `${month}-${day}`
|
||||
}
|
||||
|
||||
// 获取空状态文本
|
||||
const getEmptyText = (): string => {
|
||||
return activeTab.value === 'product' ? '暂无商品收藏' : '暂无店铺收藏'
|
||||
}
|
||||
|
||||
// 获取空状态副文本
|
||||
const getEmptySubtext = (): string => {
|
||||
return activeTab.value === 'product' ? '快去收藏喜欢的商品吧' : '快去收藏喜欢的店铺吧'
|
||||
}
|
||||
|
||||
// 切换编辑模式
|
||||
const toggleEditMode = () => {
|
||||
isEditMode.value = !isEditMode.value
|
||||
// 重置选择状态
|
||||
favorites.value.forEach(item => {
|
||||
item.selected = false
|
||||
})
|
||||
}
|
||||
|
||||
// 切换标签页
|
||||
const changeTab = (tab: string) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
// 切换选择状态
|
||||
const toggleSelect = (item: FavoriteType) => {
|
||||
item.selected = !item.selected
|
||||
favorites.value = [...favorites.value]
|
||||
}
|
||||
|
||||
// 全选/取消全选
|
||||
const toggleSelectAll = () => {
|
||||
const newSelectedState = !isAllSelected.value
|
||||
favorites.value.forEach(item => {
|
||||
item.selected = newSelectedState
|
||||
})
|
||||
favorites.value = [...favorites.value]
|
||||
}
|
||||
|
||||
// 删除选中项
|
||||
const deleteSelected = async () => {
|
||||
const selectedItems = favorites.value.filter(item => item.selected)
|
||||
if (selectedItems.length === 0) {
|
||||
uni.showToast({
|
||||
title: '请选择要删除的收藏',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除选中的${selectedItems.length}个收藏吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const ids = selectedItems.map(item => item.id)
|
||||
|
||||
const { error } = await supa
|
||||
.from('user_favorites')
|
||||
.delete()
|
||||
.in('id', ids)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// 从列表中移除
|
||||
favorites.value = favorites.value.filter(item => !item.selected)
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 如果删完了,退出编辑模式
|
||||
if (favorites.value.length === 0) {
|
||||
isEditMode.value = false
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('删除收藏失败:', err)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查看商品
|
||||
const viewProduct = (item: FavoriteType) => {
|
||||
if (isEditMode.value) return
|
||||
|
||||
if (!item.product_id || !item.product?.status) {
|
||||
uni.showToast({
|
||||
title: '商品已下架',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?id=${item.product_id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 查看店铺
|
||||
const viewShop = (item: FavoriteType) => {
|
||||
if (isEditMode.value) return
|
||||
|
||||
if (!item.merchant_id || !item.shop?.shop_status) {
|
||||
uni.showToast({
|
||||
title: '店铺已关闭',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/shop-detail?id=${item.merchant_id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !isLoading.value) {
|
||||
loadFavorites(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 去逛逛
|
||||
const goShopping = () => {
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/index'
|
||||
})
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/index'
|
||||
})
|
||||
}
|
||||
|
||||
const goToDetail = (id: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?productId=${id}`
|
||||
})
|
||||
}
|
||||
|
||||
const removeFavorite = (id: string) => {
|
||||
uni.showModal({
|
||||
title: '取消收藏',
|
||||
content: '确定要取消收藏该商品吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const index = favorites.value.findIndex(item => item.id === id)
|
||||
if (index !== -1) {
|
||||
favorites.value.splice(index, 1)
|
||||
uni.setStorageSync('favorites', JSON.stringify(favorites.value))
|
||||
uni.showToast({
|
||||
title: '已取消收藏',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
.favorites-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.favorites-header {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.product-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.edit-text {
|
||||
color: #007aff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.favorites-tabs {
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.favorites-tab {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.favorites-tab.active {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.favorites-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #007aff;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.favorites-tab.active .tab-text {
|
||||
color: #007aff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.favorites-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-favorites {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
background-color: #ffffff;
|
||||
.empty-state {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.empty-subtext {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
margin-bottom: 30px;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.go-shopping-btn {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
padding: 10px 40px;
|
||||
border-radius: 25px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
background-color: #ff5000;
|
||||
color: white;
|
||||
padding: 8px 24px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.product-favorites,
|
||||
.shop-favorites {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.product-item,
|
||||
.shop-item {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.item-selector {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.select-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.select-icon.selected {
|
||||
background-color: #007aff;
|
||||
border-color: #007aff;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
.product-item {
|
||||
width: calc(50% - 8px);
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 5px;
|
||||
margin-right: 15px;
|
||||
width: 100%;
|
||||
height: 170px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
height: 40px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 16px;
|
||||
color: #ff4757;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.product-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.product-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
.product-sales {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.shop-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
margin-right: 15px;
|
||||
.action-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shop-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.cart-btn, .remove-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.shop-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
.cart-btn {
|
||||
background-color: #ff5000;
|
||||
}
|
||||
|
||||
.shop-rating {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 10px;
|
||||
.cart-icon {
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rating-text,
|
||||
.sales-text {
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
.remove-btn {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.shop-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-more,
|
||||
.no-more {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.no-more-text {
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.edit-bar {
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.select-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.all-select-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 10px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.all-select-icon.selected {
|
||||
background-color: #007aff;
|
||||
border-color: #007aff;
|
||||
}
|
||||
|
||||
.select-all-text {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: #ff4757;
|
||||
padding: 10px 20px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.delete-text {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
.remove-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -39,17 +39,17 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-content" @click="viewProduct(item)">
|
||||
<image class="product-image" :src="getProductImage(item)" />
|
||||
<image class="product-image" :src="item.image" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ item.product?.name || '商品已下架' }}</text>
|
||||
<text class="product-name">{{ item.name }}</text>
|
||||
<view class="product-price-row">
|
||||
<text class="current-price">¥{{ item.product?.price || 0 }}</text>
|
||||
<text v-if="item.product?.original_price && item.product.original_price > item.product.price"
|
||||
class="original-price">¥{{ item.product.original_price }}</text>
|
||||
<text class="current-price">¥{{ item.price }}</text>
|
||||
<text v-if="item.original_price && item.original_price > item.price"
|
||||
class="original-price">¥{{ item.original_price }}</text>
|
||||
</view>
|
||||
<view class="product-meta">
|
||||
<text class="sales-text">已售{{ item.product?.sales || 0 }}</text>
|
||||
<text class="time-text">{{ formatTime(item.created_at) }}</text>
|
||||
<text class="sales-text">已售{{ item.sales }}</text>
|
||||
<text class="time-text">{{ formatTime(item.viewTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -83,31 +83,24 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type FootprintType = {
|
||||
id: string
|
||||
user_id: string
|
||||
product_id: string
|
||||
created_at: string
|
||||
name: string
|
||||
price: number
|
||||
original_price?: number
|
||||
image: string
|
||||
sales: number
|
||||
shopId: string
|
||||
shopName: string
|
||||
viewTime: number
|
||||
selected?: boolean
|
||||
product?: {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
original_price: number | null
|
||||
images: string[]
|
||||
sales: number
|
||||
status: number
|
||||
}
|
||||
}
|
||||
|
||||
const footprints = ref<Array<FootprintType>>([])
|
||||
const isEditMode = ref<boolean>(false)
|
||||
const isLoading = ref<boolean>(false)
|
||||
const currentPage = ref<number>(1)
|
||||
const pageSize = ref<number>(30)
|
||||
const hasMore = ref<boolean>(true)
|
||||
const hasMore = ref<boolean>(false)
|
||||
|
||||
// 计算属性
|
||||
const selectedCount = computed(() => {
|
||||
@@ -122,7 +115,7 @@ const groupedFootprints = computed(() => {
|
||||
const groups: Record<string, FootprintType[]> = {}
|
||||
|
||||
footprints.value.forEach(item => {
|
||||
const date = item.created_at.split('T')[0]
|
||||
const date = new Date(item.viewTime).toDateString()
|
||||
if (!groups[date]) {
|
||||
groups[date] = []
|
||||
}
|
||||
@@ -138,72 +131,28 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// 加载足迹数据
|
||||
const loadFootprints = async (loadMore: boolean = false) => {
|
||||
if (isLoading.value || (!hasMore.value && loadMore)) {
|
||||
return
|
||||
}
|
||||
|
||||
const loadFootprints = (loadMore: boolean = false) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
return
|
||||
|
||||
// 从本地存储获取足迹数据
|
||||
const storedFootprints = uni.getStorageSync('footprints')
|
||||
if (storedFootprints) {
|
||||
try {
|
||||
const data = JSON.parse(storedFootprints as string) as any[]
|
||||
footprints.value = data.map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}))
|
||||
} catch (e) {
|
||||
console.error('Failed to parse footprints', e)
|
||||
footprints.value = []
|
||||
}
|
||||
|
||||
const page = loadMore ? currentPage.value + 1 : 1
|
||||
|
||||
const { data, error } = await supa
|
||||
.from('user_footprints')
|
||||
.select(`
|
||||
*,
|
||||
product:product_id(*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false })
|
||||
.range((page - 1) * pageSize.value, page * pageSize.value - 1)
|
||||
|
||||
if (error !== null) {
|
||||
console.error('加载足迹失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
const newFootprints = (data || []).map((item: any) => ({
|
||||
...item,
|
||||
selected: false
|
||||
}))
|
||||
|
||||
if (loadMore) {
|
||||
footprints.value.push(...newFootprints)
|
||||
currentPage.value = page
|
||||
} else {
|
||||
footprints.value = newFootprints
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
hasMore.value = newFootprints.length === pageSize.value
|
||||
} catch (err) {
|
||||
console.error('加载足迹异常:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
} else {
|
||||
footprints.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
const getCurrentUserId = (): string => {
|
||||
const userStore = uni.getStorageSync('userInfo')
|
||||
return userStore?.id || ''
|
||||
}
|
||||
|
||||
// 获取商品图片
|
||||
const getProductImage = (item: FootprintType): string => {
|
||||
if (!item.product?.images?.[0]) {
|
||||
return '/static/default-product.png'
|
||||
}
|
||||
return item.product.images[0]
|
||||
|
||||
isLoading.value = false
|
||||
hasMore.value = false // 本地存储一次性加载完
|
||||
}
|
||||
|
||||
// 格式化日期分组
|
||||
@@ -225,8 +174,8 @@ const formatGroupDate = (dateStr: string): string => {
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string): string => {
|
||||
const date = new Date(timeStr)
|
||||
const formatTime = (timestamp: number): string => {
|
||||
const date = new Date(timestamp)
|
||||
const hours = date.getHours().toString().padStart(2, '0')
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${hours}:${minutes}`
|
||||
@@ -248,35 +197,15 @@ const clearAll = () => {
|
||||
uni.showModal({
|
||||
title: '清空足迹',
|
||||
content: '确定要清空所有浏览记录吗?',
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) return
|
||||
|
||||
const { error } = await supa
|
||||
.from('user_footprints')
|
||||
.delete()
|
||||
.eq('user_id', userId)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
footprints.value = []
|
||||
|
||||
uni.showToast({
|
||||
title: '已清空',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
console.error('清空足迹失败:', err)
|
||||
uni.showToast({
|
||||
title: '清空失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
footprints.value = []
|
||||
uni.removeStorageSync('footprints')
|
||||
|
||||
uni.showToast({
|
||||
title: '已清空',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -289,8 +218,8 @@ const toggleSelect = (item: FootprintType) => {
|
||||
}
|
||||
|
||||
// 切换分组全选
|
||||
const toggleGroupSelect = (date: string) => {
|
||||
const group = groupedFootprints.value[date]
|
||||
const toggleGroupSelect = (dateStr: string) => {
|
||||
const group = groupedFootprints.value[dateStr]
|
||||
if (!group) return
|
||||
|
||||
const isAllSelected = group.every(item => item.selected)
|
||||
@@ -304,8 +233,8 @@ const toggleGroupSelect = (date: string) => {
|
||||
}
|
||||
|
||||
// 检查组是否全选
|
||||
const isGroupSelected = (date: string): boolean => {
|
||||
const group = groupedFootprints.value[date]
|
||||
const isGroupSelected = (dateStr: string): boolean => {
|
||||
const group = groupedFootprints.value[dateStr]
|
||||
if (!group || group.length === 0) return false
|
||||
return group.every(item => item.selected)
|
||||
}
|
||||
@@ -320,7 +249,7 @@ const toggleSelectAll = () => {
|
||||
}
|
||||
|
||||
// 删除选中项
|
||||
const deleteSelected = async () => {
|
||||
const deleteSelected = () => {
|
||||
const selectedItems = footprints.value.filter(item => item.selected)
|
||||
if (selectedItems.length === 0) {
|
||||
uni.showToast({
|
||||
@@ -333,39 +262,26 @@ const deleteSelected = async () => {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
|
||||
success: async (res) => {
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const ids = selectedItems.map(item => item.id)
|
||||
|
||||
const { error } = await supa
|
||||
.from('user_footprints')
|
||||
.delete()
|
||||
.in('id', ids)
|
||||
|
||||
if (error !== null) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// 从列表中移除
|
||||
footprints.value = footprints.value.filter(item => !item.selected)
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 如果删完了,退出编辑模式
|
||||
if (footprints.value.length === 0) {
|
||||
isEditMode.value = false
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('删除足迹失败:', err)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
// 从列表中移除
|
||||
footprints.value = footprints.value.filter(item => !item.selected)
|
||||
|
||||
// 保存回本地存储
|
||||
const dataToSave = footprints.value.map(item => {
|
||||
const { selected, ...rest } = item
|
||||
return rest
|
||||
})
|
||||
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 如果删完了,退出编辑模式
|
||||
if (footprints.value.length === 0) {
|
||||
isEditMode.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,24 +292,14 @@ const deleteSelected = async () => {
|
||||
const viewProduct = (item: FootprintType) => {
|
||||
if (isEditMode.value) return
|
||||
|
||||
if (!item.product_id || !item.product?.status) {
|
||||
uni.showToast({
|
||||
title: '商品已下架',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?id=${item.product_id}`
|
||||
url: `/pages/mall/consumer/product-detail?productId=${item.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !isLoading.value) {
|
||||
loadFootprints(true)
|
||||
}
|
||||
// 本地存储模式下暂不需要加载更多逻辑
|
||||
}
|
||||
|
||||
// 去逛逛
|
||||
|
||||
@@ -6,40 +6,34 @@
|
||||
class="smart-navbar"
|
||||
:style="{
|
||||
paddingTop: statusBarHeight + 'px',
|
||||
transform: showNavbar ? 'translateY(0)' : 'translateY(-100%)',
|
||||
opacity: showNavbar ? 1 : 0
|
||||
transform: showNavbar ? 'translateY(0)' : 'translateY(-100%)'
|
||||
}"
|
||||
>
|
||||
<view class="nav-container">
|
||||
<!-- 品牌标识区域 - 响应式显示 -->
|
||||
<view class="brand-section">
|
||||
<!-- 品牌标识 - 在电脑端和平板端显示,小屏幕隐藏 -->
|
||||
<view class="brand-logo-area" :class="{ 'hidden-on-mobile': isMobile }">
|
||||
<view class="brand-info">
|
||||
<text class="brand-name">康乐医药商城</text>
|
||||
<text class="brand-tag">官方认证·正品保障</text>
|
||||
</view>
|
||||
<view class="search-container">
|
||||
<view class="search-box" @click="navigateToSearch" :style="{ height: '30px' }">
|
||||
<!-- 模拟输入框 -->
|
||||
<text class="search-placeholder">请输入药品名称、症状或品牌</text>
|
||||
|
||||
<!-- 扫码图标 -->
|
||||
<view class="nav-icon-btn" @click.stop="onScan">
|
||||
<text class="nav-icon">🔳</text>
|
||||
</view>
|
||||
|
||||
<!-- 相机图标 -->
|
||||
<view class="nav-camera-btn" @click.stop="onCamera">
|
||||
<text class="nav-camera-icon">📷</text>
|
||||
</view>
|
||||
|
||||
<!-- 导航栏内搜索框 - 自适应平铺 -->
|
||||
<view class="nav-search-container">
|
||||
<view class="nav-search" @click="navigateToSearch">
|
||||
<view class="nav-search-box">
|
||||
<text class="nav-search-icon">🔍</text>
|
||||
<!-- 小屏幕只显示搜索图标,隐藏搜索工具 -->
|
||||
<view class="nav-search-tools" :class="{ 'hidden-on-mobile': isMobile }">
|
||||
<text class="nav-tool-item">语音</text>
|
||||
<text class="nav-tool-item">拍照</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 搜索按钮 -->
|
||||
<view class="nav-inner-search-btn" :style="{ height: '22px' }">
|
||||
<text class="nav-inner-search-text">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 导航栏占位符 - 确保内容正确布局 -->
|
||||
<view class="navbar-placeholder" :style="{ height: (statusBarHeight + 100) + 'px' }"></view>
|
||||
<!-- 导航栏占位符 - 移除,改为使用 margin-top -->
|
||||
<!-- <view class="navbar-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view> -->
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<scroll-view
|
||||
@@ -53,7 +47,7 @@
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<!-- 智能健康卡片 -->
|
||||
<view class="smart-health-card">
|
||||
<view class="smart-health-card" :style="{ marginTop: (statusBarHeight + 44 + 10) + 'px' }">
|
||||
<view class="health-content">
|
||||
<view class="health-header">
|
||||
<text class="health-title">智能健康助手</text>
|
||||
@@ -239,7 +233,7 @@
|
||||
v-for="item in familyItems"
|
||||
:key="item.id"
|
||||
class="family-item"
|
||||
@click="navigateToCategory(item.categoryId)"
|
||||
@click="navigateToCategory(item)"
|
||||
>
|
||||
<view class="family-icon" :style="{ backgroundColor: item.color }">
|
||||
<text>{{ item.icon }}</text>
|
||||
@@ -363,6 +357,11 @@ const activeSort = ref('sales')
|
||||
const activeFilter = ref('recommend')
|
||||
const currentPage = ref(1)
|
||||
|
||||
// 数据源
|
||||
const allProducts = ref<any[]>([])
|
||||
const hotProducts = ref<any[]>([])
|
||||
const recommendedProducts = ref<any[]>([])
|
||||
|
||||
// 屏幕尺寸检测
|
||||
const isMobile = ref(false)
|
||||
|
||||
@@ -424,53 +423,100 @@ const healthNews = [
|
||||
}
|
||||
]
|
||||
|
||||
// 热销药品
|
||||
const hotProducts = [
|
||||
{
|
||||
id: 'hot1',
|
||||
name: '布洛芬缓释胶囊',
|
||||
specification: '0.3g*24粒',
|
||||
price: 18.5,
|
||||
originalPrice: 25.8,
|
||||
image: 'https://picsum.photos/300/300?random=medicine1',
|
||||
manufacturer: '修正药业',
|
||||
sales: 2560,
|
||||
badge: '热销'
|
||||
},
|
||||
{
|
||||
id: 'hot2',
|
||||
name: '板蓝根颗粒',
|
||||
specification: '10g*20袋',
|
||||
price: 22.8,
|
||||
originalPrice: 29.9,
|
||||
image: 'https://picsum.photos/300/300?random=medicine2',
|
||||
manufacturer: '白云山',
|
||||
sales: 1890,
|
||||
badge: '推荐'
|
||||
},
|
||||
{
|
||||
id: 'hot3',
|
||||
name: '维生素C片',
|
||||
specification: '100mg*100片',
|
||||
price: 15.9,
|
||||
originalPrice: 19.9,
|
||||
image: 'https://picsum.photos/300/300?random=medicine3',
|
||||
manufacturer: '养生堂',
|
||||
sales: 1420,
|
||||
badge: '特价'
|
||||
},
|
||||
{
|
||||
id: 'hot4',
|
||||
name: '胃康灵胶囊',
|
||||
specification: '0.4g*24粒',
|
||||
price: 32.8,
|
||||
originalPrice: 38.5,
|
||||
image: 'https://picsum.photos/300/300?random=medicine4',
|
||||
manufacturer: '三九医药',
|
||||
sales: 890,
|
||||
badge: '新品'
|
||||
// 初始化数据
|
||||
const initData = () => {
|
||||
const manufacturers = ['修正药业', '白云山', '养生堂', '三九医药', '同仁堂', '云南白药', '拜耳', '辉瑞']
|
||||
const names = ['布洛芬', '板蓝根', '维生素C', '胃康灵', '阿莫西林', '连花清瘟', '氨溴索', '氯雷他定', '感冒灵', '健胃消食片', '阿司匹林', '蒙脱石散']
|
||||
const tags = ['处方药', '中成药', '止咳化痰', '抗过敏', '感冒发烧', '肠胃用药', '消炎镇痛']
|
||||
const featureds = ['医生推荐', '热销爆款', '家庭必备', '季节必备', '店长推荐']
|
||||
|
||||
const products = [] as any[]
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const nameIdx = Math.floor(Math.random() * names.length)
|
||||
const name = names[nameIdx]
|
||||
const price = parseFloat((10 + Math.random() * 100).toFixed(1))
|
||||
const originalPrice = parseFloat((price * (1.1 + Math.random() * 0.5)).toFixed(1))
|
||||
const sales = Math.floor(Math.random() * 5000)
|
||||
|
||||
// 随机店铺ID,避免全部是同一家
|
||||
const randomShopSuffix = Math.floor(Math.random() * 20) + 1
|
||||
|
||||
products.push({
|
||||
id: `prod_${i}`,
|
||||
shopId: `shop_${randomShopSuffix}`,
|
||||
shopName: manufacturers[Math.floor(Math.random() * manufacturers.length)] + '官方旗舰店',
|
||||
name: name + (Math.random() > 0.5 ? '胶囊' : '颗粒'),
|
||||
specification: Math.random() > 0.5 ? '0.3g*24粒' : '10g*10袋',
|
||||
price: price,
|
||||
originalPrice: originalPrice,
|
||||
image: '/static/images/default-product.png',
|
||||
manufacturer: manufacturers[Math.floor(Math.random() * manufacturers.length)],
|
||||
sales: sales,
|
||||
rating: (3.5 + Math.random() * 1.5).toFixed(1),
|
||||
reviews: Math.floor(Math.random() * 500),
|
||||
tag: tags[Math.floor(Math.random() * tags.length)],
|
||||
featured: Math.random() > 0.7 ? featureds[Math.floor(Math.random() * featureds.length)] : '',
|
||||
badge: sales > 3000 ? '热销' : (price < 20 ? '特价' : (Math.random() > 0.8 ? '新品' : '')),
|
||||
// Attributes for filtering
|
||||
isNew: Math.random() > 0.8,
|
||||
isRecommend: Math.random() > 0.6,
|
||||
isHot: sales > 2000,
|
||||
isDiscount: (originalPrice - price) > 15,
|
||||
isQuality: price > 60
|
||||
})
|
||||
}
|
||||
]
|
||||
allProducts.value = products
|
||||
|
||||
filterHotProducts()
|
||||
filterRecommendedProducts()
|
||||
}
|
||||
|
||||
// 筛选热销商品
|
||||
const filterHotProducts = () => {
|
||||
let list = [...allProducts.value]
|
||||
|
||||
if (activeSort.value === 'sales') {
|
||||
list.sort((a, b) => b.sales - a.sales)
|
||||
} else if (activeSort.value === 'price') {
|
||||
list.sort((a, b) => a.price - b.price)
|
||||
} else if (activeSort.value === 'new') {
|
||||
list = list.filter(p => p.isNew)
|
||||
} else if (activeSort.value === 'recommend') {
|
||||
list = list.filter(p => p.isRecommend)
|
||||
}
|
||||
|
||||
// 如果筛选后数量不足4个,补足
|
||||
if (list.length < 4) {
|
||||
const remaining = allProducts.value.filter(p => !list.includes(p))
|
||||
list = [...list, ...remaining]
|
||||
}
|
||||
|
||||
hotProducts.value = list.slice(0, 4)
|
||||
}
|
||||
|
||||
// 筛选推荐商品
|
||||
const filterRecommendedProducts = () => {
|
||||
let list = [...allProducts.value]
|
||||
|
||||
if (activeFilter.value === 'hot') {
|
||||
list = list.filter(p => p.isHot)
|
||||
} else if (activeFilter.value === 'discount') {
|
||||
list = list.filter(p => p.isDiscount)
|
||||
} else if (activeFilter.value === 'quality') {
|
||||
list = list.filter(p => p.isQuality)
|
||||
} else {
|
||||
// 默认随机排序
|
||||
list.sort(() => Math.random() - 0.5)
|
||||
}
|
||||
|
||||
// 如果筛选后数量不足4个,补足
|
||||
if (list.length < 4) {
|
||||
const remaining = allProducts.value.filter(p => !list.includes(p))
|
||||
list = [...list, ...remaining]
|
||||
}
|
||||
|
||||
recommendedProducts.value = list.slice(0, 4)
|
||||
}
|
||||
|
||||
// 家庭常备药
|
||||
const familyItems = [
|
||||
@@ -524,61 +570,10 @@ const familyItems = [
|
||||
}
|
||||
]
|
||||
|
||||
// 推荐商品
|
||||
const recommendedProducts = [
|
||||
{
|
||||
id: 'rec1',
|
||||
name: '阿莫西林胶囊',
|
||||
specification: '0.25g*24粒',
|
||||
price: 28.5,
|
||||
originalPrice: 35.0,
|
||||
image: 'https://picsum.photos/350/350?random=rec1',
|
||||
rating: 4.8,
|
||||
reviews: 156,
|
||||
tag: '处方药',
|
||||
featured: '医生推荐'
|
||||
},
|
||||
{
|
||||
id: 'rec2',
|
||||
name: '连花清瘟胶囊',
|
||||
specification: '0.35g*36粒',
|
||||
price: 42.8,
|
||||
originalPrice: 48.0,
|
||||
image: 'https://picsum.photos/350/350?random=rec2',
|
||||
rating: 4.9,
|
||||
reviews: 289,
|
||||
tag: '中成药',
|
||||
featured: '热销爆款'
|
||||
},
|
||||
{
|
||||
id: 'rec3',
|
||||
name: '盐酸氨溴索口服液',
|
||||
specification: '100ml',
|
||||
price: 35.9,
|
||||
originalPrice: 42.0,
|
||||
image: 'https://picsum.photos/350/350?random=rec3',
|
||||
rating: 4.7,
|
||||
reviews: 132,
|
||||
tag: '止咳化痰',
|
||||
featured: '家庭必备'
|
||||
},
|
||||
{
|
||||
id: 'rec4',
|
||||
name: '氯雷他定片',
|
||||
specification: '10mg*6片',
|
||||
price: 18.6,
|
||||
originalPrice: 22.0,
|
||||
image: 'https://picsum.photos/350/350?random=rec4',
|
||||
rating: 4.6,
|
||||
reviews: 98,
|
||||
tag: '抗过敏',
|
||||
featured: '季节必备'
|
||||
}
|
||||
]
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initPage()
|
||||
initData()
|
||||
})
|
||||
|
||||
// 页面显示时重置状态
|
||||
@@ -693,13 +688,13 @@ const switchCategory = (category: any) => {
|
||||
// 切换排序
|
||||
const switchSort = (sortId: string) => {
|
||||
activeSort.value = sortId
|
||||
// 这里可以添加排序逻辑
|
||||
filterHotProducts()
|
||||
}
|
||||
|
||||
// 切换筛选器
|
||||
const switchFilter = (filterId: string) => {
|
||||
activeFilter.value = filterId
|
||||
// 这里可以添加筛选逻辑
|
||||
filterRecommendedProducts()
|
||||
}
|
||||
|
||||
// 查看新闻详情
|
||||
@@ -747,6 +742,41 @@ const loadMore = () => {
|
||||
|
||||
// 添加到购物车
|
||||
const addToCart = (product: any) => {
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let cartItems: any[] = []
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商品是否已存在
|
||||
const existingItem = cartItems.find((item: any) => item.id === product.id)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity++
|
||||
} else {
|
||||
// 添加新商品
|
||||
cartItems.push({
|
||||
id: product.id,
|
||||
shopId: product.shopId || 'shop_default',
|
||||
shopName: product.shopName || product.manufacturer || '自营店铺',
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
spec: product.specification || '默认规格',
|
||||
quantity: 1,
|
||||
selected: true
|
||||
})
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
@@ -758,12 +788,12 @@ const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/searc
|
||||
const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
|
||||
const navigateToProduct = (product: any) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/medicine/detail?id=${product.id}`
|
||||
url: `/pages/mall/consumer/product-detail?productId=${product.id}`
|
||||
})
|
||||
}
|
||||
const navigateToCategory = (categoryId: string) => {
|
||||
const navigateToCategory = (item: any) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/medicine/category?id=${categoryId}`
|
||||
url: `/pages/mall/consumer/search?keyword=${encodeURIComponent(item.name)}&type=family`
|
||||
})
|
||||
}
|
||||
const navigateToConsultation = () => uni.navigateTo({ url: '/pages/medicine/consultation' })
|
||||
@@ -799,7 +829,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
right: 0;
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 20px rgba(76, 175, 80, 0.15);
|
||||
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15); /* 调整为与分类页一致 */
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform, opacity;
|
||||
pointer-events: auto; /* 确保导航栏可以交互 */
|
||||
@@ -807,10 +837,12 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
height: 100px; /* 增加导航栏高度 */
|
||||
/* 导航栏搜索框容器内边距调整 */
|
||||
.search-container {
|
||||
height: 44px; /* 调整为与消息页一致的高度 */
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
flex-direction: row; /* 显式设置 flex-direction: row */
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 1400px;
|
||||
@@ -818,103 +850,66 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 品牌区域 - 重新设计为自适应平铺 */
|
||||
.brand-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
flex-wrap: nowrap; /* 强制不换行 */
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* 品牌标识区域 - 自适应平铺 */
|
||||
.brand-logo-area {
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
min-width: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 小屏幕隐藏品牌标识 */
|
||||
.brand-logo-area.hidden-on-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 小屏幕隐藏搜索工具 */
|
||||
.nav-search-tools.hidden-on-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.brand-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap; /* 防止文字换行 */
|
||||
}
|
||||
|
||||
.brand-tag {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 2px;
|
||||
white-space: nowrap; /* 防止文字换行 */
|
||||
}
|
||||
|
||||
/* 导航栏搜索框容器 - 自适应平铺 */
|
||||
.nav-search-container {
|
||||
flex: 1; /* 占据剩余空间 */
|
||||
min-width: 0; /* 允许缩小 */
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.nav-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-search-box {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 25px;
|
||||
padding: 12px 20px;
|
||||
/* 搜索框 hover 效果 */
|
||||
.search-box:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 导航栏搜索框容器内边距调整 */
|
||||
.search-box {
|
||||
flex: 1;
|
||||
max-width: 600px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 20px;
|
||||
padding: 0 4px 0 12px;
|
||||
display: flex;
|
||||
flex-direction: row; /* UVUE 显式设置 row */
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
width: 100%;
|
||||
height: 32px; /* 减小高度,与顶部高度44px适配,略小于顶部高度 */
|
||||
}
|
||||
|
||||
.nav-search-box:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.nav-search-icon {
|
||||
font-size: 18px;
|
||||
color: #4CAF50;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-search-tools {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-tool-item {
|
||||
font-size: 12px;
|
||||
color: #4CAF50;
|
||||
padding: 4px 10px;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border-radius: 12px;
|
||||
.search-placeholder {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nav-icon-btn {
|
||||
padding: 4px 8px 4px 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid #ddd;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* 搜索按钮高度微调 */
|
||||
.nav-inner-search-btn {
|
||||
padding: 0 12px; /* 减小内边距 */
|
||||
background-color: #87CEEB; /* 天空蓝 */
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px; /* 随搜索框高度减小而减小 */
|
||||
}
|
||||
|
||||
.nav-inner-search-text {
|
||||
font-size: 12px; /* 字体稍微变小 */
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 导航栏占位符 */
|
||||
@@ -923,10 +918,27 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-camera-btn {
|
||||
padding: 4px 8px 4px 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right-width: 1px;
|
||||
border-right-style: solid;
|
||||
border-right-color: #ddd;
|
||||
border-right: 1px solid #ddd; /* 修复UVUE样式 */
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.nav-camera-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.main-scroll {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
padding: 0 16px 16px;
|
||||
max-width: 1400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@@ -941,6 +953,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
/* margin-top 由 style 动态控制 */
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -1226,25 +1239,27 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
|
||||
.sort-tabs {
|
||||
display: flex;
|
||||
flex-direction: row; /* UVUE 显式设置 row */
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap; /* 强制不换行 */
|
||||
justify-content: space-between; /* 两端对齐 */
|
||||
width: 100%; /* 占满宽度 */
|
||||
margin-top: 12px; /* 增加顶部间距,与标题分开 */
|
||||
flex-wrap: wrap; /* 允许换行,实现自适应 */
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.sort-tab {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
padding: 8px 0; /* 上下padding,左右由flex均分 */
|
||||
padding: 8px 12px; /* 增加左右内边距 */
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
flex: 1; /* 均分宽度 */
|
||||
text-align: center; /* 文字居中 */
|
||||
min-width: 70px; /* 设置最小宽度防止过窄 */
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -1480,25 +1495,27 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
|
||||
.recommend-filters {
|
||||
display: flex;
|
||||
flex-direction: row; /* UVUE 显式设置 row */
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap; /* 强制不换行 */
|
||||
justify-content: space-between; /* 两端对齐 */
|
||||
width: 100%; /* 占满宽度 */
|
||||
margin-top: 12px; /* 增加顶部间距 */
|
||||
flex-wrap: wrap; /* 允许换行,实现自适应 */
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
padding: 8px 0; /* 上下padding,左右由flex均分 */
|
||||
padding: 8px 12px; /* 增加左右内边距 */
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
flex: 1; /* 均分宽度 */
|
||||
text-align: center; /* 文字居中 */
|
||||
min-width: 80px; /* 设置最小宽度防止过窄 */
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -1755,77 +1772,196 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
|
||||
/* 小屏手机 (小于414px) */
|
||||
@media screen and (max-width: 414px) {
|
||||
.nav-container {
|
||||
.search-container {
|
||||
padding: 0 12px;
|
||||
height: 90px;
|
||||
height: 44px; /* 恢复为44px,与PC和分类页逻辑保持一致 */
|
||||
}
|
||||
|
||||
.brand-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.brand-logo-area {
|
||||
min-width: auto;
|
||||
.search-box {
|
||||
padding: 8px 16px; /* 与分类页保持一致 */
|
||||
max-width: 100%;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.brand-info {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.brand-tag {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.nav-search-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.nav-search-box {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.nav-search-tools {
|
||||
display: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-scroll {
|
||||
padding: 12px;
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(5, 1fr); /* 调整为一行5个,正好两行 */
|
||||
gap: 8px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
padding: 8px 0;
|
||||
background: transparent; /* 移除卡片背景 */
|
||||
box-shadow: none; /* 移除阴影 */
|
||||
border: none; /* 移除边框 */
|
||||
}
|
||||
|
||||
.category-card:hover {
|
||||
transform: none; /* 移动端移除悬停效果 */
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 44px; /* 减小图标尺寸 */
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-icon text {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 11px; /* 减小文字大小 */
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
display: none; /* 手机端隐藏描述文字,保持界面整洁 */
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(4, 1fr); /* 服务入口也调整为一行4个 */
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
grid-template-columns: 1fr;
|
||||
.service-card {
|
||||
padding: 10px 4px;
|
||||
background: #f8f9fa; /* 保持淡色背景 */
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.service-icon text {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.service-desc {
|
||||
display: none; /* 隐藏描述 */
|
||||
}
|
||||
|
||||
.products-grid,
|
||||
.recommend-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, 1fr); /* 手机端调整为双列显示 */
|
||||
gap: 8px; /* 减小间距 */
|
||||
}
|
||||
|
||||
.product-info,
|
||||
.product-details {
|
||||
padding: 8px; /* 减小内边距 */
|
||||
}
|
||||
|
||||
.product-name,
|
||||
.product-title {
|
||||
font-size: 13px; /* 调整字体大小 */
|
||||
height: 36px; /* 限制高度,防止参差不齐 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.product-image,
|
||||
.product-image-container {
|
||||
height: 140px; /* 稍微减小图片高度适配双列 */
|
||||
}
|
||||
|
||||
/* 手机端商品卡片极简模式(热销 & 推荐) */
|
||||
.hot-products .product-spec,
|
||||
.hot-products .manufacturer,
|
||||
.hot-products .original-price,
|
||||
.hot-products .cart-text,
|
||||
.hot-products .sales-info,
|
||||
.hot-products .product-action, /* 隐藏热销区加购按钮 */
|
||||
.smart-recommend .product-specification,
|
||||
.smart-recommend .product-rating,
|
||||
.smart-recommend .original-price,
|
||||
.smart-recommend .product-actions /* 隐藏推荐区加购按钮 */
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hot-products .product-info,
|
||||
.smart-recommend .product-details {
|
||||
padding: 6px; /* 极小内边距 */
|
||||
}
|
||||
|
||||
.hot-products .product-image,
|
||||
.hot-products .product-image-container,
|
||||
.smart-recommend .product-image,
|
||||
.smart-recommend .product-image-container {
|
||||
height: 110px; /* 进一步减小图片高度 */
|
||||
}
|
||||
|
||||
.hot-products .product-name,
|
||||
.smart-recommend .product-title {
|
||||
margin-bottom: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
height: 32px; /* 限制2行高度 */
|
||||
}
|
||||
|
||||
.hot-products .price-section,
|
||||
.smart-recommend .price-section {
|
||||
margin-bottom: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.hot-products .price-symbol,
|
||||
.smart-recommend .price-symbol {
|
||||
font-size: 10px;
|
||||
color: #FF5722;
|
||||
}
|
||||
|
||||
.hot-products .price-value,
|
||||
.smart-recommend .price-value {
|
||||
font-size: 14px; /* 字体变小 */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.family-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr); /* 家庭常备药也调整为一行4个 */
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.family-item {
|
||||
padding: 8px 4px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.family-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.family-icon text {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.family-name {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.family-desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.news-swiper {
|
||||
@@ -1835,16 +1971,19 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
.sort-tabs,
|
||||
.recommend-filters {
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
justify-content: flex-start; /* 保持左对齐 */
|
||||
overflow-x: auto; /* 允许横向滚动 */
|
||||
flex-wrap: nowrap; /* 禁止换行 */
|
||||
padding-bottom: 4px; /* 滚动条空间 */
|
||||
}
|
||||
|
||||
.sort-tab,
|
||||
.filter-item {
|
||||
padding: 5px 12px;
|
||||
font-size: 12px;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
min-width: auto;
|
||||
flex: 0 0 auto; /* 取消均分 */
|
||||
}
|
||||
|
||||
.section-header {
|
||||
@@ -1865,38 +2004,9 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
|
||||
/* 中屏手机/小平板 (415px-768px) */
|
||||
@media screen and (min-width: 415px) and (max-width: 768px) {
|
||||
.nav-container {
|
||||
.search-container {
|
||||
padding: 0 16px;
|
||||
height: 95px;
|
||||
}
|
||||
|
||||
.brand-section {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.brand-logo-area {
|
||||
min-width: 140px;
|
||||
max-width: 160px;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.nav-search-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.nav-search-tools {
|
||||
display: none;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.main-scroll {
|
||||
@@ -1956,10 +2066,10 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
|
||||
/* 平板设备 (769px-1024px) */
|
||||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||||
.nav-container {
|
||||
.search-container {
|
||||
padding: 0 24px;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.nav-search-tools .nav-tool-item {
|
||||
@@ -1968,7 +2078,7 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
|
||||
.main-scroll {
|
||||
max-width: 100%;
|
||||
padding: 20px 24px;
|
||||
padding: 0 24px 20px;
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
@@ -1998,17 +2108,18 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
|
||||
/* 桌面端 (1025px以上) */
|
||||
@media screen and (min-width: 1025px) {
|
||||
.nav-container {
|
||||
.search-container {
|
||||
padding: 0 32px;
|
||||
height: 100px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.main-scroll {
|
||||
padding: 24px 32px;
|
||||
max-width: 100%;
|
||||
padding: 0 32px 24px;
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -25,13 +25,13 @@
|
||||
<view class="shop-info" @click="goToShop">
|
||||
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" />
|
||||
<view class="shop-details">
|
||||
<text class="shop-name">{{ merchant.shop_name }}</text>
|
||||
<view class="shop-rating">
|
||||
<text class="rating-text">评分: {{ merchant.rating.toFixed(1) }}</text>
|
||||
<text class="sales-text">销量: {{ merchant.total_sales }}</text>
|
||||
<text class="shop-name" @click.stop="goToShop">{{ merchant.shop_name }}</text>
|
||||
<view class="shop-stats-row">
|
||||
<text class="rating-text">评分: {{ merchant.rating.toFixed(1) }}</text>
|
||||
<text class="sales-text">销量: {{ merchant.total_sales }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="enter-shop">进店 ></text>
|
||||
<text class="enter-shop" @click.stop="goToShop">进店 ></text>
|
||||
</view>
|
||||
|
||||
<!-- 规格选择 -->
|
||||
@@ -50,6 +50,16 @@
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-actions">
|
||||
<view class="action-buttons">
|
||||
<view class="action-btn" @click="goToCart">
|
||||
<text class="action-icon">🛒</text>
|
||||
<text class="action-text">购物车</text>
|
||||
</view>
|
||||
<view class="action-btn" @click="toggleFavorite">
|
||||
<text class="action-icon">{{ isFavorite ? '❤️' : '🤍' }}</text>
|
||||
<text class="action-text">{{ isFavorite ? '已收藏' : '收藏' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-group">
|
||||
<button class="cart-btn" @click="addToCart">加入购物车</button>
|
||||
<button class="buy-btn" @click="buyNow">立即购买</button>
|
||||
</view>
|
||||
@@ -116,16 +126,55 @@ export default {
|
||||
showSpec: false,
|
||||
selectedSkuId: '',
|
||||
selectedSpec: '',
|
||||
quantity: 1
|
||||
quantity: 1,
|
||||
isFavorite: false
|
||||
}
|
||||
},
|
||||
onLoad(options: any) {
|
||||
const productId = options.productId as string
|
||||
if (productId) {
|
||||
this.loadProductDetail(productId)
|
||||
this.checkFavoriteStatus(productId)
|
||||
this.saveFootprint(productId)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
saveFootprint(productId: string) {
|
||||
const footprintData = uni.getStorageSync('footprints')
|
||||
let footprints: any[] = []
|
||||
|
||||
if (footprintData) {
|
||||
try {
|
||||
footprints = JSON.parse(footprintData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('Failed to parse footprints', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除已存在的相同商品(为了将其移到最新位置)
|
||||
footprints = footprints.filter(item => item.id !== productId)
|
||||
|
||||
// 添加到头部
|
||||
footprints.unshift({
|
||||
id: this.product.id,
|
||||
name: this.product.name,
|
||||
price: this.product.price,
|
||||
original_price: this.product.original_price, // 添加原价
|
||||
image: this.product.images[0],
|
||||
sales: this.product.sales,
|
||||
shopId: this.merchant.id,
|
||||
shopName: this.merchant.shop_name,
|
||||
viewTime: Date.now()
|
||||
})
|
||||
|
||||
// 限制数量,例如最近50条
|
||||
if (footprints.length > 50) {
|
||||
footprints = footprints.slice(0, 50)
|
||||
}
|
||||
|
||||
uni.setStorageSync('footprints', JSON.stringify(footprints))
|
||||
},
|
||||
|
||||
loadProductDetail(productId: string) {
|
||||
// 模拟加载商品详情数据
|
||||
this.product = {
|
||||
@@ -222,6 +271,45 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let cartItems: any[] = []
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商品是否已存在 (同一SKU)
|
||||
const existingItem = cartItems.find((item: any) => item.id === this.selectedSkuId)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity += this.quantity
|
||||
} else {
|
||||
// 查找SKU信息
|
||||
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
|
||||
|
||||
// 添加新商品
|
||||
cartItems.push({
|
||||
id: this.selectedSkuId, // 使用SKU ID作为购物车条目ID
|
||||
productId: this.product.id,
|
||||
shopId: this.merchant.id,
|
||||
shopName: this.merchant.shop_name,
|
||||
name: this.product.name,
|
||||
price: sku ? sku.price : this.product.price,
|
||||
image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
|
||||
spec: this.selectedSpec,
|
||||
quantity: this.quantity,
|
||||
selected: true
|
||||
})
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||
|
||||
// 模拟添加到购物车
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
@@ -248,6 +336,70 @@ export default {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}`
|
||||
})
|
||||
},
|
||||
|
||||
checkFavoriteStatus(id: string) {
|
||||
const storedFavorites = uni.getStorageSync('favorites')
|
||||
if (storedFavorites) {
|
||||
try {
|
||||
const favorites = JSON.parse(storedFavorites as string) as any[]
|
||||
this.isFavorite = favorites.some(item => item.id === id)
|
||||
} catch (e) {
|
||||
console.error('Failed to parse favorites', e)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
toggleFavorite() {
|
||||
const storedFavorites = uni.getStorageSync('favorites')
|
||||
let favorites: any[] = []
|
||||
|
||||
if (storedFavorites) {
|
||||
try {
|
||||
favorites = JSON.parse(storedFavorites as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('Failed to parse favorites', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isFavorite) {
|
||||
// 取消收藏
|
||||
favorites = favorites.filter(item => item.id !== this.product.id)
|
||||
uni.showToast({
|
||||
title: '已取消收藏',
|
||||
icon: 'none'
|
||||
})
|
||||
} else {
|
||||
// 添加收藏
|
||||
favorites.push({
|
||||
id: this.product.id,
|
||||
name: this.product.name,
|
||||
price: this.product.price,
|
||||
image: this.product.images[0],
|
||||
sales: this.product.sales,
|
||||
shopId: this.merchant.id,
|
||||
shopName: this.merchant.shop_name
|
||||
})
|
||||
uni.showToast({
|
||||
title: '收藏成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
uni.setStorageSync('favorites', JSON.stringify(favorites))
|
||||
this.isFavorite = !this.isFavorite
|
||||
},
|
||||
|
||||
goToHome() {
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/home'
|
||||
})
|
||||
},
|
||||
|
||||
goToCart() {
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/cart'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,6 +480,7 @@ export default {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: row; /* 显式横向排列 */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -340,6 +493,9 @@ export default {
|
||||
|
||||
.shop-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column; /* 内部信息保持纵向,或者根据需要改为横向 */
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.shop-name {
|
||||
@@ -349,8 +505,10 @@ export default {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.shop-rating {
|
||||
.shop-stats-row {
|
||||
display: flex;
|
||||
flex-direction: row; /* 显式横向排列 */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rating-text, .sales-text {
|
||||
@@ -414,21 +572,55 @@ export default {
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: row; /* 显式设置横向排列 */
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
flex-direction: row; /* 显式设置横向排列 */
|
||||
align-items: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column; /* 图标文字保持纵向 */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
min-width: 80rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 40rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row; /* 显式设置横向排列 */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cart-btn, .buy-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
font-size: 26rpx;
|
||||
border: none;
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
|
||||
@@ -1,40 +1,110 @@
|
||||
<!-- 消费者端 - 个人中心 -->
|
||||
<template>
|
||||
<view class="consumer-profile">
|
||||
<!-- 用户信息头部 -->
|
||||
<view class="profile-header">
|
||||
<image :src="userInfo.avatar_url || '/static/default-avatar.png'" class="user-avatar" @click="editProfile" />
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{ userInfo.nickname || userInfo.phone }}</text>
|
||||
<text class="user-level">{{ getUserLevel() }}</text>
|
||||
<view class="user-stats">
|
||||
<text class="stat-item">积分: {{ userStats.points }}</text>
|
||||
<text class="stat-item">余额: ¥{{ userStats.balance }}</text>
|
||||
<!-- 智能顶部导航栏 - 与消息页保持一致 -->
|
||||
<view class="smart-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-container">
|
||||
<!-- 头像 -->
|
||||
<image
|
||||
:src="userInfo.avatar_url || '/static/default-avatar.png'"
|
||||
class="nav-avatar"
|
||||
@click="editProfile"
|
||||
/>
|
||||
|
||||
<!-- 用户信息横向排列 (名字、积分、余额、优惠券) -->
|
||||
<view class="nav-user-stats">
|
||||
<text class="nav-user-name">{{ userInfo.nickname || userInfo.phone }}</text>
|
||||
|
||||
<view class="nav-stat-item">
|
||||
<text class="nav-stat-label">积分</text>
|
||||
<text class="nav-stat-value">{{ userStats.points }}</text>
|
||||
</view>
|
||||
|
||||
<view class="nav-stat-item">
|
||||
<text class="nav-stat-label">余额</text>
|
||||
<text class="nav-stat-value">¥{{ userStats.balance }}</text>
|
||||
</view>
|
||||
|
||||
<view class="nav-stat-item" @click="goToCoupons">
|
||||
<text class="nav-stat-label">券</text>
|
||||
<text class="nav-stat-value">{{ serviceCounts.coupons }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设置按钮 (右侧) -->
|
||||
<view class="nav-actions">
|
||||
<view class="action-btn" @click="goToSettings">
|
||||
<text class="action-icon">⚙️</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 导航栏占位符 - 恢复 -->
|
||||
<view :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
|
||||
|
||||
<!-- 我的服务 (移到订单上方) -->
|
||||
<view class="my-services" style="margin-top: 10px;">
|
||||
<view class="section-title">我的服务</view>
|
||||
<view class="service-grid">
|
||||
<view class="service-item" @click="goToCoupons">
|
||||
<text class="service-icon">🎫</text>
|
||||
<text class="service-text">优惠券</text>
|
||||
<text v-if="serviceCounts.coupons > 0" class="service-badge">{{ serviceCounts.coupons }}</text>
|
||||
</view>
|
||||
<view class="service-item" @click="goToAddress">
|
||||
<text class="service-icon">📍</text>
|
||||
<text class="service-text">收货地址</text>
|
||||
</view>
|
||||
<view class="service-item" @click="goToFavorites">
|
||||
<text class="service-icon">❤️</text>
|
||||
<text class="service-text">我的收藏</text>
|
||||
<text v-if="serviceCounts.favorites > 0" class="service-badge">{{ serviceCounts.favorites }}</text>
|
||||
</view>
|
||||
|
||||
<view class="service-item" @click="goToFootprint">
|
||||
<text class="service-icon">👣</text>
|
||||
<text class="service-text">浏览足迹</text>
|
||||
</view>
|
||||
<view class="service-item" @click="goToRefund">
|
||||
<text class="service-icon">🔄</text>
|
||||
<text class="service-text">退款/售后</text>
|
||||
</view>
|
||||
<view class="service-item" @click="contactService">
|
||||
<text class="service-icon">💬</text>
|
||||
<text class="service-text">在线客服</text>
|
||||
</view>
|
||||
<view class="service-item" @click="goToMySubscriptions">
|
||||
<text class="service-icon">🧩</text>
|
||||
<text class="service-text">我的订阅</text>
|
||||
</view>
|
||||
<view class="service-item" @click="goToSubscriptions">
|
||||
<text class="service-icon">📱</text>
|
||||
<text class="service-text">软件订阅</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="settings-icon" @click="goToSettings">⚙️</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单状态快捷入口 -->
|
||||
<view class="order-shortcuts">
|
||||
<view class="section-title">我的订单</view>
|
||||
<view class="order-tabs">
|
||||
<view class="order-tab" @click="goToOrders('all')">
|
||||
<view class="order-tab" :class="{ active: currentOrderTab === 'all' }" @click="switchOrderTab('all')">
|
||||
<text class="tab-icon">📋</text>
|
||||
<text class="tab-text">全部订单</text>
|
||||
<text class="tab-text">全部</text>
|
||||
<text v-if="orderCounts.total > 0" class="tab-badge">{{ orderCounts.total }}</text>
|
||||
</view>
|
||||
<view class="order-tab" @click="goToOrders('pending')">
|
||||
<view class="order-tab" :class="{ active: currentOrderTab === 'pending' }" @click="switchOrderTab('pending')">
|
||||
<text class="tab-icon">💰</text>
|
||||
<text class="tab-text">待支付</text>
|
||||
<text v-if="orderCounts.pending > 0" class="tab-badge">{{ orderCounts.pending }}</text>
|
||||
</view>
|
||||
<view class="order-tab" @click="goToOrders('shipped')">
|
||||
<view class="order-tab" :class="{ active: currentOrderTab === 'shipped' }" @click="switchOrderTab('shipped')">
|
||||
<text class="tab-icon">🚚</text>
|
||||
<text class="tab-text">待收货</text>
|
||||
<text v-if="orderCounts.shipped > 0" class="tab-badge">{{ orderCounts.shipped }}</text>
|
||||
</view>
|
||||
<view class="order-tab" @click="goToOrders('completed')">
|
||||
<view class="order-tab" :class="{ active: currentOrderTab === 'review' }" @click="switchOrderTab('review')">
|
||||
<text class="tab-icon">⭐</text>
|
||||
<text class="tab-text">待评价</text>
|
||||
<text v-if="orderCounts.review > 0" class="tab-badge">{{ orderCounts.review }}</text>
|
||||
@@ -42,25 +112,25 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近订单 -->
|
||||
<!-- 最近订单列表 (根据Tab切换显示) -->
|
||||
<view class="recent-orders">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近订单</text>
|
||||
<text class="view-all" @click="goToOrders('all')">查看全部 ></text>
|
||||
<text class="section-title">{{ getOrderSectionTitle() }}</text>
|
||||
<text class="view-all" @click="goToOrders(currentOrderTab)">查看更多 ></text>
|
||||
</view>
|
||||
|
||||
<view v-if="recentOrders.length === 0" class="empty-orders">
|
||||
<text class="empty-text">暂无订单记录</text>
|
||||
<view v-if="filteredOrders.length === 0" class="empty-orders">
|
||||
<text class="empty-text">暂无相关订单记录</text>
|
||||
<button class="start-shopping" @click="goShopping">去逛逛</button>
|
||||
</view>
|
||||
|
||||
<view v-for="order in recentOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
|
||||
<view v-for="order in filteredOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
|
||||
<view class="order-header">
|
||||
<text class="order-no">订单号: {{ order.order_no }}</text>
|
||||
<text class="order-status" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
|
||||
</view>
|
||||
<view class="order-content">
|
||||
<image :src="getOrderMainImage(order)" class="order-image" />
|
||||
<image :src="getOrderMainImage(order)" class="order-image" mode="aspectFill" />
|
||||
<view class="order-info">
|
||||
<text class="order-title">{{ getOrderTitle(order) }}</text>
|
||||
<text class="order-amount">¥{{ order.actual_amount }}</text>
|
||||
@@ -76,7 +146,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 我的服务 -->
|
||||
<view class="my-services">
|
||||
<!-- <view class="my-services">
|
||||
<view class="section-title">我的服务</view>
|
||||
<view class="service-grid">
|
||||
<view class="service-item" @click="goToCoupons">
|
||||
@@ -114,7 +184,7 @@
|
||||
<text class="service-text">软件订阅</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 消费统计 -->
|
||||
<view class="consumption-stats">
|
||||
@@ -251,16 +321,150 @@ export default {
|
||||
order_count: 0,
|
||||
avg_amount: 0,
|
||||
save_amount: 0
|
||||
} as ConsumptionStatsType
|
||||
} as ConsumptionStatsType,
|
||||
statusBarHeight: 0,
|
||||
currentOrderTab: 'all' as string, // 当前选中的订单Tab
|
||||
allOrders: [] as Array<OrderType> // 存储所有订单数据
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.initPage()
|
||||
this.loadUserProfile()
|
||||
this.loadMockOrders() // 加载Mock订单数据
|
||||
},
|
||||
onShow() {
|
||||
this.refreshData()
|
||||
computed: {
|
||||
// 根据当前Tab筛选订单
|
||||
filteredOrders(): Array<OrderType> {
|
||||
if (this.currentOrderTab === 'all') {
|
||||
return this.allOrders
|
||||
} else if (this.currentOrderTab === 'pending') {
|
||||
return this.allOrders.filter((order: OrderType): boolean => order.status === 1)
|
||||
} else if (this.currentOrderTab === 'shipped') {
|
||||
return this.allOrders.filter((order: OrderType): boolean => order.status === 2 || order.status === 3)
|
||||
} else if (this.currentOrderTab === 'review') {
|
||||
return this.allOrders.filter((order: OrderType): boolean => order.status === 4)
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 加载Mock订单数据
|
||||
loadMockOrders() {
|
||||
// 创建Mock数据
|
||||
const mockData: Array<OrderType> = [
|
||||
// 待支付订单
|
||||
{
|
||||
id: 'order_001',
|
||||
order_no: 'ORD202401250001',
|
||||
user_id: 'user_001',
|
||||
merchant_id: 'merchant_001',
|
||||
status: 1, // 待支付
|
||||
total_amount: 299.00,
|
||||
discount_amount: 30.00,
|
||||
delivery_fee: 0.00,
|
||||
actual_amount: 269.00,
|
||||
payment_method: 1,
|
||||
payment_status: 0,
|
||||
delivery_address: {},
|
||||
created_at: '2024-01-25T14:30:00'
|
||||
},
|
||||
// 待发货
|
||||
{
|
||||
id: 'order_002',
|
||||
order_no: 'ORD202401240002',
|
||||
user_id: 'user_001',
|
||||
merchant_id: 'merchant_002',
|
||||
status: 2, // 待发货
|
||||
total_amount: 158.00,
|
||||
discount_amount: 0,
|
||||
delivery_fee: 6.00,
|
||||
actual_amount: 164.00,
|
||||
payment_method: 1,
|
||||
payment_status: 1,
|
||||
delivery_address: {},
|
||||
created_at: '2024-01-24T09:20:00'
|
||||
},
|
||||
// 待收货
|
||||
{
|
||||
id: 'order_003',
|
||||
order_no: 'ORD202401230003',
|
||||
user_id: 'user_001',
|
||||
merchant_id: 'merchant_001',
|
||||
status: 3, // 待收货
|
||||
total_amount: 89.90,
|
||||
discount_amount: 10.00,
|
||||
delivery_fee: 0.00,
|
||||
actual_amount: 79.90,
|
||||
payment_method: 1,
|
||||
payment_status: 1,
|
||||
delivery_address: {},
|
||||
created_at: '2024-01-23T18:15:00'
|
||||
},
|
||||
// 待评价 (已完成)
|
||||
{
|
||||
id: 'order_004',
|
||||
order_no: 'ORD202401200004',
|
||||
user_id: 'user_001',
|
||||
merchant_id: 'merchant_003',
|
||||
status: 4, // 待评价
|
||||
total_amount: 399.00,
|
||||
discount_amount: 50.00,
|
||||
delivery_fee: 0.00,
|
||||
actual_amount: 349.00,
|
||||
payment_method: 1,
|
||||
payment_status: 1,
|
||||
delivery_address: {},
|
||||
created_at: '2024-01-20T11:30:00'
|
||||
},
|
||||
// 已完成 (已评价)
|
||||
{
|
||||
id: 'order_005',
|
||||
order_no: 'ORD202401180005',
|
||||
user_id: 'user_001',
|
||||
merchant_id: 'merchant_001',
|
||||
status: 5, // 已完成
|
||||
total_amount: 128.00,
|
||||
discount_amount: 0,
|
||||
delivery_fee: 0.00,
|
||||
actual_amount: 128.00,
|
||||
payment_method: 1,
|
||||
payment_status: 1,
|
||||
delivery_address: {},
|
||||
created_at: '2024-01-18T16:45:00'
|
||||
}
|
||||
]
|
||||
this.allOrders = mockData
|
||||
this.recentOrders = mockData // 初始显示全部
|
||||
|
||||
// 更新角标统计
|
||||
this.orderCounts = {
|
||||
total: mockData.length,
|
||||
pending: mockData.filter((o: OrderType): boolean => o.status === 1).length,
|
||||
shipped: mockData.filter((o: OrderType): boolean => o.status === 2 || o.status === 3).length,
|
||||
review: mockData.filter((o: OrderType): boolean => o.status === 4).length
|
||||
}
|
||||
},
|
||||
|
||||
// 切换订单Tab
|
||||
switchOrderTab(tab: string) {
|
||||
this.currentOrderTab = tab
|
||||
},
|
||||
|
||||
// 获取当前订单部分标题
|
||||
getOrderSectionTitle(): string {
|
||||
const titles: Record<string, string> = {
|
||||
'all': '全部订单',
|
||||
'pending': '待支付订单',
|
||||
'shipped': '待收货订单',
|
||||
'review': '待评价订单'
|
||||
}
|
||||
return titles[this.currentOrderTab] || '我的订单'
|
||||
},
|
||||
|
||||
initPage() {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = systemInfo.statusBarHeight || 0
|
||||
},
|
||||
loadUserProfile() {
|
||||
// 模拟加载用户信息
|
||||
this.userInfo = {
|
||||
@@ -364,6 +568,15 @@ export default {
|
||||
refreshData() {
|
||||
// 刷新页面数据
|
||||
this.loadUserProfile()
|
||||
this.loadMockOrders() // 加载Mock订单数据
|
||||
this.updateCouponCount() // 更新优惠券数量
|
||||
},
|
||||
|
||||
updateCouponCount() {
|
||||
// 从本地存储读取领取的优惠券数量并叠加到基础数量上
|
||||
const baseCoupons = 5
|
||||
const claimedCoupons = uni.getStorageSync('claimedCoupons') || 0
|
||||
this.serviceCounts.coupons = baseCoupons + (claimedCoupons as number)
|
||||
},
|
||||
|
||||
getUserLevel(): string {
|
||||
@@ -476,8 +689,9 @@ export default {
|
||||
},
|
||||
|
||||
goToAddress() {
|
||||
// 暂时跳转到设置页的地址管理
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/address'
|
||||
url: '/pages/mall/consumer/address-list'
|
||||
})
|
||||
},
|
||||
|
||||
@@ -542,115 +756,183 @@ export default {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 60rpx 30rpx 40rpx;
|
||||
/* 智能顶部导航栏 */
|
||||
.smart-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.15);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
/* 导航栏用户信息区域 */
|
||||
.nav-user-stats {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start; /* 靠左对齐,紧跟头像 */
|
||||
margin-right: 12px;
|
||||
overflow: hidden; /* 防止溢出 */
|
||||
}
|
||||
|
||||
.nav-user-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-right: 12px;
|
||||
max-width: 30%; /* 限制名字宽度 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-stat-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 2px 8px;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
}
|
||||
|
||||
.nav-stat-label {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.nav-stat-value {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
margin-right: 30rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
.action-icon {
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.user-level {
|
||||
font-size: 24rpx;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 15rpx;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.user-stats {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
font-size: 32rpx;
|
||||
padding: 10rpx;
|
||||
/* 导航栏占位符 */
|
||||
.navbar-placeholder {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.order-shortcuts, .recent-orders, .my-services, .consumption-stats, .account-security {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin: 15px 15px; /* 顶部恢复 margin */
|
||||
border-radius: 12px; /* 统一圆角 */
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.view-all {
|
||||
font-size: 24rpx;
|
||||
color: #007aff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.order-tabs {
|
||||
display: flex;
|
||||
flex-direction: row; /* 显式横向排列 */
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.order-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row; /* 关键:改为横向排列 */
|
||||
align-items: center;
|
||||
justify-content: center; /* 居中 */
|
||||
position: relative;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 40rpx;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 20px;
|
||||
margin-right: 6px; /* 图标和文字间距 */
|
||||
margin-bottom: 0; /* 移除底部间距 */
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 选中状态的Tab */
|
||||
.order-tab.active .tab-icon,
|
||||
.order-tab.active .tab-text {
|
||||
color: #4CAF50;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.order-tab.active {
|
||||
background-color: #f0f9f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: 20rpx;
|
||||
top: 0;
|
||||
right: 10%; /* 调整位置 */
|
||||
background-color: #ff4444;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 10rpx;
|
||||
min-width: 32rpx;
|
||||
font-size: 10px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 8px;
|
||||
min-width: 14px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.empty-orders {
|
||||
@@ -782,16 +1064,19 @@ export default {
|
||||
|
||||
.service-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 30rpx;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap; /* 允许换行 */
|
||||
gap: 16px 0; /* 行间距16px,列间距由 flex 控制 */
|
||||
justify-content: flex-start; /* 从左开始排列 */
|
||||
}
|
||||
|
||||
.service-item {
|
||||
width: 30%;
|
||||
width: 25%; /* 每行4个 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
box-sizing: border-box; /* 确保 padding 不影响宽度 */
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
<!-- 搜索头部 -->
|
||||
<view class="search-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="search-bar-container">
|
||||
<!-- 返回按钮 -->
|
||||
<!-- 返回按钮:小于号加粗 -->
|
||||
<view class="back-btn" @click="goBack">
|
||||
<text class="back-icon">←</text>
|
||||
<text class="back-icon"><</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-input-container">
|
||||
<view class="search-icon">🔍</view>
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
@@ -21,14 +20,21 @@
|
||||
placeholder-class="placeholder"
|
||||
:focus="autoFocus"
|
||||
/>
|
||||
|
||||
<!-- 清除按钮 -->
|
||||
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
|
||||
<text class="clear-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索按钮 -->
|
||||
<view class="search-btn" @click="onSearch">
|
||||
<text class="search-btn-text">搜索</text>
|
||||
|
||||
<!-- 相机图标 -->
|
||||
<view class="camera-btn" @click="openCamera">
|
||||
<text class="camera-icon">📷</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索按钮:移入输入框内部 -->
|
||||
<view class="inner-search-btn" @click="onSearch">
|
||||
<text class="inner-search-text">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -149,9 +155,23 @@
|
||||
<view class="results-header">
|
||||
<text class="results-title">搜索结果</text>
|
||||
<view class="filter-tabs">
|
||||
<text class="filter-tab active">综合</text>
|
||||
<text class="filter-tab">销量</text>
|
||||
<text class="filter-tab">价格</text>
|
||||
<text
|
||||
class="filter-tab"
|
||||
:class="{ active: activeSort === 'default' }"
|
||||
@click="switchSort('default')"
|
||||
>综合</text>
|
||||
<text
|
||||
class="filter-tab"
|
||||
:class="{ active: activeSort === 'sales' }"
|
||||
@click="switchSort('sales')"
|
||||
>销量</text>
|
||||
<text
|
||||
class="filter-tab"
|
||||
:class="{ active: activeSort === 'price' }"
|
||||
@click="switchSort('price')"
|
||||
>
|
||||
价格 {{ activeSort === 'price' ? (priceSortAsc ? '↑' : '↓') : '' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -175,7 +195,7 @@
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-value">{{ product.price }}</text>
|
||||
</view>
|
||||
<view class="add-cart-btn">
|
||||
<view class="add-cart-btn" @click.stop="addToCart(product)">
|
||||
<text class="cart-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -183,14 +203,14 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空结果 -->
|
||||
<view v-if="searchResults.length === 0 && !loading" class="empty-result">
|
||||
<!-- 空结果 - 仅在非加载状态且无结果时显示 -->
|
||||
<view v-if="!loading && searchResults.length === 0" class="empty-result">
|
||||
<text class="empty-icon">🤔</text>
|
||||
<text class="empty-text">未找到相关商品</text>
|
||||
<text class="empty-sub">换个关键词试试吧</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<!-- 加载更多/加载中 - 在加载状态或有更多数据时显示 -->
|
||||
<view v-if="loading" class="loading-more">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
@@ -219,6 +239,8 @@ const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const isError = ref(false) // 错误状态控制
|
||||
const autoFocus = ref(true)
|
||||
const activeSort = ref('default') // 当前排序方式: default, sales, price
|
||||
const priceSortAsc = ref(false) // 价格排序是否为升序
|
||||
|
||||
// 数据定义
|
||||
const searchHistory = ref<string[]>([])
|
||||
@@ -271,6 +293,31 @@ const initPage = () => {
|
||||
scrollHeight.value = windowHeight - (60 + statusBarHeight.value)
|
||||
|
||||
loadData()
|
||||
|
||||
// 检查页面参数
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1]
|
||||
// @ts-ignore
|
||||
const options = currentPage.options
|
||||
if (options && options['keyword']) {
|
||||
const keyword = decodeURIComponent(options['keyword'])
|
||||
searchKeyword.value = keyword
|
||||
|
||||
if (options['type'] === 'family') {
|
||||
// 如果是家庭常备药类型,直接添加到历史并搜索
|
||||
addToHistory(keyword)
|
||||
// 立即显示结果区域并设置为加载中
|
||||
showResults.value = true
|
||||
loading.value = true
|
||||
// 确保searchResults不为空数组导致闪烁(虽然loading=true已经拦截了empty-result,但双重保险)
|
||||
// 此时不要置空searchResults,或者给一个初始值
|
||||
|
||||
// 直接调用搜索,移除setTimeout,防止中间状态
|
||||
performSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('初始化失败', e)
|
||||
isError.value = true
|
||||
@@ -279,7 +326,7 @@ const initPage = () => {
|
||||
|
||||
// 加载基础数据
|
||||
const loadData = () => {
|
||||
loading.value = true
|
||||
// loading.value = true // 不使用全局loading,避免影响搜索状态
|
||||
isError.value = false
|
||||
|
||||
// 模拟网络请求
|
||||
@@ -288,10 +335,10 @@ const loadData = () => {
|
||||
loadSearchHistory()
|
||||
hotSearchList.value = mockDatabase.hot
|
||||
guessList.value = mockDatabase.guess
|
||||
loading.value = false
|
||||
// loading.value = false // 不使用全局loading
|
||||
} catch (e) {
|
||||
isError.value = true
|
||||
loading.value = false
|
||||
// loading.value = false
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
@@ -391,37 +438,84 @@ const selectSuggestion = (suggestion: string) => {
|
||||
}
|
||||
|
||||
const performSearch = () => {
|
||||
// 再次强制设置状态,确保万无一失
|
||||
showResults.value = true
|
||||
loading.value = true
|
||||
searchResults.value = [] // 清空旧结果
|
||||
// 注意:这里不要清空 searchResults.value = [],否则如果 loading 状态切换有微小延迟,可能会短暂满足 "无数据且非加载" 的条件
|
||||
// 保持旧数据直到新数据回来,或者依靠 loading 状态完全遮罩
|
||||
|
||||
// 模拟搜索请求
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
// 生成模拟结果
|
||||
searchResults.value = Array.from({ length: 6 }, (_, i) => ({
|
||||
id: `s${i}`,
|
||||
const newResults = Array.from({ length: 6 }, (_, i) => ({
|
||||
id: `s${Date.now()}${i}`, // 确保ID唯一
|
||||
shopId: i % 2 === 0 ? 'shop_self' : `shop_${i}_${Date.now()}`,
|
||||
shopName: i % 2 === 0 ? '平台自营大药房' : '阿里健康大药房',
|
||||
name: `${searchKeyword.value}相关药品-${i+1}`,
|
||||
specification: '10g*12袋',
|
||||
price: (Math.random() * 50 + 10).toFixed(1),
|
||||
image: `https://picsum.photos/300/300?random=s${i}`,
|
||||
image: '/static/images/default-product.png', // 使用本地默认图片
|
||||
sales: Math.floor(Math.random() * 1000),
|
||||
tag: i % 2 === 0 ? '自营' : ''
|
||||
}))
|
||||
|
||||
// 数据准备好后再关闭 loading,确保无缝衔接
|
||||
searchResults.value = newResults
|
||||
// 应用当前排序
|
||||
sortResults()
|
||||
|
||||
loading.value = false
|
||||
hasMore.value = true
|
||||
}, 800)
|
||||
}
|
||||
|
||||
// 切换排序
|
||||
const switchSort = (type: string) => {
|
||||
if (type === 'price') {
|
||||
if (activeSort.value === 'price') {
|
||||
priceSortAsc.value = !priceSortAsc.value
|
||||
} else {
|
||||
activeSort.value = 'price'
|
||||
priceSortAsc.value = true // 默认升序
|
||||
}
|
||||
} else {
|
||||
activeSort.value = type
|
||||
}
|
||||
sortResults()
|
||||
}
|
||||
|
||||
// 执行排序逻辑
|
||||
const sortResults = () => {
|
||||
const list = [...searchResults.value]
|
||||
if (activeSort.value === 'sales') {
|
||||
// 销量降序
|
||||
list.sort((a, b) => b.sales - a.sales)
|
||||
} else if (activeSort.value === 'price') {
|
||||
// 价格排序
|
||||
list.sort((a, b) => {
|
||||
const p1 = parseFloat(a.price)
|
||||
const p2 = parseFloat(b.price)
|
||||
return priceSortAsc.value ? (p1 - p2) : (p2 - p1)
|
||||
})
|
||||
} else {
|
||||
// 综合排序(这里简单按ID倒序模拟)
|
||||
list.sort((a, b) => (a.id > b.id ? -1 : 1))
|
||||
}
|
||||
searchResults.value = list
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
if (loading.value || !hasMore.value) return
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
const newItems = Array.from({ length: 4 }, (_, i) => ({
|
||||
id: `more${Date.now()}${i}`,
|
||||
shopId: i % 2 === 0 ? 'shop_self' : `shop_more_${i}_${Date.now()}`,
|
||||
shopName: i % 2 === 0 ? '平台自营大药房' : '好药师大药房',
|
||||
name: `${searchKeyword.value}更多药品-${i+1}`,
|
||||
specification: '盒装',
|
||||
price: (Math.random() * 50 + 10).toFixed(1),
|
||||
image: `https://picsum.photos/300/300?random=m${i}`,
|
||||
image: '/static/images/default-product.png',
|
||||
sales: Math.floor(Math.random() * 500),
|
||||
tag: ''
|
||||
}))
|
||||
@@ -442,11 +536,77 @@ const refreshGuessList = () => {
|
||||
const viewProductDetail = (item: any) => {
|
||||
// 跳转详情页逻辑
|
||||
console.log('查看商品', item)
|
||||
uni.showToast({ title: '点击了商品: ' + item.name, icon: 'none' })
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?productId=${item.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 添加到购物车
|
||||
const addToCart = (product: any) => {
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let cartItems: any[] = []
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商品是否已存在
|
||||
const existingItem = cartItems.find((item: any) => item.id === product.id)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity++
|
||||
} else {
|
||||
// 添加新商品
|
||||
cartItems.push({
|
||||
id: product.id,
|
||||
shopId: product.shopId || 'shop_search_default',
|
||||
shopName: product.shopName || (product.tag === '自营' ? '平台自营大药房' : '优质大药房'),
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image,
|
||||
spec: product.specification || '默认规格',
|
||||
quantity: 1,
|
||||
selected: true
|
||||
})
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const openCamera = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['camera'],
|
||||
success: (res) => {
|
||||
console.log('拍摄图片路径:', res.tempFilePaths[0])
|
||||
uni.showToast({ title: '已启用相机', icon: 'none' })
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('启用相机失败', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
if (showResults.value) {
|
||||
// 如果在搜索结果页,先返回到搜索初始页
|
||||
showResults.value = false
|
||||
searchKeyword.value = ''
|
||||
} else {
|
||||
// 如果在搜索初始页,则返回上一页
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -463,41 +623,46 @@ const goBack = () => {
|
||||
.search-header {
|
||||
background-color: #ffffff;
|
||||
padding-bottom: 10px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
/* #ifdef APP-PLUS */
|
||||
padding-top: 0; /* 在App端由style动态控制 */
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.search-bar-container {
|
||||
display: flex;
|
||||
flex-direction: row; /* UVUE 必须显式设置 row */
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
gap: 12px;
|
||||
width: 100%; /* 确保占满宽度 */
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px; /* 固定宽度防止压缩 */
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
flex: 1; /* 占据剩余空间 */
|
||||
height: 40px; /*稍微增高一点以容纳按钮*/
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 18px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-direction: row; /* UVUE 必须显式设置 row */
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
color: #999;
|
||||
padding: 0 4px 0 12px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
@@ -505,6 +670,7 @@ const goBack = () => {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
height: 100%;
|
||||
background-color: transparent; /* 确保背景透明 */
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
@@ -513,6 +679,10 @@ const goBack = () => {
|
||||
|
||||
.clear-btn {
|
||||
padding: 4px;
|
||||
margin-right: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
@@ -520,13 +690,37 @@ const goBack = () => {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 4px 0;
|
||||
.camera-btn {
|
||||
padding: 4px 8px 4px 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right-width: 1px; /* UVUE 边框写法 */
|
||||
border-right-style: solid;
|
||||
border-right-color: #ddd;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-btn-text {
|
||||
font-size: 15px;
|
||||
color: #4CAF50;
|
||||
.camera-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* 内部搜索按钮样式 */
|
||||
.inner-search-btn {
|
||||
padding: 0 16px;
|
||||
background-color: #87CEEB;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.inner-search-text {
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -540,22 +734,27 @@ const goBack = () => {
|
||||
/* 模块通用头部 */
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-direction: row; /* UVUE 显式设置 row */
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1; /* 占据左侧空间 */
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
flex-direction: row; /* UVUE 显式设置 row */
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
}
|
||||
|
||||
.clear-text {
|
||||
@@ -570,12 +769,16 @@ const goBack = () => {
|
||||
/* 搜索历史 */
|
||||
.search-history {
|
||||
margin-bottom: 24px;
|
||||
padding: 0 4px; /* 微调内边距 */
|
||||
}
|
||||
|
||||
.history-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row; /* UVUE 显式设置 row */
|
||||
gap: 10px;
|
||||
flex-wrap: wrap; /* 允许换行 */
|
||||
padding: 0 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.history-tag {
|
||||
@@ -585,7 +788,7 @@ const goBack = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
}
|
||||
|
||||
.history-text {
|
||||
@@ -619,17 +822,21 @@ const goBack = () => {
|
||||
|
||||
.hot-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row; /* UVUE 显式设置 row */
|
||||
flex-wrap: wrap; /* 允许换行 */
|
||||
gap: 10px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.hot-tag {
|
||||
background-color: #fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border-radius: 16px; /* 增加圆角,像胶囊一样 */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
}
|
||||
|
||||
.hot-tag.hot {
|
||||
@@ -773,9 +980,12 @@ const goBack = () => {
|
||||
|
||||
.results-header {
|
||||
display: flex;
|
||||
flex-direction: row; /* UVUE 显式设置 row */
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap; /* 允许换行以适应小屏 */
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.results-title {
|
||||
@@ -786,12 +996,16 @@ const goBack = () => {
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
flex-direction: row; /* UVUE 显式设置 row */
|
||||
gap: 16px;
|
||||
flex: 1; /* 自适应填充剩余空间 */
|
||||
justify-content: flex-end; /* 靠右对齐 */
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
padding: 4px 8px; /* 增加点击区域 */
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
@@ -800,22 +1014,56 @@ const goBack = () => {
|
||||
}
|
||||
|
||||
.results-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr); /* 默认移动端双列 */
|
||||
gap: 10px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
/* 平板设备 (768px以上) */
|
||||
@media screen and (min-width: 768px) {
|
||||
.results-list {
|
||||
grid-template-columns: repeat(3, 1fr); /* 平板显示3列 */
|
||||
gap: 16px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.guess-grid {
|
||||
grid-template-columns: repeat(4, 1fr); /* 猜你喜欢在平板上显示4列 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 桌面设备 (1024px以上) */
|
||||
@media screen and (min-width: 1024px) {
|
||||
.results-list {
|
||||
grid-template-columns: repeat(4, 1fr); /* 桌面显示4列 */
|
||||
gap: 20px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.guess-grid {
|
||||
grid-template-columns: repeat(6, 1fr); /* 猜你喜欢在桌面上显示6列 */
|
||||
}
|
||||
|
||||
/* 桌面端调整图片高度 */
|
||||
.product-image {
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-item {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-direction: column; /* 垂直排列 */
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
height: 120px; /* 调整图片高度 */
|
||||
border-radius: 4px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
@@ -828,44 +1076,47 @@ const goBack = () => {
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 15px;
|
||||
font-size: 13px; /* 减小字号 */
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
line-height: 1.3;
|
||||
height: 34px; /* 限制高度 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.product-tags-row {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.product-tag {
|
||||
font-size: 10px;
|
||||
color: #ff5000;
|
||||
border: 1px solid #ff5000;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
margin-top: 2px;
|
||||
display: none; /* 隐藏标签以保持简洁 */
|
||||
}
|
||||
|
||||
.product-spec {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
display: none; /* 隐藏规格 */
|
||||
}
|
||||
|
||||
.product-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-top: 8px;
|
||||
align-items: center; /* 垂直居中 */
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.price-box {
|
||||
color: #ff5000;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.price-symbol {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-size: 16px; /* 减小价格字号 */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.add-cart-btn {
|
||||
@@ -880,7 +1131,7 @@ const goBack = () => {
|
||||
|
||||
.cart-icon {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
375
pages/mall/consumer/shop-detail.uvue
Normal file
375
pages/mall/consumer/shop-detail.uvue
Normal file
@@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<view class="shop-detail-page">
|
||||
<!-- 店铺头部信息 -->
|
||||
<view class="shop-header">
|
||||
<image :src="merchant.shop_banner || '/static/default-banner.png'" class="shop-banner" mode="aspectFill" />
|
||||
<view class="shop-info-card">
|
||||
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" />
|
||||
<view class="shop-basic-info">
|
||||
<text class="shop-name">{{ merchant.shop_name }}</text>
|
||||
<view class="shop-stats">
|
||||
<text class="stat-item">⭐ {{ merchant.rating.toFixed(1) }}</text>
|
||||
<text class="stat-item">销量 {{ merchant.total_sales }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<button class="follow-btn" @click="toggleFollow">{{ isFollowed ? '已关注' : '+ 关注' }}</button>
|
||||
</view>
|
||||
<text class="shop-desc">{{ merchant.shop_description || '这家店很懒,什么都没写~' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view class="product-section">
|
||||
<view class="section-title">全部商品</view>
|
||||
<view class="product-grid">
|
||||
<view v-for="product in products" :key="product.id" class="product-item" @click="goToProduct(product.id)">
|
||||
<image :src="product.images[0]" class="product-image" mode="aspectFill" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<view class="price-row">
|
||||
<view class="price-left">
|
||||
<text class="product-price">¥{{ product.price }}</text>
|
||||
<text class="product-sales">已售 {{ product.sales }}</text>
|
||||
</view>
|
||||
<view class="cart-btn" @click.stop="addToCart(product)">
|
||||
<text class="cart-icon">🛒</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { MerchantType, ProductType } from '@/types/mall-types.uts'
|
||||
|
||||
const merchant = ref<MerchantType>({
|
||||
id: '',
|
||||
user_id: '',
|
||||
shop_name: '',
|
||||
shop_logo: '',
|
||||
shop_banner: '',
|
||||
shop_description: '',
|
||||
contact_name: '',
|
||||
contact_phone: '',
|
||||
shop_status: 0,
|
||||
rating: 0,
|
||||
total_sales: 0,
|
||||
created_at: ''
|
||||
} as MerchantType)
|
||||
|
||||
const products = ref<ProductType[]>([])
|
||||
const isFollowed = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const options = pages[pages.length - 1].options as any
|
||||
const merchantId = options['merchantId'] as string
|
||||
|
||||
if (merchantId) {
|
||||
loadShopData(merchantId)
|
||||
loadShopProducts(merchantId)
|
||||
}
|
||||
})
|
||||
|
||||
const loadShopData = (id: string) => {
|
||||
// 模拟加载店铺数据
|
||||
merchant.value = {
|
||||
id: id,
|
||||
user_id: 'user_001',
|
||||
shop_name: '优质好店',
|
||||
shop_logo: '/static/shop-logo.png',
|
||||
shop_banner: '/static/shop-banner.png',
|
||||
shop_description: '专注品质生活,为您提供最优质的商品和服务。',
|
||||
contact_name: '店主小王',
|
||||
contact_phone: '13800138000',
|
||||
shop_status: 1,
|
||||
rating: 4.8,
|
||||
total_sales: 15680,
|
||||
created_at: '2023-06-01'
|
||||
}
|
||||
}
|
||||
|
||||
const loadShopProducts = (id: string) => {
|
||||
// 模拟加载店铺商品列表
|
||||
products.value = [
|
||||
{
|
||||
id: 'prod_001',
|
||||
merchant_id: id,
|
||||
category_id: 'cat_001',
|
||||
name: '精选好物商品 A',
|
||||
description: '商品描述 A',
|
||||
images: ['/static/product1.jpg'],
|
||||
price: 199.99,
|
||||
original_price: 299.99,
|
||||
stock: 100,
|
||||
sales: 1256,
|
||||
status: 1,
|
||||
created_at: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 'prod_002',
|
||||
merchant_id: id,
|
||||
category_id: 'cat_001',
|
||||
name: '精选好物商品 B',
|
||||
description: '商品描述 B',
|
||||
images: ['/static/product2.jpg'],
|
||||
price: 299.00,
|
||||
original_price: 399.00,
|
||||
stock: 50,
|
||||
sales: 856,
|
||||
status: 1,
|
||||
created_at: '2024-01-16'
|
||||
},
|
||||
{
|
||||
id: 'prod_003',
|
||||
merchant_id: id,
|
||||
category_id: 'cat_002',
|
||||
name: '精选好物商品 C',
|
||||
description: '商品描述 C',
|
||||
images: ['/static/product3.jpg'],
|
||||
price: 99.00,
|
||||
original_price: 129.00,
|
||||
stock: 200,
|
||||
sales: 3256,
|
||||
status: 1,
|
||||
created_at: '2024-01-17'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const toggleFollow = () => {
|
||||
isFollowed.value = !isFollowed.value
|
||||
uni.showToast({
|
||||
title: isFollowed.value ? '关注成功' : '已取消关注',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const addToCart = (product: ProductType) => {
|
||||
// 获取现有购物车数据
|
||||
const cartData = uni.getStorageSync('cart')
|
||||
let cartItems: any[] = []
|
||||
|
||||
if (cartData) {
|
||||
try {
|
||||
cartItems = JSON.parse(cartData as string) as any[]
|
||||
} catch (e) {
|
||||
console.error('解析购物车数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查商品是否已存在
|
||||
const existingItem = cartItems.find((item: any) => item.productId === product.id)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.quantity++
|
||||
} else {
|
||||
// 添加新商品
|
||||
cartItems.push({
|
||||
id: product.id, // 简单使用产品ID作为购物车ID,实际可能有规格
|
||||
productId: product.id,
|
||||
shopId: merchant.value.id,
|
||||
shopName: merchant.value.shop_name,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.images[0],
|
||||
spec: '默认规格',
|
||||
quantity: 1,
|
||||
selected: true
|
||||
})
|
||||
}
|
||||
|
||||
// 保存回存储
|
||||
uni.setStorageSync('cart', JSON.stringify(cartItems))
|
||||
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const goToProduct = (id: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?productId=${id}`
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.shop-detail-page {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.shop-header {
|
||||
background-color: #fff;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.shop-banner {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.shop-info-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 15px;
|
||||
margin-top: -30px; /* Logo 向上重叠 banner */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.shop-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #fff;
|
||||
background-color: #fff;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.shop-basic-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 30px; /* 给 logo 上浮留空间 */
|
||||
}
|
||||
|
||||
.shop-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.shop-stats {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-right: 12px;
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.follow-btn {
|
||||
font-size: 14px;
|
||||
background-color: #ff4444;
|
||||
color: white;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
margin-top: 30px; /* 对齐 */
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.shop-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
padding: 10px 15px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.product-section {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
padding-left: 8px;
|
||||
border-left: 4px solid #ff4444;
|
||||
}
|
||||
|
||||
.product-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
width: calc(50% - 5px);
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 170px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
height: 40px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.price-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #ff4444;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cart-icon {
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 16px;
|
||||
color: #ff4444;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-sales {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user